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.



28
29
30
# File 'lib/datadog/tracing/contrib/rack/middlewares.rb', line 28

def initialize(app)
  @app = app
end

Instance Method Details

#call(env) ⇒ Object



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
103
104
105
106
107
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
# File 'lib/datadog/tracing/contrib/rack/middlewares.rb', line 69

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 = Tracing::Propagation::HTTP.extract(env)
    Tracing.continue_trace!(trace_digest)
  end

  # Create a root Span to keep track of frontend web servers
  # (i.e. Apache, nginx) if the header is properly set
  frontend_span = compute_queue_time(env)

  trace_options = { span_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]

# rubocop:disable Lint/RescueException
# 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
  # 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

  frontend_span.finish if frontend_span
end

#compute_queue_time(env) ⇒ Object



32
33
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
# File 'lib/datadog/tracing/contrib/rack/middlewares.rb', line 32

def compute_queue_time(env)
  return unless configuration[:request_queuing]

  # parse the request queue time
  request_start = Contrib::Rack::QueueTime.get_request_start(env)
  return if request_start.nil?

  case configuration[:request_queuing]
  when true, :include_request # DEV: Switch `true` to `:exclude_request` in v2.0
    queue_span = trace_http_server(Ext::SPAN_HTTP_SERVER_QUEUE, start_time: request_start)
    # Tag this span as belonging to Rack
    queue_span.set_tag(Tracing::Metadata::Ext::TAG_COMPONENT, Ext::TAG_COMPONENT)
    queue_span.set_tag(Tracing::Metadata::Ext::TAG_OPERATION, Ext::TAG_OPERATION_HTTP_SERVER_QUEUE)
    queue_span.set_tag(Tracing::Metadata::Ext::TAG_KIND, Tracing::Metadata::Ext::SpanKind::TAG_SERVER)
    queue_span
  when :exclude_request
    request_span = trace_http_server(Ext::SPAN_HTTP_PROXY_REQUEST, start_time: request_start)
    request_span.set_tag(Tracing::Metadata::Ext::TAG_COMPONENT, Ext::TAG_COMPONENT_HTTP_PROXY)
    request_span.set_tag(Tracing::Metadata::Ext::TAG_OPERATION, Ext::TAG_OPERATION_HTTP_PROXY_REQUEST)
    request_span.set_tag(Tracing::Metadata::Ext::TAG_KIND, Tracing::Metadata::Ext::SpanKind::TAG_PROXY)

    queue_span = trace_http_server(Ext::SPAN_HTTP_PROXY_QUEUE, start_time: request_start)

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

    queue_span.set_tag(Tracing::Metadata::Ext::TAG_COMPONENT, Ext::TAG_COMPONENT_HTTP_PROXY)
    queue_span.set_tag(Tracing::Metadata::Ext::TAG_OPERATION, Ext::TAG_OPERATION_HTTP_PROXY_QUEUE)
    queue_span.set_tag(Tracing::Metadata::Ext::TAG_KIND, Tracing::Metadata::Ext::SpanKind::TAG_PROXY)
    # finish the `queue` span now to record only the time spent *in queue*,
    # excluding the time spent processing the request itself
    queue_span.finish

    request_span
  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



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
232
233
234
235
236
237
238
239
240
241
242
243
244
245
# File 'lib/datadog/tracing/contrib/rack/middlewares.rb', line 149

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)

  # 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

  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