opentelemetry-instrumentation-rack 0.26.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,269 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright The OpenTelemetry Authors
4
+ #
5
+ # SPDX-License-Identifier: Apache-2.0
6
+
7
+ require_relative '../../util'
8
+ require 'opentelemetry/trace/status'
9
+
10
+ module OpenTelemetry
11
+ module Instrumentation
12
+ module Rack
13
+ module Middlewares
14
+ module Stable
15
+ # OTel Rack Event Handler
16
+ #
17
+ # This seeds the root context for this service with the server span as the `current_span`
18
+ # allowing for callers later in the stack to reference it using {OpenTelemetry::Trace.current_span}
19
+ #
20
+ # It also registers the server span in a context dedicated to this instrumentation that users may look up
21
+ # using {OpenTelemetry::Instrumentation::Rack.current_span}, which makes it possible for users to mutate the span,
22
+ # e.g. add events or update the span name like in the {ActionPack} instrumentation.
23
+ #
24
+ # @example Rack App Using BodyProxy
25
+ # GLOBAL_LOGGER = Logger.new($stderr)
26
+ # APP_TRACER = OpenTelemetry.tracer_provider.tracer('my-app', '1.0.0')
27
+ #
28
+ # Rack::Builder.new do
29
+ # use Rack::Events, [OpenTelemetry::Instrumentation::Rack::Middlewares::EventHandler.new]
30
+ # run lambda { |_arg|
31
+ # APP_TRACER.in_span('hello-world') do |_span|
32
+ # body = Rack::BodyProxy.new(['hello world!']) do
33
+ # rack_span = OpenTelemetry::Instrumentation::Rack.current_span
34
+ # GLOBAL_LOGGER.info("otel.trace_id=#{rack_span.context.hex_trace_id} otel.span_id=#{rack_span.context.hex_span_id}")
35
+ # end
36
+ # [200, { 'Content-Type' => 'text/plain' }, body]
37
+ # end
38
+ # }
39
+ # end
40
+ #
41
+ # @see Rack::Events
42
+ # @see OpenTelemetry::Instrumentation::Rack.current_span
43
+ class EventHandler
44
+ include ::Rack::Events::Abstract
45
+
46
+ OTEL_TOKEN_AND_SPAN = 'otel.rack.token_and_span'
47
+ EMPTY_HASH = {}.freeze
48
+
49
+ # Creates a server span for this current request using the incoming parent context
50
+ # and registers them as the {current_span}
51
+ #
52
+ # @param [Rack::Request] The current HTTP request
53
+ # @param [Rack::Response] This is nil in practice
54
+ # @return [void]
55
+ def on_start(request, _)
56
+ parent_context = if untraced_request?(request.env)
57
+ extract_remote_context(request, OpenTelemetry::Common::Utilities.untraced)
58
+ else
59
+ extract_remote_context(request)
60
+ end
61
+
62
+ span = create_span(parent_context, request)
63
+ span_ctx = OpenTelemetry::Trace.context_with_span(span, parent_context: parent_context)
64
+ rack_ctx = OpenTelemetry::Instrumentation::Rack.context_with_span(span, parent_context: span_ctx)
65
+ request.env[OTEL_TOKEN_AND_SPAN] = [OpenTelemetry::Context.attach(rack_ctx), span]
66
+ rescue StandardError => e
67
+ OpenTelemetry.handle_error(exception: e)
68
+ end
69
+
70
+ # Optionally adds debugging response headers injected from {response_propagators}
71
+ #
72
+ # @param [Rack::Request] The current HTTP request
73
+ # @param [Rack::Response] This current HTTP response
74
+ # @return [void]
75
+ def on_commit(request, response)
76
+ span = OpenTelemetry::Instrumentation::Rack.current_span
77
+ return unless span.recording?
78
+
79
+ response_propagators&.each do |propagator|
80
+ propagator.inject(response.headers)
81
+ rescue StandardError => e
82
+ OpenTelemetry.handle_error(message: 'Unable to inject response propagation headers', exception: e)
83
+ end
84
+ rescue StandardError => e
85
+ OpenTelemetry.handle_error(exception: e)
86
+ end
87
+
88
+ # Records Unexpected Exceptions on the Rack span and set the Span Status to Error
89
+ #
90
+ # @note does nothing if the span is a non-recording span
91
+ # @param [Rack::Request] The current HTTP request
92
+ # @param [Rack::Response] The current HTTP response
93
+ # @param [Exception] An unxpected error raised by the application
94
+ def on_error(request, _, error)
95
+ span = OpenTelemetry::Instrumentation::Rack.current_span
96
+ return unless span.recording?
97
+
98
+ span.record_exception(error)
99
+ span.status = OpenTelemetry::Trace::Status.error(error.class.name)
100
+ rescue StandardError => e
101
+ OpenTelemetry.handle_error(exception: e)
102
+ end
103
+
104
+ # Finishes the span making it eligible to be exported and cleans up existing contexts
105
+ #
106
+ # @note does nothing if the span is a non-recording span
107
+ # @param [Rack::Request] The current HTTP request
108
+ # @param [Rack::Response] The current HTTP response
109
+ def on_finish(request, response)
110
+ span = OpenTelemetry::Instrumentation::Rack.current_span
111
+ return unless span.recording?
112
+
113
+ add_response_attributes(span, response) if response
114
+ rescue StandardError => e
115
+ OpenTelemetry.handle_error(exception: e)
116
+ ensure
117
+ detach_context(request)
118
+ end
119
+
120
+ private
121
+
122
+ def extract_request_headers(env)
123
+ return EMPTY_HASH if allowed_request_headers.empty?
124
+
125
+ allowed_request_headers.each_with_object({}) do |(key, value), result|
126
+ result[value] = env[key] if env.key?(key)
127
+ end
128
+ end
129
+
130
+ def extract_response_attributes(response)
131
+ attributes = { 'http.response.status_code' => response.status.to_i }
132
+ attributes.merge!(extract_response_headers(response.headers))
133
+ attributes
134
+ end
135
+
136
+ def extract_response_headers(headers)
137
+ return EMPTY_HASH if allowed_response_headers.empty?
138
+
139
+ allowed_response_headers.each_with_object({}) do |(key, value), result|
140
+ if headers.key?(key)
141
+ result[value] = headers[key]
142
+ else
143
+ # do case-insensitive match:
144
+ headers.each do |k, v|
145
+ if k.upcase == key
146
+ result[value] = v
147
+ break
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
153
+
154
+ def untraced_request?(env)
155
+ return true if untraced_endpoints.include?(env['PATH_INFO'])
156
+ return true if untraced_requests&.call(env)
157
+
158
+ false
159
+ end
160
+
161
+ # https://opentelemetry.io/docs/specs/semconv/http/http-spans/#name
162
+ #
163
+ # recommendation: span.name(s) should be low-cardinality (e.g.,
164
+ # strip off query param value, keep param name)
165
+ def create_request_span_name(request)
166
+ # NOTE: dd-trace-rb has implemented 'quantization' (which lowers url cardinality)
167
+ # see Datadog::Quantization::HTTP.url
168
+
169
+ if (implementation = url_quantization)
170
+ request_uri_or_path_info = request.env['REQUEST_URI'] || request.path_info
171
+ implementation.call(request_uri_or_path_info, request.env)
172
+ else
173
+ request.request_method.to_s
174
+ end
175
+ end
176
+
177
+ def extract_remote_context(request, context = Context.current)
178
+ OpenTelemetry.propagation.extract(
179
+ request.env,
180
+ context: context,
181
+ getter: OpenTelemetry::Common::Propagation.rack_env_getter
182
+ )
183
+ end
184
+
185
+ def request_span_attributes(env)
186
+ attributes = {
187
+ 'http.request.method' => env['REQUEST_METHOD'],
188
+ 'server.address' => env['HTTP_HOST'] || 'unknown',
189
+ 'url.scheme' => env['rack.url_scheme'],
190
+ 'url.path' => env['PATH_INFO']
191
+ }
192
+
193
+ attributes['url.query'] = env['QUERY_STRING'] unless env['QUERY_STRING'].empty?
194
+ attributes['user_agent.original'] = env['HTTP_USER_AGENT'] if env['HTTP_USER_AGENT']
195
+ attributes.merge!(extract_request_headers(env))
196
+ attributes
197
+ end
198
+
199
+ def detach_context(request)
200
+ return nil unless request.env[OTEL_TOKEN_AND_SPAN]
201
+
202
+ token, span = request.env[OTEL_TOKEN_AND_SPAN]
203
+ span.finish
204
+ OpenTelemetry::Context.detach(token)
205
+ rescue StandardError => e
206
+ OpenTelemetry.handle_error(exception: e)
207
+ end
208
+
209
+ def add_response_attributes(span, response)
210
+ span.status = OpenTelemetry::Trace::Status.error if response.server_error?
211
+ attributes = extract_response_attributes(response)
212
+ span.add_attributes(attributes)
213
+ rescue StandardError => e
214
+ OpenTelemetry.handle_error(exception: e)
215
+ end
216
+
217
+ def record_frontend_span?
218
+ config[:record_frontend_span] == true
219
+ end
220
+
221
+ def untraced_endpoints
222
+ config[:untraced_endpoints]
223
+ end
224
+
225
+ def untraced_requests
226
+ config[:untraced_requests]
227
+ end
228
+
229
+ def url_quantization
230
+ config[:url_quantization]
231
+ end
232
+
233
+ def response_propagators
234
+ config[:response_propagators]
235
+ end
236
+
237
+ def allowed_request_headers
238
+ config[:allowed_rack_request_headers]
239
+ end
240
+
241
+ def allowed_response_headers
242
+ config[:allowed_rack_response_headers]
243
+ end
244
+
245
+ def tracer
246
+ OpenTelemetry::Instrumentation::Rack::Instrumentation.instance.tracer
247
+ end
248
+
249
+ def config
250
+ OpenTelemetry::Instrumentation::Rack::Instrumentation.instance.config
251
+ end
252
+
253
+ def create_span(parent_context, request)
254
+ span = tracer.start_span(
255
+ create_request_span_name(request),
256
+ with_parent: parent_context,
257
+ kind: :server,
258
+ attributes: request_span_attributes(request.env)
259
+ )
260
+ request_start_time = OpenTelemetry::Instrumentation::Rack::Util::QueueTime.get_request_start(request.env)
261
+ span.add_event('http.proxy.request.started', timestamp: request_start_time) unless request_start_time.nil?
262
+ span
263
+ end
264
+ end
265
+ end
266
+ end
267
+ end
268
+ end
269
+ end
@@ -0,0 +1,206 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright The OpenTelemetry Authors
4
+ #
5
+ # SPDX-License-Identifier: Apache-2.0
6
+
7
+ require 'opentelemetry/trace/status'
8
+
9
+ module OpenTelemetry
10
+ module Instrumentation
11
+ module Rack
12
+ module Middlewares
13
+ module Stable
14
+ # TracerMiddleware propagates context and instruments Rack requests
15
+ # by way of its middleware system
16
+ class TracerMiddleware
17
+ class << self
18
+ def allowed_rack_request_headers
19
+ @allowed_rack_request_headers ||= Array(config[:allowed_request_headers]).each_with_object({}) do |header, memo|
20
+ key = header.to_s.upcase.gsub(/[-\s]/, '_')
21
+ case key
22
+ when 'CONTENT_TYPE', 'CONTENT_LENGTH'
23
+ memo[key] = build_attribute_name('http.request.header.', header)
24
+ else
25
+ memo["HTTP_#{key}"] = build_attribute_name('http.request.header.', header)
26
+ end
27
+ end
28
+ end
29
+
30
+ def allowed_response_headers
31
+ @allowed_response_headers ||= Array(config[:allowed_response_headers]).each_with_object({}) do |header, memo|
32
+ memo[header] = build_attribute_name('http.response.header.', header)
33
+ memo[header.to_s.upcase] = build_attribute_name('http.response.header.', header)
34
+ end
35
+ end
36
+
37
+ def build_attribute_name(prefix, suffix)
38
+ prefix + suffix.to_s.downcase.gsub(/[-\s]/, '_')
39
+ end
40
+
41
+ def config
42
+ Rack::Instrumentation.instance.config
43
+ end
44
+
45
+ private
46
+
47
+ def clear_cached_config
48
+ @allowed_rack_request_headers = nil
49
+ @allowed_response_headers = nil
50
+ end
51
+ end
52
+
53
+ EMPTY_HASH = {}.freeze
54
+
55
+ def initialize(app)
56
+ @app = app
57
+ @untraced_endpoints = config[:untraced_endpoints]
58
+ end
59
+
60
+ def call(env)
61
+ if untraced_request?(env)
62
+ OpenTelemetry::Common::Utilities.untraced do
63
+ return @app.call(env)
64
+ end
65
+ end
66
+
67
+ original_env = env.dup
68
+ extracted_context = OpenTelemetry.propagation.extract(
69
+ env,
70
+ getter: OpenTelemetry::Common::Propagation.rack_env_getter
71
+ )
72
+ frontend_context = create_frontend_span(env, extracted_context)
73
+
74
+ # restore extracted context in this process:
75
+ OpenTelemetry::Context.with_current(frontend_context || extracted_context) do
76
+ request_span_name = create_request_span_name(env['REQUEST_URI'] || original_env['PATH_INFO'], env)
77
+ request_span_kind = frontend_context.nil? ? :server : :internal
78
+ tracer.in_span(request_span_name,
79
+ attributes: request_span_attributes(env: env),
80
+ kind: request_span_kind) do |request_span|
81
+ request_start_time = OpenTelemetry::Instrumentation::Rack::Util::QueueTime.get_request_start(env)
82
+ request_span.add_event('http.proxy.request.started', timestamp: request_start_time) unless request_start_time.nil?
83
+ OpenTelemetry::Instrumentation::Rack.with_span(request_span) do
84
+ @app.call(env).tap do |status, headers, response|
85
+ set_attributes_after_request(request_span, status, headers, response)
86
+ config[:response_propagators].each { |propagator| propagator.inject(headers) }
87
+ end
88
+ end
89
+ end
90
+ end
91
+ ensure
92
+ finish_span(frontend_context)
93
+ end
94
+
95
+ private
96
+
97
+ def untraced_request?(env)
98
+ return true if @untraced_endpoints.include?(env['PATH_INFO'])
99
+ return true if config[:untraced_requests]&.call(env)
100
+
101
+ false
102
+ end
103
+
104
+ # return Context with the frontend span as the current span
105
+ def create_frontend_span(env, extracted_context)
106
+ request_start_time = OpenTelemetry::Instrumentation::Rack::Util::QueueTime.get_request_start(env)
107
+
108
+ return unless config[:record_frontend_span] && !request_start_time.nil?
109
+
110
+ span = tracer.start_span('http_server.proxy',
111
+ with_parent: extracted_context,
112
+ start_timestamp: request_start_time,
113
+ kind: :server)
114
+
115
+ OpenTelemetry::Trace.context_with_span(span, parent_context: extracted_context)
116
+ end
117
+
118
+ def finish_span(context)
119
+ OpenTelemetry::Trace.current_span(context).finish if context
120
+ end
121
+
122
+ def tracer
123
+ OpenTelemetry::Instrumentation::Rack::Instrumentation.instance.tracer
124
+ end
125
+
126
+ def request_span_attributes(env:)
127
+ attributes = {
128
+ 'http.request.method' => env['REQUEST_METHOD'],
129
+ 'server.address' => env['HTTP_HOST'] || 'unknown',
130
+ 'url.scheme' => env['rack.url_scheme'],
131
+ 'url.path' => env['PATH_INFO']
132
+ }
133
+
134
+ attributes['url.query'] = env['QUERY_STRING'] unless env['QUERY_STRING'].empty?
135
+ attributes['user_agent.original'] = env['HTTP_USER_AGENT'] if env['HTTP_USER_AGENT']
136
+ attributes.merge!(allowed_request_headers(env))
137
+ end
138
+
139
+ # https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/data-http.md#name
140
+ #
141
+ # recommendation: span.name(s) should be low-cardinality (e.g.,
142
+ # strip off query param value, keep param name)
143
+ #
144
+ # see http://github.com/open-telemetry/opentelemetry-specification/pull/416/files
145
+ def create_request_span_name(request_uri_or_path_info, env)
146
+ # NOTE: dd-trace-rb has implemented 'quantization' (which lowers url cardinality)
147
+ # see Datadog::Quantization::HTTP.url
148
+
149
+ if (implementation = config[:url_quantization])
150
+ implementation.call(request_uri_or_path_info, env)
151
+ else
152
+ env['REQUEST_METHOD']
153
+ end
154
+ end
155
+
156
+ def set_attributes_after_request(span, status, headers, _response)
157
+ span.status = OpenTelemetry::Trace::Status.error if (500..599).cover?(status.to_i)
158
+ span.set_attribute('http.response.status_code', status)
159
+
160
+ # NOTE: if data is available, it would be good to do this:
161
+ # set_attribute('http.route', ...
162
+ # e.g., "/users/:userID?
163
+
164
+ allowed_response_headers(headers).each { |k, v| span.set_attribute(k, v) }
165
+ end
166
+
167
+ def allowed_request_headers(env)
168
+ return EMPTY_HASH if self.class.allowed_rack_request_headers.empty?
169
+
170
+ {}.tap do |result|
171
+ self.class.allowed_rack_request_headers.each do |key, value|
172
+ result[value] = env[key] if env.key?(key)
173
+ end
174
+ end
175
+ end
176
+
177
+ def allowed_response_headers(headers)
178
+ return EMPTY_HASH if headers.nil?
179
+ return EMPTY_HASH if self.class.allowed_response_headers.empty?
180
+
181
+ {}.tap do |result|
182
+ self.class.allowed_response_headers.each do |key, value|
183
+ if headers.key?(key)
184
+ result[value] = headers[key]
185
+ else
186
+ # do case-insensitive match:
187
+ headers.each do |k, v|
188
+ if k.upcase == key
189
+ result[value] = v
190
+ break
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
196
+ end
197
+
198
+ def config
199
+ Rack::Instrumentation.instance.config
200
+ end
201
+ end
202
+ end
203
+ end
204
+ end
205
+ end
206
+ end
@@ -7,7 +7,7 @@
7
7
  module OpenTelemetry
8
8
  module Instrumentation
9
9
  module Rack
10
- VERSION = '0.26.0'
10
+ VERSION = '0.27.0'
11
11
  end
12
12
  end
13
13
  end