Class: Datadog::Tracing::Contrib::Rack::TraceMiddleware

Inherits:
Object
  • Object
show all
Defined in:
lib/datadog/tracing/contrib/rack/middlewares.rb

Overview

TraceMiddleware ensures that the Rack Request is properly traced from the beginning to the end. The middleware adds the request span in the Rack environment so that it can be retrieved by the underlying application. If request tags are not set by the app, they will be set using information available at the Rack level.

Instance Method Summary collapse

Constructor Details

#initialize(app) ⇒ TraceMiddleware

Returns a new instance of TraceMiddleware.



30
31
32
# File 'lib/datadog/tracing/contrib/rack/middlewares.rb', line 30

def initialize(app)
  @app = app
end

Instance Method Details

#call(env) ⇒ Object



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/datadog/tracing/contrib/rack/middlewares.rb', line 34

def call(env)
  # Find out if this is rack within rack
  previous_request_span = env[Ext::RACK_ENV_REQUEST_SPAN]

  return @app.call(env) if previous_request_span

  boot = Datadog::Core::Remote::Tie.boot

  # Extract distributed tracing context before creating any spans,
  # so that all spans will be added to the distributed trace.
  if configuration[:distributed_tracing]
    trace_digest = Contrib::HTTP.extract(env)
    Tracing.continue_trace!(trace_digest)
  end

  TraceProxyMiddleware.call(env, configuration) do
    trace_options = { type: Tracing::Metadata::Ext::HTTP::TYPE_INBOUND }
    trace_options[:service] = configuration[:service_name] if configuration[:service_name]

    # start a new request span and attach it to the current Rack environment;
    # we must ensure that the span `resource` is set later
    request_span = Tracing.trace(Ext::SPAN_REQUEST, **trace_options)
    request_span.resource = nil

    # When tracing and distributed tracing are both disabled, `.active_trace` will be `nil`,
    # Return a null object to continue operation
    request_trace = Tracing.active_trace || TraceOperation.new
    env[Ext::RACK_ENV_REQUEST_SPAN] = request_span

    Datadog::Core::Remote::Tie::Tracing.tag(boot, request_span)

    # Copy the original env, before the rest of the stack executes.
    # Values may change; we want values before that happens.
    original_env = env.dup

    # call the rest of the stack
    status, headers, response = @app.call(env)

    [status, headers, response]

  # Here we really want to catch *any* exception, not only StandardError,
  # as we really have no clue of what is in the block,
  # and it is user code which should be executed no matter what.
  # It's not a problem since we re-raise it afterwards so for example a
  # SignalException::Interrupt would still bubble up.
  rescue Exception => e # rubocop:disable Lint/RescueException
    # catch exceptions that may be raised in the middleware chain
    # Note: if a middleware catches an Exception without re raising,
    # the Exception cannot be recorded here.
    request_span.set_error(e) unless request_span.nil?
    raise e
  ensure
    env[Ext::RACK_ENV_REQUEST_SPAN] = previous_request_span if previous_request_span

    if request_span
      # Rack is a really low level interface and it doesn't provide any
      # advanced functionality like routers. Because of that, we assume that
      # the underlying framework or application has more knowledge about
      # the result for this request; `resource` and `tags` are expected to
      # be set in another level but if they're missing, reasonable defaults
      # are used.
      set_request_tags!(request_trace, request_span, env, status, headers, response, original_env || env)

      # ensure the request_span is finished and the context reset;
      # this assumes that the Rack middleware creates a root span
      request_span.finish
    end
  end
end

#set_request_tags!(trace, request_span, env, status, headers, response, original_env) ⇒ Object

rubocop:disable Metrics/AbcSize rubocop:disable Metrics/CyclomaticComplexity rubocop:disable Metrics/PerceivedComplexity rubocop:disable Metrics/MethodLength



108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'lib/datadog/tracing/contrib/rack/middlewares.rb', line 108

def set_request_tags!(trace, request_span, env, status, headers, response, original_env)
  request_header_collection = Header::RequestHeaderCollection.new(env)

  # Since it could be mutated, it would be more accurate to fetch from the original env,
  # e.g. ActionDispatch::ShowExceptions middleware with Rails exceptions_app configuration
  original_request_method = original_env['REQUEST_METHOD']

  # request_headers is subject to filtering and configuration so we
  # get the user agent separately
  user_agent = parse_user_agent_header(request_header_collection)

  # The priority
  # 1. User overrides span.resource
  # 2. Configuration
  # 3. Nested App override trace.resource
  # 4. Fallback with verb + status, eq `GET 200`
  request_span.resource ||=
    if configuration[:middleware_names] && env['RESPONSE_MIDDLEWARE']
      "#{env['RESPONSE_MIDDLEWARE']}##{original_request_method}"
    elsif trace.resource_override?
      trace.resource
    else
      "#{original_request_method} #{status}".strip
    end

  # Overrides the trace resource if it never been set
  # Otherwise, the getter method would delegate to its root span
  trace.resource = request_span.resource unless trace.resource_override?

  request_span.set_tag(Tracing::Metadata::Ext::TAG_COMPONENT, Ext::TAG_COMPONENT)
  request_span.set_tag(Tracing::Metadata::Ext::TAG_OPERATION, Ext::TAG_OPERATION_REQUEST)
  request_span.set_tag(Tracing::Metadata::Ext::TAG_KIND, Tracing::Metadata::Ext::SpanKind::TAG_SERVER)

  if status != 404 && (last_route = trace.get_tag(Tracing::Metadata::Ext::HTTP::TAG_ROUTE))
    last_script_name = trace.get_tag(Tracing::Metadata::Ext::HTTP::TAG_ROUTE_PATH) || ''

    # If the last_script_name is empty but the env['SCRIPT_NAME'] is NOT empty
    # then the current rack request was not routed and must be accounted for
    # which only happens in pure nested rack requests i.e /rack/rack/hello/world
    #
    # To account for the unaccounted nested rack requests of /rack/hello/world,
    # we use 'PATH_INFO knowing that rack cannot have named parameters
    if last_script_name == '' && env['SCRIPT_NAME'] && env['SCRIPT_NAME'] != ''
      last_script_name = last_route
      last_route = env['PATH_INFO']
    end

    # Clear the route and route path tags from the request trace to avoid possibility of misplacement
    trace.clear_tag(Tracing::Metadata::Ext::HTTP::TAG_ROUTE)
    trace.clear_tag(Tracing::Metadata::Ext::HTTP::TAG_ROUTE_PATH)

    # Ensure tags are placed in rack.request span as desired
    request_span.set_tag(Tracing::Metadata::Ext::HTTP::TAG_ROUTE, last_script_name + last_route)
    request_span.clear_tag(Tracing::Metadata::Ext::HTTP::TAG_ROUTE_PATH)
  end

  # Set analytics sample rate
  if Contrib::Analytics.enabled?(configuration[:analytics_enabled])
    Contrib::Analytics.set_sample_rate(request_span, configuration[:analytics_sample_rate])
  end

  # Measure service stats
  Contrib::Analytics.set_measured(request_span)

  if request_span.get_tag(Tracing::Metadata::Ext::HTTP::TAG_METHOD).nil?
    request_span.set_tag(Tracing::Metadata::Ext::HTTP::TAG_METHOD, original_request_method)
  end

  url = parse_url(env, original_env)

  if request_span.get_tag(Tracing::Metadata::Ext::HTTP::TAG_URL).nil?
    options = configuration[:quantize] || {}

    # Quantization::HTTP.url base defaults to :show, but we are transitioning
    options[:base] ||= :exclude

    request_span.set_tag(
      Tracing::Metadata::Ext::HTTP::TAG_URL,
      Contrib::Utils::Quantization::HTTP.url(url, options)
    )
  end

  if request_span.get_tag(Tracing::Metadata::Ext::HTTP::TAG_BASE_URL).nil?
    options = configuration[:quantize]

    unless options[:base] == :show
      base_url = Contrib::Utils::Quantization::HTTP.base_url(url)

      unless base_url.empty?
        request_span.set_tag(
          Tracing::Metadata::Ext::HTTP::TAG_BASE_URL,
          base_url
        )
      end
    end
  end

  if request_span.get_tag(Tracing::Metadata::Ext::HTTP::TAG_CLIENT_IP).nil?
    Tracing::ClientIp.set_client_ip_tag(
      request_span,
      headers: request_header_collection,
      remote_ip: env['REMOTE_ADDR']
    )
  end

  if request_span.get_tag(Tracing::Metadata::Ext::HTTP::TAG_STATUS_CODE).nil? && status
    request_span.set_tag(Tracing::Metadata::Ext::HTTP::TAG_STATUS_CODE, status)
  end

  if request_span.get_tag(Tracing::Metadata::Ext::HTTP::TAG_USER_AGENT).nil? && user_agent
    request_span.set_tag(Tracing::Metadata::Ext::HTTP::TAG_USER_AGENT, user_agent)
  end

  if request_span.get_tag(Tracing::Metadata::Ext::HTTP::TAG_ROUTE).nil? && status != 404
    request_span.set_tag(Tracing::Metadata::Ext::HTTP::TAG_ROUTE, env['PATH_INFO'])
  end

  HeaderTagging.tag_request_headers(request_span, request_header_collection, configuration)
  HeaderTagging.tag_response_headers(request_span, headers, configuration) if headers

  # detect if the status code is a 5xx and flag the request span as an error
  # unless it has been already set by the underlying framework
  request_span.status = 1 if status.to_s.start_with?('5') && request_span.status.zero?
end