Skip to content

Commit 5dc0c52

Browse files
HoneyryderChuckollymarielvalentin
authored andcommitted
fix: httpx instrumentation trace context propagation (open-telemetry#1456)
* fix: Follow latest datadog plugin fixes the trace context propagation * chore: do not test anymore against httpx 0.x * chore: correct error description --------- Co-authored-by: Oliver Morgan <[email protected]> Co-authored-by: Ariel Valentin <[email protected]>
1 parent 5a5114e commit 5dc0c52

File tree

3 files changed

+76
-49
lines changed

3 files changed

+76
-49
lines changed

instrumentation/httpx/Appraisals

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,3 @@
77
appraise 'httpx-1' do
88
gem 'httpx', '~> 1.0'
99
end
10-
11-
appraise 'httpx-0' do
12-
gem 'httpx', '~> 0.24'
13-
end

instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/plugin.rb

Lines changed: 75 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -10,83 +10,114 @@ module HTTPX
1010
module Plugin
1111
# Instruments around HTTPX's request/response lifecycle in order to generate
1212
# an OTEL trace.
13-
class RequestTracer
14-
# Constant for the HTTP status range
15-
HTTP_STATUS_SUCCESS_RANGE = (100..399)
13+
module RequestTracer
14+
module_function
15+
16+
# initializes tracing on the +request+.
17+
def call(request)
18+
span = nil
19+
20+
# request objects are reused, when already buffered requests get rerouted to a different
21+
# connection due to connection issues, or when they already got a response, but need to
22+
# be retried. In such situations, the original span needs to be extended for the former,
23+
# while a new is required for the latter.
24+
request.on(:idle) do
25+
span = nil
26+
end
27+
# the span is initialized when the request is buffered in the parser, which is the closest
28+
# one gets to actually sending the request.
29+
request.on(:headers) do
30+
next if span
31+
32+
span = initialize_span(request)
33+
end
34+
35+
request.on(:response) do |response|
36+
unless span
37+
next unless response.is_a?(::HTTPX::ErrorResponse) && response.error.respond_to?(:connection)
1638

17-
def initialize(request)
18-
@request = request
39+
# handles the case when the +error+ happened during name resolution, which means
40+
# that the tracing start point hasn't been triggered yet; in such cases, the approximate
41+
# initial resolving time is collected from the connection, and used as span start time,
42+
# and the tracing object in inserted before the on response callback is called.
43+
span = initialize_span(request, response.error.connection.init_time)
44+
45+
end
46+
47+
finish(response, span)
48+
end
1949
end
2050

21-
def call
22-
@request.on(:response, &method(:finish)) # rubocop:disable Performance/MethodObjectAsBlock
51+
def finish(response, span)
52+
if response.is_a?(::HTTPX::ErrorResponse)
53+
span.record_exception(response.error)
54+
span.status = Trace::Status.error(response.error.to_s)
55+
else
56+
span.set_attribute(OpenTelemetry::SemanticConventions::Trace::HTTP_STATUS_CODE, response.status)
57+
58+
if response.status.between?(400, 599)
59+
err = ::HTTPX::HTTPError.new(response)
60+
span.record_exception(err)
61+
span.status = Trace::Status.error(err.to_s)
62+
end
63+
end
2364

24-
uri = @request.uri
25-
request_method = @request.verb
26-
span_name = "HTTP #{request_method}"
65+
span.finish
66+
end
67+
68+
# return a span initialized with the +@request+ state.
69+
def initialize_span(request, start_time = ::Time.now)
70+
verb = request.verb
71+
uri = request.uri
72+
73+
config = HTTPX::Instrumentation.instance.config
2774

2875
attributes = {
2976
OpenTelemetry::SemanticConventions::Trace::HTTP_HOST => uri.host,
30-
OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => request_method,
77+
OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => verb,
3178
OpenTelemetry::SemanticConventions::Trace::HTTP_SCHEME => uri.scheme,
3279
OpenTelemetry::SemanticConventions::Trace::HTTP_TARGET => uri.path,
3380
OpenTelemetry::SemanticConventions::Trace::HTTP_URL => "#{uri.scheme}://#{uri.host}",
3481
OpenTelemetry::SemanticConventions::Trace::NET_PEER_NAME => uri.host,
3582
OpenTelemetry::SemanticConventions::Trace::NET_PEER_PORT => uri.port
3683
}
37-
config = HTTPX::Instrumentation.instance.config
38-
attributes[OpenTelemetry::SemanticConventions::Trace::PEER_SERVICE] = config[:peer_service] if config[:peer_service]
39-
attributes.merge!(
40-
OpenTelemetry::Common::HTTP::ClientContext.attributes
41-
)
4284

43-
@span = tracer.start_span(span_name, attributes: attributes, kind: :client)
44-
trace_ctx = OpenTelemetry::Trace.context_with_span(@span)
45-
@trace_token = OpenTelemetry::Context.attach(trace_ctx)
46-
47-
OpenTelemetry.propagation.inject(@request.headers)
48-
rescue StandardError => e
49-
OpenTelemetry.handle_error(exception: e)
50-
end
85+
attributes[OpenTelemetry::SemanticConventions::Trace::PEER_SERVICE] = config[:peer_service] if config[:peer_service]
86+
attributes.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes)
5187

52-
def finish(response)
53-
return unless @span
88+
span = tracer.start_span("HTTP #{verb}", attributes: attributes, kind: :client, start_timestamp: start_time)
5489

55-
if response.is_a?(::HTTPX::ErrorResponse)
56-
@span.record_exception(response.error)
57-
@span.status = Trace::Status.error("Unhandled exception of type: #{response.error.class}")
58-
else
59-
@span.set_attribute(OpenTelemetry::SemanticConventions::Trace::HTTP_STATUS_CODE, response.status)
60-
@span.status = Trace::Status.error unless HTTP_STATUS_SUCCESS_RANGE.cover?(response.status)
90+
OpenTelemetry::Trace.with_span(span) do
91+
OpenTelemetry.propagation.inject(request.headers)
6192
end
6293

63-
OpenTelemetry::Context.detach(@trace_token) if @trace_token
64-
@span.finish
94+
span
95+
rescue StandardError => e
96+
OpenTelemetry.handle_error(exception: e)
6597
end
6698

67-
private
68-
6999
def tracer
70100
HTTPX::Instrumentation.instance.tracer
71101
end
72102
end
73103

74-
# HTTPX::Request overrides
104+
# Request patch to initiate the trace on initialization.
75105
module RequestMethods
76-
def __otel_enable_trace!
77-
return if @__otel_enable_trace
106+
def initialize(*)
107+
super
78108

79-
RequestTracer.new(self).call
80-
@__otel_enable_trace = true
109+
RequestTracer.call(self)
81110
end
82111
end
83112

84-
# HTTPX::Connection overrides
113+
# Connection patch to start monitoring on initialization.
85114
module ConnectionMethods
86-
def send(request)
87-
request.__otel_enable_trace!
115+
attr_reader :init_time
88116

117+
def initialize(*)
89118
super
119+
120+
@init_time = ::Time.now
90121
end
91122
end
92123
end

instrumentation/httpx/test/instrumentation/plugin_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@
8282
OpenTelemetry::Trace::Status::ERROR
8383
)
8484
_(span.status.description).must_equal(
85-
'Unhandled exception of type: HTTPX::TimeoutError'
85+
'Timed out'
8686
)
8787
assert_requested(
8888
:get,

0 commit comments

Comments
 (0)