diff --git a/Rakefile b/Rakefile index 460c4237a2..e904b3f036 100644 --- a/Rakefile +++ b/Rakefile @@ -36,7 +36,8 @@ task default: [:each] def foreach_gem(cmd) Dir.glob("**/opentelemetry-*.gemspec") do |gemspec| - name = File.basename(gemspec, ".gemspec") + next if gemspec.start_with?('.') + dir = File.dirname(gemspec) puts "**** Entering #{dir}" Dir.chdir(dir) do diff --git a/instrumentation/all/Gemfile b/instrumentation/all/Gemfile index 337a431b87..224c7f3b91 100644 --- a/instrumentation/all/Gemfile +++ b/instrumentation/all/Gemfile @@ -33,6 +33,7 @@ group :test do Dir.entries('../') .select { |entry| File.directory?(File.join('../', entry)) } .reject { |entry| excluded_instrumentations.include?(entry) } + .reject { |entry| entry.start_with?('.') } .sort .each { |dir| gem "opentelemetry-instrumentation-#{dir}", path: "../#{dir}" } end diff --git a/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon.rb b/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon.rb index 29676df325..d200929ab2 100644 --- a/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon.rb +++ b/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon.rb @@ -15,5 +15,6 @@ module Ethon end end +require_relative 'ethon/http_helper' require_relative 'ethon/instrumentation' require_relative 'ethon/version' diff --git a/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/http_helper.rb b/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/http_helper.rb new file mode 100644 index 0000000000..a5fc210b1a --- /dev/null +++ b/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/http_helper.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Instrumentation + module Ethon + # Helper module for HTTP method normalization + # @api private + module HttpHelper + # Lightweight struct to hold span creation attributes + SpanCreationAttributes = Struct.new(:span_name, :attributes, keyword_init: true) + + # Pre-computed mapping to avoid string allocations during normalization + METHOD_CACHE = { + 'CONNECT' => 'CONNECT', + 'DELETE' => 'DELETE', + 'GET' => 'GET', + 'HEAD' => 'HEAD', + 'OPTIONS' => 'OPTIONS', + 'PATCH' => 'PATCH', + 'POST' => 'POST', + 'PUT' => 'PUT', + 'TRACE' => 'TRACE', + 'connect' => 'CONNECT', + 'delete' => 'DELETE', + 'get' => 'GET', + 'head' => 'HEAD', + 'options' => 'OPTIONS', + 'patch' => 'PATCH', + 'post' => 'POST', + 'put' => 'PUT', + 'trace' => 'TRACE', + :connect => 'CONNECT', + :delete => 'DELETE', + :get => 'GET', + :head => 'HEAD', + :options => 'OPTIONS', + :patch => 'PATCH', + :post => 'POST', + :put => 'PUT', + :trace => 'TRACE' + }.freeze + + private_constant :METHOD_CACHE + + # Prepares span data using old semantic conventions + # @param method [String, Symbol] The HTTP method + # @return [SpanCreationAttributes] struct containing span_name and attributes hash + def self.span_attrs_for_old(method) + client_context_attrs = OpenTelemetry::Common::HTTP::ClientContext.attributes + normalized = METHOD_CACHE[method] + attributes = client_context_attrs.dup + + # Determine base span name and method value + if normalized + span_name = normalized + method_value = normalized + else + span_name = 'HTTP' + method_value = '_OTHER' + end + + attributes['http.method'] ||= method_value + + SpanCreationAttributes.new(span_name: span_name, attributes: attributes) + end + + # Prepares span data using stable semantic conventions + # @param method [String, Symbol] The HTTP method + # @return [SpanCreationAttributes] struct containing span_name and attributes hash + def self.span_attrs_for_stable(method) + client_context_attrs = OpenTelemetry::Common::HTTP::ClientContext.attributes + url_template = client_context_attrs['url.template'] + normalized = METHOD_CACHE[method] + attributes = client_context_attrs.dup + + # Determine base span name and method value + if normalized + base_name = normalized + method_value = normalized + original = nil + else + base_name = 'HTTP' + method_value = '_OTHER' + original = method.to_s + end + + span_name = url_template ? "#{base_name} #{url_template}" : base_name + attributes['http.request.method'] ||= method_value + attributes['http.request.method_original'] ||= original if original + + SpanCreationAttributes.new(span_name: span_name, attributes: attributes) + end + + # Prepares span data using both old and stable semantic conventions + # @param method [String, Symbol] The HTTP method + # @return [SpanCreationAttributes] struct containing span_name and attributes hash + def self.span_attrs_for_dup(method) + client_context_attrs = OpenTelemetry::Common::HTTP::ClientContext.attributes + url_template = client_context_attrs['url.template'] + normalized = METHOD_CACHE[method] + attributes = client_context_attrs.dup + + # Determine base span name and method value + if normalized + base_name = normalized + method_value = normalized + original = nil + else + base_name = 'HTTP' + method_value = '_OTHER' + original = method.to_s + end + + span_name = url_template ? "#{base_name} #{url_template}" : base_name + attributes['http.method'] ||= method_value + attributes['http.request.method'] ||= method_value + attributes['http.request.method_original'] ||= original if original + + SpanCreationAttributes.new(span_name: span_name, attributes: attributes) + end + end + end + end +end diff --git a/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/dup/easy.rb b/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/dup/easy.rb index e4f4a9d0fe..4bf519916c 100644 --- a/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/dup/easy.rb +++ b/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/dup/easy.rb @@ -12,20 +12,11 @@ module Patches module Dup # Ethon::Easy patch for instrumentation module Easy - ACTION_NAMES_TO_HTTP_METHODS = Hash.new do |h, k| - # #to_s is required because user input could be symbol or string - h[k] = k.to_s.upcase - end - HTTP_METHODS_TO_SPAN_NAMES = Hash.new do |h, k| - h[k] = k.to_s - h[k] = 'HTTP' if k == '_OTHER' - end - # Constant for the HTTP status range HTTP_STATUS_SUCCESS_RANGE = (100..399) def http_request(url, action_name, options = {}) - @otel_method = ACTION_NAMES_TO_HTTP_METHODS[action_name] + @otel_method = action_name super end @@ -77,12 +68,11 @@ def reset end def otel_before_request - method = '_OTHER' # Could be GET or not HTTP at all - method = @otel_method if instance_variable_defined?(:@otel_method) && !@otel_method.nil? + span_data = HttpHelper.span_attrs_for_dup(@otel_method) @otel_span = tracer.start_span( - HTTP_METHODS_TO_SPAN_NAMES[method], - attributes: span_creation_attributes(method), + span_data.span_name, + attributes: span_creation_attributes(span_data), kind: :client ) @@ -99,12 +89,8 @@ def otel_span_started? private - def span_creation_attributes(method) - http_method = (method == '_OTHER' ? 'N/A' : method) - instrumentation_attrs = { - 'http.method' => http_method, - 'http.request.method' => method - } + def span_creation_attributes(span_data) + instrumentation_attrs = {} uri = _otel_cleanse_uri(url) if uri @@ -116,9 +102,7 @@ def span_creation_attributes(method) config = Ethon::Instrumentation.instance.config instrumentation_attrs['peer.service'] = config[:peer_service] if config[:peer_service] - instrumentation_attrs.merge!( - OpenTelemetry::Common::HTTP::ClientContext.attributes - ) + instrumentation_attrs.merge!(span_data.attributes) end # Returns a URL string with userinfo removed. diff --git a/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/old/easy.rb b/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/old/easy.rb index 4f057f05d5..b84efb1c90 100644 --- a/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/old/easy.rb +++ b/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/old/easy.rb @@ -12,17 +12,11 @@ module Patches module Old # Ethon::Easy patch for instrumentation module Easy - ACTION_NAMES_TO_HTTP_METHODS = Hash.new do |h, k| - # #to_s is required because user input could be symbol or string - h[k] = k.to_s.upcase - end - HTTP_METHODS_TO_SPAN_NAMES = Hash.new { |h, k| h[k] = "HTTP #{k}" } - # Constant for the HTTP status range HTTP_STATUS_SUCCESS_RANGE = (100..399) def http_request(url, action_name, options = {}) - @otel_method = ACTION_NAMES_TO_HTTP_METHODS[action_name] + @otel_method = action_name super end @@ -73,12 +67,11 @@ def reset end def otel_before_request - method = 'N/A' # Could be GET or not HTTP at all - method = @otel_method if instance_variable_defined?(:@otel_method) && !@otel_method.nil? + span_data = HttpHelper.span_attrs_for_old(@otel_method) @otel_span = tracer.start_span( - HTTP_METHODS_TO_SPAN_NAMES[method], - attributes: span_creation_attributes(method), + span_data.span_name, + attributes: span_creation_attributes(span_data), kind: :client ) @@ -95,10 +88,8 @@ def otel_span_started? private - def span_creation_attributes(method) - instrumentation_attrs = { - 'http.method' => method - } + def span_creation_attributes(span_data) + instrumentation_attrs = {} uri = _otel_cleanse_uri(url) if uri @@ -108,9 +99,7 @@ def span_creation_attributes(method) config = Ethon::Instrumentation.instance.config instrumentation_attrs['peer.service'] = config[:peer_service] if config[:peer_service] - instrumentation_attrs.merge!( - OpenTelemetry::Common::HTTP::ClientContext.attributes - ) + instrumentation_attrs.merge!(span_data.attributes) end # Returns a URL string with userinfo removed. diff --git a/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/stable/easy.rb b/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/stable/easy.rb index 2d8b934080..d87cf20588 100644 --- a/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/stable/easy.rb +++ b/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/stable/easy.rb @@ -12,20 +12,11 @@ module Patches module Stable # Ethon::Easy patch for instrumentation module Easy - ACTION_NAMES_TO_HTTP_METHODS = Hash.new do |h, k| - # #to_s is required because user input could be symbol or string - h[k] = k.to_s.upcase - end - HTTP_METHODS_TO_SPAN_NAMES = Hash.new do |h, k| - h[k] = k.to_s - h[k] = 'HTTP' if k == '_OTHER' - end - # Constant for the HTTP status range HTTP_STATUS_SUCCESS_RANGE = (100..399) def http_request(url, action_name, options = {}) - @otel_method = ACTION_NAMES_TO_HTTP_METHODS[action_name] + @otel_method = action_name super end @@ -76,12 +67,11 @@ def reset end def otel_before_request - method = '_OTHER' # Could be GET or not HTTP at all - method = @otel_method if instance_variable_defined?(:@otel_method) && !@otel_method.nil? + span_data = HttpHelper.span_attrs_for_stable(@otel_method) @otel_span = tracer.start_span( - HTTP_METHODS_TO_SPAN_NAMES[method], - attributes: span_creation_attributes(method), + span_data.span_name, + attributes: span_creation_attributes(span_data), kind: :client ) @@ -98,10 +88,8 @@ def otel_span_started? private - def span_creation_attributes(method) - instrumentation_attrs = { - 'http.request.method' => method - } + def span_creation_attributes(span_data) + instrumentation_attrs = {} uri = _otel_cleanse_uri(url) if uri @@ -111,9 +99,7 @@ def span_creation_attributes(method) config = Ethon::Instrumentation.instance.config instrumentation_attrs['peer.service'] = config[:peer_service] if config[:peer_service] - instrumentation_attrs.merge!( - OpenTelemetry::Common::HTTP::ClientContext.attributes - ) + instrumentation_attrs.merge!(span_data.attributes) end # Returns a URL string with userinfo removed. diff --git a/instrumentation/ethon/test/opentelemetry/instrumentation/ethon/dup/instrumentation_test.rb b/instrumentation/ethon/test/opentelemetry/instrumentation/ethon/dup/instrumentation_test.rb index 8c27b3300e..c49aee3abd 100644 --- a/instrumentation/ethon/test/opentelemetry/instrumentation/ethon/dup/instrumentation_test.rb +++ b/instrumentation/ethon/test/opentelemetry/instrumentation/ethon/dup/instrumentation_test.rb @@ -71,7 +71,7 @@ easy.perform _(span.name).must_equal 'HTTP' - _(span.attributes['http.method']).must_equal 'N/A' + _(span.attributes['http.method']).must_equal '_OTHER' _(span.attributes['http.status_code']).must_be_nil _(span.attributes['http.url']).must_equal 'http://example.com/test' _(span.attributes['net.peer.name']).must_equal 'example.com' @@ -92,7 +92,7 @@ # NOTE: check the finished spans since we expect to have closed it span = exporter.finished_spans.first _(span.name).must_equal 'HTTP' - _(span.attributes['http.method']).must_equal 'N/A' + _(span.attributes['http.method']).must_equal '_OTHER' _(span.attributes['http.status_code']).must_be_nil _(span.attributes['http.url']).must_equal 'http://example.com/test' _(span.attributes['http.request.method']).must_equal '_OTHER' @@ -121,7 +121,7 @@ def stub_response(options) it 'when response is successful' do stub_response(response_code: 200) do _(span.name).must_equal 'HTTP' - _(span.attributes['http.method']).must_equal 'N/A' + _(span.attributes['http.method']).must_equal '_OTHER' _(span.attributes['http.request.method']).must_equal '_OTHER' _(span.attributes['http.status_code']).must_equal 200 _(span.attributes['http.response.status_code']).must_equal 200 @@ -137,7 +137,7 @@ def stub_response(options) it 'when response is not successful' do stub_response(response_code: 500) do _(span.name).must_equal 'HTTP' - _(span.attributes['http.method']).must_equal 'N/A' + _(span.attributes['http.method']).must_equal '_OTHER' _(span.attributes['http.request.method']).must_equal '_OTHER' _(span.attributes['http.status_code']).must_equal 500 _(span.attributes['http.response.status_code']).must_equal 500 @@ -153,7 +153,7 @@ def stub_response(options) it 'when request times out' do stub_response(response_code: 0, return_code: :operation_timedout) do _(span.name).must_equal 'HTTP' - _(span.attributes['http.method']).must_equal 'N/A' + _(span.attributes['http.method']).must_equal '_OTHER' _(span.attributes['http.request.method']).must_equal '_OTHER' _(span.attributes['http.status_code']).must_be_nil _(span.attributes['http.response.status_code']).must_be_nil @@ -232,7 +232,7 @@ def stub_response(options) end it 'cleans up @otel_method' do - _(easy.instance_eval { @otel_method }).must_equal 'PUT' + _(easy.instance_eval { @otel_method }).must_equal :put easy.reset @@ -256,6 +256,32 @@ def stub_response(options) end end end + + describe 'with unknown HTTP method' do + def stub_response(options) + easy.stub(:mirror, Ethon::Easy::Mirror.new(options)) do + easy.otel_before_request + # NOTE: perform calls complete + easy.complete + + yield + end + end + + it 'normalizes unknown HTTP methods' do + easy.http_request('http://example.com/purge', :purge) + + stub_response(response_code: 200) do + _(exporter.finished_spans.size).must_equal 1 + _(span.name).must_equal 'HTTP' + _(span.attributes['http.method']).must_equal '_OTHER' + _(span.attributes['http.url']).must_equal 'http://example.com/purge' + _(span.attributes['http.request.method']).must_equal '_OTHER' + _(span.attributes['http.request.method_original']).must_equal 'purge' + _(span.attributes['url.full']).must_equal 'http://example.com/purge' + end + end + end end describe 'multi' do diff --git a/instrumentation/ethon/test/opentelemetry/instrumentation/ethon/old/instrumentation_test.rb b/instrumentation/ethon/test/opentelemetry/instrumentation/ethon/old/instrumentation_test.rb index f8c307a97e..ed8aa13ad1 100644 --- a/instrumentation/ethon/test/opentelemetry/instrumentation/ethon/old/instrumentation_test.rb +++ b/instrumentation/ethon/test/opentelemetry/instrumentation/ethon/old/instrumentation_test.rb @@ -69,8 +69,8 @@ easy.stub(:complete, nil) do easy.perform - _(span.name).must_equal 'HTTP N/A' - _(span.attributes['http.method']).must_equal 'N/A' + _(span.name).must_equal 'HTTP' + _(span.attributes['http.method']).must_equal '_OTHER' _(span.attributes['http.status_code']).must_be_nil _(span.attributes['http.url']).must_equal 'http://example.com/test' _(span.attributes['net.peer.name']).must_equal 'example.com' @@ -87,8 +87,8 @@ # NOTE: check the finished spans since we expect to have closed it span = exporter.finished_spans.first - _(span.name).must_equal 'HTTP N/A' - _(span.attributes['http.method']).must_equal 'N/A' + _(span.name).must_equal 'HTTP' + _(span.attributes['http.method']).must_equal '_OTHER' _(span.attributes['http.status_code']).must_be_nil _(span.attributes['http.url']).must_equal 'http://example.com/test' _(span.status.code).must_equal( @@ -113,8 +113,8 @@ def stub_response(options) it 'when response is successful' do stub_response(response_code: 200) do - _(span.name).must_equal 'HTTP N/A' - _(span.attributes['http.method']).must_equal 'N/A' + _(span.name).must_equal 'HTTP' + _(span.attributes['http.method']).must_equal '_OTHER' _(span.attributes['http.status_code']).must_equal 200 _(span.attributes['http.url']).must_equal 'http://example.com/test' _(easy.instance_eval { @otel_span }).must_be_nil @@ -126,8 +126,8 @@ def stub_response(options) it 'when response is not successful' do stub_response(response_code: 500) do - _(span.name).must_equal 'HTTP N/A' - _(span.attributes['http.method']).must_equal 'N/A' + _(span.name).must_equal 'HTTP' + _(span.attributes['http.method']).must_equal '_OTHER' _(span.attributes['http.status_code']).must_equal 500 _(span.attributes['http.url']).must_equal 'http://example.com/test' _(easy.instance_eval { @otel_span }).must_be_nil @@ -139,8 +139,8 @@ def stub_response(options) it 'when request times out' do stub_response(response_code: 0, return_code: :operation_timedout) do - _(span.name).must_equal 'HTTP N/A' - _(span.attributes['http.method']).must_equal 'N/A' + _(span.name).must_equal 'HTTP' + _(span.attributes['http.method']).must_equal '_OTHER' _(span.attributes['http.status_code']).must_be_nil _(span.attributes['http.url']).must_equal 'http://example.com/test' _(span.status.code).must_equal( @@ -214,7 +214,7 @@ def stub_response(options) end it 'cleans up @otel_method' do - _(easy.instance_eval { @otel_method }).must_equal 'PUT' + _(easy.instance_eval { @otel_method }).must_equal :put easy.reset @@ -238,6 +238,29 @@ def stub_response(options) end end end + + describe 'with unknown HTTP method' do + def stub_response(options) + easy.stub(:mirror, Ethon::Easy::Mirror.new(options)) do + easy.otel_before_request + # NOTE: perform calls complete + easy.complete + + yield + end + end + + it 'normalizes unknown HTTP methods' do + easy.http_request('http://example.com/purge', :purge) + + stub_response(response_code: 200) do + _(exporter.finished_spans.size).must_equal 1 + _(span.name).must_equal 'HTTP' + _(span.attributes['http.method']).must_equal '_OTHER' + _(span.attributes['http.url']).must_equal 'http://example.com/purge' + end + end + end end describe 'multi' do diff --git a/instrumentation/ethon/test/opentelemetry/instrumentation/ethon/stable/instrumentation_test.rb b/instrumentation/ethon/test/opentelemetry/instrumentation/ethon/stable/instrumentation_test.rb index d9317fd008..2d3df83391 100644 --- a/instrumentation/ethon/test/opentelemetry/instrumentation/ethon/stable/instrumentation_test.rb +++ b/instrumentation/ethon/test/opentelemetry/instrumentation/ethon/stable/instrumentation_test.rb @@ -215,7 +215,7 @@ def stub_response(options) end it 'cleans up @otel_method' do - _(easy.instance_eval { @otel_method }).must_equal 'PUT' + _(easy.instance_eval { @otel_method }).must_equal :put easy.reset @@ -239,6 +239,30 @@ def stub_response(options) end end end + + describe 'with unknown HTTP method' do + def stub_response(options) + easy.stub(:mirror, Ethon::Easy::Mirror.new(options)) do + easy.otel_before_request + # NOTE: perform calls complete + easy.complete + + yield + end + end + + it 'normalizes unknown HTTP methods' do + easy.http_request('http://example.com/purge', :purge) + + stub_response(response_code: 200) do + _(exporter.finished_spans.size).must_equal 1 + _(span.name).must_equal 'HTTP' + _(span.attributes['http.request.method']).must_equal '_OTHER' + _(span.attributes['http.request.method_original']).must_equal 'purge' + _(span.attributes['url.full']).must_equal 'http://example.com/purge' + end + end + end end describe 'multi' do diff --git a/instrumentation/excon/Gemfile b/instrumentation/excon/Gemfile index fdeae6f37b..5717435a9d 100644 --- a/instrumentation/excon/Gemfile +++ b/instrumentation/excon/Gemfile @@ -14,6 +14,7 @@ group :test do gem 'minitest', '~> 5.0' gem 'opentelemetry-sdk', '~> 1.1' gem 'opentelemetry-test-helpers', '~> 0.3' + gem 'rspec-mocks' gem 'rubocop', '~> 1.81.1' gem 'rubocop-performance', '~> 1.26.0' gem 'simplecov', '~> 0.22.0' diff --git a/instrumentation/excon/lib/opentelemetry/instrumentation/excon.rb b/instrumentation/excon/lib/opentelemetry/instrumentation/excon.rb index 4cb8c62c34..166772aa5d 100644 --- a/instrumentation/excon/lib/opentelemetry/instrumentation/excon.rb +++ b/instrumentation/excon/lib/opentelemetry/instrumentation/excon.rb @@ -15,5 +15,6 @@ module Excon end end +require_relative 'excon/http_helper' require_relative 'excon/instrumentation' require_relative 'excon/version' diff --git a/instrumentation/excon/lib/opentelemetry/instrumentation/excon/http_helper.rb b/instrumentation/excon/lib/opentelemetry/instrumentation/excon/http_helper.rb new file mode 100644 index 0000000000..639ea5aff0 --- /dev/null +++ b/instrumentation/excon/lib/opentelemetry/instrumentation/excon/http_helper.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Instrumentation + module Excon + # Utility module for HTTP-related helper methods + # @api private + module HttpHelper + # Lightweight struct to hold span creation data + SpanCreationAttributes = Struct.new(:span_name, :attributes, keyword_init: true) + + # Pre-computed mapping to avoid string allocations during normalization + METHOD_CACHE = { + 'CONNECT' => 'CONNECT', + 'DELETE' => 'DELETE', + 'GET' => 'GET', + 'HEAD' => 'HEAD', + 'OPTIONS' => 'OPTIONS', + 'PATCH' => 'PATCH', + 'POST' => 'POST', + 'PUT' => 'PUT', + 'TRACE' => 'TRACE', + 'connect' => 'CONNECT', + 'delete' => 'DELETE', + 'get' => 'GET', + 'head' => 'HEAD', + 'options' => 'OPTIONS', + 'patch' => 'PATCH', + 'post' => 'POST', + 'put' => 'PUT', + 'trace' => 'TRACE', + :connect => 'CONNECT', + :delete => 'DELETE', + :get => 'GET', + :head => 'HEAD', + :options => 'OPTIONS', + :patch => 'PATCH', + :post => 'POST', + :put => 'PUT', + :trace => 'TRACE' + }.freeze + + private_constant :METHOD_CACHE + + # Prepares span data using old semantic conventions + # @param method [String, Symbol] The HTTP method + # @return [SpanCreationAttributes] struct containing span_name and attributes hash + def self.span_attrs_for_old(method) + client_context_attrs = OpenTelemetry::Common::HTTP::ClientContext.attributes + normalized = METHOD_CACHE[method] + attributes = client_context_attrs.dup + + # Determine base span name and method value + if normalized + span_name = normalized + method_value = normalized + else + span_name = 'HTTP' + method_value = '_OTHER' + end + + attributes['http.method'] ||= method_value + + SpanCreationAttributes.new(span_name: span_name, attributes: attributes) + end + + # Prepares span data using stable semantic conventions + # @param method [String, Symbol] The HTTP method + # @return [SpanCreationAttributes] struct containing span_name and attributes hash + def self.span_attrs_for_stable(method) + client_context_attrs = OpenTelemetry::Common::HTTP::ClientContext.attributes + url_template = client_context_attrs['url.template'] + normalized = METHOD_CACHE[method] + attributes = client_context_attrs.dup + + # Determine base span name and method value + if normalized + base_name = normalized + method_value = normalized + original = nil + else + base_name = 'HTTP' + method_value = '_OTHER' + original = method.to_s + end + + span_name = url_template ? "#{base_name} #{url_template}" : base_name + attributes['http.request.method'] ||= method_value + attributes['http.request.method_original'] ||= original if original + + SpanCreationAttributes.new(span_name: span_name, attributes: attributes) + end + + # Prepares span data using both old and stable semantic conventions + # @param method [String, Symbol] The HTTP method + # @return [SpanCreationAttributes] struct containing span_name and attributes hash + def self.span_attrs_for_dup(method) + client_context_attrs = OpenTelemetry::Common::HTTP::ClientContext.attributes + url_template = client_context_attrs['url.template'] + normalized = METHOD_CACHE[method] + attributes = client_context_attrs.dup + + # Determine base span name and method value + if normalized + base_name = normalized + method_value = normalized + original = nil + else + base_name = 'HTTP' + method_value = '_OTHER' + original = method.to_s + end + + span_name = url_template ? "#{base_name} #{url_template}" : base_name + attributes['http.method'] ||= method_value + attributes['http.request.method'] ||= method_value + attributes['http.request.method_original'] ||= original if original + + SpanCreationAttributes.new(span_name: span_name, attributes: attributes) + end + end + end + end +end diff --git a/instrumentation/excon/lib/opentelemetry/instrumentation/excon/middlewares/dup/tracer_middleware.rb b/instrumentation/excon/lib/opentelemetry/instrumentation/excon/middlewares/dup/tracer_middleware.rb index 1dcbc169ea..387e57d958 100644 --- a/instrumentation/excon/lib/opentelemetry/instrumentation/excon/middlewares/dup/tracer_middleware.rb +++ b/instrumentation/excon/lib/opentelemetry/instrumentation/excon/middlewares/dup/tracer_middleware.rb @@ -11,34 +11,22 @@ module Middlewares module Dup # Excon middleware for instrumentation class TracerMiddleware < ::Excon::Middleware::Base - HTTP_METHODS_TO_UPPERCASE = %w[connect delete get head options patch post put trace].each_with_object({}) do |method, hash| - uppercase_method = method.upcase - hash[method] = uppercase_method - hash[method.to_sym] = uppercase_method - hash[uppercase_method] = uppercase_method - end.freeze - - HTTP_METHODS_TO_SPAN_NAMES = HTTP_METHODS_TO_UPPERCASE.values.each_with_object({}) do |uppercase_method, hash| - hash[uppercase_method] ||= uppercase_method - end.freeze - # Constant for the HTTP status range HTTP_STATUS_SUCCESS_RANGE = (100..399) def request_call(datum) return @stack.request_call(datum) if untraced?(datum) - http_method = HTTP_METHODS_TO_UPPERCASE[datum[:method]] + span_data = HttpHelper.span_attrs_for_dup(datum[:method]) + cleansed_url = OpenTelemetry::Common::Utilities.cleanse_url(::Excon::Utils.request_uri(datum)) attributes = { OpenTelemetry::SemanticConventions::Trace::HTTP_HOST => datum[:host], - OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => http_method, OpenTelemetry::SemanticConventions::Trace::HTTP_SCHEME => datum[:scheme], OpenTelemetry::SemanticConventions::Trace::HTTP_TARGET => datum[:path], OpenTelemetry::SemanticConventions::Trace::HTTP_URL => cleansed_url, OpenTelemetry::SemanticConventions::Trace::NET_PEER_NAME => datum[:hostname], OpenTelemetry::SemanticConventions::Trace::NET_PEER_PORT => datum[:port], - 'http.request.method' => http_method, 'url.scheme' => datum[:scheme], 'url.path' => datum[:path], 'url.full' => cleansed_url, @@ -48,8 +36,8 @@ def request_call(datum) attributes['url.query'] = datum[:query] if datum[:query] peer_service = Excon::Instrumentation.instance.config[:peer_service] attributes[OpenTelemetry::SemanticConventions::Trace::PEER_SERVICE] = peer_service if peer_service - attributes.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) - span = tracer.start_span(HTTP_METHODS_TO_SPAN_NAMES[http_method], attributes: attributes, kind: :client) + attributes.merge!(span_data.attributes) + span = tracer.start_span(span_data.span_name, attributes: attributes, kind: :client) ctx = OpenTelemetry::Trace.context_with_span(span) datum[:otel_span] = span datum[:otel_token] = OpenTelemetry::Context.attach(ctx) diff --git a/instrumentation/excon/lib/opentelemetry/instrumentation/excon/middlewares/old/tracer_middleware.rb b/instrumentation/excon/lib/opentelemetry/instrumentation/excon/middlewares/old/tracer_middleware.rb index 264c2de6ad..7c298e1e02 100644 --- a/instrumentation/excon/lib/opentelemetry/instrumentation/excon/middlewares/old/tracer_middleware.rb +++ b/instrumentation/excon/lib/opentelemetry/instrumentation/excon/middlewares/old/tracer_middleware.rb @@ -11,27 +11,16 @@ module Middlewares module Old # Excon middleware for instrumentation class TracerMiddleware < ::Excon::Middleware::Base - HTTP_METHODS_TO_UPPERCASE = %w[connect delete get head options patch post put trace].each_with_object({}) do |method, hash| - uppercase_method = method.upcase - hash[method] = uppercase_method - hash[method.to_sym] = uppercase_method - hash[uppercase_method] = uppercase_method - end.freeze - - HTTP_METHODS_TO_SPAN_NAMES = HTTP_METHODS_TO_UPPERCASE.values.each_with_object({}) do |uppercase_method, hash| - hash[uppercase_method] ||= "HTTP #{uppercase_method}" - end.freeze - # Constant for the HTTP status range HTTP_STATUS_SUCCESS_RANGE = (100..399) def request_call(datum) return @stack.request_call(datum) if untraced?(datum) - http_method = HTTP_METHODS_TO_UPPERCASE[datum[:method]] + span_data = HttpHelper.span_attrs_for_old(datum[:method]) + attributes = { OpenTelemetry::SemanticConventions::Trace::HTTP_HOST => datum[:host], - OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => http_method, OpenTelemetry::SemanticConventions::Trace::HTTP_SCHEME => datum[:scheme], OpenTelemetry::SemanticConventions::Trace::HTTP_TARGET => datum[:path], OpenTelemetry::SemanticConventions::Trace::HTTP_URL => OpenTelemetry::Common::Utilities.cleanse_url(::Excon::Utils.request_uri(datum)), @@ -40,8 +29,8 @@ def request_call(datum) } peer_service = Excon::Instrumentation.instance.config[:peer_service] attributes[OpenTelemetry::SemanticConventions::Trace::PEER_SERVICE] = peer_service if peer_service - attributes.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) - span = tracer.start_span(HTTP_METHODS_TO_SPAN_NAMES[http_method], attributes: attributes, kind: :client) + attributes.merge!(span_data.attributes) + span = tracer.start_span(span_data.span_name, attributes: attributes, kind: :client) ctx = OpenTelemetry::Trace.context_with_span(span) datum[:otel_span] = span datum[:otel_token] = OpenTelemetry::Context.attach(ctx) diff --git a/instrumentation/excon/lib/opentelemetry/instrumentation/excon/middlewares/stable/tracer_middleware.rb b/instrumentation/excon/lib/opentelemetry/instrumentation/excon/middlewares/stable/tracer_middleware.rb index 384ed0462e..beda80cffd 100644 --- a/instrumentation/excon/lib/opentelemetry/instrumentation/excon/middlewares/stable/tracer_middleware.rb +++ b/instrumentation/excon/lib/opentelemetry/instrumentation/excon/middlewares/stable/tracer_middleware.rb @@ -11,37 +11,24 @@ module Middlewares module Stable # Excon middleware for instrumentation class TracerMiddleware < ::Excon::Middleware::Base - HTTP_METHODS_TO_UPPERCASE = %w[connect delete get head options patch post put trace].each_with_object({}) do |method, hash| - uppercase_method = method.upcase - hash[method] = uppercase_method - hash[method.to_sym] = uppercase_method - hash[uppercase_method] = uppercase_method - end.freeze - - HTTP_METHODS_TO_SPAN_NAMES = HTTP_METHODS_TO_UPPERCASE.values.each_with_object({}) do |uppercase_method, hash| - hash[uppercase_method] ||= uppercase_method - end.freeze - # Constant for the HTTP status range HTTP_STATUS_SUCCESS_RANGE = (100..399) def request_call(datum) return @stack.request_call(datum) if untraced?(datum) - http_method = HTTP_METHODS_TO_UPPERCASE[datum[:method]] - attributes = { - 'http.request.method' => http_method, - 'url.scheme' => datum[:scheme], - 'url.path' => datum[:path], - 'url.full' => OpenTelemetry::Common::Utilities.cleanse_url(::Excon::Utils.request_uri(datum)), - 'server.address' => datum[:hostname], - 'server.port' => datum[:port] - } + span_data = HttpHelper.span_attrs_for_stable(datum[:method]) + + attributes = { 'url.scheme' => datum[:scheme], + 'url.path' => datum[:path], + 'url.full' => OpenTelemetry::Common::Utilities.cleanse_url(::Excon::Utils.request_uri(datum)), + 'server.address' => datum[:hostname], + 'server.port' => datum[:port] } attributes['url.query'] = datum[:query] if datum[:query] peer_service = Excon::Instrumentation.instance.config[:peer_service] attributes[OpenTelemetry::SemanticConventions::Trace::PEER_SERVICE] = peer_service if peer_service - attributes.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) - span = tracer.start_span(HTTP_METHODS_TO_SPAN_NAMES[http_method], attributes: attributes, kind: :client) + attributes.merge!(span_data.attributes) + span = tracer.start_span(span_data.span_name, attributes: attributes, kind: :client) ctx = OpenTelemetry::Trace.context_with_span(span) datum[:otel_span] = span datum[:otel_token] = OpenTelemetry::Context.attach(ctx) diff --git a/instrumentation/excon/lib/opentelemetry/instrumentation/excon/patches/old/socket.rb b/instrumentation/excon/lib/opentelemetry/instrumentation/excon/patches/old/socket.rb index 01e20a1e16..ef844a3fa1 100644 --- a/instrumentation/excon/lib/opentelemetry/instrumentation/excon/patches/old/socket.rb +++ b/instrumentation/excon/lib/opentelemetry/instrumentation/excon/patches/old/socket.rb @@ -27,7 +27,7 @@ def connect attributes = { OpenTelemetry::SemanticConventions::Trace::NET_PEER_NAME => conn_address, OpenTelemetry::SemanticConventions::Trace::NET_PEER_PORT => conn_port }.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) if is_a?(::Excon::SSLSocket) && @data[:proxy] - span_name = 'HTTP CONNECT' + span_name = 'CONNECT' span_kind = :client else span_name = 'connect' diff --git a/instrumentation/excon/test/opentelemetry/instrumentation/excon/dup/instrumentation_test.rb b/instrumentation/excon/test/opentelemetry/instrumentation/excon/dup/instrumentation_test.rb index 5fde02176d..cbdcaa1ef1 100644 --- a/instrumentation/excon/test/opentelemetry/instrumentation/excon/dup/instrumentation_test.rb +++ b/instrumentation/excon/test/opentelemetry/instrumentation/excon/dup/instrumentation_test.rb @@ -78,6 +78,34 @@ _(span.attributes['http.method']).must_equal 'GET' end + it 'handles unknown HTTP method' do + stub_request(:purge, 'http://example.com/purge').to_return(status: 200) + + Excon.new('http://example.com/purge').request(method: 'PURGE') + + _(exporter.finished_spans.size).must_equal 1 + _(span.name).must_equal 'HTTP' + # old semconv + _(span.attributes['http.host']).must_equal 'example.com' + _(span.attributes['http.method']).must_equal '_OTHER' + _(span.attributes['http.scheme']).must_equal 'http' + _(span.attributes['http.status_code']).must_equal 200 + _(span.attributes['http.target']).must_equal '/purge' + _(span.attributes['http.url']).must_equal 'http://example.com/purge' + # stable semconv + _(span.attributes['http.request.method']).must_equal '_OTHER' + _(span.attributes['http.request.method_original']).must_equal 'PURGE' + _(span.attributes['url.scheme']).must_equal 'http' + _(span.attributes['http.response.status_code']).must_equal 200 + _(span.attributes['url.path']).must_equal '/purge' + _(span.attributes['url.full']).must_equal 'http://example.com/purge' + assert_requested( + :purge, + 'http://example.com/purge', + headers: { 'Traceparent' => "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" } + ) + end + it 'after request with failure code' do Excon.get('http://example.com/failure') diff --git a/instrumentation/excon/test/opentelemetry/instrumentation/excon/old/instrumentation_test.rb b/instrumentation/excon/test/opentelemetry/instrumentation/excon/old/instrumentation_test.rb index 6842834a94..5c38e79ff6 100644 --- a/instrumentation/excon/test/opentelemetry/instrumentation/excon/old/instrumentation_test.rb +++ b/instrumentation/excon/test/opentelemetry/instrumentation/excon/old/instrumentation_test.rb @@ -49,7 +49,7 @@ Excon.get('http://example.com/success') _(exporter.finished_spans.size).must_equal 1 - _(span.name).must_equal 'HTTP GET' + _(span.name).must_equal 'GET' _(span.attributes['http.host']).must_equal 'example.com' _(span.attributes['http.method']).must_equal 'GET' _(span.attributes['http.scheme']).must_equal 'http' @@ -69,11 +69,31 @@ _(span.attributes['http.method']).must_equal 'GET' end + it 'handles unknown HTTP method' do + stub_request(:purge, 'http://example.com/purge').to_return(status: 200) + + Excon.new('http://example.com/purge').request(method: 'PURGE') + + _(exporter.finished_spans.size).must_equal 1 + _(span.name).must_equal 'HTTP' + _(span.attributes['http.host']).must_equal 'example.com' + _(span.attributes['http.method']).must_equal '_OTHER' + _(span.attributes['http.scheme']).must_equal 'http' + _(span.attributes['http.status_code']).must_equal 200 + _(span.attributes['http.target']).must_equal '/purge' + _(span.attributes['http.url']).must_equal 'http://example.com/purge' + assert_requested( + :purge, + 'http://example.com/purge', + headers: { 'Traceparent' => "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" } + ) + end + it 'after request with failure code' do Excon.get('http://example.com/failure') _(exporter.finished_spans.size).must_equal 1 - _(span.name).must_equal 'HTTP GET' + _(span.name).must_equal 'GET' _(span.attributes['http.host']).must_equal 'example.com' _(span.attributes['http.method']).must_equal 'GET' _(span.attributes['http.scheme']).must_equal 'http' @@ -93,7 +113,7 @@ end.must_raise Excon::Error::Timeout _(exporter.finished_spans.size).must_equal 1 - _(span.name).must_equal 'HTTP GET' + _(span.name).must_equal 'GET' _(span.attributes['http.host']).must_equal 'example.com' _(span.attributes['http.method']).must_equal 'GET' _(span.attributes['http.scheme']).must_equal 'http' @@ -123,7 +143,7 @@ end _(exporter.finished_spans.size).must_equal 1 - _(span.name).must_equal 'HTTP GET' + _(span.name).must_equal 'GET' _(span.attributes['http.host']).must_equal 'example.com' _(span.attributes['http.method']).must_equal 'OVERRIDE' _(span.attributes['http.scheme']).must_equal 'http' @@ -204,7 +224,7 @@ Excon.get('http://example.com/body') _(exporter.finished_spans.size).must_equal 1 - _(span.name).must_equal 'HTTP GET' + _(span.name).must_equal 'GET' _(span.attributes['http.host']).must_equal 'example.com' _(span.attributes['http.method']).must_equal 'GET' end @@ -289,7 +309,7 @@ _(-> { Excon.get('https://localhost/', proxy: 'https://proxy_user:proxy_pass@localhost') }).must_raise(Excon::Error::Socket) _(exporter.finished_spans.size).must_equal(3) - _(span.name).must_equal 'HTTP CONNECT' + _(span.name).must_equal 'CONNECT' _(span.kind).must_equal(:client) _(span.attributes['net.peer.name']).must_equal('localhost') _(span.attributes['net.peer.port']).must_equal(443) @@ -320,7 +340,7 @@ def assert_http_spans(scheme: 'http', host: 'localhost', port: nil, target: '/', exception: nil) exporter.finished_spans[1..].each do |http_span| - _(http_span.name).must_equal 'HTTP GET' + _(http_span.name).must_equal 'GET' _(http_span.attributes['http.host']).must_equal host _(http_span.attributes['http.method']).must_equal 'GET' _(http_span.attributes['http.scheme']).must_equal scheme diff --git a/instrumentation/excon/test/opentelemetry/instrumentation/excon/stable/instrumentation_test.rb b/instrumentation/excon/test/opentelemetry/instrumentation/excon/stable/instrumentation_test.rb index 46695aa6fc..bd27a5e7d4 100644 --- a/instrumentation/excon/test/opentelemetry/instrumentation/excon/stable/instrumentation_test.rb +++ b/instrumentation/excon/test/opentelemetry/instrumentation/excon/stable/instrumentation_test.rb @@ -71,6 +71,26 @@ _(span.attributes['http.request.method']).must_equal 'GET' end + it 'handles unknown HTTP method' do + stub_request(:purge, 'http://example.com/purge').to_return(status: 200) + + Excon.new('http://example.com/purge').request(method: 'PURGE') + + _(exporter.finished_spans.size).must_equal 1 + _(span.name).must_equal 'HTTP' + _(span.attributes['http.request.method']).must_equal '_OTHER' + _(span.attributes['http.request.method_original']).must_equal 'PURGE' + _(span.attributes['url.scheme']).must_equal 'http' + _(span.attributes['http.response.status_code']).must_equal 200 + _(span.attributes['url.path']).must_equal '/purge' + _(span.attributes['url.full']).must_equal 'http://example.com/purge' + assert_requested( + :purge, + 'http://example.com/purge', + headers: { 'Traceparent' => "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" } + ) + end + it 'after request with failure code' do Excon.get('http://example.com/failure') diff --git a/instrumentation/excon/test/test_helper.rb b/instrumentation/excon/test/test_helper.rb index 8d79d20b1b..c85b970b7a 100644 --- a/instrumentation/excon/test/test_helper.rb +++ b/instrumentation/excon/test/test_helper.rb @@ -9,6 +9,7 @@ Bundler.require(:default, :development, :test) require 'minitest/autorun' +require 'rspec/mocks/minitest_integration' require 'webmock/minitest' # global opentelemetry-sdk setup: diff --git a/instrumentation/faraday/Gemfile b/instrumentation/faraday/Gemfile index fdeae6f37b..5717435a9d 100644 --- a/instrumentation/faraday/Gemfile +++ b/instrumentation/faraday/Gemfile @@ -14,6 +14,7 @@ group :test do gem 'minitest', '~> 5.0' gem 'opentelemetry-sdk', '~> 1.1' gem 'opentelemetry-test-helpers', '~> 0.3' + gem 'rspec-mocks' gem 'rubocop', '~> 1.81.1' gem 'rubocop-performance', '~> 1.26.0' gem 'simplecov', '~> 0.22.0' diff --git a/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday.rb b/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday.rb index 90356f5185..8c4c1df6ab 100644 --- a/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday.rb +++ b/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday.rb @@ -16,5 +16,6 @@ module Faraday end end +require_relative 'faraday/http_helper' require_relative 'faraday/instrumentation' require_relative 'faraday/version' diff --git a/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/http_helper.rb b/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/http_helper.rb new file mode 100644 index 0000000000..9cd2d55056 --- /dev/null +++ b/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/http_helper.rb @@ -0,0 +1,130 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Instrumentation + module Faraday + # Utility module for HTTP-related helper methods + # @api private + module HttpHelper + # Lightweight struct to hold span creation attributes + SpanCreationAttributes = Struct.new(:span_name, :attributes, keyword_init: true) + + # Pre-computed mapping to avoid string allocations during normalization + METHOD_CACHE = { + 'CONNECT' => 'CONNECT', + 'DELETE' => 'DELETE', + 'GET' => 'GET', + 'HEAD' => 'HEAD', + 'OPTIONS' => 'OPTIONS', + 'PATCH' => 'PATCH', + 'POST' => 'POST', + 'PUT' => 'PUT', + 'TRACE' => 'TRACE', + 'connect' => 'CONNECT', + 'delete' => 'DELETE', + 'get' => 'GET', + 'head' => 'HEAD', + 'options' => 'OPTIONS', + 'patch' => 'PATCH', + 'post' => 'POST', + 'put' => 'PUT', + 'trace' => 'TRACE', + :connect => 'CONNECT', + :delete => 'DELETE', + :get => 'GET', + :head => 'HEAD', + :options => 'OPTIONS', + :patch => 'PATCH', + :post => 'POST', + :put => 'PUT', + :trace => 'TRACE' + }.freeze + + private_constant :METHOD_CACHE + + module_function + + # Prepares span data using old semantic conventions + # @param method [String, Symbol] The HTTP method + # @return [SpanCreationAttributes] struct containing span_name and attributes hash + def span_attrs_for_old(method) + client_context_attrs = OpenTelemetry::Common::HTTP::ClientContext.attributes + normalized = METHOD_CACHE[method] + attributes = client_context_attrs.dup + + # Determine base span name and method value + if normalized + span_name = normalized + method_value = normalized + else + span_name = 'HTTP' + method_value = '_OTHER' + end + + attributes['http.method'] ||= method_value + + SpanCreationAttributes.new(span_name: span_name, attributes: attributes) + end + + # Prepares span data using stable semantic conventions + # @param method [String, Symbol] The HTTP method + # @return [SpanCreationAttributes] struct containing span_name and attributes hash + def span_attrs_for_stable(method) + client_context_attrs = OpenTelemetry::Common::HTTP::ClientContext.attributes + url_template = client_context_attrs['url.template'] + normalized = METHOD_CACHE[method] + attributes = client_context_attrs.dup + + # Determine base span name and method value + if normalized + base_name = normalized + method_value = normalized + original = nil + else + base_name = 'HTTP' + method_value = '_OTHER' + original = method.to_s + end + + span_name = url_template ? "#{base_name} #{url_template}" : base_name + attributes['http.request.method'] ||= method_value + attributes['http.request.method_original'] ||= original if original + + SpanCreationAttributes.new(span_name: span_name, attributes: attributes) + end + + # Prepares span data using both old and stable semantic conventions + # @param method [String, Symbol] The HTTP method + # @return [SpanCreationAttributes] struct containing span_name and attributes hash + def span_attrs_for_dup(method) + client_context_attrs = OpenTelemetry::Common::HTTP::ClientContext.attributes + url_template = client_context_attrs['url.template'] + normalized = METHOD_CACHE[method] + attributes = client_context_attrs.dup + + # Determine base span name and method value + if normalized + base_name = normalized + method_value = normalized + original = nil + else + base_name = 'HTTP' + method_value = '_OTHER' + original = method.to_s + end + + span_name = url_template ? "#{base_name} #{url_template}" : base_name + attributes['http.method'] ||= method_value + attributes['http.request.method'] ||= method_value + attributes['http.request.method_original'] ||= original if original + + SpanCreationAttributes.new(span_name: span_name, attributes: attributes) + end + end + end + end +end diff --git a/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/middlewares/dup/tracer_middleware.rb b/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/middlewares/dup/tracer_middleware.rb index b1de1d7521..ab8391cf88 100644 --- a/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/middlewares/dup/tracer_middleware.rb +++ b/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/middlewares/dup/tracer_middleware.rb @@ -12,32 +12,20 @@ module Dup # TracerMiddleware propagates context and instruments Faraday requests # by way of its middleware system class TracerMiddleware < ::Faraday::Middleware - HTTP_METHODS_SYMBOL_TO_STRING = { - connect: 'CONNECT', - delete: 'DELETE', - get: 'GET', - head: 'HEAD', - options: 'OPTIONS', - patch: 'PATCH', - post: 'POST', - put: 'PUT', - trace: 'TRACE' - }.freeze - # Constant for the HTTP status range HTTP_STATUS_SUCCESS_RANGE = (100..399) def call(env) - http_method = HTTP_METHODS_SYMBOL_TO_STRING[env.method] + span_data = HttpHelper.span_attrs_for_dup(env.method) + config = Faraday::Instrumentation.instance.config - attributes = span_creation_attributes( - http_method: http_method, url: env.url, config: config - ) + attributes = span_creation_attributes(url: env.url, config: config) + attributes.merge!(span_data.attributes) OpenTelemetry::Common::HTTP::ClientContext.with_attributes(attributes) do |attrs, _| tracer.in_span( - http_method, attributes: attrs, kind: config.fetch(:span_kind) + span_data.span_name, attributes: attrs, kind: config.fetch(:span_kind) ) do |span| OpenTelemetry.propagation.inject(env.request_headers) @@ -58,11 +46,9 @@ def call(env) private - def span_creation_attributes(http_method:, url:, config:) + def span_creation_attributes(url:, config:) cleansed_url = OpenTelemetry::Common::Utilities.cleanse_url(url.to_s) attrs = { - 'http.method' => http_method, - 'http.request.method' => http_method, 'http.url' => cleansed_url, 'url.full' => cleansed_url, 'faraday.adapter.name' => app.class.name @@ -73,9 +59,7 @@ def span_creation_attributes(http_method:, url:, config:) end attrs['peer.service'] = config[:peer_service] if config[:peer_service] - attrs.merge!( - OpenTelemetry::Common::HTTP::ClientContext.attributes - ) + attrs end # Versions prior to 1.0 do not define an accessor for app diff --git a/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/middlewares/old/tracer_middleware.rb b/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/middlewares/old/tracer_middleware.rb index 7b263a2f9e..a51c91087a 100644 --- a/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/middlewares/old/tracer_middleware.rb +++ b/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/middlewares/old/tracer_middleware.rb @@ -12,32 +12,20 @@ module Old # TracerMiddleware propagates context and instruments Faraday requests # by way of its middleware system class TracerMiddleware < ::Faraday::Middleware - HTTP_METHODS_SYMBOL_TO_STRING = { - connect: 'CONNECT', - delete: 'DELETE', - get: 'GET', - head: 'HEAD', - options: 'OPTIONS', - patch: 'PATCH', - post: 'POST', - put: 'PUT', - trace: 'TRACE' - }.freeze - # Constant for the HTTP status range HTTP_STATUS_SUCCESS_RANGE = (100..399) def call(env) - http_method = HTTP_METHODS_SYMBOL_TO_STRING[env.method] + span_data = HttpHelper.span_attrs_for_old(env.method) + config = Faraday::Instrumentation.instance.config - attributes = span_creation_attributes( - http_method: http_method, url: env.url, config: config - ) + attributes = span_creation_attributes(url: env.url, config: config) + attributes.merge!(span_data.attributes) OpenTelemetry::Common::HTTP::ClientContext.with_attributes(attributes) do |attrs, _| tracer.in_span( - "HTTP #{http_method}", attributes: attrs, kind: config.fetch(:span_kind) + span_data.span_name, attributes: attrs, kind: config.fetch(:span_kind) ) do |span| OpenTelemetry.propagation.inject(env.request_headers) @@ -58,18 +46,15 @@ def call(env) private - def span_creation_attributes(http_method:, url:, config:) + def span_creation_attributes(url:, config:) attrs = { - 'http.method' => http_method, 'http.url' => OpenTelemetry::Common::Utilities.cleanse_url(url.to_s), 'faraday.adapter.name' => app.class.name } attrs['net.peer.name'] = url.host if url.host attrs['peer.service'] = config[:peer_service] if config[:peer_service] - attrs.merge!( - OpenTelemetry::Common::HTTP::ClientContext.attributes - ) + attrs end # Versions prior to 1.0 do not define an accessor for app diff --git a/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/middlewares/stable/tracer_middleware.rb b/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/middlewares/stable/tracer_middleware.rb index 4f169e27d6..59531049cf 100644 --- a/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/middlewares/stable/tracer_middleware.rb +++ b/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/middlewares/stable/tracer_middleware.rb @@ -12,32 +12,20 @@ module Stable # TracerMiddleware propagates context and instruments Faraday requests # by way of its middleware system class TracerMiddleware < ::Faraday::Middleware - HTTP_METHODS_SYMBOL_TO_STRING = { - connect: 'CONNECT', - delete: 'DELETE', - get: 'GET', - head: 'HEAD', - options: 'OPTIONS', - patch: 'PATCH', - post: 'POST', - put: 'PUT', - trace: 'TRACE' - }.freeze - # Constant for the HTTP status range HTTP_STATUS_SUCCESS_RANGE = (100..399) def call(env) - http_method = HTTP_METHODS_SYMBOL_TO_STRING[env.method] + span_data = HttpHelper.span_attrs_for_stable(env.method) + config = Faraday::Instrumentation.instance.config - attributes = span_creation_attributes( - http_method: http_method, url: env.url, config: config - ) + attributes = span_creation_attributes(url: env.url, config: config) + attributes.merge!(span_data.attributes) OpenTelemetry::Common::HTTP::ClientContext.with_attributes(attributes) do |attrs, _| tracer.in_span( - http_method, attributes: attrs, kind: config.fetch(:span_kind) + span_data.span_name, attributes: attrs, kind: config.fetch(:span_kind) ) do |span| OpenTelemetry.propagation.inject(env.request_headers) @@ -58,18 +46,15 @@ def call(env) private - def span_creation_attributes(http_method:, url:, config:) + def span_creation_attributes(url:, config:) attrs = { - 'http.request.method' => http_method, 'url.full' => OpenTelemetry::Common::Utilities.cleanse_url(url.to_s), 'faraday.adapter.name' => app.class.name } attrs['server.address'] = url.host if url.host attrs['peer.service'] = config[:peer_service] if config[:peer_service] - attrs.merge!( - OpenTelemetry::Common::HTTP::ClientContext.attributes - ) + attrs end # Versions prior to 1.0 do not define an accessor for app diff --git a/instrumentation/faraday/test/opentelemetry/instrumentation/faraday/middlewares/dup/tracer_middleware_test.rb b/instrumentation/faraday/test/opentelemetry/instrumentation/faraday/middlewares/dup/tracer_middleware_test.rb index 99348cf31b..73c96de86c 100644 --- a/instrumentation/faraday/test/opentelemetry/instrumentation/faraday/middlewares/dup/tracer_middleware_test.rb +++ b/instrumentation/faraday/test/opentelemetry/instrumentation/faraday/middlewares/dup/tracer_middleware_test.rb @@ -96,6 +96,37 @@ ) end + it 'handles unknown http method' do + # Stub Faraday::Connection::METHODS to include :purge + stub_const('Faraday::Connection::METHODS', Faraday::Connection::METHODS + [:purge]) + + # Create a new client - Faraday test adapter will accept any stubbed method through method_missing + purge_client = Faraday.new('http://example.com') do |builder| + builder.adapter(:test) do |stub| + # Use send to define the purge stub since the method doesn't exist yet + stub.send(:new_stub, :purge, '/purge') { |_| [200, {}, 'OK'] } + end + end + + response = purge_client.run_request(:purge, '/purge', nil, nil) + + _(span.name).must_equal 'HTTP' + # old semantic conventions + _(span.attributes['http.method']).must_equal '_OTHER' + _(span.attributes['http.status_code']).must_equal 200 + _(span.attributes['http.url']).must_equal 'http://example.com/purge' + _(span.attributes['net.peer.name']).must_equal 'example.com' + # stable semantic conventions + _(span.attributes['http.request.method']).must_equal '_OTHER' + _(span.attributes['http.request.method_original']).must_equal 'purge' + _(span.attributes['http.response.status_code']).must_equal 200 + _(span.attributes['url.full']).must_equal 'http://example.com/purge' + _(span.attributes['server.address']).must_equal 'example.com' + _(response.env.request_headers['Traceparent']).must_equal( + "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" + ) + end + it 'merges http client attributes' do client_context_attrs = { 'test.attribute' => 'test.value', 'http.method' => 'OVERRIDE', 'http.request.method' => 'OVERRIDE' diff --git a/instrumentation/faraday/test/opentelemetry/instrumentation/faraday/middlewares/old/tracer_middleware_test.rb b/instrumentation/faraday/test/opentelemetry/instrumentation/faraday/middlewares/old/tracer_middleware_test.rb index 4f88b61b0c..c301789a18 100644 --- a/instrumentation/faraday/test/opentelemetry/instrumentation/faraday/middlewares/old/tracer_middleware_test.rb +++ b/instrumentation/faraday/test/opentelemetry/instrumentation/faraday/middlewares/old/tracer_middleware_test.rb @@ -47,7 +47,7 @@ it 'has http 200 attributes' do response = client.get('/success') - _(span.name).must_equal 'HTTP GET' + _(span.name).must_equal 'GET' _(span.attributes['http.method']).must_equal 'GET' _(span.attributes['http.status_code']).must_equal 200 _(span.attributes['http.url']).must_equal 'http://example.com/success' @@ -60,7 +60,7 @@ it 'has http.status_code 404' do response = client.get('/not_found') - _(span.name).must_equal 'HTTP GET' + _(span.name).must_equal 'GET' _(span.attributes['http.method']).must_equal 'GET' _(span.attributes['http.status_code']).must_equal 404 _(span.attributes['http.url']).must_equal 'http://example.com/not_found' @@ -73,7 +73,7 @@ it 'has http.status_code 500' do response = client.get('/failure') - _(span.name).must_equal 'HTTP GET' + _(span.name).must_equal 'GET' _(span.attributes['http.method']).must_equal 'GET' _(span.attributes['http.status_code']).must_equal 500 _(span.attributes['http.url']).must_equal 'http://example.com/failure' @@ -83,6 +83,31 @@ ) end + it 'handles unknown http method' do + # Stub Faraday::Connection::METHODS to include :purge + stub_const('Faraday::Connection::METHODS', Faraday::Connection::METHODS + [:purge]) + + # Create a new client - Faraday test adapter will accept any stubbed method through method_missing + purge_client = Faraday.new('http://example.com') do |builder| + builder.adapter(:test) do |stub| + # Use send to define the purge stub since the method doesn't exist yet + stub.send(:new_stub, :purge, '/purge') { |_| [200, {}, 'OK'] } + end + end + + response = purge_client.run_request(:purge, '/purge', nil, nil) + + _(span.name).must_equal 'HTTP' + _(span.attributes['http.method']).must_equal '_OTHER' + _(span.attributes['http.method.original']).must_be_nil + _(span.attributes['http.status_code']).must_equal 200 + _(span.attributes['http.url']).must_equal 'http://example.com/purge' + _(span.attributes['net.peer.name']).must_equal 'example.com' + _(response.env.request_headers['Traceparent']).must_equal( + "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" + ) + end + it 'merges http client attributes' do client_context_attrs = { 'test.attribute' => 'test.value', 'http.method' => 'OVERRIDE' @@ -91,7 +116,7 @@ client.get('/success') end - _(span.name).must_equal 'HTTP GET' + _(span.name).must_equal 'GET' _(span.attributes['http.method']).must_equal 'OVERRIDE' _(span.attributes['http.status_code']).must_equal 200 _(span.attributes['http.url']).must_equal 'http://example.com/success' @@ -181,7 +206,7 @@ it 'omits missing attributes' do response = client.get('/success') - _(span.name).must_equal 'HTTP GET' + _(span.name).must_equal 'GET' _(span.attributes['http.method']).must_equal 'GET' _(span.attributes['http.status_code']).must_equal 200 _(span.attributes['http.url']).must_equal 'http:/success' diff --git a/instrumentation/faraday/test/opentelemetry/instrumentation/faraday/middlewares/stable/tracer_middleware_test.rb b/instrumentation/faraday/test/opentelemetry/instrumentation/faraday/middlewares/stable/tracer_middleware_test.rb index 5435518d95..37bd67c020 100644 --- a/instrumentation/faraday/test/opentelemetry/instrumentation/faraday/middlewares/stable/tracer_middleware_test.rb +++ b/instrumentation/faraday/test/opentelemetry/instrumentation/faraday/middlewares/stable/tracer_middleware_test.rb @@ -84,6 +84,31 @@ ) end + it 'handles unknown http method' do + # Stub Faraday::Connection::METHODS to include :purge + stub_const('Faraday::Connection::METHODS', Faraday::Connection::METHODS + [:purge]) + + # Create a new client - Faraday test adapter will accept any stubbed method through method_missing + purge_client = Faraday.new('http://example.com') do |builder| + builder.adapter(:test) do |stub| + # Use send to define the purge stub since the method doesn't exist yet + stub.send(:new_stub, :purge, '/purge') { |_| [200, {}, 'OK'] } + end + end + + response = purge_client.run_request(:purge, '/purge', nil, nil) + + _(span.name).must_equal 'HTTP' + _(span.attributes['http.request.method']).must_equal '_OTHER' + _(span.attributes['http.request.method_original']).must_equal 'purge' + _(span.attributes['http.response.status_code']).must_equal 200 + _(span.attributes['url.full']).must_equal 'http://example.com/purge' + _(span.attributes['server.address']).must_equal 'example.com' + _(response.env.request_headers['Traceparent']).must_equal( + "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" + ) + end + it 'merges http client attributes' do client_context_attrs = { 'test.attribute' => 'test.value', 'http.request.method' => 'OVERRIDE' diff --git a/instrumentation/faraday/test/test_helper.rb b/instrumentation/faraday/test/test_helper.rb index 49b3ed979c..c059a313c8 100644 --- a/instrumentation/faraday/test/test_helper.rb +++ b/instrumentation/faraday/test/test_helper.rb @@ -9,6 +9,7 @@ Bundler.require(:default, :development, :test) require 'minitest/autorun' +require 'rspec/mocks/minitest_integration' require 'webmock/minitest' require 'opentelemetry-instrumentation-faraday' diff --git a/instrumentation/http/lib/opentelemetry/instrumentation/http.rb b/instrumentation/http/lib/opentelemetry/instrumentation/http.rb index 092425822b..c1b449f4a2 100644 --- a/instrumentation/http/lib/opentelemetry/instrumentation/http.rb +++ b/instrumentation/http/lib/opentelemetry/instrumentation/http.rb @@ -15,5 +15,6 @@ module HTTP end end +require_relative 'http/http_helper' require_relative 'http/instrumentation' require_relative 'http/version' diff --git a/instrumentation/http/lib/opentelemetry/instrumentation/http/http_helper.rb b/instrumentation/http/lib/opentelemetry/instrumentation/http/http_helper.rb new file mode 100644 index 0000000000..d9305e812d --- /dev/null +++ b/instrumentation/http/lib/opentelemetry/instrumentation/http/http_helper.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Instrumentation + module HTTP + # Module for normalizing HTTP methods + # @api private + module HttpHelper + # Lightweight struct to hold span creation attributes + SpanCreationAttributes = Struct.new(:span_name, :attributes, keyword_init: true) + + # Pre-computed mapping to avoid string allocations during normalization + METHOD_CACHE = { + 'CONNECT' => 'CONNECT', + 'DELETE' => 'DELETE', + 'GET' => 'GET', + 'HEAD' => 'HEAD', + 'OPTIONS' => 'OPTIONS', + 'PATCH' => 'PATCH', + 'POST' => 'POST', + 'PUT' => 'PUT', + 'TRACE' => 'TRACE', + 'connect' => 'CONNECT', + 'delete' => 'DELETE', + 'get' => 'GET', + 'head' => 'HEAD', + 'options' => 'OPTIONS', + 'patch' => 'PATCH', + 'post' => 'POST', + 'put' => 'PUT', + 'trace' => 'TRACE', + :connect => 'CONNECT', + :delete => 'DELETE', + :get => 'GET', + :head => 'HEAD', + :options => 'OPTIONS', + :patch => 'PATCH', + :post => 'POST', + :put => 'PUT', + :trace => 'TRACE' + }.freeze + + private_constant :METHOD_CACHE + + # Prepares span data using old semantic conventions + # @param method [String, Symbol] The HTTP method + # @return [SpanCreationAttributes] struct containing span_name and attributes hash + def self.span_attrs_for_old(method) + client_context_attrs = OpenTelemetry::Common::HTTP::ClientContext.attributes + normalized = METHOD_CACHE[method] + attributes = client_context_attrs.dup + + # Determine base span name and method value + if normalized + span_name = normalized + method_value = normalized + else + span_name = 'HTTP' + method_value = '_OTHER' + end + + attributes['http.method'] ||= method_value + + SpanCreationAttributes.new(span_name: span_name, attributes: attributes) + end + + # Prepares span data using stable semantic conventions + # @param method [String, Symbol] The HTTP method + # @return [SpanCreationAttributes] struct containing span_name and attributes hash + def self.span_attrs_for_stable(method) + client_context_attrs = OpenTelemetry::Common::HTTP::ClientContext.attributes + url_template = client_context_attrs['url.template'] + normalized = METHOD_CACHE[method] + attributes = client_context_attrs.dup + + # Determine base span name and method value + if normalized + base_name = normalized + method_value = normalized + original = nil + else + base_name = 'HTTP' + method_value = '_OTHER' + original = method.to_s + end + + span_name = url_template ? "#{base_name} #{url_template}" : base_name + attributes['http.request.method'] ||= method_value + attributes['http.request.method_original'] ||= original if original + + SpanCreationAttributes.new(span_name: span_name, attributes: attributes) + end + + # Prepares span data using both old and stable semantic conventions + # @param method [String, Symbol] The HTTP method + # @return [SpanCreationAttributes] struct containing span_name and attributes hash + def self.span_attrs_for_dup(method) + client_context_attrs = OpenTelemetry::Common::HTTP::ClientContext.attributes + url_template = client_context_attrs['url.template'] + normalized = METHOD_CACHE[method] + attributes = client_context_attrs.dup + + # Determine base span name and method value + if normalized + base_name = normalized + method_value = normalized + original = nil + else + base_name = 'HTTP' + method_value = '_OTHER' + original = method.to_s + end + + span_name = url_template ? "#{base_name} #{url_template}" : base_name + attributes['http.method'] ||= method_value + attributes['http.request.method'] ||= method_value + attributes['http.request.method_original'] ||= original if original + + SpanCreationAttributes.new(span_name: span_name, attributes: attributes) + end + end + end + end +end diff --git a/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/dup/client.rb b/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/dup/client.rb index c32717525f..9fabc48418 100644 --- a/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/dup/client.rb +++ b/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/dup/client.rb @@ -16,20 +16,19 @@ module Client HTTP_STATUS_SUCCESS_RANGE = (100..399) def perform(req, options) + span_data = HttpHelper.span_attrs_for_dup(req.verb) + uri = req.uri - request_method = req.verb.to_s.upcase - span_name = create_request_span_name(request_method, uri.path) + span_name = create_span_name(span_data, uri.path) attributes = { # old semconv - 'http.method' => request_method, 'http.scheme' => uri.scheme, 'http.target' => uri.path, 'http.url' => "#{uri.scheme}://#{uri.host}", 'net.peer.name' => uri.host, 'net.peer.port' => uri.port, # stable semconv - 'http.request.method' => request_method, 'url.scheme' => uri.scheme, 'url.path' => uri.path, 'url.full' => "#{uri.scheme}://#{uri.host}", @@ -37,7 +36,7 @@ def perform(req, options) 'server.port' => uri.port } attributes['url.query'] = uri.query unless uri.query.nil? - attributes.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) + attributes.merge!(span_data.attributes) tracer.in_span(span_name, attributes: attributes, kind: :client) do |span| OpenTelemetry.propagation.inject(req.headers) @@ -62,15 +61,19 @@ def annotate_span_with_response!(span, response) span.status = OpenTelemetry::Trace::Status.error unless HTTP_STATUS_SUCCESS_RANGE.cover?(status_code) end - def create_request_span_name(request_method, request_path) + def create_span_name(span_data, request_path) + default_span_name = span_data.span_name + if (implementation = config[:span_name_formatter]) - updated_span_name = implementation.call(request_method, request_path) - updated_span_name.is_a?(String) ? updated_span_name : request_method.to_s + # Extract the HTTP method from attributes (old semconv key) + http_method = span_data.attributes['http.method'] || span_data.attributes['http.request.method'] + updated_span_name = implementation.call(http_method, request_path) + updated_span_name.is_a?(String) ? updated_span_name : default_span_name else - request_method.to_s + default_span_name end rescue StandardError - request_method.to_s + default_span_name end def tracer diff --git a/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/old/client.rb b/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/old/client.rb index 356a7caee9..568809a8f2 100644 --- a/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/old/client.rb +++ b/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/old/client.rb @@ -16,18 +16,18 @@ module Client HTTP_STATUS_SUCCESS_RANGE = (100..399) def perform(req, options) + span_data = HttpHelper.span_attrs_for_old(req.verb) + uri = req.uri - request_method = req.verb.to_s.upcase - span_name = create_request_span_name(request_method, uri.path) + span_name = create_span_name(span_data, uri.path) attributes = { - 'http.method' => request_method, 'http.scheme' => uri.scheme, 'http.target' => uri.path, 'http.url' => "#{uri.scheme}://#{uri.host}", 'net.peer.name' => uri.host, 'net.peer.port' => uri.port - }.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) + }.merge!(span_data.attributes) tracer.in_span(span_name, attributes: attributes, kind: :client) do |span| OpenTelemetry.propagation.inject(req.headers) @@ -51,15 +51,19 @@ def annotate_span_with_response!(span, response) span.status = OpenTelemetry::Trace::Status.error unless HTTP_STATUS_SUCCESS_RANGE.cover?(status_code) end - def create_request_span_name(request_method, request_path) + def create_span_name(span_data, request_path) + default_span_name = span_data.span_name + if (implementation = config[:span_name_formatter]) - updated_span_name = implementation.call(request_method, request_path) - updated_span_name.is_a?(String) ? updated_span_name : "HTTP #{request_method}" + # Extract the HTTP method from attributes + http_method = span_data.attributes[OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD] + updated_span_name = implementation.call(http_method, request_path) + updated_span_name.is_a?(String) ? updated_span_name : default_span_name else - "HTTP #{request_method}" + default_span_name end rescue StandardError - "HTTP #{request_method}" + default_span_name end def tracer diff --git a/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/stable/client.rb b/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/stable/client.rb index 51706faa2d..bfd056978c 100644 --- a/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/stable/client.rb +++ b/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/stable/client.rb @@ -16,20 +16,18 @@ module Client HTTP_STATUS_SUCCESS_RANGE = (100..399) def perform(req, options) + span_data = HttpHelper.span_attrs_for_stable(req.verb) + uri = req.uri - request_method = req.verb.to_s.upcase - span_name = create_request_span_name(request_method, uri.path) + span_name = create_span_name(span_data, uri.path) - attributes = { - 'http.request.method' => request_method, - 'url.scheme' => uri.scheme, - 'url.path' => uri.path, - 'url.full' => "#{uri.scheme}://#{uri.host}", - 'server.address' => uri.host, - 'server.port' => uri.port - } + attributes = { 'url.scheme' => uri.scheme, + 'url.path' => uri.path, + 'url.full' => "#{uri.scheme}://#{uri.host}", + 'server.address' => uri.host, + 'server.port' => uri.port } attributes['url.query'] = uri.query unless uri.query.nil? - attributes.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) + attributes.merge!(span_data.attributes) tracer.in_span(span_name, attributes: attributes, kind: :client) do |span| OpenTelemetry.propagation.inject(req.headers) @@ -53,15 +51,19 @@ def annotate_span_with_response!(span, response) span.status = OpenTelemetry::Trace::Status.error unless HTTP_STATUS_SUCCESS_RANGE.cover?(status_code) end - def create_request_span_name(request_method, request_path) + def create_span_name(span_data, request_path) + default_span_name = span_data.span_name + if (implementation = config[:span_name_formatter]) - updated_span_name = implementation.call(request_method, request_path) - updated_span_name.is_a?(String) ? updated_span_name : request_method.to_s + # Extract the HTTP method from attributes + http_method = span_data.attributes['http.request.method'] + updated_span_name = implementation.call(http_method, request_path) + updated_span_name.is_a?(String) ? updated_span_name : default_span_name else - request_method.to_s + default_span_name end rescue StandardError - request_method.to_s + default_span_name end def tracer diff --git a/instrumentation/http/test/instrumentation/http/patches/dup/client_test.rb b/instrumentation/http/test/instrumentation/http/patches/dup/client_test.rb index be0adb3a3a..079984b201 100644 --- a/instrumentation/http/test/instrumentation/http/patches/dup/client_test.rb +++ b/instrumentation/http/test/instrumentation/http/patches/dup/client_test.rb @@ -246,5 +246,31 @@ ) end end + + it 'traces a request with non-standard HTTP method' do + stub_request(:search, 'http://example.com/query').to_return(status: 200) + HTTP.request(:search, 'http://example.com/query') + + _(exporter.finished_spans.size).must_equal 1 + _(span.name).must_equal 'HTTP' + # Old semantic conventions + _(span.attributes['http.method']).must_equal '_OTHER' + _(span.attributes['http.status_code']).must_equal 200 + _(span.attributes['http.scheme']).must_equal 'http' + _(span.attributes['net.peer.name']).must_equal 'example.com' + _(span.attributes['http.target']).must_equal '/query' + # Stable semantic conventions + _(span.attributes['http.request.method']).must_equal '_OTHER' + _(span.attributes['http.request.method_original']).must_equal 'search' + _(span.attributes['http.response.status_code']).must_equal 200 + _(span.attributes['url.scheme']).must_equal 'http' + _(span.attributes['server.address']).must_equal 'example.com' + _(span.attributes['url.path']).must_equal '/query' + assert_requested( + :search, + 'http://example.com/query', + headers: { 'Traceparent' => "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" } + ) + end end end diff --git a/instrumentation/http/test/instrumentation/http/patches/old/client_test.rb b/instrumentation/http/test/instrumentation/http/patches/old/client_test.rb index 6579dc8b5d..d01b755ff1 100644 --- a/instrumentation/http/test/instrumentation/http/patches/old/client_test.rb +++ b/instrumentation/http/test/instrumentation/http/patches/old/client_test.rb @@ -47,7 +47,7 @@ HTTP.get('http://example.com/success') _(exporter.finished_spans.size).must_equal(1) - _(span.name).must_equal 'HTTP GET' + _(span.name).must_equal 'GET' _(span.attributes['http.method']).must_equal 'GET' _(span.attributes['http.scheme']).must_equal 'http' _(span.attributes['http.status_code']).must_equal 200 @@ -65,7 +65,7 @@ HTTP.post('http://example.com/failure') _(exporter.finished_spans.size).must_equal 1 - _(span.name).must_equal 'HTTP POST' + _(span.name).must_equal 'POST' _(span.attributes['http.method']).must_equal 'POST' _(span.attributes['http.scheme']).must_equal 'http' _(span.attributes['http.status_code']).must_equal 500 @@ -85,7 +85,7 @@ end.must_raise HTTP::TimeoutError _(exporter.finished_spans.size).must_equal 1 - _(span.name).must_equal 'HTTP GET' + _(span.name).must_equal 'GET' _(span.attributes['http.method']).must_equal 'GET' _(span.attributes['http.scheme']).must_equal 'https' _(span.attributes['http.status_code']).must_be_nil @@ -111,7 +111,7 @@ end _(exporter.finished_spans.size).must_equal 1 - _(span.name).must_equal 'HTTP GET' + _(span.name).must_equal 'GET' _(span.attributes['http.method']).must_equal 'GET' _(span.attributes['http.scheme']).must_equal 'http' _(span.attributes['http.status_code']).must_equal 200 @@ -169,7 +169,7 @@ end _(exporter.finished_spans.size).must_equal 1 - _(span.name).must_equal 'HTTP GET' + _(span.name).must_equal 'GET' _(span.attributes['http.method']).must_equal 'GET' _(span.attributes['http.scheme']).must_equal 'http' _(span.attributes['http.status_code']).must_equal 200 @@ -184,5 +184,23 @@ ) end end + + it 'traces a request with non-standard HTTP method' do + stub_request(:search, 'http://example.com/query').to_return(status: 200) + HTTP.request(:search, 'http://example.com/query') + + _(exporter.finished_spans.size).must_equal 1 + _(span.name).must_equal 'HTTP' + _(span.attributes['http.method']).must_equal '_OTHER' + _(span.attributes['http.status_code']).must_equal 200 + _(span.attributes['http.scheme']).must_equal 'http' + _(span.attributes['net.peer.name']).must_equal 'example.com' + _(span.attributes['http.target']).must_equal '/query' + assert_requested( + :search, + 'http://example.com/query', + headers: { 'Traceparent' => "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" } + ) + end end end diff --git a/instrumentation/http/test/instrumentation/http/patches/stable/client_test.rb b/instrumentation/http/test/instrumentation/http/patches/stable/client_test.rb index ef0fad1211..f2a28581b4 100644 --- a/instrumentation/http/test/instrumentation/http/patches/stable/client_test.rb +++ b/instrumentation/http/test/instrumentation/http/patches/stable/client_test.rb @@ -195,5 +195,24 @@ ) end end + + it 'traces a request with non-standard HTTP method' do + stub_request(:search, 'http://example.com/query').to_return(status: 200) + HTTP.request(:search, 'http://example.com/query') + + _(exporter.finished_spans.size).must_equal 1 + _(span.name).must_equal 'HTTP' + _(span.attributes['http.request.method']).must_equal '_OTHER' + _(span.attributes['http.request.method_original']).must_equal 'search' + _(span.attributes['http.response.status_code']).must_equal 200 + _(span.attributes['url.scheme']).must_equal 'http' + _(span.attributes['server.address']).must_equal 'example.com' + _(span.attributes['url.path']).must_equal '/query' + assert_requested( + :search, + 'http://example.com/query', + headers: { 'Traceparent' => "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" } + ) + end end end diff --git a/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client.rb b/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client.rb index 75f43750a3..aff148f58a 100644 --- a/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client.rb +++ b/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client.rb @@ -15,5 +15,6 @@ module HttpClient end end +require_relative 'http_client/http_helper' require_relative 'http_client/instrumentation' require_relative 'http_client/version' diff --git a/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/http_helper.rb b/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/http_helper.rb new file mode 100644 index 0000000000..036439eae3 --- /dev/null +++ b/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/http_helper.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Instrumentation + module HttpClient + # Module for normalizing HTTP methods + # @api private + module HttpHelper + # Lightweight struct to hold span creation attributes + SpanCreationAttributes = Struct.new(:span_name, :attributes, keyword_init: true) + + # Pre-computed mapping to avoid string allocations during normalization + METHOD_CACHE = { + 'CONNECT' => 'CONNECT', + 'DELETE' => 'DELETE', + 'GET' => 'GET', + 'HEAD' => 'HEAD', + 'OPTIONS' => 'OPTIONS', + 'PATCH' => 'PATCH', + 'POST' => 'POST', + 'PUT' => 'PUT', + 'TRACE' => 'TRACE', + 'connect' => 'CONNECT', + 'delete' => 'DELETE', + 'get' => 'GET', + 'head' => 'HEAD', + 'options' => 'OPTIONS', + 'patch' => 'PATCH', + 'post' => 'POST', + 'put' => 'PUT', + 'trace' => 'TRACE', + :connect => 'CONNECT', + :delete => 'DELETE', + :get => 'GET', + :head => 'HEAD', + :options => 'OPTIONS', + :patch => 'PATCH', + :post => 'POST', + :put => 'PUT', + :trace => 'TRACE' + }.freeze + + private_constant :METHOD_CACHE + + # Prepares span data using old semantic conventions + # @param method [String, Symbol] The HTTP method + # @return [SpanCreationAttributes] struct containing span_name and attributes hash + def self.span_attrs_for_old(method) + client_context_attrs = OpenTelemetry::Common::HTTP::ClientContext.attributes + normalized = METHOD_CACHE[method] + attributes = client_context_attrs.dup + + # Determine base span name and method value + if normalized + span_name = normalized + method_value = normalized + else + span_name = 'HTTP' + method_value = '_OTHER' + end + + attributes['http.method'] ||= method_value + + SpanCreationAttributes.new(span_name: span_name, attributes: attributes) + end + + # Prepares span data using stable semantic conventions + # @param method [String, Symbol] The HTTP method + # @return [SpanCreationAttributes] struct containing span_name and attributes hash + def self.span_attrs_for_stable(method) + client_context_attrs = OpenTelemetry::Common::HTTP::ClientContext.attributes + url_template = client_context_attrs['url.template'] + normalized = METHOD_CACHE[method] + attributes = client_context_attrs.dup + + # Determine base span name and method value + if normalized + base_name = normalized + method_value = normalized + original = nil + else + base_name = 'HTTP' + method_value = '_OTHER' + original = method.to_s + end + + span_name = url_template ? "#{base_name} #{url_template}" : base_name + attributes['http.request.method'] ||= method_value + attributes['http.request.method_original'] ||= original if original + + SpanCreationAttributes.new(span_name: span_name, attributes: attributes) + end + + # Prepares span data using both old and stable semantic conventions + # @param method [String, Symbol] The HTTP method + # @return [SpanCreationAttributes] struct containing span_name and attributes hash + def self.span_attrs_for_dup(method) + client_context_attrs = OpenTelemetry::Common::HTTP::ClientContext.attributes + url_template = client_context_attrs['url.template'] + normalized = METHOD_CACHE[method] + attributes = client_context_attrs.dup + + # Determine base span name and method value + if normalized + base_name = normalized + method_value = normalized + original = nil + else + base_name = 'HTTP' + method_value = '_OTHER' + original = method.to_s + end + + span_name = url_template ? "#{base_name} #{url_template}" : base_name + attributes['http.method'] ||= method_value + attributes['http.request.method'] ||= method_value + attributes['http.request.method_original'] ||= original if original + + SpanCreationAttributes.new(span_name: span_name, attributes: attributes) + end + end + end + end +end diff --git a/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/dup/client.rb b/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/dup/client.rb index 6113e8e5ea..3355c296a0 100644 --- a/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/dup/client.rb +++ b/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/dup/client.rb @@ -20,26 +20,25 @@ def do_get_block(req, proxy, conn, &) uri = req.header.request_uri url = "#{uri.scheme}://#{uri.host}" request_method = req.header.request_method + span_data = HttpHelper.span_attrs_for_dup(request_method) attributes = { - 'http.method' => request_method, 'http.scheme' => uri.scheme, 'http.target' => uri.path, 'http.url' => url, 'net.peer.name' => uri.host, 'net.peer.port' => uri.port, # stable semantic conventions - 'http.request.method' => request_method, 'url.scheme' => uri.scheme, 'url.path' => uri.path, 'url.full' => url, 'server.address' => uri.host, 'server.port' => uri.port - }.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) + }.merge!(span_data.attributes) attributes['url.query'] = uri.query unless uri.query.nil? - tracer.in_span(request_method, attributes: attributes, kind: :client) do |span| + tracer.in_span(span_data.span_name, attributes: attributes, kind: :client) do |span| OpenTelemetry.propagation.inject(req.header) super.tap do response = conn.pop diff --git a/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/old/client.rb b/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/old/client.rb index d6d15e6594..7cd158066b 100644 --- a/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/old/client.rb +++ b/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/old/client.rb @@ -21,16 +21,17 @@ def do_get_block(req, proxy, conn, &) url = "#{uri.scheme}://#{uri.host}" request_method = req.header.request_method + span_data = HttpHelper.span_attrs_for_old(request_method) + attributes = { - 'http.method' => request_method, 'http.scheme' => uri.scheme, 'http.target' => uri.path, 'http.url' => url, 'net.peer.name' => uri.host, 'net.peer.port' => uri.port - }.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) + }.merge!(span_data.attributes) - tracer.in_span("HTTP #{request_method}", attributes: attributes, kind: :client) do |span| + tracer.in_span(span_data.span_name, attributes: attributes, kind: :client) do |span| OpenTelemetry.propagation.inject(req.header) super.tap do response = conn.pop diff --git a/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/old/session.rb b/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/old/session.rb index 7309f0a4d9..f158c55e9d 100644 --- a/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/old/session.rb +++ b/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/old/session.rb @@ -16,7 +16,7 @@ def connect url = site.addr attributes = { 'http.url' => url }.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) - tracer.in_span('HTTP CONNECT', attributes: attributes) do + tracer.in_span('CONNECT', attributes: attributes) do super end end diff --git a/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/stable/client.rb b/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/stable/client.rb index df2bee0846..1698efffdb 100644 --- a/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/stable/client.rb +++ b/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/stable/client.rb @@ -21,18 +21,17 @@ def do_get_block(req, proxy, conn, &) url = "#{uri.scheme}://#{uri.host}" request_method = req.header.request_method - attributes = { - 'http.request.method' => request_method, - 'url.scheme' => uri.scheme, - 'url.path' => uri.path, - 'url.full' => url, - 'server.address' => uri.host, - 'server.port' => uri.port - }.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) + span_data = HttpHelper.span_attrs_for_stable(request_method) + + attributes = { 'url.scheme' => uri.scheme, + 'url.path' => uri.path, + 'url.full' => url, + 'server.address' => uri.host, + 'server.port' => uri.port }.merge!(span_data.attributes) attributes['url.query'] = uri.query unless uri.query.nil? - tracer.in_span(request_method, attributes: attributes, kind: :client) do |span| + tracer.in_span(span_data.span_name, attributes: attributes, kind: :client) do |span| OpenTelemetry.propagation.inject(req.header) super.tap do response = conn.pop diff --git a/instrumentation/http_client/test/instrumentation/http_client/patches/dup/client_test.rb b/instrumentation/http_client/test/instrumentation/http_client/patches/dup/client_test.rb index 3ed03d46c6..77aae6fb33 100644 --- a/instrumentation/http_client/test/instrumentation/http_client/patches/dup/client_test.rb +++ b/instrumentation/http_client/test/instrumentation/http_client/patches/dup/client_test.rb @@ -172,5 +172,31 @@ headers: { 'Traceparent' => "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" } ) end + + it 'traces a request with non-standard HTTP method' do + stub_request(:purge, 'http://example.com/cache').to_return(status: 200) + http = HTTPClient.new + http.request(:purge, 'http://example.com/cache') + + _(span.name).must_equal 'HTTP' + # old semantic conventions + _(span.attributes['http.method']).must_equal '_OTHER' + _(span.attributes['http.status_code']).must_equal 200 + _(span.attributes['http.scheme']).must_equal 'http' + _(span.attributes['net.peer.name']).must_equal 'example.com' + _(span.attributes['http.target']).must_equal '/cache' + # stable semantic conventions + _(span.attributes['http.request.method']).must_equal '_OTHER' + _(span.attributes['http.request.method_original']).must_equal 'PURGE' + _(span.attributes['http.response.status_code']).must_equal 200 + _(span.attributes['url.scheme']).must_equal 'http' + _(span.attributes['server.address']).must_equal 'example.com' + _(span.attributes['url.path']).must_equal '/cache' + assert_requested( + :purge, + 'http://example.com/cache', + headers: { 'Traceparent' => "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" } + ) + end end end diff --git a/instrumentation/http_client/test/instrumentation/http_client/patches/old/client_test.rb b/instrumentation/http_client/test/instrumentation/http_client/patches/old/client_test.rb index b008283091..c5bce23aad 100644 --- a/instrumentation/http_client/test/instrumentation/http_client/patches/old/client_test.rb +++ b/instrumentation/http_client/test/instrumentation/http_client/patches/old/client_test.rb @@ -41,7 +41,7 @@ http.get('http://example.com/success') _(exporter.finished_spans.size).must_equal(1) - _(span.name).must_equal 'HTTP GET' + _(span.name).must_equal 'GET' _(span.attributes['http.method']).must_equal 'GET' _(span.attributes['http.scheme']).must_equal 'http' _(span.attributes['http.status_code']).must_equal 200 @@ -61,7 +61,7 @@ http.post('http://example.com/failure') _(exporter.finished_spans.size).must_equal 1 - _(span.name).must_equal 'HTTP POST' + _(span.name).must_equal 'POST' _(span.attributes['http.method']).must_equal 'POST' _(span.attributes['http.scheme']).must_equal 'http' _(span.attributes['http.status_code']).must_equal 500 @@ -83,7 +83,7 @@ end.must_raise HTTPClient::TimeoutError _(exporter.finished_spans.size).must_equal 1 - _(span.name).must_equal 'HTTP GET' + _(span.name).must_equal 'GET' _(span.attributes['http.method']).must_equal 'GET' _(span.attributes['http.scheme']).must_equal 'https' _(span.attributes['http.status_code']).must_be_nil @@ -111,7 +111,7 @@ end _(exporter.finished_spans.size).must_equal 1 - _(span.name).must_equal 'HTTP GET' + _(span.name).must_equal 'GET' _(span.attributes['http.method']).must_equal 'GET' _(span.attributes['http.scheme']).must_equal 'http' _(span.attributes['http.status_code']).must_equal 200 @@ -125,5 +125,23 @@ headers: { 'Traceparent' => "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" } ) end + + it 'traces a request with non-standard HTTP method' do + stub_request(:purge, 'http://example.com/cache').to_return(status: 200) + http = HTTPClient.new + http.request(:purge, 'http://example.com/cache') + + _(span.name).must_equal 'HTTP' + _(span.attributes['http.method']).must_equal '_OTHER' + _(span.attributes['http.status_code']).must_equal 200 + _(span.attributes['http.scheme']).must_equal 'http' + _(span.attributes['net.peer.name']).must_equal 'example.com' + _(span.attributes['http.target']).must_equal '/cache' + assert_requested( + :purge, + 'http://example.com/cache', + headers: { 'Traceparent' => "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" } + ) + end end end diff --git a/instrumentation/http_client/test/instrumentation/http_client/patches/old/session_test.rb b/instrumentation/http_client/test/instrumentation/http_client/patches/old/session_test.rb index 73fadbd686..95826a9121 100644 --- a/instrumentation/http_client/test/instrumentation/http_client/patches/old/session_test.rb +++ b/instrumentation/http_client/test/instrumentation/http_client/patches/old/session_test.rb @@ -39,7 +39,7 @@ end _(exporter.finished_spans.size).must_equal(2) - _(span.name).must_equal 'HTTP CONNECT' + _(span.name).must_equal 'CONNECT' _(span.attributes['http.url']).must_match(%r{http://localhost:}) ensure WebMock.disable_net_connect! diff --git a/instrumentation/http_client/test/instrumentation/http_client/patches/stable/client_test.rb b/instrumentation/http_client/test/instrumentation/http_client/patches/stable/client_test.rb index 4fc41e6590..12b9641f3b 100644 --- a/instrumentation/http_client/test/instrumentation/http_client/patches/stable/client_test.rb +++ b/instrumentation/http_client/test/instrumentation/http_client/patches/stable/client_test.rb @@ -143,5 +143,24 @@ headers: { 'Traceparent' => "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" } ) end + + it 'traces a request with non-standard HTTP method' do + stub_request(:purge, 'http://example.com/cache').to_return(status: 200) + http = HTTPClient.new + http.request(:purge, 'http://example.com/cache') + + _(span.name).must_equal 'HTTP' + _(span.attributes['http.request.method']).must_equal '_OTHER' + _(span.attributes['http.request.method_original']).must_equal 'PURGE' + _(span.attributes['http.response.status_code']).must_equal 200 + _(span.attributes['url.scheme']).must_equal 'http' + _(span.attributes['server.address']).must_equal 'example.com' + _(span.attributes['url.path']).must_equal '/cache' + assert_requested( + :purge, + 'http://example.com/cache', + headers: { 'Traceparent' => "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" } + ) + end end end diff --git a/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx.rb b/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx.rb index 70e00f6cc2..24b8015af4 100644 --- a/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx.rb +++ b/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx.rb @@ -15,5 +15,6 @@ module HTTPX end end +require_relative 'httpx/http_helper' require_relative 'httpx/instrumentation' require_relative 'httpx/version' diff --git a/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/dup/plugin.rb b/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/dup/plugin.rb index efe51dd19e..dc5292d53f 100644 --- a/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/dup/plugin.rb +++ b/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/dup/plugin.rb @@ -72,17 +72,17 @@ def initialize_span(request, start_time = ::Time.now) verb = request.verb uri = request.uri + span_data = HttpHelper.span_attrs_for_dup(verb) + config = HTTPX::Instrumentation.instance.config attributes = { OpenTelemetry::SemanticConventions::Trace::HTTP_HOST => uri.host, - OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => verb, OpenTelemetry::SemanticConventions::Trace::HTTP_SCHEME => uri.scheme, OpenTelemetry::SemanticConventions::Trace::HTTP_TARGET => uri.path, OpenTelemetry::SemanticConventions::Trace::HTTP_URL => "#{uri.scheme}://#{uri.host}", OpenTelemetry::SemanticConventions::Trace::NET_PEER_NAME => uri.host, OpenTelemetry::SemanticConventions::Trace::NET_PEER_PORT => uri.port, - 'http.request.method' => verb, 'url.scheme' => uri.scheme, 'url.path' => uri.path, 'url.full' => "#{uri.scheme}://#{uri.host}", @@ -92,9 +92,9 @@ def initialize_span(request, start_time = ::Time.now) attributes['url.query'] = uri.query unless uri.query.nil? attributes[OpenTelemetry::SemanticConventions::Trace::PEER_SERVICE] = config[:peer_service] if config[:peer_service] - attributes.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) + attributes.merge!(span_data.attributes) - span = tracer.start_span(verb, attributes: attributes, kind: :client, start_timestamp: start_time) + span = tracer.start_span(span_data.span_name, attributes: attributes, kind: :client, start_timestamp: start_time) OpenTelemetry::Trace.with_span(span) do OpenTelemetry.propagation.inject(request.headers) diff --git a/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/http_helper.rb b/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/http_helper.rb new file mode 100644 index 0000000000..52b25a4a5e --- /dev/null +++ b/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/http_helper.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Instrumentation + module HTTPX + # Utility module for normalizing HTTP methods according to OpenTelemetry semantic conventions + # @api private + module HttpHelper + # Lightweight struct to hold span creation attributes + SpanCreationAttributes = Struct.new(:span_name, :attributes, keyword_init: true) + + # Pre-computed mapping to avoid string allocations during normalization + METHOD_CACHE = { + 'CONNECT' => 'CONNECT', + 'DELETE' => 'DELETE', + 'GET' => 'GET', + 'HEAD' => 'HEAD', + 'OPTIONS' => 'OPTIONS', + 'PATCH' => 'PATCH', + 'POST' => 'POST', + 'PUT' => 'PUT', + 'TRACE' => 'TRACE', + 'connect' => 'CONNECT', + 'delete' => 'DELETE', + 'get' => 'GET', + 'head' => 'HEAD', + 'options' => 'OPTIONS', + 'patch' => 'PATCH', + 'post' => 'POST', + 'put' => 'PUT', + 'trace' => 'TRACE', + :connect => 'CONNECT', + :delete => 'DELETE', + :get => 'GET', + :head => 'HEAD', + :options => 'OPTIONS', + :patch => 'PATCH', + :post => 'POST', + :put => 'PUT', + :trace => 'TRACE' + }.freeze + + private_constant :METHOD_CACHE + + # Prepares span data using old semantic conventions + # @param method [String, Symbol] The HTTP method + # @return [SpanCreationAttributes] struct containing span_name and attributes hash + def self.span_attrs_for_old(method) + client_context_attrs = OpenTelemetry::Common::HTTP::ClientContext.attributes + normalized = METHOD_CACHE[method] + attributes = client_context_attrs.dup + + # Determine base span name and method value + if normalized + span_name = normalized + method_value = normalized + else + span_name = 'HTTP' + method_value = '_OTHER' + end + + attributes['http.method'] ||= method_value + + SpanCreationAttributes.new(span_name: span_name, attributes: attributes) + end + + # Prepares span data using stable semantic conventions + # @param method [String, Symbol] The HTTP method + # @return [SpanCreationAttributes] struct containing span_name and attributes hash + def self.span_attrs_for_stable(method) + client_context_attrs = OpenTelemetry::Common::HTTP::ClientContext.attributes + url_template = client_context_attrs['url.template'] + normalized = METHOD_CACHE[method] + attributes = client_context_attrs.dup + + # Determine base span name and method value + if normalized + base_name = normalized + method_value = normalized + original = nil + else + base_name = 'HTTP' + method_value = '_OTHER' + original = method.to_s + end + + span_name = url_template ? "#{base_name} #{url_template}" : base_name + attributes['http.request.method'] ||= method_value + attributes['http.request.method_original'] ||= original if original + + SpanCreationAttributes.new(span_name: span_name, attributes: attributes) + end + + # Prepares span data using both old and stable semantic conventions + # @param method [String, Symbol] The HTTP method + # @return [SpanCreationAttributes] struct containing span_name and attributes hash + def self.span_attrs_for_dup(method) + client_context_attrs = OpenTelemetry::Common::HTTP::ClientContext.attributes + url_template = client_context_attrs['url.template'] + normalized = METHOD_CACHE[method] + attributes = client_context_attrs.dup + + # Determine base span name and method value + if normalized + base_name = normalized + method_value = normalized + original = nil + else + base_name = 'HTTP' + method_value = '_OTHER' + original = method.to_s + end + + span_name = url_template ? "#{base_name} #{url_template}" : base_name + attributes['http.method'] ||= method_value + attributes['http.request.method'] ||= method_value + attributes['http.request.method_original'] ||= original if original + + SpanCreationAttributes.new(span_name: span_name, attributes: attributes) + end + end + end + end +end diff --git a/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/old/plugin.rb b/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/old/plugin.rb index d56fc47c1a..3f04b09c0c 100644 --- a/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/old/plugin.rb +++ b/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/old/plugin.rb @@ -71,11 +71,12 @@ def initialize_span(request, start_time = ::Time.now) verb = request.verb uri = request.uri + span_data = HttpHelper.span_attrs_for_old(verb) + config = HTTPX::Instrumentation.instance.config attributes = { OpenTelemetry::SemanticConventions::Trace::HTTP_HOST => uri.host, - OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => verb, OpenTelemetry::SemanticConventions::Trace::HTTP_SCHEME => uri.scheme, OpenTelemetry::SemanticConventions::Trace::HTTP_TARGET => uri.path, OpenTelemetry::SemanticConventions::Trace::HTTP_URL => "#{uri.scheme}://#{uri.host}", @@ -84,9 +85,9 @@ def initialize_span(request, start_time = ::Time.now) } attributes[OpenTelemetry::SemanticConventions::Trace::PEER_SERVICE] = config[:peer_service] if config[:peer_service] - attributes.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) + attributes.merge!(span_data.attributes) - span = tracer.start_span("HTTP #{verb}", attributes: attributes, kind: :client, start_timestamp: start_time) + span = tracer.start_span(span_data.span_name, attributes: attributes, kind: :client, start_timestamp: start_time) OpenTelemetry::Trace.with_span(span) do OpenTelemetry.propagation.inject(request.headers) diff --git a/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/stable/plugin.rb b/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/stable/plugin.rb index e5f73ca0eb..cfc6db9454 100644 --- a/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/stable/plugin.rb +++ b/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/stable/plugin.rb @@ -71,10 +71,11 @@ def initialize_span(request, start_time = ::Time.now) verb = request.verb uri = request.uri + span_data = HttpHelper.span_attrs_for_stable(verb) + config = HTTPX::Instrumentation.instance.config attributes = { - 'http.request.method' => verb, 'url.scheme' => uri.scheme, 'url.path' => uri.path, 'url.full' => "#{uri.scheme}://#{uri.host}", @@ -83,9 +84,9 @@ def initialize_span(request, start_time = ::Time.now) } attributes['url.query'] = uri.query unless uri.query.nil? attributes[OpenTelemetry::SemanticConventions::Trace::PEER_SERVICE] = config[:peer_service] if config[:peer_service] - attributes.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) + attributes.merge!(span_data.attributes) - span = tracer.start_span(verb, attributes: attributes, kind: :client, start_timestamp: start_time) + span = tracer.start_span(span_data.span_name, attributes: attributes, kind: :client, start_timestamp: start_time) OpenTelemetry::Trace.with_span(span) do OpenTelemetry.propagation.inject(request.headers) diff --git a/instrumentation/httpx/test/instrumentation/dup/plugin_test.rb b/instrumentation/httpx/test/instrumentation/dup/plugin_test.rb index cba93d3318..f5098ccc2f 100644 --- a/instrumentation/httpx/test/instrumentation/dup/plugin_test.rb +++ b/instrumentation/httpx/test/instrumentation/dup/plugin_test.rb @@ -36,6 +36,30 @@ _(exporter.finished_spans.size).must_equal 0 end + it 'after request with non-standard HTTP method' do + stub_request(:purge, 'http://example.com/cache').to_return(status: 200) + HTTPX.request('PURGE', 'http://example.com/cache') + + _(exporter.finished_spans.size).must_equal 1 + _(span.name).must_equal 'HTTP' + _(span.attributes['http.method']).must_equal '_OTHER' + _(span.attributes['http.status_code']).must_equal 200 + _(span.attributes['http.scheme']).must_equal 'http' + _(span.attributes['http.target']).must_equal '/cache' + # stable semantic conventions + _(span.attributes['http.request.method']).must_equal '_OTHER' + _(span.attributes['http.request.method_original']).must_equal 'PURGE' + _(span.attributes['http.response.status_code']).must_equal 200 + _(span.attributes['url.scheme']).must_equal 'http' + _(span.attributes['http.host']).must_equal 'example.com' + _(span.attributes['url.path']).must_equal '/cache' + assert_requested( + :purge, + 'http://example.com/cache', + headers: { 'Traceparent' => "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" } + ) + end + it 'after request with success code' do HTTPX.get('http://example.com/success') diff --git a/instrumentation/httpx/test/instrumentation/old/plugin_test.rb b/instrumentation/httpx/test/instrumentation/old/plugin_test.rb index 82af7fd6f7..8431546a40 100644 --- a/instrumentation/httpx/test/instrumentation/old/plugin_test.rb +++ b/instrumentation/httpx/test/instrumentation/old/plugin_test.rb @@ -35,11 +35,29 @@ _(exporter.finished_spans.size).must_equal 0 end + it 'after request with non-standard HTTP method' do + stub_request(:purge, 'http://example.com/cache').to_return(status: 200) + HTTPX.request('PURGE', 'http://example.com/cache') + + _(exporter.finished_spans.size).must_equal 1 + _(span.name).must_equal 'HTTP' + _(span.attributes['http.method']).must_equal '_OTHER' + _(span.attributes['http.status_code']).must_equal 200 + _(span.attributes['http.scheme']).must_equal 'http' + _(span.attributes['net.peer.name']).must_equal 'example.com' + _(span.attributes['http.target']).must_equal '/cache' + assert_requested( + :purge, + 'http://example.com/cache', + headers: { 'Traceparent' => "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" } + ) + end + it 'after request with success code' do HTTPX.get('http://example.com/success') _(exporter.finished_spans.size).must_equal 1 - _(span.name).must_equal 'HTTP GET' + _(span.name).must_equal 'GET' _(span.attributes['http.method']).must_equal 'GET' _(span.attributes['http.status_code']).must_equal 200 _(span.attributes['http.scheme']).must_equal 'http' @@ -56,7 +74,7 @@ HTTPX.get('http://example.com/failure') _(exporter.finished_spans.size).must_equal 1 - _(span.name).must_equal 'HTTP GET' + _(span.name).must_equal 'GET' _(span.attributes['http.method']).must_equal 'GET' _(span.attributes['http.status_code']).must_equal 500 _(span.attributes['http.scheme']).must_equal 'http' @@ -75,7 +93,7 @@ assert response.error.is_a?(HTTPX::TimeoutError) _(exporter.finished_spans.size).must_equal 1 - _(span.name).must_equal 'HTTP GET' + _(span.name).must_equal 'GET' _(span.attributes['http.method']).must_equal 'GET' _(span.attributes['http.scheme']).must_equal 'http' _(span.attributes['http.host']).must_equal 'example.com' @@ -103,7 +121,7 @@ end _(exporter.finished_spans.size).must_equal 1 - _(span.name).must_equal 'HTTP GET' + _(span.name).must_equal 'GET' _(span.attributes['http.method']).must_equal 'OVERRIDE' _(span.attributes['http.status_code']).must_equal 200 _(span.attributes['http.scheme']).must_equal 'http' diff --git a/instrumentation/httpx/test/instrumentation/stable/plugin_test.rb b/instrumentation/httpx/test/instrumentation/stable/plugin_test.rb index 12be42acb5..4298a60f18 100644 --- a/instrumentation/httpx/test/instrumentation/stable/plugin_test.rb +++ b/instrumentation/httpx/test/instrumentation/stable/plugin_test.rb @@ -36,6 +36,25 @@ _(exporter.finished_spans.size).must_equal 0 end + it 'after request with non-standard HTTP method' do + stub_request(:purge, 'http://example.com/cache').to_return(status: 200) + HTTPX.request('PURGE', 'http://example.com/cache') + + _(exporter.finished_spans.size).must_equal 1 + _(span.name).must_equal 'HTTP' + _(span.attributes['http.request.method']).must_equal '_OTHER' + _(span.attributes['http.request.method_original']).must_equal 'PURGE' + _(span.attributes['http.response.status_code']).must_equal 200 + _(span.attributes['url.scheme']).must_equal 'http' + _(span.attributes['server.address']).must_equal 'example.com' + _(span.attributes['url.path']).must_equal '/cache' + assert_requested( + :purge, + 'http://example.com/cache', + headers: { 'Traceparent' => "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" } + ) + end + it 'after request with success code' do HTTPX.get('http://example.com/success') diff --git a/instrumentation/httpx/test/test_helper.rb b/instrumentation/httpx/test/test_helper.rb index 4768b0ea46..1608769406 100644 --- a/instrumentation/httpx/test/test_helper.rb +++ b/instrumentation/httpx/test/test_helper.rb @@ -9,6 +9,7 @@ Bundler.require(:default, :development, :test) require 'minitest/autorun' +require 'rspec/mocks/minitest_integration' require 'httpx/adapters/webmock' require 'webmock/minitest' diff --git a/instrumentation/net_http/lib/opentelemetry/instrumentation/net/http.rb b/instrumentation/net_http/lib/opentelemetry/instrumentation/net/http.rb index 8f44d2e41e..f504a1ad87 100644 --- a/instrumentation/net_http/lib/opentelemetry/instrumentation/net/http.rb +++ b/instrumentation/net_http/lib/opentelemetry/instrumentation/net/http.rb @@ -18,5 +18,6 @@ module HTTP end end +require_relative 'http/http_helper' require_relative 'http/instrumentation' require_relative 'http/version' diff --git a/instrumentation/net_http/lib/opentelemetry/instrumentation/net/http/http_helper.rb b/instrumentation/net_http/lib/opentelemetry/instrumentation/net/http/http_helper.rb new file mode 100644 index 0000000000..3b221fe06a --- /dev/null +++ b/instrumentation/net_http/lib/opentelemetry/instrumentation/net/http/http_helper.rb @@ -0,0 +1,130 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Instrumentation + module Net + module HTTP + # Utility module for HTTP-related helper methods + # @api private + module HttpHelper + # Lightweight struct to hold span creation attributes + SpanCreationAttributes = Struct.new(:span_name, :attributes, keyword_init: true) + + # Pre-computed mapping to avoid string allocations during normalization + METHOD_CACHE = { + 'CONNECT' => 'CONNECT', + 'DELETE' => 'DELETE', + 'GET' => 'GET', + 'HEAD' => 'HEAD', + 'OPTIONS' => 'OPTIONS', + 'PATCH' => 'PATCH', + 'POST' => 'POST', + 'PUT' => 'PUT', + 'TRACE' => 'TRACE', + 'connect' => 'CONNECT', + 'delete' => 'DELETE', + 'get' => 'GET', + 'head' => 'HEAD', + 'options' => 'OPTIONS', + 'patch' => 'PATCH', + 'post' => 'POST', + 'put' => 'PUT', + 'trace' => 'TRACE', + :connect => 'CONNECT', + :delete => 'DELETE', + :get => 'GET', + :head => 'HEAD', + :options => 'OPTIONS', + :patch => 'PATCH', + :post => 'POST', + :put => 'PUT', + :trace => 'TRACE' + }.freeze + + private_constant :METHOD_CACHE + + # Prepares span data using old semantic conventions + # @param method [String, Symbol] The HTTP method + # @return [SpanCreationAttributes] struct containing span_name and attributes hash + def self.span_attrs_for_old(method) + client_context_attrs = OpenTelemetry::Common::HTTP::ClientContext.attributes + normalized = METHOD_CACHE[method] + attributes = client_context_attrs.dup + + # Determine base span name and method value + if normalized + span_name = normalized + method_value = normalized + else + span_name = 'HTTP' + method_value = '_OTHER' + end + + attributes['http.method'] ||= method_value + + SpanCreationAttributes.new(span_name: span_name, attributes: attributes) + end + + # Prepares span data using stable semantic conventions + # @param method [String, Symbol] The HTTP method + # @return [SpanCreationAttributes] struct containing span_name and attributes hash + def self.span_attrs_for_stable(method) + client_context_attrs = OpenTelemetry::Common::HTTP::ClientContext.attributes + url_template = client_context_attrs['url.template'] + normalized = METHOD_CACHE[method] + attributes = client_context_attrs.dup + + # Determine base span name and method value + if normalized + base_name = normalized + method_value = normalized + original = nil + else + base_name = 'HTTP' + method_value = '_OTHER' + original = method.to_s + end + + span_name = url_template ? "#{base_name} #{url_template}" : base_name + attributes['http.request.method'] ||= method_value + attributes['http.request.method_original'] ||= original if original + + SpanCreationAttributes.new(span_name: span_name, attributes: attributes) + end + + # Prepares span data using both old and stable semantic conventions + # @param method [String, Symbol] The HTTP method + # @return [SpanCreationAttributes] struct containing span_name and attributes hash + def self.span_attrs_for_dup(method) + client_context_attrs = OpenTelemetry::Common::HTTP::ClientContext.attributes + url_template = client_context_attrs['url.template'] + normalized = METHOD_CACHE[method] + attributes = client_context_attrs.dup + + # Determine base span name and method value + if normalized + base_name = normalized + method_value = normalized + original = nil + else + base_name = 'HTTP' + method_value = '_OTHER' + original = method.to_s + end + + span_name = url_template ? "#{base_name} #{url_template}" : base_name + attributes['http.method'] ||= method_value + attributes['http.request.method'] ||= method_value + attributes['http.request.method_original'] ||= original if original + + SpanCreationAttributes.new(span_name: span_name, attributes: attributes) + end + end + end + end + end +end diff --git a/instrumentation/net_http/lib/opentelemetry/instrumentation/net/http/patches/dup/instrumentation.rb b/instrumentation/net_http/lib/opentelemetry/instrumentation/net/http/patches/dup/instrumentation.rb index 9bda0080ab..b0c407b236 100644 --- a/instrumentation/net_http/lib/opentelemetry/instrumentation/net/http/patches/dup/instrumentation.rb +++ b/instrumentation/net_http/lib/opentelemetry/instrumentation/net/http/patches/dup/instrumentation.rb @@ -23,25 +23,22 @@ def request(req, body = nil, &) return super if untraced? - attributes = { - OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => req.method, - OpenTelemetry::SemanticConventions::Trace::HTTP_SCHEME => USE_SSL_TO_SCHEME[use_ssl?], - OpenTelemetry::SemanticConventions::Trace::HTTP_TARGET => req.path, - OpenTelemetry::SemanticConventions::Trace::NET_PEER_NAME => @address, - OpenTelemetry::SemanticConventions::Trace::NET_PEER_PORT => @port, - 'http.request.method' => req.method, - 'url.scheme' => USE_SSL_TO_SCHEME[use_ssl?], - 'server.address' => @address, - 'server.port' => @port - } + span_data = HttpHelper.span_attrs_for_dup(req.method) + + attributes = { OpenTelemetry::SemanticConventions::Trace::HTTP_SCHEME => USE_SSL_TO_SCHEME[use_ssl?], + OpenTelemetry::SemanticConventions::Trace::HTTP_TARGET => req.path, + OpenTelemetry::SemanticConventions::Trace::NET_PEER_NAME => @address, + OpenTelemetry::SemanticConventions::Trace::NET_PEER_PORT => @port, 'url.scheme' => USE_SSL_TO_SCHEME[use_ssl?], + 'server.address' => @address, + 'server.port' => @port } path, query = split_path_and_query(req.path) attributes['url.path'] = path attributes['url.query'] = query if query - attributes.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) + attributes.merge!(span_data.attributes) tracer.in_span( - req.method, + span_data.span_name, attributes: attributes, kind: :client ) do |span| diff --git a/instrumentation/net_http/lib/opentelemetry/instrumentation/net/http/patches/old/instrumentation.rb b/instrumentation/net_http/lib/opentelemetry/instrumentation/net/http/patches/old/instrumentation.rb index 2ec4c65767..f4a97adaef 100644 --- a/instrumentation/net_http/lib/opentelemetry/instrumentation/net/http/patches/old/instrumentation.rb +++ b/instrumentation/net_http/lib/opentelemetry/instrumentation/net/http/patches/old/instrumentation.rb @@ -12,7 +12,6 @@ module Patches module Old # Module to prepend to Net::HTTP for instrumentation module Instrumentation - HTTP_METHODS_TO_SPAN_NAMES = Hash.new { |h, k| h[k] = "HTTP #{k}" } USE_SSL_TO_SCHEME = { false => 'http', true => 'https' }.freeze # Constant for the HTTP status range @@ -24,16 +23,15 @@ def request(req, body = nil, &) return super if untraced? - attributes = { - OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => req.method, - OpenTelemetry::SemanticConventions::Trace::HTTP_SCHEME => USE_SSL_TO_SCHEME[use_ssl?], - OpenTelemetry::SemanticConventions::Trace::HTTP_TARGET => req.path, - OpenTelemetry::SemanticConventions::Trace::NET_PEER_NAME => @address, - OpenTelemetry::SemanticConventions::Trace::NET_PEER_PORT => @port - }.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) + span_data = HttpHelper.span_attrs_for_old(req.method) + + attributes = { OpenTelemetry::SemanticConventions::Trace::HTTP_SCHEME => USE_SSL_TO_SCHEME[use_ssl?], + OpenTelemetry::SemanticConventions::Trace::HTTP_TARGET => req.path, + OpenTelemetry::SemanticConventions::Trace::NET_PEER_NAME => @address, + OpenTelemetry::SemanticConventions::Trace::NET_PEER_PORT => @port }.merge!(span_data.attributes) tracer.in_span( - HTTP_METHODS_TO_SPAN_NAMES[req.method], + span_data.span_name, attributes: attributes, kind: :client ) do |span| @@ -64,7 +62,7 @@ def connect }.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) if use_ssl? && proxy? - span_name = 'HTTP CONNECT' + span_name = 'CONNECT' span_kind = :client else span_name = 'connect' diff --git a/instrumentation/net_http/lib/opentelemetry/instrumentation/net/http/patches/stable/instrumentation.rb b/instrumentation/net_http/lib/opentelemetry/instrumentation/net/http/patches/stable/instrumentation.rb index 4923aad083..7f8e7b3fef 100644 --- a/instrumentation/net_http/lib/opentelemetry/instrumentation/net/http/patches/stable/instrumentation.rb +++ b/instrumentation/net_http/lib/opentelemetry/instrumentation/net/http/patches/stable/instrumentation.rb @@ -23,20 +23,19 @@ def request(req, body = nil, &) return super if untraced? - attributes = { - 'http.request.method' => req.method, - 'url.scheme' => USE_SSL_TO_SCHEME[use_ssl?], - 'server.address' => @address, - 'server.port' => @port - } + span_data = HttpHelper.span_attrs_for_stable(req.method) + + attributes = { 'url.scheme' => USE_SSL_TO_SCHEME[use_ssl?], + 'server.address' => @address, + 'server.port' => @port } path, query = split_path_and_query(req.path) attributes['url.path'] = path attributes['url.query'] = query if query - attributes.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) + attributes.merge!(span_data.attributes) tracer.in_span( - req.method.to_s, + span_data.span_name, attributes: attributes, kind: :client ) do |span| diff --git a/instrumentation/net_http/test/opentelemetry/instrumentation/net/http/dup/instrumentation_test.rb b/instrumentation/net_http/test/opentelemetry/instrumentation/net/http/dup/instrumentation_test.rb index 8939ded74c..4be38b26e8 100644 --- a/instrumentation/net_http/test/opentelemetry/instrumentation/net/http/dup/instrumentation_test.rb +++ b/instrumentation/net_http/test/opentelemetry/instrumentation/net/http/dup/instrumentation_test.rb @@ -93,6 +93,37 @@ ) end + it 'after request with unknown http method' do + stub_request(:purge, 'http://example.com/purge').to_return(status: 200) + uri = URI('http://example.com/purge') + Net::HTTP.start(uri.host, uri.port) do |http| + http.request(Net::HTTP::Purge.new(uri)) + end + + _(exporter.finished_spans.size).must_equal 1 + _(span.name).must_equal 'HTTP' + # old semantic conventions + _(span.attributes['http.method']).must_equal '_OTHER' + _(span.attributes['http.scheme']).must_equal 'http' + _(span.attributes['http.status_code']).must_equal 200 + _(span.attributes['http.target']).must_equal '/purge' + _(span.attributes['net.peer.name']).must_equal 'example.com' + _(span.attributes['net.peer.port']).must_equal 80 + # stable semantic conventions + _(span.attributes['http.request.method']).must_equal '_OTHER' + _(span.attributes['http.request.method_original']).must_equal 'PURGE' + _(span.attributes['url.scheme']).must_equal 'http' + _(span.attributes['http.response.status_code']).must_equal 200 + _(span.attributes['url.path']).must_equal '/purge' + _(span.attributes['server.address']).must_equal 'example.com' + _(span.attributes['server.port']).must_equal 80 + assert_requested( + :purge, + 'http://example.com/purge', + headers: { 'Traceparent' => "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" } + ) + end + it 'after request timeout' do expect do Net::HTTP.get(URI('https://example.com/timeout')) diff --git a/instrumentation/net_http/test/opentelemetry/instrumentation/net/http/old/instrumentation_test.rb b/instrumentation/net_http/test/opentelemetry/instrumentation/net/http/old/instrumentation_test.rb index 000d390062..eaf48debc2 100644 --- a/instrumentation/net_http/test/opentelemetry/instrumentation/net/http/old/instrumentation_test.rb +++ b/instrumentation/net_http/test/opentelemetry/instrumentation/net/http/old/instrumentation_test.rb @@ -45,7 +45,7 @@ Net::HTTP.get('example.com', '/success') _(exporter.finished_spans.size).must_equal 1 - _(span.name).must_equal 'HTTP GET' + _(span.name).must_equal 'GET' _(span.attributes['http.method']).must_equal 'GET' _(span.attributes['http.scheme']).must_equal 'http' _(span.attributes['http.status_code']).must_equal 200 @@ -63,7 +63,7 @@ Net::HTTP.post(URI('http://example.com/failure'), 'q' => 'ruby') _(exporter.finished_spans.size).must_equal 1 - _(span.name).must_equal 'HTTP POST' + _(span.name).must_equal 'POST' _(span.attributes['http.method']).must_equal 'POST' _(span.attributes['http.scheme']).must_equal 'http' _(span.attributes['http.status_code']).must_equal 500 @@ -77,13 +77,36 @@ ) end + it 'after request with unknown http method' do + stub_request(:purge, 'http://example.com/purge').to_return(status: 200) + uri = URI('http://example.com/purge') + Net::HTTP.start(uri.host, uri.port) do |http| + http.request(Net::HTTP::Purge.new(uri)) + end + + _(exporter.finished_spans.size).must_equal 1 + _(span.name).must_equal 'HTTP' + _(span.attributes['http.method']).must_equal '_OTHER' + _(span.attributes['http.scheme']).must_equal 'http' + _(span.attributes['http.status_code']).must_equal 200 + _(span.attributes['http.target']).must_equal '/purge' + _(span.attributes['net.peer.name']).must_equal 'example.com' + _(span.attributes['net.peer.port']).must_equal 80 + _(span.attributes['http.method.original']).must_be_nil + assert_requested( + :purge, + 'http://example.com/purge', + headers: { 'Traceparent' => "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" } + ) + end + it 'after request timeout' do expect do Net::HTTP.get(URI('https://example.com/timeout')) end.must_raise Net::OpenTimeout _(exporter.finished_spans.size).must_equal 1 - _(span.name).must_equal 'HTTP GET' + _(span.name).must_equal 'GET' _(span.attributes['http.method']).must_equal 'GET' _(span.attributes['http.scheme']).must_equal 'https' _(span.attributes['http.status_code']).must_be_nil @@ -109,7 +132,7 @@ end _(exporter.finished_spans.size).must_equal 1 - _(span.name).must_equal 'HTTP GET' + _(span.name).must_equal 'GET' _(span.attributes['http.method']).must_equal 'GET' _(span.attributes['http.scheme']).must_equal 'http' _(span.attributes['http.status_code']).must_equal 200 @@ -164,7 +187,7 @@ it 'creates a span for a non-ignored request' do Net::HTTP.get('example.com', '/body') _(exporter.finished_spans.size).must_equal 1 - _(span.name).must_equal 'HTTP GET' + _(span.name).must_equal 'GET' _(span.attributes['http.method']).must_equal 'GET' _(span.attributes['net.peer.name']).must_equal 'example.com' end @@ -282,7 +305,7 @@ def fake_socket.close; end # rubocop:enable Lint/SuppressedException _(exporter.finished_spans.size).must_equal(2) - _(span.name).must_equal 'HTTP CONNECT' + _(span.name).must_equal 'CONNECT' _(span.kind).must_equal(:client) _(span.attributes['net.peer.name']).must_equal('localhost') _(span.attributes['net.peer.port']).must_equal(443) diff --git a/instrumentation/net_http/test/opentelemetry/instrumentation/net/http/stable/instrumentation_test.rb b/instrumentation/net_http/test/opentelemetry/instrumentation/net/http/stable/instrumentation_test.rb index 68d362c968..62b5332f4d 100644 --- a/instrumentation/net_http/test/opentelemetry/instrumentation/net/http/stable/instrumentation_test.rb +++ b/instrumentation/net_http/test/opentelemetry/instrumentation/net/http/stable/instrumentation_test.rb @@ -79,6 +79,32 @@ ) end + it 'after request with unknown http method' do + stub_request(:purge, 'http://example.com/purge').to_return(status: 200) + + uri = URI('http://example.com/purge') + request = Net::HTTP::Purge.new(uri) + + Net::HTTP.start(uri.hostname, uri.port) do |http| + http.request(request) + end + + _(exporter.finished_spans.size).must_equal 1 + _(span.name).must_equal 'HTTP' + _(span.attributes['http.request.method']).must_equal '_OTHER' + _(span.attributes['http.request.method_original']).must_equal 'PURGE' + _(span.attributes['url.scheme']).must_equal 'http' + _(span.attributes['http.response.status_code']).must_equal 200 + _(span.attributes['url.path']).must_equal '/purge' + _(span.attributes['server.address']).must_equal 'example.com' + _(span.attributes['server.port']).must_equal 80 + assert_requested( + :purge, + 'http://example.com/purge', + headers: { 'Traceparent' => "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" } + ) + end + it 'after request timeout' do expect do Net::HTTP.get(URI('https://example.com/timeout')) diff --git a/instrumentation/net_http/test/test_helper.rb b/instrumentation/net_http/test/test_helper.rb index 82b97ec0ee..d519ca589e 100644 --- a/instrumentation/net_http/test/test_helper.rb +++ b/instrumentation/net_http/test/test_helper.rb @@ -9,6 +9,17 @@ require 'bundler/setup' Bundler.require(:default, :development, :test) +# Define custom HTTP method for testing unknown method handling +module Net + class HTTP + class Purge < HTTPRequest + METHOD = 'PURGE' + REQUEST_HAS_BODY = false + RESPONSE_HAS_BODY = true + end + end +end + require 'minitest/autorun' require 'rspec/mocks/minitest_integration' require 'webmock/minitest'