Skip to content

Commit 9ef18b3

Browse files
committed
feat: HTTP semantic convention stability migration
1 parent 51f6328 commit 9ef18b3

File tree

10 files changed

+1069
-140
lines changed

10 files changed

+1069
-140
lines changed

instrumentation/ethon/Appraisals

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
# frozen_string_literal: true
22

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

7-
appraise 'ethon-latest' do
8-
gem 'ethon'
15+
appraise "ethon-#{mode}" do
16+
gem 'ethon'
17+
end
918
end

instrumentation/ethon/README.md

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

instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/instrumentation.rb

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ module Ethon
1111
# instrumentation
1212
class Instrumentation < OpenTelemetry::Instrumentation::Base
1313
install do |_config|
14-
require_dependencies
15-
add_patches
14+
patch_type = determine_semconv
15+
send(:"require_dependencies_#{patch_type}")
16+
send(:"add_patches_#{patch_type}")
1617
end
1718

1819
present do
@@ -23,13 +24,46 @@ class Instrumentation < OpenTelemetry::Instrumentation::Base
2324

2425
private
2526

26-
def require_dependencies
27-
require_relative 'patches/easy'
27+
def determine_semconv
28+
stability_opt_in = ENV.fetch('OTEL_SEMCONV_STABILITY_OPT_IN', '')
29+
values = stability_opt_in.split(',').map(&:strip)
30+
31+
if values.include?('http/dup')
32+
'dup'
33+
elsif values.include?('http')
34+
'stable'
35+
else
36+
'old'
37+
end
38+
end
39+
40+
def require_dependencies_dup
41+
require_relative 'patches/dup/easy'
42+
require_relative 'patches/multi'
43+
end
44+
45+
def require_dependencies_stable
46+
require_relative 'patches/stable/easy'
2847
require_relative 'patches/multi'
2948
end
3049

31-
def add_patches
32-
::Ethon::Easy.prepend(Patches::Easy)
50+
def require_dependencies_old
51+
require_relative 'patches/old/easy'
52+
require_relative 'patches/multi'
53+
end
54+
55+
def add_patches_dup
56+
::Ethon::Easy.prepend(Patches::Dup::Easy)
57+
::Ethon::Multi.prepend(Patches::Multi)
58+
end
59+
60+
def add_patches_stable
61+
::Ethon::Easy.prepend(Patches::Stable::Easy)
62+
::Ethon::Multi.prepend(Patches::Multi)
63+
end
64+
65+
def add_patches_old
66+
::Ethon::Easy.prepend(Patches::Old::Easy)
3367
::Ethon::Multi.prepend(Patches::Multi)
3468
end
3569
end
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
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 Ethon
10+
module Patches
11+
# Module using old and stable HTTP semantic conventions
12+
module Dup
13+
# Ethon::Easy patch for instrumentation
14+
module Easy
15+
ACTION_NAMES_TO_HTTP_METHODS = Hash.new do |h, k|
16+
# #to_s is required because user input could be symbol or string
17+
h[k] = k.to_s.upcase
18+
end
19+
HTTP_METHODS_TO_SPAN_NAMES = Hash.new { |h, k| h[k] = "HTTP #{k}" }
20+
21+
# Constant for the HTTP status range
22+
HTTP_STATUS_SUCCESS_RANGE = (100..399)
23+
24+
def http_request(url, action_name, options = {})
25+
@otel_method = ACTION_NAMES_TO_HTTP_METHODS[action_name]
26+
super
27+
end
28+
29+
def headers=(headers)
30+
# Store headers to call this method again when span is ready
31+
@otel_original_headers = headers
32+
super
33+
end
34+
35+
def perform
36+
otel_before_request
37+
super
38+
end
39+
40+
def complete
41+
begin
42+
response_options = mirror.options
43+
response_code = (response_options[:response_code] || response_options[:code]).to_i
44+
if response_code.zero?
45+
return_code = response_options[:return_code]
46+
message = return_code ? ::Ethon::Curl.easy_strerror(return_code) : 'unknown reason'
47+
@otel_span.status = OpenTelemetry::Trace::Status.error("Request has failed: #{message}")
48+
else
49+
@otel_span.set_attribute('http.status_code', response_code)
50+
@otel_span.set_attribute('http.response.status_code', response_code)
51+
@otel_span.status = OpenTelemetry::Trace::Status.error unless HTTP_STATUS_SUCCESS_RANGE.cover?(response_code.to_i)
52+
end
53+
ensure
54+
@otel_span&.finish
55+
@otel_span = nil
56+
end
57+
super
58+
end
59+
60+
def reset
61+
super
62+
ensure
63+
@otel_span = nil
64+
@otel_method = nil
65+
@otel_original_headers = nil
66+
end
67+
68+
def otel_before_request
69+
method = 'N/A' # Could be GET or not HTTP at all
70+
method = @otel_method if instance_variable_defined?(:@otel_method) && !@otel_method.nil?
71+
72+
@otel_span = tracer.start_span(
73+
HTTP_METHODS_TO_SPAN_NAMES[method],
74+
attributes: span_creation_attributes(method),
75+
kind: :client
76+
)
77+
78+
@otel_original_headers ||= {}
79+
OpenTelemetry::Trace.with_span(@otel_span) do
80+
OpenTelemetry.propagation.inject(@otel_original_headers)
81+
end
82+
self.headers = @otel_original_headers
83+
end
84+
85+
def otel_span_started?
86+
instance_variable_defined?(:@otel_span) && !@otel_span.nil?
87+
end
88+
89+
private
90+
91+
def span_creation_attributes(method)
92+
instrumentation_attrs = {
93+
'http.method' => method,
94+
'http.request.method' => method
95+
}
96+
97+
uri = _otel_cleanse_uri(url)
98+
if uri
99+
instrumentation_attrs['http.url'] = uri.to_s
100+
instrumentation_attrs['url.full'] = uri.to_s
101+
instrumentation_attrs['net.peer.name'] = uri.host if uri.host
102+
instrumentation_attrs['server.address'] = uri.host if uri.host
103+
end
104+
105+
config = Ethon::Instrumentation.instance.config
106+
instrumentation_attrs['peer.service'] = config[:peer_service] if config[:peer_service]
107+
instrumentation_attrs.merge!(
108+
OpenTelemetry::Common::HTTP::ClientContext.attributes
109+
)
110+
end
111+
112+
# Returns a URL string with userinfo removed.
113+
#
114+
# @param [String] url The URL string to cleanse.
115+
#
116+
# @return [String] the cleansed URL.
117+
def _otel_cleanse_uri(url)
118+
cleansed_url = URI.parse(url)
119+
cleansed_url.password = nil
120+
cleansed_url.user = nil
121+
cleansed_url
122+
rescue URI::Error
123+
nil
124+
end
125+
126+
def tracer
127+
Ethon::Instrumentation.instance.tracer
128+
end
129+
end
130+
end
131+
end
132+
end
133+
end
134+
end

instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/easy.rb

Lines changed: 0 additions & 127 deletions
This file was deleted.

0 commit comments

Comments
 (0)