Skip to content

Commit 07de1d6

Browse files
feat: HTTPX semantic convention stability opt in (#1589)
* feat: semantic convention stability opt in * fix: appease rubocop whitespace error * Fix: remove redundant .to_s and deprecated http.host --------- Co-authored-by: Kayla Reopelle <[email protected]>
1 parent 975e5ee commit 07de1d6

File tree

11 files changed

+771
-137
lines changed

11 files changed

+771
-137
lines changed

instrumentation/httpx/Appraisals

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@
44
#
55
# SPDX-License-Identifier: Apache-2.0
66

7-
appraise 'httpx-1' do
8-
gem 'httpx', '~> 1.0'
7+
# To faclitate HTTP semantic convention stability migration, we are using
8+
# appraisal to test the different semantic convention modes along with different
9+
# gem versions. For more information on the semantic convention modes, see:
10+
# https://opentelemetry.io/docs/specs/semconv/non-normative/http-migration/
11+
12+
semconv_stability = %w[dup stable old]
13+
14+
semconv_stability.each do |mode|
15+
appraise "httpx-1-#{mode}" do
16+
gem 'httpx', '~> 1.0'
17+
end
918
end

instrumentation/httpx/README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,19 @@ The `opentelemetry-instrumentation-httpx` gem is distributed under the Apache 2.
4848
[slack-channel]: https://cloud-native.slack.com/archives/C01NWKKMKMY
4949
[discussions-url]: https://github.com/open-telemetry/opentelemetry-ruby/discussions
5050
[httpx-home]: https://github.com/HoneyryderChuck/httpx
51+
52+
## HTTP semantic convention stability
53+
54+
In the OpenTelemetry ecosystem, HTTP semantic conventions have now reached a stable state. However, the initial HTTPX instrumentation was introduced before this stability was achieved, which resulted in HTTP attributes being based on an older version of the semantic conventions.
55+
56+
To facilitate the migration to stable semantic conventions, you can use the `OTEL_SEMCONV_STABILITY_OPT_IN` environment variable. This variable allows you to opt-in to the new stable conventions, ensuring compatibility and future-proofing your instrumentation.
57+
58+
When setting the value for `OTEL_SEMCONV_STABILITY_OPT_IN`, you can specify which conventions you wish to adopt:
59+
60+
- `http` - Emits the stable HTTP and networking conventions and ceases emitting the old conventions previously emitted by the instrumentation.
61+
- `http/dup` - Emits both the old and stable HTTP and networking conventions, enabling a phased rollout of the stable semantic conventions.
62+
- Default behavior (in the absence of either value) is to continue emitting the old HTTP and networking conventions the instrumentation previously emitted.
63+
64+
During the transition from old to stable conventions, HTTPX instrumentation code comes in three patch versions: `dup`, `old`, and `stable`. These versions are identical except for the attributes they send. Any changes to HTTPX instrumentation should consider all three patches.
65+
66+
For additional information on migration, please refer to our [documentation](https://opentelemetry.io/docs/specs/semconv/non-normative/http-migration/).
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
# frozen_string_literal: true
2+
3+
# Copyright The OpenTelemetry Authors
4+
#
5+
# SPDX-License-Identifier: Apache-2.0
6+
7+
module OpenTelemetry
8+
module Instrumentation
9+
module HTTPX
10+
module Dup
11+
module Plugin
12+
# Instruments around HTTPX's request/response lifecycle in order to generate
13+
# an OTEL trace.
14+
module RequestTracer
15+
module_function
16+
17+
# initializes tracing on the +request+.
18+
def call(request)
19+
span = nil
20+
21+
# request objects are reused, when already buffered requests get rerouted to a different
22+
# connection due to connection issues, or when they already got a response, but need to
23+
# be retried. In such situations, the original span needs to be extended for the former,
24+
# while a new is required for the latter.
25+
request.on(:idle) do
26+
span = nil
27+
end
28+
# the span is initialized when the request is buffered in the parser, which is the closest
29+
# one gets to actually sending the request.
30+
request.on(:headers) do
31+
next if span
32+
33+
span = initialize_span(request)
34+
end
35+
36+
request.on(:response) do |response|
37+
unless span
38+
next unless response.is_a?(::HTTPX::ErrorResponse) && response.error.respond_to?(:connection)
39+
40+
# handles the case when the +error+ happened during name resolution, which means
41+
# that the tracing start point hasn't been triggered yet; in such cases, the approximate
42+
# initial resolving time is collected from the connection, and used as span start time,
43+
# and the tracing object in inserted before the on response callback is called.
44+
span = initialize_span(request, response.error.connection.init_time)
45+
46+
end
47+
48+
finish(response, span)
49+
end
50+
end
51+
52+
def finish(response, span)
53+
if response.is_a?(::HTTPX::ErrorResponse)
54+
span.record_exception(response.error)
55+
span.status = Trace::Status.error(response.error.to_s)
56+
else
57+
span.set_attribute(OpenTelemetry::SemanticConventions::Trace::HTTP_STATUS_CODE, response.status)
58+
span.set_attribute('http.response.status_code', response.status)
59+
60+
if response.status.between?(400, 599)
61+
err = ::HTTPX::HTTPError.new(response)
62+
span.record_exception(err)
63+
span.status = Trace::Status.error(err.to_s)
64+
end
65+
end
66+
67+
span.finish
68+
end
69+
70+
# return a span initialized with the +@request+ state.
71+
def initialize_span(request, start_time = ::Time.now)
72+
verb = request.verb
73+
uri = request.uri
74+
75+
config = HTTPX::Instrumentation.instance.config
76+
77+
attributes = {
78+
OpenTelemetry::SemanticConventions::Trace::HTTP_HOST => uri.host,
79+
OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => verb,
80+
OpenTelemetry::SemanticConventions::Trace::HTTP_SCHEME => uri.scheme,
81+
OpenTelemetry::SemanticConventions::Trace::HTTP_TARGET => uri.path,
82+
OpenTelemetry::SemanticConventions::Trace::HTTP_URL => "#{uri.scheme}://#{uri.host}",
83+
OpenTelemetry::SemanticConventions::Trace::NET_PEER_NAME => uri.host,
84+
OpenTelemetry::SemanticConventions::Trace::NET_PEER_PORT => uri.port,
85+
'http.request.method' => verb,
86+
'url.scheme' => uri.scheme,
87+
'url.path' => uri.path,
88+
'url.full' => "#{uri.scheme}://#{uri.host}",
89+
'server.address' => uri.host,
90+
'server.port' => uri.port
91+
}
92+
93+
attributes['url.query'] = uri.query unless uri.query.nil?
94+
attributes[OpenTelemetry::SemanticConventions::Trace::PEER_SERVICE] = config[:peer_service] if config[:peer_service]
95+
attributes.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes)
96+
97+
span = tracer.start_span(verb, attributes: attributes, kind: :client, start_timestamp: start_time)
98+
99+
OpenTelemetry::Trace.with_span(span) do
100+
OpenTelemetry.propagation.inject(request.headers)
101+
end
102+
103+
span
104+
rescue StandardError => e
105+
OpenTelemetry.handle_error(exception: e)
106+
end
107+
108+
def tracer
109+
HTTPX::Instrumentation.instance.tracer
110+
end
111+
end
112+
113+
# Request patch to initiate the trace on initialization.
114+
module RequestMethods
115+
def initialize(*)
116+
super
117+
118+
RequestTracer.call(self)
119+
end
120+
end
121+
122+
# Connection patch to start monitoring on initialization.
123+
module ConnectionMethods
124+
attr_reader :init_time
125+
126+
def initialize(*)
127+
super
128+
129+
@init_time = ::Time.now
130+
end
131+
end
132+
end
133+
end
134+
end
135+
end
136+
end

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

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ module HTTPX
1010
# The Instrumentation class contains logic to detect and install the Http instrumentation
1111
class Instrumentation < OpenTelemetry::Instrumentation::Base
1212
install do |_config|
13-
require_dependencies
14-
patch
13+
patch_type = determine_semconv
14+
send(:"require_dependencies_#{patch_type}")
15+
send(:"patch_#{patch_type}")
1516
end
1617

1718
compatible do
@@ -24,15 +25,50 @@ class Instrumentation < OpenTelemetry::Instrumentation::Base
2425

2526
option :peer_service, default: nil, validate: :string
2627

27-
def patch
28-
otel_session = ::HTTPX.plugin(Plugin)
28+
def determine_semconv
29+
stability_opt_in = ENV.fetch('OTEL_SEMCONV_STABILITY_OPT_IN', '')
30+
values = stability_opt_in.split(',').map(&:strip)
31+
32+
if values.include?('http/dup')
33+
'dup'
34+
elsif values.include?('http')
35+
'stable'
36+
else
37+
'old'
38+
end
39+
end
40+
41+
def patch_old
42+
otel_session = ::HTTPX.plugin(Old::Plugin)
2943

3044
::HTTPX.send(:remove_const, :Session)
3145
::HTTPX.send(:const_set, :Session, otel_session.class)
3246
end
3347

34-
def require_dependencies
35-
require_relative 'plugin'
48+
def patch_stable
49+
otel_session = ::HTTPX.plugin(Stable::Plugin)
50+
51+
::HTTPX.send(:remove_const, :Session)
52+
::HTTPX.send(:const_set, :Session, otel_session.class)
53+
end
54+
55+
def patch_dup
56+
otel_session = ::HTTPX.plugin(Dup::Plugin)
57+
58+
::HTTPX.send(:remove_const, :Session)
59+
::HTTPX.send(:const_set, :Session, otel_session.class)
60+
end
61+
62+
def require_dependencies_old
63+
require_relative 'old/plugin'
64+
end
65+
66+
def require_dependencies_stable
67+
require_relative 'stable/plugin'
68+
end
69+
70+
def require_dependencies_dup
71+
require_relative 'dup/plugin'
3672
end
3773
end
3874
end
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# frozen_string_literal: true
2+
3+
# Copyright The OpenTelemetry Authors
4+
#
5+
# SPDX-License-Identifier: Apache-2.0
6+
7+
module OpenTelemetry
8+
module Instrumentation
9+
module HTTPX
10+
module Old
11+
module Plugin
12+
# Instruments around HTTPX's request/response lifecycle in order to generate
13+
# an OTEL trace.
14+
module RequestTracer
15+
module_function
16+
17+
# initializes tracing on the +request+.
18+
def call(request)
19+
span = nil
20+
21+
# request objects are reused, when already buffered requests get rerouted to a different
22+
# connection due to connection issues, or when they already got a response, but need to
23+
# be retried. In such situations, the original span needs to be extended for the former,
24+
# while a new is required for the latter.
25+
request.on(:idle) do
26+
span = nil
27+
end
28+
# the span is initialized when the request is buffered in the parser, which is the closest
29+
# one gets to actually sending the request.
30+
request.on(:headers) do
31+
next if span
32+
33+
span = initialize_span(request)
34+
end
35+
36+
request.on(:response) do |response|
37+
unless span
38+
next unless response.is_a?(::HTTPX::ErrorResponse) && response.error.respond_to?(:connection)
39+
40+
# handles the case when the +error+ happened during name resolution, which means
41+
# that the tracing start point hasn't been triggered yet; in such cases, the approximate
42+
# initial resolving time is collected from the connection, and used as span start time,
43+
# and the tracing object in inserted before the on response callback is called.
44+
span = initialize_span(request, response.error.connection.init_time)
45+
46+
end
47+
48+
finish(response, span)
49+
end
50+
end
51+
52+
def finish(response, span)
53+
if response.is_a?(::HTTPX::ErrorResponse)
54+
span.record_exception(response.error)
55+
span.status = Trace::Status.error(response.error.to_s)
56+
else
57+
span.set_attribute(OpenTelemetry::SemanticConventions::Trace::HTTP_STATUS_CODE, response.status)
58+
59+
if response.status.between?(400, 599)
60+
err = ::HTTPX::HTTPError.new(response)
61+
span.record_exception(err)
62+
span.status = Trace::Status.error(err.to_s)
63+
end
64+
end
65+
66+
span.finish
67+
end
68+
69+
# return a span initialized with the +@request+ state.
70+
def initialize_span(request, start_time = ::Time.now)
71+
verb = request.verb
72+
uri = request.uri
73+
74+
config = HTTPX::Instrumentation.instance.config
75+
76+
attributes = {
77+
OpenTelemetry::SemanticConventions::Trace::HTTP_HOST => uri.host,
78+
OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => verb,
79+
OpenTelemetry::SemanticConventions::Trace::HTTP_SCHEME => uri.scheme,
80+
OpenTelemetry::SemanticConventions::Trace::HTTP_TARGET => uri.path,
81+
OpenTelemetry::SemanticConventions::Trace::HTTP_URL => "#{uri.scheme}://#{uri.host}",
82+
OpenTelemetry::SemanticConventions::Trace::NET_PEER_NAME => uri.host,
83+
OpenTelemetry::SemanticConventions::Trace::NET_PEER_PORT => uri.port
84+
}
85+
86+
attributes[OpenTelemetry::SemanticConventions::Trace::PEER_SERVICE] = config[:peer_service] if config[:peer_service]
87+
attributes.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes)
88+
89+
span = tracer.start_span("HTTP #{verb}", attributes: attributes, kind: :client, start_timestamp: start_time)
90+
91+
OpenTelemetry::Trace.with_span(span) do
92+
OpenTelemetry.propagation.inject(request.headers)
93+
end
94+
95+
span
96+
rescue StandardError => e
97+
OpenTelemetry.handle_error(exception: e)
98+
end
99+
100+
def tracer
101+
HTTPX::Instrumentation.instance.tracer
102+
end
103+
end
104+
105+
# Request patch to initiate the trace on initialization.
106+
module RequestMethods
107+
def initialize(*)
108+
super
109+
110+
RequestTracer.call(self)
111+
end
112+
end
113+
114+
# Connection patch to start monitoring on initialization.
115+
module ConnectionMethods
116+
attr_reader :init_time
117+
118+
def initialize(*)
119+
super
120+
121+
@init_time = ::Time.now
122+
end
123+
end
124+
end
125+
end
126+
end
127+
end
128+
end

0 commit comments

Comments
 (0)