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

Inherits:
Object
  • Object
show all
Defined in:
lib/ddtrace/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. rubocop:disable Metrics/ClassLength

Constant Summary collapse

RACK_REQUEST_SPAN =

DEPRECATED: Remove in 1.0 in favor of Datadog::Contrib::Rack::Ext::RACK_ENV_REQUEST_SPAN This constant will remain here until then, for backwards compatibility.

'datadog.rack_request_span'.freeze

Instance Method Summary collapse

Constructor Details

#initialize(app) ⇒ TraceMiddleware

Returns a new instance of TraceMiddleware.


26
27
28
# File 'lib/ddtrace/contrib/rack/middlewares.rb', line 26

def initialize(app)
  @app = app
end

Instance Method Details

#call(env) ⇒ Object


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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/ddtrace/contrib/rack/middlewares.rb', line 45

def call(env)
  # retrieve integration settings
  tracer = configuration[:tracer]

  # Extract distributed tracing context before creating any spans,
  # so that all spans will be added to the distributed trace.
  if configuration[:distributed_tracing]
    context = HTTPPropagator.extract(env)
    tracer.provider.context = context if context.trace_id
  end

  # [experimental] 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, tracer)

  trace_options = {
    service: configuration[:service_name],
    resource: nil,
    span_type: Datadog::Ext::HTTP::TYPE_INBOUND
  }

  # 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 = tracer.trace(Ext::SPAN_REQUEST, trace_options)
  env[RACK_REQUEST_SPAN] = request_span

  # TODO: Add deprecation warnings back in
  # DEV: Some third party Gems will loop over the rack env causing our deprecation
  #      warnings to be shown even when the user is not accessing them directly
  #
  # add_deprecation_warnings(env)
  # env.without_datadog_warnings do
  #   # TODO: For backwards compatibility; this attribute is deprecated.
  #   env[:datadog_rack_request_span] = env[RACK_REQUEST_SPAN]
  # end
  env[:datadog_rack_request_span] = env[RACK_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
  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_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 unless frontend_span.nil?

  # TODO: Remove this once we change how context propagation works. This
  # ensures we clean thread-local variables on each HTTP request avoiding
  # memory leaks.
  tracer.provider.context = Datadog::Context.new if tracer
end

#compute_queue_time(env, tracer) ⇒ Object


30
31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/ddtrace/contrib/rack/middlewares.rb', line 30

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

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

  tracer.trace(
    Ext::SPAN_HTTP_SERVER_QUEUE,
    span_type: Datadog::Ext::HTTP::TYPE_PROXY,
    start_time: request_start,
    service: configuration[:web_service_name]
  )
end

#resource_name_for(env, status) ⇒ Object


125
126
127
128
129
130
131
# File 'lib/ddtrace/contrib/rack/middlewares.rb', line 125

def resource_name_for(env, status)
  if configuration[:middleware_names] && env['RESPONSE_MIDDLEWARE']
    "#{env['RESPONSE_MIDDLEWARE']}##{env['REQUEST_METHOD']}"
  else
    "#{env['REQUEST_METHOD']} #{status}".strip
  end
end

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

rubocop:disable Metrics/AbcSize rubocop:disable Metrics/MethodLength


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
# File 'lib/ddtrace/contrib/rack/middlewares.rb', line 135

def set_request_tags!(request_span, env, status, headers, response, original_env)
  # http://www.rubydoc.info/github/rack/rack/file/SPEC
  # The source of truth in Rack is the PATH_INFO key that holds the
  # URL for the current request; but some frameworks may override that
  # value, especially during exception handling.
  #
  # Because of this, we prefer to use REQUEST_URI, if available, which is the
  # relative path + query string, and doesn't mutate.
  #
  # REQUEST_URI is only available depending on what web server is running though.
  # So when its not available, we want the original, unmutated PATH_INFO, which
  # is just the relative path without query strings.
  url = env['REQUEST_URI'] || original_env['PATH_INFO']
  request_headers = parse_request_headers(env)
  response_headers = parse_response_headers(headers || {})

  request_span.resource ||= resource_name_for(env, status)

  # Associate with runtime metrics
  Datadog.runtime_metrics.associate_with_span(request_span)

  # 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(Datadog::Ext::HTTP::METHOD).nil?
    request_span.set_tag(Datadog::Ext::HTTP::METHOD, env['REQUEST_METHOD'])
  end

  if request_span.get_tag(Datadog::Ext::HTTP::URL).nil?
    options = configuration[:quantize]
    request_span.set_tag(Datadog::Ext::HTTP::URL, Datadog::Quantization::HTTP.url(url, options))
  end

  if request_span.get_tag(Datadog::Ext::HTTP::BASE_URL).nil?
    request_obj = ::Rack::Request.new(env)

    base_url = if request_obj.respond_to?(:base_url)
                 request_obj.base_url
               else
                 # Compatibility for older Rack versions
                 request_obj.url.chomp(request_obj.fullpath)
               end

    request_span.set_tag(Datadog::Ext::HTTP::BASE_URL, base_url)
  end

  if request_span.get_tag(Datadog::Ext::HTTP::STATUS_CODE).nil? && status
    request_span.set_tag(Datadog::Ext::HTTP::STATUS_CODE, status)
  end

  # Request headers
  request_headers.each do |name, value|
    request_span.set_tag(name, value) if request_span.get_tag(name).nil?
  end

  # Response headers
  response_headers.each do |name, value|
    request_span.set_tag(name, value) if request_span.get_tag(name).nil?
  end

  # 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
  if status.to_s.start_with?('5') && request_span.status.zero?
    request_span.status = 1
  end
end