class OpenTelemetry::Instrumentation::Rack::Middlewares::TracerMiddleware

TracerMiddleware propagates context and instruments Rack requests by way of its middleware system

Constants

EMPTY_HASH

Public Class Methods

allowed_rack_request_headers() click to toggle source
# File lib/opentelemetry/instrumentation/rack/middlewares/tracer_middleware.rb, line 19
def allowed_rack_request_headers
  @allowed_rack_request_headers ||= Array(config[:allowed_request_headers]).each_with_object({}) do |header, memo|
    memo["HTTP_#{header.to_s.upcase.gsub(/[-\s]/, '_')}"] = build_attribute_name('http.request.headers.', header)
  end
end
allowed_response_headers() click to toggle source
# File lib/opentelemetry/instrumentation/rack/middlewares/tracer_middleware.rb, line 25
def allowed_response_headers
  @allowed_response_headers ||= Array(config[:allowed_response_headers]).each_with_object({}) do |header, memo|
    memo[header] = build_attribute_name('http.response.headers.', header)
    memo[header.to_s.upcase] = build_attribute_name('http.response.headers.', header)
  end
end
build_attribute_name(prefix, suffix) click to toggle source
# File lib/opentelemetry/instrumentation/rack/middlewares/tracer_middleware.rb, line 32
def build_attribute_name(prefix, suffix)
  prefix + suffix.to_s.downcase.gsub(/[-\s]/, '_')
end
config() click to toggle source
# File lib/opentelemetry/instrumentation/rack/middlewares/tracer_middleware.rb, line 36
def config
  Rack::Instrumentation.instance.config
end
new(app) click to toggle source
# File lib/opentelemetry/instrumentation/rack/middlewares/tracer_middleware.rb, line 50
def initialize(app)
  @app = app
  @untraced_endpoints = config[:untraced_endpoints]
end

Private Class Methods

clear_cached_config() click to toggle source
# File lib/opentelemetry/instrumentation/rack/middlewares/tracer_middleware.rb, line 42
def clear_cached_config
  @allowed_rack_request_headers = nil
  @allowed_response_headers = nil
end

Public Instance Methods

call(env) click to toggle source
# File lib/opentelemetry/instrumentation/rack/middlewares/tracer_middleware.rb, line 55
def call(env) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
  if untraced_request?(env)
    OpenTelemetry::Common::Utilities.untraced do
      return @app.call(env)
    end
  end
  original_env = env.dup
  extracted_context = OpenTelemetry.propagation.extract(
    env,
    getter: OpenTelemetry::Context::Propagation.rack_env_getter
  )
  frontend_context = create_frontend_span(env, extracted_context)

  # restore extracted context in this process:
  OpenTelemetry::Context.with_current(frontend_context || extracted_context) do
    request_span_name = create_request_span_name(env['REQUEST_URI'] || original_env['PATH_INFO'], env)
    request_span_kind = frontend_context.nil? ? :server : :internal
    tracer.in_span(request_span_name,
                   attributes: request_span_attributes(env: env),
                   kind: request_span_kind) do |request_span|
      OpenTelemetry::Instrumentation::Rack.with_span(request_span) do
        @app.call(env).tap do |status, headers, response|
          set_attributes_after_request(request_span, status, headers, response)
        end
      end
    end
  end
ensure
  finish_span(frontend_context)
end

Private Instance Methods

allowed_request_headers(env) click to toggle source
# File lib/opentelemetry/instrumentation/rack/middlewares/tracer_middleware.rb, line 159
def allowed_request_headers(env)
  return EMPTY_HASH if self.class.allowed_rack_request_headers.empty?

  {}.tap do |result|
    self.class.allowed_rack_request_headers.each do |key, value|
      result[value] = env[key] if env.key?(key)
    end
  end
end
allowed_response_headers(headers) click to toggle source
# File lib/opentelemetry/instrumentation/rack/middlewares/tracer_middleware.rb, line 169
def allowed_response_headers(headers)
  return EMPTY_HASH if headers.nil?
  return EMPTY_HASH if self.class.allowed_response_headers.empty?

  {}.tap do |result|
    self.class.allowed_response_headers.each do |key, value|
      if headers.key?(key)
        result[value] = headers[key]
      else
        # do case-insensitive match:
        headers.each do |k, v|
          if k.upcase == key
            result[value] = v
            break
          end
        end
      end
    end
  end
end
config() click to toggle source
# File lib/opentelemetry/instrumentation/rack/middlewares/tracer_middleware.rb, line 190
def config
  Rack::Instrumentation.instance.config
end
create_frontend_span(env, extracted_context) click to toggle source

return Context with the frontend span as the current span

# File lib/opentelemetry/instrumentation/rack/middlewares/tracer_middleware.rb, line 96
def create_frontend_span(env, extracted_context)
  request_start_time = OpenTelemetry::Instrumentation::Rack::Util::QueueTime.get_request_start(env)

  return unless config[:record_frontend_span] && !request_start_time.nil?

  span = tracer.start_span('http_server.proxy',
                           with_parent: extracted_context,
                           attributes: {
                             'start_time' => request_start_time.to_f
                           },
                           kind: :server)

  OpenTelemetry::Trace.context_with_span(span, parent_context: extracted_context)
end
create_request_span_name(request_uri_or_path_info, env) click to toggle source

github.com/open-telemetry/opentelemetry-specification/blob/master/specification/data-http.md#name

recommendation: span.name(s) should be low-cardinality (e.g., strip off query param value, keep param name)

see github.com/open-telemetry/opentelemetry-specification/pull/416/files

# File lib/opentelemetry/instrumentation/rack/middlewares/tracer_middleware.rb, line 137
def create_request_span_name(request_uri_or_path_info, env)
  # NOTE: dd-trace-rb has implemented 'quantization' (which lowers url cardinality)
  #       see Datadog::Quantization::HTTP.url

  if (implementation = config[:url_quantization])
    implementation.call(request_uri_or_path_info, env)
  else
    request_uri_or_path_info
  end
end
finish_span(context) click to toggle source
# File lib/opentelemetry/instrumentation/rack/middlewares/tracer_middleware.rb, line 111
def finish_span(context)
  OpenTelemetry::Trace.current_span(context).finish if context
end
request_span_attributes(env:) click to toggle source
# File lib/opentelemetry/instrumentation/rack/middlewares/tracer_middleware.rb, line 119
def request_span_attributes(env:)
  attributes = {
    'http.method' => env['REQUEST_METHOD'],
    'http.host' => env['HTTP_HOST'] || 'unknown',
    'http.scheme' => env['rack.url_scheme'],
    'http.target' => env['QUERY_STRING'].empty? ? env['PATH_INFO'] : "#{env['PATH_INFO']}?#{env['QUERY_STRING']}"
  }

  attributes['http.user_agent'] = env['HTTP_USER_AGENT'] if env['HTTP_USER_AGENT']
  attributes.merge!(allowed_request_headers(env))
end
set_attributes_after_request(span, status, headers, _response) click to toggle source
# File lib/opentelemetry/instrumentation/rack/middlewares/tracer_middleware.rb, line 148
def set_attributes_after_request(span, status, headers, _response)
  span.status = OpenTelemetry::Trace::Status.error unless (100..399).include?(status.to_i)
  span.set_attribute('http.status_code', status)

  # NOTE: if data is available, it would be good to do this:
  # set_attribute('http.route', ...
  # e.g., "/users/:userID?

  allowed_response_headers(headers).each { |k, v| span.set_attribute(k, v) }
end
tracer() click to toggle source
# File lib/opentelemetry/instrumentation/rack/middlewares/tracer_middleware.rb, line 115
def tracer
  OpenTelemetry::Instrumentation::Rack::Instrumentation.instance.tracer
end
untraced_request?(env) click to toggle source
# File lib/opentelemetry/instrumentation/rack/middlewares/tracer_middleware.rb, line 88
def untraced_request?(env)
  return true if @untraced_endpoints.include?(env['PATH_INFO'])
  return true if config[:untraced_requests]&.call(env)

  false
end