@@ -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
0 commit comments