Skip to content

Commit 9b6e48e

Browse files
committed
fix: HTTP unknown methods
Ensures that all HTTP client gems support unknown methods as described in semantic conventions: https://opentelemetry.io/docs/specs/semconv/http/http-spans/#http-client-span See #1779
1 parent 1e01808 commit 9b6e48e

File tree

55 files changed

+1069
-145
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+1069
-145
lines changed

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

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

7+
require_relative '../http_helper'
8+
79
module OpenTelemetry
810
module Instrumentation
911
module Ethon
@@ -12,20 +14,11 @@ module Patches
1214
module Dup
1315
# Ethon::Easy patch for instrumentation
1416
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 do |h, k|
20-
h[k] = k.to_s
21-
h[k] = 'HTTP' if k == '_OTHER'
22-
end
23-
2417
# Constant for the HTTP status range
2518
HTTP_STATUS_SUCCESS_RANGE = (100..399)
2619

2720
def http_request(url, action_name, options = {})
28-
@otel_method = ACTION_NAMES_TO_HTTP_METHODS[action_name]
21+
@otel_method = action_name.to_s.upcase
2922
super
3023
end
3124

@@ -80,9 +73,12 @@ def otel_before_request
8073
method = '_OTHER' # Could be GET or not HTTP at all
8174
method = @otel_method if instance_variable_defined?(:@otel_method) && !@otel_method.nil?
8275

76+
normalized_method, original_method = HttpHelper.normalize_method(method)
77+
span_name = HttpHelper.span_name_for_stable(normalized_method)
78+
8379
@otel_span = tracer.start_span(
84-
HTTP_METHODS_TO_SPAN_NAMES[method],
85-
attributes: span_creation_attributes(method),
80+
span_name,
81+
attributes: span_creation_attributes(normalized_method, original_method),
8682
kind: :client
8783
)
8884

@@ -99,12 +95,13 @@ def otel_span_started?
9995

10096
private
10197

102-
def span_creation_attributes(method)
103-
http_method = (method == '_OTHER' ? 'N/A' : method)
98+
def span_creation_attributes(normalized_method, original_method)
99+
http_method = (normalized_method == '_OTHER' ? 'N/A' : normalized_method)
104100
instrumentation_attrs = {
105101
'http.method' => http_method,
106-
'http.request.method' => method
102+
'http.request.method' => normalized_method
107103
}
104+
instrumentation_attrs['http.request.method_original'] = original_method if original_method
108105

109106
uri = _otel_cleanse_uri(url)
110107
if uri
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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+
# Helper module for HTTP method normalization
12+
module HttpHelper
13+
KNOWN_METHODS = %w[CONNECT DELETE GET HEAD OPTIONS PATCH POST PUT TRACE].freeze
14+
15+
def self.normalize_method(method)
16+
normalized = method.to_s.upcase
17+
if KNOWN_METHODS.include?(normalized)
18+
[normalized, nil]
19+
else
20+
['_OTHER', normalized]
21+
end
22+
end
23+
24+
# Generates span name for stable semantic conventions
25+
# @param normalized_method [String] the normalized HTTP method
26+
# @return [String] the span name
27+
def self.span_name_for_stable(normalized_method)
28+
normalized_method == '_OTHER' ? 'HTTP' : normalized_method
29+
end
30+
31+
# Generates span name for old semantic conventions
32+
# @param normalized_method [String] the normalized HTTP method
33+
# @return [String] the span name
34+
def self.span_name_for_old(normalized_method)
35+
normalized_method == '_OTHER' ? 'HTTP' : "HTTP #{normalized_method}"
36+
end
37+
end
38+
end
39+
end
40+
end
41+
end

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

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

7+
require_relative '../http_helper'
8+
79
module OpenTelemetry
810
module Instrumentation
911
module Ethon
@@ -12,17 +14,11 @@ module Patches
1214
module Old
1315
# Ethon::Easy patch for instrumentation
1416
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-
2117
# Constant for the HTTP status range
2218
HTTP_STATUS_SUCCESS_RANGE = (100..399)
2319

2420
def http_request(url, action_name, options = {})
25-
@otel_method = ACTION_NAMES_TO_HTTP_METHODS[action_name]
21+
@otel_method = action_name.to_s.upcase
2622
super
2723
end
2824

@@ -76,9 +72,12 @@ def otel_before_request
7672
method = 'N/A' # Could be GET or not HTTP at all
7773
method = @otel_method if instance_variable_defined?(:@otel_method) && !@otel_method.nil?
7874

75+
normalized_method, _original_method = HttpHelper.normalize_method(method)
76+
span_name = HttpHelper.span_name_for_old(normalized_method)
77+
7978
@otel_span = tracer.start_span(
80-
HTTP_METHODS_TO_SPAN_NAMES[method],
81-
attributes: span_creation_attributes(method),
79+
span_name,
80+
attributes: span_creation_attributes(normalized_method),
8281
kind: :client
8382
)
8483

@@ -95,9 +94,9 @@ def otel_span_started?
9594

9695
private
9796

98-
def span_creation_attributes(method)
97+
def span_creation_attributes(normalized_method)
9998
instrumentation_attrs = {
100-
'http.method' => method
99+
'http.method' => normalized_method
101100
}
102101

103102
uri = _otel_cleanse_uri(url)

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

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

7+
require_relative '../http_helper'
8+
79
module OpenTelemetry
810
module Instrumentation
911
module Ethon
@@ -12,20 +14,11 @@ module Patches
1214
module Stable
1315
# Ethon::Easy patch for instrumentation
1416
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 do |h, k|
20-
h[k] = k.to_s
21-
h[k] = 'HTTP' if k == '_OTHER'
22-
end
23-
2417
# Constant for the HTTP status range
2518
HTTP_STATUS_SUCCESS_RANGE = (100..399)
2619

2720
def http_request(url, action_name, options = {})
28-
@otel_method = ACTION_NAMES_TO_HTTP_METHODS[action_name]
21+
@otel_method = action_name.to_s.upcase
2922
super
3023
end
3124

@@ -79,9 +72,12 @@ def otel_before_request
7972
method = '_OTHER' # Could be GET or not HTTP at all
8073
method = @otel_method if instance_variable_defined?(:@otel_method) && !@otel_method.nil?
8174

75+
normalized_method, original_method = HttpHelper.normalize_method(method)
76+
span_name = HttpHelper.span_name_for_stable(normalized_method)
77+
8278
@otel_span = tracer.start_span(
83-
HTTP_METHODS_TO_SPAN_NAMES[method],
84-
attributes: span_creation_attributes(method),
79+
span_name,
80+
attributes: span_creation_attributes(normalized_method, original_method),
8581
kind: :client
8682
)
8783

@@ -98,10 +94,11 @@ def otel_span_started?
9894

9995
private
10096

101-
def span_creation_attributes(method)
97+
def span_creation_attributes(normalized_method, original_method)
10298
instrumentation_attrs = {
103-
'http.request.method' => method
99+
'http.request.method' => normalized_method
104100
}
101+
instrumentation_attrs['http.request.method_original'] = original_method if original_method
105102

106103
uri = _otel_cleanse_uri(url)
107104
if uri

instrumentation/ethon/test/opentelemetry/instrumentation/ethon/dup/instrumentation_test.rb

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,32 @@ def stub_response(options)
256256
end
257257
end
258258
end
259+
260+
describe 'with unknown HTTP method' do
261+
def stub_response(options)
262+
easy.stub(:mirror, Ethon::Easy::Mirror.new(options)) do
263+
easy.otel_before_request
264+
# NOTE: perform calls complete
265+
easy.complete
266+
267+
yield
268+
end
269+
end
270+
271+
it 'normalizes unknown HTTP methods' do
272+
easy.http_request('http://example.com/purge', :purge)
273+
274+
stub_response(response_code: 200) do
275+
_(exporter.finished_spans.size).must_equal 1
276+
_(span.name).must_equal 'HTTP'
277+
_(span.attributes['http.method']).must_equal 'N/A'
278+
_(span.attributes['http.url']).must_equal 'http://example.com/purge'
279+
_(span.attributes['http.request.method']).must_equal '_OTHER'
280+
_(span.attributes['http.request.method_original']).must_equal 'PURGE'
281+
_(span.attributes['url.full']).must_equal 'http://example.com/purge'
282+
end
283+
end
284+
end
259285
end
260286

261287
describe 'multi' do

instrumentation/ethon/test/opentelemetry/instrumentation/ethon/old/instrumentation_test.rb

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@
6969
easy.stub(:complete, nil) do
7070
easy.perform
7171

72-
_(span.name).must_equal 'HTTP N/A'
73-
_(span.attributes['http.method']).must_equal 'N/A'
72+
_(span.name).must_equal 'HTTP'
73+
_(span.attributes['http.method']).must_equal '_OTHER'
7474
_(span.attributes['http.status_code']).must_be_nil
7575
_(span.attributes['http.url']).must_equal 'http://example.com/test'
7676
_(span.attributes['net.peer.name']).must_equal 'example.com'
@@ -87,8 +87,8 @@
8787

8888
# NOTE: check the finished spans since we expect to have closed it
8989
span = exporter.finished_spans.first
90-
_(span.name).must_equal 'HTTP N/A'
91-
_(span.attributes['http.method']).must_equal 'N/A'
90+
_(span.name).must_equal 'HTTP'
91+
_(span.attributes['http.method']).must_equal '_OTHER'
9292
_(span.attributes['http.status_code']).must_be_nil
9393
_(span.attributes['http.url']).must_equal 'http://example.com/test'
9494
_(span.status.code).must_equal(
@@ -113,8 +113,8 @@ def stub_response(options)
113113

114114
it 'when response is successful' do
115115
stub_response(response_code: 200) do
116-
_(span.name).must_equal 'HTTP N/A'
117-
_(span.attributes['http.method']).must_equal 'N/A'
116+
_(span.name).must_equal 'HTTP'
117+
_(span.attributes['http.method']).must_equal '_OTHER'
118118
_(span.attributes['http.status_code']).must_equal 200
119119
_(span.attributes['http.url']).must_equal 'http://example.com/test'
120120
_(easy.instance_eval { @otel_span }).must_be_nil
@@ -126,8 +126,8 @@ def stub_response(options)
126126

127127
it 'when response is not successful' do
128128
stub_response(response_code: 500) do
129-
_(span.name).must_equal 'HTTP N/A'
130-
_(span.attributes['http.method']).must_equal 'N/A'
129+
_(span.name).must_equal 'HTTP'
130+
_(span.attributes['http.method']).must_equal '_OTHER'
131131
_(span.attributes['http.status_code']).must_equal 500
132132
_(span.attributes['http.url']).must_equal 'http://example.com/test'
133133
_(easy.instance_eval { @otel_span }).must_be_nil
@@ -139,8 +139,8 @@ def stub_response(options)
139139

140140
it 'when request times out' do
141141
stub_response(response_code: 0, return_code: :operation_timedout) do
142-
_(span.name).must_equal 'HTTP N/A'
143-
_(span.attributes['http.method']).must_equal 'N/A'
142+
_(span.name).must_equal 'HTTP'
143+
_(span.attributes['http.method']).must_equal '_OTHER'
144144
_(span.attributes['http.status_code']).must_be_nil
145145
_(span.attributes['http.url']).must_equal 'http://example.com/test'
146146
_(span.status.code).must_equal(
@@ -238,6 +238,29 @@ def stub_response(options)
238238
end
239239
end
240240
end
241+
242+
describe 'with unknown HTTP method' do
243+
def stub_response(options)
244+
easy.stub(:mirror, Ethon::Easy::Mirror.new(options)) do
245+
easy.otel_before_request
246+
# NOTE: perform calls complete
247+
easy.complete
248+
249+
yield
250+
end
251+
end
252+
253+
it 'normalizes unknown HTTP methods' do
254+
easy.http_request('http://example.com/purge', :purge)
255+
256+
stub_response(response_code: 200) do
257+
_(exporter.finished_spans.size).must_equal 1
258+
_(span.name).must_equal 'HTTP'
259+
_(span.attributes['http.method']).must_equal '_OTHER'
260+
_(span.attributes['http.url']).must_equal 'http://example.com/purge'
261+
end
262+
end
263+
end
241264
end
242265

243266
describe 'multi' do

instrumentation/ethon/test/opentelemetry/instrumentation/ethon/stable/instrumentation_test.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,30 @@ def stub_response(options)
239239
end
240240
end
241241
end
242+
243+
describe 'with unknown HTTP method' do
244+
def stub_response(options)
245+
easy.stub(:mirror, Ethon::Easy::Mirror.new(options)) do
246+
easy.otel_before_request
247+
# NOTE: perform calls complete
248+
easy.complete
249+
250+
yield
251+
end
252+
end
253+
254+
it 'normalizes unknown HTTP methods' do
255+
easy.http_request('http://example.com/purge', :purge)
256+
257+
stub_response(response_code: 200) do
258+
_(exporter.finished_spans.size).must_equal 1
259+
_(span.name).must_equal 'HTTP'
260+
_(span.attributes['http.request.method']).must_equal '_OTHER'
261+
_(span.attributes['http.request.method_original']).must_equal 'PURGE'
262+
_(span.attributes['url.full']).must_equal 'http://example.com/purge'
263+
end
264+
end
265+
end
242266
end
243267

244268
describe 'multi' do

instrumentation/excon/Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ group :test do
1414
gem 'minitest', '~> 5.0'
1515
gem 'opentelemetry-sdk', '~> 1.1'
1616
gem 'opentelemetry-test-helpers', '~> 0.3'
17+
gem 'rspec-mocks'
1718
gem 'rubocop', '~> 1.81.1'
1819
gem 'rubocop-performance', '~> 1.26.0'
1920
gem 'simplecov', '~> 0.22.0'

0 commit comments

Comments
 (0)