Skip to content

Commit 4903823

Browse files
committed
feat: excon HTTP semantic convention stability migration
1 parent 51f6328 commit 4903823

File tree

14 files changed

+1349
-186
lines changed

14 files changed

+1349
-186
lines changed

instrumentation/excon/Appraisals

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,21 @@
22

33
# add more tests for excon
44

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

11-
appraise 'excon-latest' do
12-
gem 'excon'
19+
appraise "excon-latest-#{mode}" do
20+
gem 'excon'
21+
end
1322
end

instrumentation/excon/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,20 @@ The `opentelemetry-instrumentation-all` gem is distributed under the Apache 2.0
4848
[community-meetings]: https://github.com/open-telemetry/community#community-meetings
4949
[slack-channel]: https://cloud-native.slack.com/archives/C01NWKKMKMY
5050
[discussions-url]: https://github.com/open-telemetry/opentelemetry-ruby/discussions
51+
52+
53+
## HTTP semantic convention stability
54+
55+
In the OpenTelemetry ecosystem, HTTP semantic conventions have now reached a stable state. However, the initial Excon instrumentation was introduced before this stability was achieved, which resulted in HTTP attributes being based on an older version of the semantic conventions.
56+
57+
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.
58+
59+
When setting the value for `OTEL_SEMCONV_STABILITY_OPT_IN`, you can specify which conventions you wish to adopt:
60+
61+
- `http` - Emits the stable HTTP and networking conventions and ceases emitting the old conventions previously emitted by the instrumentation.
62+
- `http/dup` - Emits both the old and stable HTTP and networking conventions, enabling a phased rollout of the stable semantic conventions.
63+
- Default behavior (in the absence of either value) is to continue emitting the old HTTP and networking conventions the instrumentation previously emitted.
64+
65+
During the transition from old to stable conventions, Excon instrumentation code comes in three patch versions: `dup`, `old`, and `stable`. These versions are identical except for the attributes they send. Any changes to Excon instrumentation should consider all three patches.
66+
67+
For additional information on migration, please refer to our [documentation](https://opentelemetry.io/docs/specs/semconv/non-normative/http-migration/).

instrumentation/excon/lib/opentelemetry/instrumentation/excon/instrumentation.rb

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ class Instrumentation < OpenTelemetry::Instrumentation::Base
1515
include OpenTelemetry::Instrumentation::Concerns::UntracedHosts
1616

1717
install do |_config|
18-
require_dependencies
19-
add_middleware
20-
patch
18+
patch_type = determine_semconv
19+
send(:"require_dependencies_#{patch_type}")
20+
send(:"add_middleware_#{patch_type}")
21+
send(:"patch_#{patch_type}")
2122
end
2223

2324
present do
@@ -28,17 +29,56 @@ class Instrumentation < OpenTelemetry::Instrumentation::Base
2829

2930
private
3031

31-
def require_dependencies
32-
require_relative 'middlewares/tracer_middleware'
33-
require_relative 'patches/socket'
32+
def determine_semconv
33+
stability_opt_in = ENV.fetch('OTEL_SEMCONV_STABILITY_OPT_IN', '')
34+
values = stability_opt_in.split(',').map(&:strip)
35+
36+
if values.include?('http/dup')
37+
'dup'
38+
elsif values.include?('http')
39+
'stable'
40+
else
41+
'old'
42+
end
43+
end
44+
45+
def require_dependencies_dup
46+
require_relative 'middlewares/dup/tracer_middleware'
47+
require_relative 'patches/dup/socket'
48+
end
49+
50+
def require_dependencies_stable
51+
require_relative 'middlewares/stable/tracer_middleware'
52+
require_relative 'patches/stable/socket'
53+
end
54+
55+
def require_dependencies_old
56+
require_relative 'middlewares/old/tracer_middleware'
57+
require_relative 'patches/old/socket'
58+
end
59+
60+
def add_middleware_dup
61+
::Excon.defaults[:middlewares] = Middlewares::Dup::TracerMiddleware.around_default_stack
62+
end
63+
64+
def add_middleware_stable
65+
::Excon.defaults[:middlewares] = Middlewares::Stable::TracerMiddleware.around_default_stack
66+
end
67+
68+
def add_middleware_old
69+
::Excon.defaults[:middlewares] = Middlewares::Old::TracerMiddleware.around_default_stack
70+
end
71+
72+
def patch_dup
73+
::Excon::Socket.prepend(Patches::Dup::Socket)
3474
end
3575

36-
def add_middleware
37-
::Excon.defaults[:middlewares] = Middlewares::TracerMiddleware.around_default_stack
76+
def patch_stable
77+
::Excon::Socket.prepend(Patches::Stable::Socket)
3878
end
3979

40-
def patch
41-
::Excon::Socket.prepend(Patches::Socket)
80+
def patch_old
81+
::Excon::Socket.prepend(Patches::Old::Socket)
4282
end
4383
end
4484
end
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
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 Excon
10+
module Middlewares
11+
module Dup
12+
# Excon middleware for instrumentation
13+
class TracerMiddleware < ::Excon::Middleware::Base
14+
HTTP_METHODS_TO_UPPERCASE = %w[connect delete get head options patch post put trace].each_with_object({}) do |method, hash|
15+
uppercase_method = method.upcase
16+
hash[method] = uppercase_method
17+
hash[method.to_sym] = uppercase_method
18+
hash[uppercase_method] = uppercase_method
19+
end.freeze
20+
21+
HTTP_METHODS_TO_SPAN_NAMES = HTTP_METHODS_TO_UPPERCASE.values.each_with_object({}) do |uppercase_method, hash|
22+
hash[uppercase_method] ||= "HTTP #{uppercase_method}"
23+
end.freeze
24+
25+
# Constant for the HTTP status range
26+
HTTP_STATUS_SUCCESS_RANGE = (100..399)
27+
28+
def request_call(datum)
29+
return @stack.request_call(datum) if untraced?(datum)
30+
31+
http_method = HTTP_METHODS_TO_UPPERCASE[datum[:method]]
32+
attributes = {
33+
OpenTelemetry::SemanticConventions::Trace::HTTP_HOST => datum[:host],
34+
OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => http_method,
35+
OpenTelemetry::SemanticConventions::Trace::HTTP_SCHEME => datum[:scheme],
36+
OpenTelemetry::SemanticConventions::Trace::HTTP_TARGET => datum[:path],
37+
OpenTelemetry::SemanticConventions::Trace::HTTP_URL => OpenTelemetry::Common::Utilities.cleanse_url(::Excon::Utils.request_uri(datum)),
38+
OpenTelemetry::SemanticConventions::Trace::NET_PEER_NAME => datum[:hostname],
39+
OpenTelemetry::SemanticConventions::Trace::NET_PEER_PORT => datum[:port],
40+
'http.request.method' => http_method,
41+
'url.scheme' => datum[:scheme],
42+
'url.path' => datum[:path],
43+
'url.full' => OpenTelemetry::Common::Utilities.cleanse_url(::Excon::Utils.request_uri(datum)),
44+
'server.address' => datum[:hostname],
45+
'server.port' => datum[:port]
46+
}
47+
attributes['url.query'] = datum[:query] if datum[:query]
48+
peer_service = Excon::Instrumentation.instance.config[:peer_service]
49+
attributes[OpenTelemetry::SemanticConventions::Trace::PEER_SERVICE] = peer_service if peer_service
50+
attributes.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes)
51+
span = tracer.start_span(HTTP_METHODS_TO_SPAN_NAMES[http_method], attributes: attributes, kind: :client)
52+
ctx = OpenTelemetry::Trace.context_with_span(span)
53+
datum[:otel_span] = span
54+
datum[:otel_token] = OpenTelemetry::Context.attach(ctx)
55+
OpenTelemetry.propagation.inject(datum[:headers])
56+
@stack.request_call(datum)
57+
end
58+
59+
def response_call(datum)
60+
@stack.response_call(datum).tap do |d|
61+
handle_response(d)
62+
end
63+
end
64+
65+
def error_call(datum)
66+
handle_response(datum)
67+
@stack.error_call(datum)
68+
end
69+
70+
# Returns a copy of the default stack with the trace middleware injected
71+
def self.around_default_stack
72+
::Excon.defaults[:middlewares].dup.tap do |default_stack|
73+
# If the default stack contains a version of the trace middleware already...
74+
existing_trace_middleware = default_stack.find { |m| m <= TracerMiddleware }
75+
default_stack.delete(existing_trace_middleware) if existing_trace_middleware
76+
# Inject after the ResponseParser middleware
77+
response_middleware_index = default_stack.index(::Excon::Middleware::ResponseParser).to_i
78+
default_stack.insert(response_middleware_index + 1, self)
79+
end
80+
end
81+
82+
private
83+
84+
def handle_response(datum)
85+
datum.delete(:otel_span)&.tap do |span|
86+
return unless span.recording?
87+
88+
if datum.key?(:response)
89+
response = datum[:response]
90+
span.set_attribute(OpenTelemetry::SemanticConventions::Trace::HTTP_STATUS_CODE, response[:status])
91+
span.set_attribute('http.response.status_code', response[:status])
92+
span.status = OpenTelemetry::Trace::Status.error unless HTTP_STATUS_SUCCESS_RANGE.cover?(response[:status].to_i)
93+
end
94+
95+
if datum.key?(:error)
96+
span.status = OpenTelemetry::Trace::Status.error('Request has failed')
97+
span.record_exception(datum[:error])
98+
end
99+
100+
span.finish
101+
OpenTelemetry::Context.detach(datum.delete(:otel_token)) if datum.include?(:otel_token)
102+
end
103+
rescue StandardError => e
104+
OpenTelemetry.handle_error(e)
105+
end
106+
107+
def tracer
108+
Excon::Instrumentation.instance.tracer
109+
end
110+
111+
def untraced?(datum)
112+
datum.key?(:otel_span) || Excon::Instrumentation.instance.untraced?(datum[:host])
113+
end
114+
end
115+
end
116+
end
117+
end
118+
end
119+
end
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
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 Excon
10+
module Middlewares
11+
module Old
12+
# Excon middleware for instrumentation
13+
class TracerMiddleware < ::Excon::Middleware::Base
14+
HTTP_METHODS_TO_UPPERCASE = %w[connect delete get head options patch post put trace].each_with_object({}) do |method, hash|
15+
uppercase_method = method.upcase
16+
hash[method] = uppercase_method
17+
hash[method.to_sym] = uppercase_method
18+
hash[uppercase_method] = uppercase_method
19+
end.freeze
20+
21+
HTTP_METHODS_TO_SPAN_NAMES = HTTP_METHODS_TO_UPPERCASE.values.each_with_object({}) do |uppercase_method, hash|
22+
hash[uppercase_method] ||= "HTTP #{uppercase_method}"
23+
end.freeze
24+
25+
# Constant for the HTTP status range
26+
HTTP_STATUS_SUCCESS_RANGE = (100..399)
27+
28+
def request_call(datum)
29+
return @stack.request_call(datum) if untraced?(datum)
30+
31+
http_method = HTTP_METHODS_TO_UPPERCASE[datum[:method]]
32+
attributes = {
33+
OpenTelemetry::SemanticConventions::Trace::HTTP_HOST => datum[:host],
34+
OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => http_method,
35+
OpenTelemetry::SemanticConventions::Trace::HTTP_SCHEME => datum[:scheme],
36+
OpenTelemetry::SemanticConventions::Trace::HTTP_TARGET => datum[:path],
37+
OpenTelemetry::SemanticConventions::Trace::HTTP_URL => OpenTelemetry::Common::Utilities.cleanse_url(::Excon::Utils.request_uri(datum)),
38+
OpenTelemetry::SemanticConventions::Trace::NET_PEER_NAME => datum[:hostname],
39+
OpenTelemetry::SemanticConventions::Trace::NET_PEER_PORT => datum[:port]
40+
}
41+
peer_service = Excon::Instrumentation.instance.config[:peer_service]
42+
attributes[OpenTelemetry::SemanticConventions::Trace::PEER_SERVICE] = peer_service if peer_service
43+
attributes.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes)
44+
span = tracer.start_span(HTTP_METHODS_TO_SPAN_NAMES[http_method], attributes: attributes, kind: :client)
45+
ctx = OpenTelemetry::Trace.context_with_span(span)
46+
datum[:otel_span] = span
47+
datum[:otel_token] = OpenTelemetry::Context.attach(ctx)
48+
OpenTelemetry.propagation.inject(datum[:headers])
49+
@stack.request_call(datum)
50+
end
51+
52+
def response_call(datum)
53+
@stack.response_call(datum).tap do |d|
54+
handle_response(d)
55+
end
56+
end
57+
58+
def error_call(datum)
59+
handle_response(datum)
60+
@stack.error_call(datum)
61+
end
62+
63+
# Returns a copy of the default stack with the trace middleware injected
64+
def self.around_default_stack
65+
::Excon.defaults[:middlewares].dup.tap do |default_stack|
66+
# If the default stack contains a version of the trace middleware already...
67+
existing_trace_middleware = default_stack.find { |m| m <= TracerMiddleware }
68+
default_stack.delete(existing_trace_middleware) if existing_trace_middleware
69+
# Inject after the ResponseParser middleware
70+
response_middleware_index = default_stack.index(::Excon::Middleware::ResponseParser).to_i
71+
default_stack.insert(response_middleware_index + 1, self)
72+
end
73+
end
74+
75+
private
76+
77+
def handle_response(datum)
78+
datum.delete(:otel_span)&.tap do |span|
79+
return unless span.recording?
80+
81+
if datum.key?(:response)
82+
response = datum[:response]
83+
span.set_attribute(OpenTelemetry::SemanticConventions::Trace::HTTP_STATUS_CODE, response[:status])
84+
span.status = OpenTelemetry::Trace::Status.error unless HTTP_STATUS_SUCCESS_RANGE.cover?(response[:status].to_i)
85+
end
86+
87+
if datum.key?(:error)
88+
span.status = OpenTelemetry::Trace::Status.error('Request has failed')
89+
span.record_exception(datum[:error])
90+
end
91+
92+
span.finish
93+
OpenTelemetry::Context.detach(datum.delete(:otel_token)) if datum.include?(:otel_token)
94+
end
95+
rescue StandardError => e
96+
OpenTelemetry.handle_error(e)
97+
end
98+
99+
def tracer
100+
Excon::Instrumentation.instance.tracer
101+
end
102+
103+
def untraced?(datum)
104+
datum.key?(:otel_span) || Excon::Instrumentation.instance.untraced?(datum[:host])
105+
end
106+
end
107+
end
108+
end
109+
end
110+
end
111+
end

0 commit comments

Comments
 (0)