From 9b6e48ec1420af9dc5dc6787355ecb6ac877ded2 Mon Sep 17 00:00:00 2001 From: Ariel Valentin Date: Wed, 5 Nov 2025 23:57:19 -0600 Subject: [PATCH 1/6] fix: HTTP unknown methods Ensures that all HTTP client gems support unknown methods as described in semantic conventions: https://opentelemetry.io/docs/specs/semconv/http/http-spans/#http-client-span See https://github.com/open-telemetry/opentelemetry-ruby-contrib/issues/1779 --- .../instrumentation/ethon/patches/dup/easy.rb | 27 ++++---- .../ethon/patches/http_helper.rb | 41 ++++++++++++ .../instrumentation/ethon/patches/old/easy.rb | 21 +++---- .../ethon/patches/stable/easy.rb | 25 ++++---- .../ethon/dup/instrumentation_test.rb | 26 ++++++++ .../ethon/old/instrumentation_test.rb | 43 ++++++++++--- .../ethon/stable/instrumentation_test.rb | 24 +++++++ instrumentation/excon/Gemfile | 1 + .../middlewares/dup/tracer_middleware.rb | 24 +++---- .../excon/middlewares/http_helper.rb | 50 +++++++++++++++ .../middlewares/old/tracer_middleware.rb | 21 +++---- .../middlewares/stable/tracer_middleware.rb | 22 +++---- .../excon/dup/instrumentation_test.rb | 28 +++++++++ .../excon/old/instrumentation_test.rb | 20 ++++++ .../excon/stable/instrumentation_test.rb | 20 ++++++ instrumentation/excon/test/test_helper.rb | 1 + instrumentation/faraday/Gemfile | 1 + .../middlewares/dup/tracer_middleware.rb | 17 ++++- .../faraday/middlewares/http_helper.rb | 61 ++++++++++++++++++ .../middlewares/old/tracer_middleware.rb | 11 +++- .../middlewares/stable/tracer_middleware.rb | 15 ++++- .../middlewares/dup/tracer_middleware_test.rb | 31 +++++++++ .../middlewares/old/tracer_middleware_test.rb | 25 ++++++++ .../stable/tracer_middleware_test.rb | 25 ++++++++ instrumentation/faraday/test/test_helper.rb | 1 + .../http/patches/dup/client.rb | 22 ++++--- .../http/patches/http_helper.rb | 43 +++++++++++++ .../http/patches/old/client.rb | 19 +++--- .../http/patches/stable/client.rb | 20 +++--- .../http/patches/dup/client_test.rb | 26 ++++++++ .../http/patches/old/client_test.rb | 18 ++++++ .../http/patches/stable/client_test.rb | 19 ++++++ .../http_client/patches/dup/client.rb | 12 +++- .../http_client/patches/http_helper.rb | 43 +++++++++++++ .../http_client/patches/old/client.rb | 9 ++- .../http_client/patches/stable/client.rb | 9 ++- .../http_client/patches/dup/client_test.rb | 27 ++++++++ .../http_client/patches/old/client_test.rb | 19 ++++++ .../http_client/patches/stable/client_test.rb | 20 ++++++ .../instrumentation/httpx/dup/plugin.rb | 12 +++- .../instrumentation/httpx/http_helper.rb | 48 ++++++++++++++ .../instrumentation/httpx/old/plugin.rb | 9 ++- .../instrumentation/httpx/stable/plugin.rb | 10 ++- .../test/instrumentation/dup/plugin_test.rb | 24 +++++++ .../test/instrumentation/old/plugin_test.rb | 18 ++++++ .../instrumentation/stable/plugin_test.rb | 19 ++++++ instrumentation/httpx/test/test_helper.rb | 1 + .../net/http/patches/dup/instrumentation.rb | 12 +++- .../net/http/patches/http_helper.rb | 63 +++++++++++++++++++ .../net/http/patches/old/instrumentation.rb | 10 ++- .../http/patches/stable/instrumentation.rb | 10 ++- .../net/http/dup/instrumentation_test.rb | 31 +++++++++ .../net/http/old/instrumentation_test.rb | 23 +++++++ .../net/http/stable/instrumentation_test.rb | 26 ++++++++ instrumentation/net_http/test/test_helper.rb | 11 ++++ 55 files changed, 1069 insertions(+), 145 deletions(-) create mode 100644 instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/http_helper.rb create mode 100644 instrumentation/excon/lib/opentelemetry/instrumentation/excon/middlewares/http_helper.rb create mode 100644 instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/middlewares/http_helper.rb create mode 100644 instrumentation/http/lib/opentelemetry/instrumentation/http/patches/http_helper.rb create mode 100644 instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/http_helper.rb create mode 100644 instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/http_helper.rb create mode 100644 instrumentation/net_http/lib/opentelemetry/instrumentation/net/http/patches/http_helper.rb 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..1caa75b725 100644 --- a/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/dup/easy.rb +++ b/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/dup/easy.rb @@ -4,6 +4,8 @@ # # SPDX-License-Identifier: Apache-2.0 +require_relative '../http_helper' + module OpenTelemetry module Instrumentation module Ethon @@ -12,20 +14,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.to_s.upcase super end @@ -80,9 +73,12 @@ 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? + normalized_method, original_method = HttpHelper.normalize_method(method) + span_name = HttpHelper.span_name_for_stable(normalized_method) + @otel_span = tracer.start_span( - HTTP_METHODS_TO_SPAN_NAMES[method], - attributes: span_creation_attributes(method), + span_name, + attributes: span_creation_attributes(normalized_method, original_method), kind: :client ) @@ -99,12 +95,13 @@ def otel_span_started? private - def span_creation_attributes(method) - http_method = (method == '_OTHER' ? 'N/A' : method) + def span_creation_attributes(normalized_method, original_method) + http_method = (normalized_method == '_OTHER' ? 'N/A' : normalized_method) instrumentation_attrs = { 'http.method' => http_method, - 'http.request.method' => method + 'http.request.method' => normalized_method } + instrumentation_attrs['http.request.method_original'] = original_method if original_method uri = _otel_cleanse_uri(url) if uri diff --git a/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/http_helper.rb b/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/http_helper.rb new file mode 100644 index 0000000000..ce837182dd --- /dev/null +++ b/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/http_helper.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Instrumentation + module Ethon + module Patches + # Helper module for HTTP method normalization + module HttpHelper + KNOWN_METHODS = %w[CONNECT DELETE GET HEAD OPTIONS PATCH POST PUT TRACE].freeze + + def self.normalize_method(method) + normalized = method.to_s.upcase + if KNOWN_METHODS.include?(normalized) + [normalized, nil] + else + ['_OTHER', normalized] + end + end + + # Generates span name for stable semantic conventions + # @param normalized_method [String] the normalized HTTP method + # @return [String] the span name + def self.span_name_for_stable(normalized_method) + normalized_method == '_OTHER' ? 'HTTP' : normalized_method + end + + # Generates span name for old semantic conventions + # @param normalized_method [String] the normalized HTTP method + # @return [String] the span name + def self.span_name_for_old(normalized_method) + normalized_method == '_OTHER' ? 'HTTP' : "HTTP #{normalized_method}" + end + end + end + end + end +end 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..4984bf75a5 100644 --- a/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/old/easy.rb +++ b/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/old/easy.rb @@ -4,6 +4,8 @@ # # SPDX-License-Identifier: Apache-2.0 +require_relative '../http_helper' + module OpenTelemetry module Instrumentation module Ethon @@ -12,17 +14,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.to_s.upcase super end @@ -76,9 +72,12 @@ 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? + normalized_method, _original_method = HttpHelper.normalize_method(method) + span_name = HttpHelper.span_name_for_old(normalized_method) + @otel_span = tracer.start_span( - HTTP_METHODS_TO_SPAN_NAMES[method], - attributes: span_creation_attributes(method), + span_name, + attributes: span_creation_attributes(normalized_method), kind: :client ) @@ -95,9 +94,9 @@ def otel_span_started? private - def span_creation_attributes(method) + def span_creation_attributes(normalized_method) instrumentation_attrs = { - 'http.method' => method + 'http.method' => normalized_method } uri = _otel_cleanse_uri(url) 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..5010f1f94d 100644 --- a/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/stable/easy.rb +++ b/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/stable/easy.rb @@ -4,6 +4,8 @@ # # SPDX-License-Identifier: Apache-2.0 +require_relative '../http_helper' + module OpenTelemetry module Instrumentation module Ethon @@ -12,20 +14,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.to_s.upcase super end @@ -79,9 +72,12 @@ 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? + normalized_method, original_method = HttpHelper.normalize_method(method) + span_name = HttpHelper.span_name_for_stable(normalized_method) + @otel_span = tracer.start_span( - HTTP_METHODS_TO_SPAN_NAMES[method], - attributes: span_creation_attributes(method), + span_name, + attributes: span_creation_attributes(normalized_method, original_method), kind: :client ) @@ -98,10 +94,11 @@ def otel_span_started? private - def span_creation_attributes(method) + def span_creation_attributes(normalized_method, original_method) instrumentation_attrs = { - 'http.request.method' => method + 'http.request.method' => normalized_method } + instrumentation_attrs['http.request.method_original'] = original_method if original_method uri = _otel_cleanse_uri(url) if uri 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..61da542658 100644 --- a/instrumentation/ethon/test/opentelemetry/instrumentation/ethon/dup/instrumentation_test.rb +++ b/instrumentation/ethon/test/opentelemetry/instrumentation/ethon/dup/instrumentation_test.rb @@ -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 'N/A' + _(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..4def03b896 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( @@ -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..93d8713059 100644 --- a/instrumentation/ethon/test/opentelemetry/instrumentation/ethon/stable/instrumentation_test.rb +++ b/instrumentation/ethon/test/opentelemetry/instrumentation/ethon/stable/instrumentation_test.rb @@ -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/middlewares/dup/tracer_middleware.rb b/instrumentation/excon/lib/opentelemetry/instrumentation/excon/middlewares/dup/tracer_middleware.rb index 1dcbc169ea..5711034390 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 @@ -4,6 +4,8 @@ # # SPDX-License-Identifier: Apache-2.0 +require_relative '../http_helper' + module OpenTelemetry module Instrumentation module Excon @@ -11,45 +13,37 @@ 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]] + normalized_method, original_method = HttpHelper.normalize_method(datum[:method]) + span_name = HttpHelper.span_name_for_stable(normalized_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_METHOD => normalized_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, + 'http.request.method' => normalized_method, 'url.scheme' => datum[:scheme], 'url.path' => datum[:path], 'url.full' => cleansed_url, 'server.address' => datum[:hostname], 'server.port' => datum[:port] } + attributes['http.request.method_original'] = original_method if original_method 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) + span = tracer.start_span(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/http_helper.rb b/instrumentation/excon/lib/opentelemetry/instrumentation/excon/middlewares/http_helper.rb new file mode 100644 index 0000000000..88c83810f1 --- /dev/null +++ b/instrumentation/excon/lib/opentelemetry/instrumentation/excon/middlewares/http_helper.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Instrumentation + module Excon + module Middlewares + # Utility module for HTTP-related helper methods + module HttpHelper + # Standard HTTP methods as defined in the OpenTelemetry semantic conventions + # https://opentelemetry.io/docs/specs/semconv/http/http-spans/ + KNOWN_METHODS = %w[CONNECT DELETE GET HEAD OPTIONS PATCH POST PUT TRACE].freeze + + # Normalizes an HTTP method according to OpenTelemetry semantic conventions + # @param method [String, Symbol] The HTTP method to normalize + # @return [Array] A tuple of [normalized_method, original_method] + # - For known methods: returns [uppercase_method, nil] + # - For unknown methods: returns ['_OTHER', uppercase_original_method] + def self.normalize_method(method) + return [nil, nil] if method.nil? + + normalized = method.to_s.upcase + if KNOWN_METHODS.include?(normalized) + [normalized, nil] + else + ['_OTHER', normalized] + end + end + + # Generates span name for stable semantic conventions + # @param normalized_method [String] the normalized HTTP method + # @return [String] the span name + def self.span_name_for_stable(normalized_method) + normalized_method == '_OTHER' ? 'HTTP' : normalized_method + end + + # Generates span name for old semantic conventions + # @param normalized_method [String] the normalized HTTP method + # @return [String] the span name + def self.span_name_for_old(normalized_method) + normalized_method == '_OTHER' ? 'HTTP' : "HTTP #{normalized_method}" + end + end + end + end + end +end 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..d3947e80cb 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 @@ -4,6 +4,8 @@ # # SPDX-License-Identifier: Apache-2.0 +require_relative '../http_helper' + module OpenTelemetry module Instrumentation module Excon @@ -11,27 +13,18 @@ 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]] + normalized_method, _original_method = HttpHelper.normalize_method(datum[:method]) + span_name = HttpHelper.span_name_for_old(normalized_method) + attributes = { OpenTelemetry::SemanticConventions::Trace::HTTP_HOST => datum[:host], - OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => http_method, + OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => normalized_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)), @@ -41,7 +34,7 @@ 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) + span = tracer.start_span(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..3db9ba3026 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 @@ -4,6 +4,8 @@ # # SPDX-License-Identifier: Apache-2.0 +require_relative '../http_helper' + module OpenTelemetry module Instrumentation module Excon @@ -11,37 +13,29 @@ 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]] + normalized_method, original_method = HttpHelper.normalize_method(datum[:method]) + span_name = HttpHelper.span_name_for_stable(normalized_method) + attributes = { - 'http.request.method' => http_method, + 'http.request.method' => normalized_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] } + attributes['http.request.method_original'] = original_method if original_method 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) + span = tracer.start_span(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/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..f5f493527b 100644 --- a/instrumentation/excon/test/opentelemetry/instrumentation/excon/old/instrumentation_test.rb +++ b/instrumentation/excon/test/opentelemetry/instrumentation/excon/old/instrumentation_test.rb @@ -69,6 +69,26 @@ _(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') 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/middlewares/dup/tracer_middleware.rb b/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/middlewares/dup/tracer_middleware.rb index b1de1d7521..3e036e9d44 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 @@ -4,6 +4,8 @@ # # SPDX-License-Identifier: Apache-2.0 +require_relative '../http_helper' + module OpenTelemetry module Instrumentation module Faraday @@ -29,15 +31,23 @@ class TracerMiddleware < ::Faraday::Middleware def call(env) http_method = HTTP_METHODS_SYMBOL_TO_STRING[env.method] + normalized_method, original_method = HttpHelper.normalize_method(http_method || env.method) + + # Per semantic conventions, span name uses 'HTTP' when method is unknown + span_name = HttpHelper.span_name_for_stable(normalized_method) + config = Faraday::Instrumentation.instance.config attributes = span_creation_attributes( - http_method: http_method, url: env.url, config: config + http_method: normalized_method, + original_method: original_method, + url: env.url, + config: config ) OpenTelemetry::Common::HTTP::ClientContext.with_attributes(attributes) do |attrs, _| tracer.in_span( - http_method, attributes: attrs, kind: config.fetch(:span_kind) + span_name, attributes: attrs, kind: config.fetch(:span_kind) ) do |span| OpenTelemetry.propagation.inject(env.request_headers) @@ -58,7 +68,7 @@ def call(env) private - def span_creation_attributes(http_method:, url:, config:) + def span_creation_attributes(http_method:, original_method:, url:, config:) cleansed_url = OpenTelemetry::Common::Utilities.cleanse_url(url.to_s) attrs = { 'http.method' => http_method, @@ -67,6 +77,7 @@ def span_creation_attributes(http_method:, url:, config:) 'url.full' => cleansed_url, 'faraday.adapter.name' => app.class.name } + attrs['http.request.method_original'] = original_method if original_method if url.host attrs['net.peer.name'] = url.host attrs['server.address'] = url.host diff --git a/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/middlewares/http_helper.rb b/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/middlewares/http_helper.rb new file mode 100644 index 0000000000..48beffedfa --- /dev/null +++ b/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/middlewares/http_helper.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Instrumentation + module Faraday + module Middlewares + # Utility module for HTTP-related helper methods + module HttpHelper + # Known HTTP methods per semantic conventions (stable methods only) + # https://opentelemetry.io/docs/specs/semconv/http/http-spans/ + # Includes methods from RFC9110 and RFC5789 (PATCH) + # Note: QUERY method is excluded as it's still in Development status + KNOWN_METHODS = %w[ + CONNECT + DELETE + GET + HEAD + OPTIONS + PATCH + POST + PUT + TRACE + ].freeze + + module_function + + # Normalizes an HTTP method per semantic conventions + # @param method [String, Symbol] the HTTP method to normalize + # @return [Array] normalized method and original if different + def normalize_method(method) + method_str = method.to_s.upcase + + if KNOWN_METHODS.include?(method_str) + [method_str, nil] + else + ['_OTHER', method_str] + end + end + + # Generates span name for stable semantic conventions + # @param normalized_method [String] the normalized HTTP method + # @return [String] the span name + def span_name_for_stable(normalized_method) + normalized_method == '_OTHER' ? 'HTTP' : normalized_method + end + + # Generates span name for old semantic conventions + # @param normalized_method [String] the normalized HTTP method + # @return [String] the span name + def span_name_for_old(normalized_method) + normalized_method == '_OTHER' ? 'HTTP' : "HTTP #{normalized_method}" + end + end + end + end + end +end 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..3dd0934c70 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 @@ -4,6 +4,8 @@ # # SPDX-License-Identifier: Apache-2.0 +require_relative '../http_helper' + module OpenTelemetry module Instrumentation module Faraday @@ -29,15 +31,20 @@ class TracerMiddleware < ::Faraday::Middleware def call(env) http_method = HTTP_METHODS_SYMBOL_TO_STRING[env.method] + normalized_method, _original_method = HttpHelper.normalize_method(http_method || env.method) + + # Per semantic conventions, span name uses 'HTTP' when method is unknown + span_name = HttpHelper.span_name_for_old(normalized_method) + config = Faraday::Instrumentation.instance.config attributes = span_creation_attributes( - http_method: http_method, url: env.url, config: config + http_method: normalized_method, url: env.url, config: config ) OpenTelemetry::Common::HTTP::ClientContext.with_attributes(attributes) do |attrs, _| tracer.in_span( - "HTTP #{http_method}", attributes: attrs, kind: config.fetch(:span_kind) + span_name, attributes: attrs, kind: config.fetch(:span_kind) ) do |span| OpenTelemetry.propagation.inject(env.request_headers) 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..8488286a8b 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 @@ -4,6 +4,8 @@ # # SPDX-License-Identifier: Apache-2.0 +require_relative '../http_helper' + module OpenTelemetry module Instrumentation module Faraday @@ -29,15 +31,21 @@ class TracerMiddleware < ::Faraday::Middleware def call(env) http_method = HTTP_METHODS_SYMBOL_TO_STRING[env.method] + normalized_method, original_method = HttpHelper.normalize_method(http_method || env.method) + span_name = HttpHelper.span_name_for_stable(normalized_method) + config = Faraday::Instrumentation.instance.config attributes = span_creation_attributes( - http_method: http_method, url: env.url, config: config + http_method: normalized_method, + original_method: original_method, + url: env.url, + config: config ) OpenTelemetry::Common::HTTP::ClientContext.with_attributes(attributes) do |attrs, _| tracer.in_span( - http_method, attributes: attrs, kind: config.fetch(:span_kind) + span_name, attributes: attrs, kind: config.fetch(:span_kind) ) do |span| OpenTelemetry.propagation.inject(env.request_headers) @@ -58,12 +66,13 @@ def call(env) private - def span_creation_attributes(http_method:, url:, config:) + def span_creation_attributes(http_method:, original_method:, 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['http.request.method_original'] = original_method if original_method attrs['server.address'] = url.host if url.host attrs['peer.service'] = config[:peer_service] if config[:peer_service] 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..bd76fcda71 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..ac0ddc8c67 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 @@ -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' 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..445b9c6046 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/patches/dup/client.rb b/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/dup/client.rb index c32717525f..a884ec8a70 100644 --- a/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/dup/client.rb +++ b/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/dup/client.rb @@ -4,6 +4,8 @@ # # SPDX-License-Identifier: Apache-2.0 +require_relative '../http_helper' + module OpenTelemetry module Instrumentation module HTTP @@ -18,24 +20,26 @@ module Client def perform(req, options) uri = req.uri request_method = req.verb.to_s.upcase - span_name = create_request_span_name(request_method, uri.path) + normalized_method, original_method = HttpHelper.normalize_method(request_method) + span_name = create_span_name(normalized_method, uri.path) attributes = { # old semconv - 'http.method' => request_method, + 'http.method' => normalized_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, + 'http.request.method' => normalized_method, 'url.scheme' => uri.scheme, 'url.path' => uri.path, 'url.full' => "#{uri.scheme}://#{uri.host}", 'server.address' => uri.host, 'server.port' => uri.port } + attributes['http.request.method_original'] = original_method if original_method attributes['url.query'] = uri.query unless uri.query.nil? attributes.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) @@ -62,15 +66,17 @@ 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(normalized_method, request_path) + default_span_name = HttpHelper.span_name_for_stable(normalized_method) + 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 + updated_span_name = implementation.call(normalized_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/http_helper.rb b/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/http_helper.rb new file mode 100644 index 0000000000..69cd6f61cd --- /dev/null +++ b/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/http_helper.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Instrumentation + module HTTP + module Patches + # Module for normalizing HTTP methods + module HttpHelper + # List of known HTTP methods per OpenTelemetry semantic conventions + KNOWN_METHODS = %w[CONNECT DELETE GET HEAD OPTIONS PATCH POST PUT TRACE].freeze + + # Normalizes an HTTP method according to OpenTelemetry semantic conventions + # @param method [String, Symbol] The HTTP method to normalize + # @return [Array] A tuple of [normalized_method, original_method] + # where normalized_method is either a known method or '_OTHER', + # and original_method is the uppercase original method if it was normalized to '_OTHER', or nil + def self.normalize_method(method) + normalized = method.to_s.upcase + KNOWN_METHODS.include?(normalized) ? [normalized, nil] : ['_OTHER', normalized] + end + + # Generates span name for stable semantic conventions + # @param normalized_method [String] the normalized HTTP method + # @return [String] the span name + def self.span_name_for_stable(normalized_method) + normalized_method == '_OTHER' ? 'HTTP' : normalized_method + end + + # Generates span name for old semantic conventions + # @param normalized_method [String] the normalized HTTP method + # @return [String] the span name + def self.span_name_for_old(normalized_method) + normalized_method == '_OTHER' ? 'HTTP' : "HTTP #{normalized_method}" + end + end + end + end + end +end 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..ace57f3d6e 100644 --- a/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/old/client.rb +++ b/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/old/client.rb @@ -4,6 +4,8 @@ # # SPDX-License-Identifier: Apache-2.0 +require_relative '../http_helper' + module OpenTelemetry module Instrumentation module HTTP @@ -18,10 +20,11 @@ module Client def perform(req, options) uri = req.uri request_method = req.verb.to_s.upcase - span_name = create_request_span_name(request_method, uri.path) + normalized_method, _original_method = HttpHelper.normalize_method(request_method) + span_name = create_span_name(normalized_method, uri.path) attributes = { - 'http.method' => request_method, + 'http.method' => normalized_method, 'http.scheme' => uri.scheme, 'http.target' => uri.path, 'http.url' => "#{uri.scheme}://#{uri.host}", @@ -51,15 +54,17 @@ 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(normalized_method, request_path) + default_span_name = HttpHelper.span_name_for_old(normalized_method) + 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}" + updated_span_name = implementation.call(normalized_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..8fd958b6a8 100644 --- a/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/stable/client.rb +++ b/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/stable/client.rb @@ -4,6 +4,8 @@ # # SPDX-License-Identifier: Apache-2.0 +require_relative '../http_helper' + module OpenTelemetry module Instrumentation module HTTP @@ -18,16 +20,18 @@ module Client def perform(req, options) uri = req.uri request_method = req.verb.to_s.upcase - span_name = create_request_span_name(request_method, uri.path) + normalized_method, original_method = HttpHelper.normalize_method(request_method) + span_name = create_span_name(normalized_method, uri.path) attributes = { - 'http.request.method' => request_method, + 'http.request.method' => normalized_method, 'url.scheme' => uri.scheme, 'url.path' => uri.path, 'url.full' => "#{uri.scheme}://#{uri.host}", 'server.address' => uri.host, 'server.port' => uri.port } + attributes['http.request.method_original'] = original_method if original_method attributes['url.query'] = uri.query unless uri.query.nil? attributes.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) @@ -53,15 +57,17 @@ 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(normalized_method, request_path) + default_span_name = HttpHelper.span_name_for_stable(normalized_method) + 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 + updated_span_name = implementation.call(normalized_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..2252c06aae 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..292e4e91c0 100644 --- a/instrumentation/http/test/instrumentation/http/patches/old/client_test.rb +++ b/instrumentation/http/test/instrumentation/http/patches/old/client_test.rb @@ -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..3e74728734 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/patches/dup/client.rb b/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/dup/client.rb index 6113e8e5ea..e4632cbca4 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 @@ -4,6 +4,8 @@ # # SPDX-License-Identifier: Apache-2.0 +require_relative '../http_helper' + module OpenTelemetry module Instrumentation module HttpClient @@ -20,16 +22,19 @@ def do_get_block(req, proxy, conn, &) uri = req.header.request_uri url = "#{uri.scheme}://#{uri.host}" request_method = req.header.request_method + normalized_method, original_method = HttpHelper.normalize_method(request_method) + + span_name = HttpHelper.span_name_for_stable(normalized_method) attributes = { - 'http.method' => request_method, + 'http.method' => normalized_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, + 'http.request.method' => normalized_method, 'url.scheme' => uri.scheme, 'url.path' => uri.path, 'url.full' => url, @@ -37,9 +42,10 @@ def do_get_block(req, proxy, conn, &) 'server.port' => uri.port }.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) + attributes['http.request.method_original'] = original_method if original_method attributes['url.query'] = uri.query unless uri.query.nil? - tracer.in_span(request_method, attributes: attributes, kind: :client) do |span| + tracer.in_span(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/http_helper.rb b/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/http_helper.rb new file mode 100644 index 0000000000..87f4a330da --- /dev/null +++ b/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/http_helper.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Instrumentation + module HttpClient + module Patches + # Module for normalizing HTTP methods + module HttpHelper + # List of known HTTP methods per OpenTelemetry semantic conventions + KNOWN_METHODS = %w[CONNECT DELETE GET HEAD OPTIONS PATCH POST PUT TRACE].freeze + + # Normalizes an HTTP method according to OpenTelemetry semantic conventions + # @param method [String, Symbol] The HTTP method to normalize + # @return [Array] A tuple of [normalized_method, original_method] + # where normalized_method is either a known method or '_OTHER', + # and original_method is the uppercase original method if it was normalized to '_OTHER', or nil + def self.normalize_method(method) + normalized = method.to_s.upcase + KNOWN_METHODS.include?(normalized) ? [normalized, nil] : ['_OTHER', normalized] + end + + # Generates span name for stable semantic conventions + # @param normalized_method [String] the normalized HTTP method + # @return [String] the span name + def self.span_name_for_stable(normalized_method) + normalized_method == '_OTHER' ? 'HTTP' : normalized_method + end + + # Generates span name for old semantic conventions + # @param normalized_method [String] the normalized HTTP method + # @return [String] the span name + def self.span_name_for_old(normalized_method) + normalized_method == '_OTHER' ? 'HTTP' : "HTTP #{normalized_method}" + end + end + end + end + end +end 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..ef061313d8 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 @@ -4,6 +4,8 @@ # # SPDX-License-Identifier: Apache-2.0 +require_relative '../http_helper' + module OpenTelemetry module Instrumentation module HttpClient @@ -20,9 +22,12 @@ def do_get_block(req, proxy, conn, &) uri = req.header.request_uri url = "#{uri.scheme}://#{uri.host}" request_method = req.header.request_method + normalized_method, _original_method = HttpHelper.normalize_method(request_method) + + span_name = HttpHelper.span_name_for_old(normalized_method) attributes = { - 'http.method' => request_method, + 'http.method' => normalized_method, 'http.scheme' => uri.scheme, 'http.target' => uri.path, 'http.url' => url, @@ -30,7 +35,7 @@ def do_get_block(req, proxy, conn, &) 'net.peer.port' => uri.port }.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) - tracer.in_span("HTTP #{request_method}", attributes: attributes, kind: :client) do |span| + tracer.in_span(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/stable/client.rb b/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/stable/client.rb index df2bee0846..44812e623a 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 @@ -4,6 +4,8 @@ # # SPDX-License-Identifier: Apache-2.0 +require_relative '../http_helper' + module OpenTelemetry module Instrumentation module HttpClient @@ -20,9 +22,11 @@ def do_get_block(req, proxy, conn, &) uri = req.header.request_uri url = "#{uri.scheme}://#{uri.host}" request_method = req.header.request_method + normalized_method, original_method = HttpHelper.normalize_method(request_method) + span_name = HttpHelper.span_name_for_stable(normalized_method) attributes = { - 'http.request.method' => request_method, + 'http.request.method' => normalized_method, 'url.scheme' => uri.scheme, 'url.path' => uri.path, 'url.full' => url, @@ -30,9 +34,10 @@ def do_get_block(req, proxy, conn, &) 'server.port' => uri.port }.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) + attributes['http.request.method_original'] = original_method if original_method attributes['url.query'] = uri.query unless uri.query.nil? - tracer.in_span(request_method, attributes: attributes, kind: :client) do |span| + tracer.in_span(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..1ec8dfe789 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,32 @@ 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') + + _(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 '/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..210762c473 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 @@ -125,5 +125,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') + + _(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 end end 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..f9844a4f22 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,25 @@ 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') + + _(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 end end diff --git a/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/dup/plugin.rb b/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/dup/plugin.rb index efe51dd19e..b8cd3b58fd 100644 --- a/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/dup/plugin.rb +++ b/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/dup/plugin.rb @@ -4,6 +4,8 @@ # # SPDX-License-Identifier: Apache-2.0 +require_relative '../http_helper' + module OpenTelemetry module Instrumentation module HTTPX @@ -72,17 +74,20 @@ def initialize_span(request, start_time = ::Time.now) verb = request.verb uri = request.uri + normalized_method, original_method = HttpHelper.normalize_method(verb) + span_name = HttpHelper.span_name_for_stable(normalized_method) + config = HTTPX::Instrumentation.instance.config attributes = { OpenTelemetry::SemanticConventions::Trace::HTTP_HOST => uri.host, - OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => verb, + OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => normalized_method, 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, + 'http.request.method' => normalized_method, 'url.scheme' => uri.scheme, 'url.path' => uri.path, 'url.full' => "#{uri.scheme}://#{uri.host}", @@ -90,11 +95,12 @@ def initialize_span(request, start_time = ::Time.now) 'server.port' => uri.port } + attributes['http.request.method_original'] = original_method if original_method 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) - span = tracer.start_span(verb, attributes: attributes, kind: :client, start_timestamp: start_time) + span = tracer.start_span(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..2b7d818109 --- /dev/null +++ b/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/http_helper.rb @@ -0,0 +1,48 @@ +# 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 + module HttpHelper + # Standard HTTP methods as defined in the OpenTelemetry semantic conventions + # https://opentelemetry.io/docs/specs/semconv/http/http-spans/ + KNOWN_METHODS = %w[CONNECT DELETE GET HEAD OPTIONS PATCH POST PUT TRACE].freeze + + # Normalizes an HTTP method according to OpenTelemetry semantic conventions + # @param method [String, Symbol] The HTTP method to normalize + # @return [Array] A tuple of [normalized_method, original_method] + # - For known methods: returns [uppercase_method, nil] + # - For unknown methods: returns ['_OTHER', uppercase_original_method] + def self.normalize_method(method) + return [nil, nil] if method.nil? + + normalized = method.to_s.upcase + if KNOWN_METHODS.include?(normalized) + [normalized, nil] + else + ['_OTHER', normalized] + end + end + + # Generates span name for stable semantic conventions + # @param normalized_method [String] the normalized HTTP method + # @return [String] the span name + def self.span_name_for_stable(normalized_method) + normalized_method == '_OTHER' ? 'HTTP' : normalized_method + end + + # Generates span name for old semantic conventions + # @param normalized_method [String] the normalized HTTP method + # @return [String] the span name + def self.span_name_for_old(normalized_method) + normalized_method == '_OTHER' ? 'HTTP' : "HTTP #{normalized_method}" + 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..5d6a75bd4c 100644 --- a/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/old/plugin.rb +++ b/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/old/plugin.rb @@ -4,6 +4,8 @@ # # SPDX-License-Identifier: Apache-2.0 +require_relative '../http_helper' + module OpenTelemetry module Instrumentation module HTTPX @@ -71,11 +73,14 @@ def initialize_span(request, start_time = ::Time.now) verb = request.verb uri = request.uri + normalized_method, _original_method = HttpHelper.normalize_method(verb) + span_name = HttpHelper.span_name_for_old(normalized_method) + config = HTTPX::Instrumentation.instance.config attributes = { OpenTelemetry::SemanticConventions::Trace::HTTP_HOST => uri.host, - OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => verb, + OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => normalized_method, OpenTelemetry::SemanticConventions::Trace::HTTP_SCHEME => uri.scheme, OpenTelemetry::SemanticConventions::Trace::HTTP_TARGET => uri.path, OpenTelemetry::SemanticConventions::Trace::HTTP_URL => "#{uri.scheme}://#{uri.host}", @@ -86,7 +91,7 @@ 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) - span = tracer.start_span("HTTP #{verb}", attributes: attributes, kind: :client, start_timestamp: start_time) + span = tracer.start_span(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..74975ce8db 100644 --- a/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/stable/plugin.rb +++ b/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/stable/plugin.rb @@ -4,6 +4,8 @@ # # SPDX-License-Identifier: Apache-2.0 +require_relative '../http_helper' + module OpenTelemetry module Instrumentation module HTTPX @@ -71,21 +73,25 @@ def initialize_span(request, start_time = ::Time.now) verb = request.verb uri = request.uri + normalized_method, original_method = HttpHelper.normalize_method(verb) + span_name = HttpHelper.span_name_for_stable(normalized_method) + config = HTTPX::Instrumentation.instance.config attributes = { - 'http.request.method' => verb, + 'http.request.method' => normalized_method, 'url.scheme' => uri.scheme, 'url.path' => uri.path, 'url.full' => "#{uri.scheme}://#{uri.host}", 'server.address' => uri.host, 'server.port' => uri.port } + attributes['http.request.method_original'] = original_method if original_method 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) - span = tracer.start_span(verb, attributes: attributes, kind: :client, start_timestamp: start_time) + span = tracer.start_span(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..b4fe220a19 100644 --- a/instrumentation/httpx/test/instrumentation/old/plugin_test.rb +++ b/instrumentation/httpx/test/instrumentation/old/plugin_test.rb @@ -35,6 +35,24 @@ _(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') 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/patches/dup/instrumentation.rb b/instrumentation/net_http/lib/opentelemetry/instrumentation/net/http/patches/dup/instrumentation.rb index 9bda0080ab..310d88722a 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 @@ -4,6 +4,8 @@ # # SPDX-License-Identifier: Apache-2.0 +require_relative '../http_helper' + module OpenTelemetry module Instrumentation module Net @@ -23,17 +25,21 @@ def request(req, body = nil, &) return super if untraced? + normalized_method, original_method = HttpHelper.normalize_method(req.method) + span_name = HttpHelper.span_name_for_stable(normalized_method) + attributes = { - OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => req.method, + OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => normalized_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, + 'http.request.method' => normalized_method, 'url.scheme' => USE_SSL_TO_SCHEME[use_ssl?], 'server.address' => @address, 'server.port' => @port } + attributes['http.request.method_original'] = original_method if original_method path, query = split_path_and_query(req.path) attributes['url.path'] = path attributes['url.query'] = query if query @@ -41,7 +47,7 @@ def request(req, body = nil, &) attributes.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) tracer.in_span( - req.method, + span_name, attributes: attributes, kind: :client ) do |span| diff --git a/instrumentation/net_http/lib/opentelemetry/instrumentation/net/http/patches/http_helper.rb b/instrumentation/net_http/lib/opentelemetry/instrumentation/net/http/patches/http_helper.rb new file mode 100644 index 0000000000..c3d33959ff --- /dev/null +++ b/instrumentation/net_http/lib/opentelemetry/instrumentation/net/http/patches/http_helper.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Instrumentation + module Net + module HTTP + module Patches + # Utility module for HTTP-related helper methods + module HttpHelper + # Known HTTP methods per semantic conventions (stable methods only) + # https://opentelemetry.io/docs/specs/semconv/http/http-spans/ + # Includes methods from RFC9110 and RFC5789 (PATCH) + # Note: QUERY method is excluded as it's still in Development status + KNOWN_METHODS = %w[ + CONNECT + DELETE + GET + HEAD + OPTIONS + PATCH + POST + PUT + TRACE + ].freeze + + module_function + + # Normalizes an HTTP method per semantic conventions + # @param method [String] the HTTP method to normalize + # @return [Array] normalized method and original if different + def normalize_method(method) + method_str = method.to_s.upcase + + if KNOWN_METHODS.include?(method_str) + [method_str, nil] + else + ['_OTHER', method_str] + end + end + + # Generates span name for stable semantic conventions + # @param normalized_method [String] the normalized HTTP method + # @return [String] the span name + def span_name_for_stable(normalized_method) + normalized_method == '_OTHER' ? 'HTTP' : normalized_method + end + + # Generates span name for old semantic conventions + # @param normalized_method [String] the normalized HTTP method + # @return [String] the span name + def span_name_for_old(normalized_method) + normalized_method == '_OTHER' ? 'HTTP' : "HTTP #{normalized_method}" + end + end + end + end + end + end +end 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..d92acac0ca 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 @@ -4,6 +4,8 @@ # # SPDX-License-Identifier: Apache-2.0 +require_relative '../http_helper' + module OpenTelemetry module Instrumentation module Net @@ -12,7 +14,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,8 +25,11 @@ def request(req, body = nil, &) return super if untraced? + normalized_method, _original_method = HttpHelper.normalize_method(req.method) + span_name = HttpHelper.span_name_for_old(normalized_method) + attributes = { - OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => req.method, + OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => normalized_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, @@ -33,7 +37,7 @@ def request(req, body = nil, &) }.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) tracer.in_span( - HTTP_METHODS_TO_SPAN_NAMES[req.method], + span_name, attributes: attributes, kind: :client ) do |span| 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..54ac0a8a0e 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 @@ -4,6 +4,8 @@ # # SPDX-License-Identifier: Apache-2.0 +require_relative '../http_helper' + module OpenTelemetry module Instrumentation module Net @@ -23,12 +25,16 @@ def request(req, body = nil, &) return super if untraced? + normalized_method, original_method = HttpHelper.normalize_method(req.method) + span_name = HttpHelper.span_name_for_stable(normalized_method) + attributes = { - 'http.request.method' => req.method, + 'http.request.method' => normalized_method, 'url.scheme' => USE_SSL_TO_SCHEME[use_ssl?], 'server.address' => @address, 'server.port' => @port } + attributes['http.request.method_original'] = original_method if original_method path, query = split_path_and_query(req.path) attributes['url.path'] = path attributes['url.query'] = query if query @@ -36,7 +42,7 @@ def request(req, body = nil, &) attributes.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) tracer.in_span( - req.method.to_s, + 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..133559602f 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 @@ -77,6 +77,29 @@ ) 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')) 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' From 678595678bd6f6f3a86245c12e0dc9facb3dd9ff Mon Sep 17 00:00:00 2001 From: Ariel Valentin Date: Thu, 6 Nov 2025 09:00:10 -0600 Subject: [PATCH 2/6] squash: remove n/a --- .../lib/opentelemetry/instrumentation/ethon/patches/old/easy.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 4984bf75a5..f908653d41 100644 --- a/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/old/easy.rb +++ b/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/old/easy.rb @@ -69,7 +69,7 @@ def reset end def otel_before_request - method = 'N/A' # Could be GET or not HTTP at all + method = '_OTHER' # Could be GET or not HTTP at all method = @otel_method if instance_variable_defined?(:@otel_method) && !@otel_method.nil? normalized_method, _original_method = HttpHelper.normalize_method(method) From 0efff9cf80d29adb98bc6316b85172abd497cb1e Mon Sep 17 00:00:00 2001 From: Ariel Valentin Date: Thu, 6 Nov 2025 09:16:30 -0600 Subject: [PATCH 3/6] squash: reduce allocations --- .../ethon/patches/http_helper.rb | 17 +++++++++++++++-- .../excon/middlewares/http_helper.rb | 17 +++++++++++++++-- .../faraday/middlewares/http_helper.rb | 17 +++++++++++++++-- .../instrumentation/http/patches/http_helper.rb | 17 +++++++++++++++-- .../http_client/patches/http_helper.rb | 17 +++++++++++++++-- .../instrumentation/httpx/http_helper.rb | 17 +++++++++++++++-- .../net/http/patches/http_helper.rb | 17 +++++++++++++++-- 7 files changed, 105 insertions(+), 14 deletions(-) diff --git a/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/http_helper.rb b/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/http_helper.rb index ce837182dd..7490567df6 100644 --- a/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/http_helper.rb +++ b/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/http_helper.rb @@ -12,8 +12,21 @@ module Patches module HttpHelper KNOWN_METHODS = %w[CONNECT DELETE GET HEAD OPTIONS PATCH POST PUT TRACE].freeze + # Pre-computed span names for old semantic conventions to avoid allocations + OLD_SPAN_NAMES = { + 'CONNECT' => 'HTTP CONNECT', + 'DELETE' => 'HTTP DELETE', + 'GET' => 'HTTP GET', + 'HEAD' => 'HTTP HEAD', + 'OPTIONS' => 'HTTP OPTIONS', + 'PATCH' => 'HTTP PATCH', + 'POST' => 'HTTP POST', + 'PUT' => 'HTTP PUT', + 'TRACE' => 'HTTP TRACE' + }.freeze + def self.normalize_method(method) - normalized = method.to_s.upcase + normalized = method.is_a?(String) ? method.upcase : method.to_s.upcase if KNOWN_METHODS.include?(normalized) [normalized, nil] else @@ -32,7 +45,7 @@ def self.span_name_for_stable(normalized_method) # @param normalized_method [String] the normalized HTTP method # @return [String] the span name def self.span_name_for_old(normalized_method) - normalized_method == '_OTHER' ? 'HTTP' : "HTTP #{normalized_method}" + OLD_SPAN_NAMES.fetch(normalized_method, 'HTTP') end end end diff --git a/instrumentation/excon/lib/opentelemetry/instrumentation/excon/middlewares/http_helper.rb b/instrumentation/excon/lib/opentelemetry/instrumentation/excon/middlewares/http_helper.rb index 88c83810f1..66f50ffdd1 100644 --- a/instrumentation/excon/lib/opentelemetry/instrumentation/excon/middlewares/http_helper.rb +++ b/instrumentation/excon/lib/opentelemetry/instrumentation/excon/middlewares/http_helper.rb @@ -14,6 +14,19 @@ module HttpHelper # https://opentelemetry.io/docs/specs/semconv/http/http-spans/ KNOWN_METHODS = %w[CONNECT DELETE GET HEAD OPTIONS PATCH POST PUT TRACE].freeze + # Pre-computed span names for old semantic conventions to avoid allocations + OLD_SPAN_NAMES = { + 'CONNECT' => 'HTTP CONNECT', + 'DELETE' => 'HTTP DELETE', + 'GET' => 'HTTP GET', + 'HEAD' => 'HTTP HEAD', + 'OPTIONS' => 'HTTP OPTIONS', + 'PATCH' => 'HTTP PATCH', + 'POST' => 'HTTP POST', + 'PUT' => 'HTTP PUT', + 'TRACE' => 'HTTP TRACE' + }.freeze + # Normalizes an HTTP method according to OpenTelemetry semantic conventions # @param method [String, Symbol] The HTTP method to normalize # @return [Array] A tuple of [normalized_method, original_method] @@ -22,7 +35,7 @@ module HttpHelper def self.normalize_method(method) return [nil, nil] if method.nil? - normalized = method.to_s.upcase + normalized = method.is_a?(String) ? method.upcase : method.to_s.upcase if KNOWN_METHODS.include?(normalized) [normalized, nil] else @@ -41,7 +54,7 @@ def self.span_name_for_stable(normalized_method) # @param normalized_method [String] the normalized HTTP method # @return [String] the span name def self.span_name_for_old(normalized_method) - normalized_method == '_OTHER' ? 'HTTP' : "HTTP #{normalized_method}" + OLD_SPAN_NAMES.fetch(normalized_method, 'HTTP') end end end diff --git a/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/middlewares/http_helper.rb b/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/middlewares/http_helper.rb index 48beffedfa..64393b61e7 100644 --- a/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/middlewares/http_helper.rb +++ b/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/middlewares/http_helper.rb @@ -26,13 +26,26 @@ module HttpHelper TRACE ].freeze + # Pre-computed span names for old semantic conventions to avoid allocations + OLD_SPAN_NAMES = { + 'CONNECT' => 'HTTP CONNECT', + 'DELETE' => 'HTTP DELETE', + 'GET' => 'HTTP GET', + 'HEAD' => 'HTTP HEAD', + 'OPTIONS' => 'HTTP OPTIONS', + 'PATCH' => 'HTTP PATCH', + 'POST' => 'HTTP POST', + 'PUT' => 'HTTP PUT', + 'TRACE' => 'HTTP TRACE' + }.freeze + module_function # Normalizes an HTTP method per semantic conventions # @param method [String, Symbol] the HTTP method to normalize # @return [Array] normalized method and original if different def normalize_method(method) - method_str = method.to_s.upcase + method_str = method.is_a?(String) ? method.upcase : method.to_s.upcase if KNOWN_METHODS.include?(method_str) [method_str, nil] @@ -52,7 +65,7 @@ def span_name_for_stable(normalized_method) # @param normalized_method [String] the normalized HTTP method # @return [String] the span name def span_name_for_old(normalized_method) - normalized_method == '_OTHER' ? 'HTTP' : "HTTP #{normalized_method}" + OLD_SPAN_NAMES.fetch(normalized_method, 'HTTP') end end end diff --git a/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/http_helper.rb b/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/http_helper.rb index 69cd6f61cd..3c577db2cd 100644 --- a/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/http_helper.rb +++ b/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/http_helper.rb @@ -13,13 +13,26 @@ module HttpHelper # List of known HTTP methods per OpenTelemetry semantic conventions KNOWN_METHODS = %w[CONNECT DELETE GET HEAD OPTIONS PATCH POST PUT TRACE].freeze + # Pre-computed span names for old semantic conventions to avoid allocations + OLD_SPAN_NAMES = { + 'CONNECT' => 'HTTP CONNECT', + 'DELETE' => 'HTTP DELETE', + 'GET' => 'HTTP GET', + 'HEAD' => 'HTTP HEAD', + 'OPTIONS' => 'HTTP OPTIONS', + 'PATCH' => 'HTTP PATCH', + 'POST' => 'HTTP POST', + 'PUT' => 'HTTP PUT', + 'TRACE' => 'HTTP TRACE' + }.freeze + # Normalizes an HTTP method according to OpenTelemetry semantic conventions # @param method [String, Symbol] The HTTP method to normalize # @return [Array] A tuple of [normalized_method, original_method] # where normalized_method is either a known method or '_OTHER', # and original_method is the uppercase original method if it was normalized to '_OTHER', or nil def self.normalize_method(method) - normalized = method.to_s.upcase + normalized = method.is_a?(String) ? method.upcase : method.to_s.upcase KNOWN_METHODS.include?(normalized) ? [normalized, nil] : ['_OTHER', normalized] end @@ -34,7 +47,7 @@ def self.span_name_for_stable(normalized_method) # @param normalized_method [String] the normalized HTTP method # @return [String] the span name def self.span_name_for_old(normalized_method) - normalized_method == '_OTHER' ? 'HTTP' : "HTTP #{normalized_method}" + OLD_SPAN_NAMES.fetch(normalized_method, 'HTTP') end end end diff --git a/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/http_helper.rb b/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/http_helper.rb index 87f4a330da..92ddae68a4 100644 --- a/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/http_helper.rb +++ b/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/http_helper.rb @@ -13,13 +13,26 @@ module HttpHelper # List of known HTTP methods per OpenTelemetry semantic conventions KNOWN_METHODS = %w[CONNECT DELETE GET HEAD OPTIONS PATCH POST PUT TRACE].freeze + # Pre-computed span names for old semantic conventions to avoid allocations + OLD_SPAN_NAMES = { + 'CONNECT' => 'HTTP CONNECT', + 'DELETE' => 'HTTP DELETE', + 'GET' => 'HTTP GET', + 'HEAD' => 'HTTP HEAD', + 'OPTIONS' => 'HTTP OPTIONS', + 'PATCH' => 'HTTP PATCH', + 'POST' => 'HTTP POST', + 'PUT' => 'HTTP PUT', + 'TRACE' => 'HTTP TRACE' + }.freeze + # Normalizes an HTTP method according to OpenTelemetry semantic conventions # @param method [String, Symbol] The HTTP method to normalize # @return [Array] A tuple of [normalized_method, original_method] # where normalized_method is either a known method or '_OTHER', # and original_method is the uppercase original method if it was normalized to '_OTHER', or nil def self.normalize_method(method) - normalized = method.to_s.upcase + normalized = method.is_a?(String) ? method.upcase : method.to_s.upcase KNOWN_METHODS.include?(normalized) ? [normalized, nil] : ['_OTHER', normalized] end @@ -34,7 +47,7 @@ def self.span_name_for_stable(normalized_method) # @param normalized_method [String] the normalized HTTP method # @return [String] the span name def self.span_name_for_old(normalized_method) - normalized_method == '_OTHER' ? 'HTTP' : "HTTP #{normalized_method}" + OLD_SPAN_NAMES.fetch(normalized_method, 'HTTP') end end end diff --git a/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/http_helper.rb b/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/http_helper.rb index 2b7d818109..dd7b99b188 100644 --- a/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/http_helper.rb +++ b/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/http_helper.rb @@ -13,6 +13,19 @@ module HttpHelper # https://opentelemetry.io/docs/specs/semconv/http/http-spans/ KNOWN_METHODS = %w[CONNECT DELETE GET HEAD OPTIONS PATCH POST PUT TRACE].freeze + # Pre-computed span names for old semantic conventions to avoid allocations + OLD_SPAN_NAMES = { + 'CONNECT' => 'HTTP CONNECT', + 'DELETE' => 'HTTP DELETE', + 'GET' => 'HTTP GET', + 'HEAD' => 'HTTP HEAD', + 'OPTIONS' => 'HTTP OPTIONS', + 'PATCH' => 'HTTP PATCH', + 'POST' => 'HTTP POST', + 'PUT' => 'HTTP PUT', + 'TRACE' => 'HTTP TRACE' + }.freeze + # Normalizes an HTTP method according to OpenTelemetry semantic conventions # @param method [String, Symbol] The HTTP method to normalize # @return [Array] A tuple of [normalized_method, original_method] @@ -21,7 +34,7 @@ module HttpHelper def self.normalize_method(method) return [nil, nil] if method.nil? - normalized = method.to_s.upcase + normalized = method.is_a?(String) ? method.upcase : method.to_s.upcase if KNOWN_METHODS.include?(normalized) [normalized, nil] else @@ -40,7 +53,7 @@ def self.span_name_for_stable(normalized_method) # @param normalized_method [String] the normalized HTTP method # @return [String] the span name def self.span_name_for_old(normalized_method) - normalized_method == '_OTHER' ? 'HTTP' : "HTTP #{normalized_method}" + OLD_SPAN_NAMES.fetch(normalized_method, 'HTTP') end end end diff --git a/instrumentation/net_http/lib/opentelemetry/instrumentation/net/http/patches/http_helper.rb b/instrumentation/net_http/lib/opentelemetry/instrumentation/net/http/patches/http_helper.rb index c3d33959ff..f1e100c4fc 100644 --- a/instrumentation/net_http/lib/opentelemetry/instrumentation/net/http/patches/http_helper.rb +++ b/instrumentation/net_http/lib/opentelemetry/instrumentation/net/http/patches/http_helper.rb @@ -27,13 +27,26 @@ module HttpHelper TRACE ].freeze + # Pre-computed span names for old semantic conventions to avoid allocations + OLD_SPAN_NAMES = { + 'CONNECT' => 'HTTP CONNECT', + 'DELETE' => 'HTTP DELETE', + 'GET' => 'HTTP GET', + 'HEAD' => 'HTTP HEAD', + 'OPTIONS' => 'HTTP OPTIONS', + 'PATCH' => 'HTTP PATCH', + 'POST' => 'HTTP POST', + 'PUT' => 'HTTP PUT', + 'TRACE' => 'HTTP TRACE' + }.freeze + module_function # Normalizes an HTTP method per semantic conventions # @param method [String] the HTTP method to normalize # @return [Array] normalized method and original if different def normalize_method(method) - method_str = method.to_s.upcase + method_str = method.is_a?(String) ? method.upcase : method.to_s.upcase if KNOWN_METHODS.include?(method_str) [method_str, nil] @@ -53,7 +66,7 @@ def span_name_for_stable(normalized_method) # @param normalized_method [String] the normalized HTTP method # @return [String] the span name def span_name_for_old(normalized_method) - normalized_method == '_OTHER' ? 'HTTP' : "HTTP #{normalized_method}" + OLD_SPAN_NAMES.fetch(normalized_method, 'HTTP') end end end From 078f3fd763187aacacbd545c68aee3b3c19ea3f4 Mon Sep 17 00:00:00 2001 From: Ariel Valentin Date: Thu, 6 Nov 2025 10:11:50 -0600 Subject: [PATCH 4/6] squash: refactoring more --- .../instrumentation/ethon/patches/dup/easy.rb | 10 +-- .../ethon/patches/http_helper.rb | 49 ++++++++++++-- .../instrumentation/ethon/patches/old/easy.rb | 7 +- .../ethon/patches/stable/easy.rb | 7 +- .../ethon/dup/instrumentation_test.rb | 16 ++--- .../ethon/old/instrumentation_test.rb | 2 +- .../ethon/stable/instrumentation_test.rb | 4 +- .../excon/middlewares/http_helper.rb | 54 +++++++++++---- .../middlewares/dup/tracer_middleware.rb | 15 +---- .../faraday/middlewares/http_helper.rb | 65 ++++++++++++------- .../middlewares/old/tracer_middleware.rb | 15 +---- .../middlewares/stable/tracer_middleware.rb | 15 +---- .../middlewares/dup/tracer_middleware_test.rb | 2 +- .../stable/tracer_middleware_test.rb | 2 +- .../http/patches/dup/client.rb | 3 +- .../http/patches/http_helper.rb | 43 ++++++++++-- .../http/patches/old/client.rb | 3 +- .../http/patches/stable/client.rb | 3 +- .../http/patches/dup/client_test.rb | 2 +- .../http/patches/stable/client_test.rb | 2 +- .../http_client/patches/http_helper.rb | 41 ++++++++++-- .../instrumentation/httpx/http_helper.rb | 54 +++++++++++---- .../net/http/patches/http_helper.rb | 65 ++++++++++++------- 23 files changed, 307 insertions(+), 172 deletions(-) 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 1caa75b725..cd0be97723 100644 --- a/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/dup/easy.rb +++ b/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/dup/easy.rb @@ -18,7 +18,7 @@ module Easy HTTP_STATUS_SUCCESS_RANGE = (100..399) def http_request(url, action_name, options = {}) - @otel_method = action_name.to_s.upcase + @otel_method = action_name super end @@ -70,10 +70,7 @@ 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? - - normalized_method, original_method = HttpHelper.normalize_method(method) + normalized_method, original_method = HttpHelper.normalize_method(@otel_method) span_name = HttpHelper.span_name_for_stable(normalized_method) @otel_span = tracer.start_span( @@ -96,9 +93,8 @@ def otel_span_started? private def span_creation_attributes(normalized_method, original_method) - http_method = (normalized_method == '_OTHER' ? 'N/A' : normalized_method) instrumentation_attrs = { - 'http.method' => http_method, + 'http.method' => normalized_method, 'http.request.method' => normalized_method } instrumentation_attrs['http.request.method_original'] = original_method if original_method diff --git a/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/http_helper.rb b/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/http_helper.rb index 7490567df6..d75be0a19f 100644 --- a/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/http_helper.rb +++ b/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/http_helper.rb @@ -10,7 +10,36 @@ module Ethon module Patches # Helper module for HTTP method normalization module HttpHelper - KNOWN_METHODS = %w[CONNECT DELETE GET HEAD OPTIONS PATCH POST PUT TRACE].freeze + # 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 # Pre-computed span names for old semantic conventions to avoid allocations OLD_SPAN_NAMES = { @@ -25,13 +54,19 @@ module HttpHelper 'TRACE' => 'HTTP TRACE' }.freeze + private_constant :METHOD_CACHE, :OLD_SPAN_NAMES + + # Normalizes an HTTP method to uppercase per OpenTelemetry semantic conventions. + # @param method [String, Symbol] The HTTP method to normalize + # @return [Array] A tuple of [normalized_method, original_method] + # where normalized_method is either a known method or '_OTHER', + # and original_method is the original value if it was normalized to '_OTHER', or nil def self.normalize_method(method) - normalized = method.is_a?(String) ? method.upcase : method.to_s.upcase - if KNOWN_METHODS.include?(normalized) - [normalized, nil] - else - ['_OTHER', normalized] - end + normalized = METHOD_CACHE[method] + return [normalized, nil] if normalized + + # Mixed case or unknown methods are treated as '_OTHER' + ['_OTHER', method.to_s] end # Generates span name for stable semantic conventions 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 f908653d41..fd466acc41 100644 --- a/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/old/easy.rb +++ b/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/old/easy.rb @@ -18,7 +18,7 @@ module Easy HTTP_STATUS_SUCCESS_RANGE = (100..399) def http_request(url, action_name, options = {}) - @otel_method = action_name.to_s.upcase + @otel_method = action_name super end @@ -69,10 +69,7 @@ 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? - - normalized_method, _original_method = HttpHelper.normalize_method(method) + normalized_method, _original_method = HttpHelper.normalize_method(@otel_method) span_name = HttpHelper.span_name_for_old(normalized_method) @otel_span = tracer.start_span( 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 5010f1f94d..c0f5c57354 100644 --- a/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/stable/easy.rb +++ b/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/stable/easy.rb @@ -18,7 +18,7 @@ module Easy HTTP_STATUS_SUCCESS_RANGE = (100..399) def http_request(url, action_name, options = {}) - @otel_method = action_name.to_s.upcase + @otel_method = action_name super end @@ -69,10 +69,7 @@ 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? - - normalized_method, original_method = HttpHelper.normalize_method(method) + normalized_method, original_method = HttpHelper.normalize_method(@otel_method) span_name = HttpHelper.span_name_for_stable(normalized_method) @otel_span = tracer.start_span( 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 61da542658..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 @@ -274,10 +274,10 @@ def stub_response(options) stub_response(response_code: 200) do _(exporter.finished_spans.size).must_equal 1 _(span.name).must_equal 'HTTP' - _(span.attributes['http.method']).must_equal 'N/A' + _(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['http.request.method_original']).must_equal 'purge' _(span.attributes['url.full']).must_equal 'http://example.com/purge' end end 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 4def03b896..ed8aa13ad1 100644 --- a/instrumentation/ethon/test/opentelemetry/instrumentation/ethon/old/instrumentation_test.rb +++ b/instrumentation/ethon/test/opentelemetry/instrumentation/ethon/old/instrumentation_test.rb @@ -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 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 93d8713059..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 @@ -258,7 +258,7 @@ def stub_response(options) _(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.request.method_original']).must_equal 'purge' _(span.attributes['url.full']).must_equal 'http://example.com/purge' end end diff --git a/instrumentation/excon/lib/opentelemetry/instrumentation/excon/middlewares/http_helper.rb b/instrumentation/excon/lib/opentelemetry/instrumentation/excon/middlewares/http_helper.rb index 66f50ffdd1..73ac6f71d1 100644 --- a/instrumentation/excon/lib/opentelemetry/instrumentation/excon/middlewares/http_helper.rb +++ b/instrumentation/excon/lib/opentelemetry/instrumentation/excon/middlewares/http_helper.rb @@ -10,9 +10,36 @@ module Excon module Middlewares # Utility module for HTTP-related helper methods module HttpHelper - # Standard HTTP methods as defined in the OpenTelemetry semantic conventions - # https://opentelemetry.io/docs/specs/semconv/http/http-spans/ - KNOWN_METHODS = %w[CONNECT DELETE GET HEAD OPTIONS PATCH POST PUT TRACE].freeze + # 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 # Pre-computed span names for old semantic conventions to avoid allocations OLD_SPAN_NAMES = { @@ -27,20 +54,19 @@ module HttpHelper 'TRACE' => 'HTTP TRACE' }.freeze - # Normalizes an HTTP method according to OpenTelemetry semantic conventions + private_constant :METHOD_CACHE, :OLD_SPAN_NAMES + + # Normalizes an HTTP method to uppercase per OpenTelemetry semantic conventions. # @param method [String, Symbol] The HTTP method to normalize # @return [Array] A tuple of [normalized_method, original_method] - # - For known methods: returns [uppercase_method, nil] - # - For unknown methods: returns ['_OTHER', uppercase_original_method] + # where normalized_method is either a known method or '_OTHER', + # and original_method is the original value if it was normalized to '_OTHER', or nil def self.normalize_method(method) - return [nil, nil] if method.nil? - - normalized = method.is_a?(String) ? method.upcase : method.to_s.upcase - if KNOWN_METHODS.include?(normalized) - [normalized, nil] - else - ['_OTHER', normalized] - end + normalized = METHOD_CACHE[method] + return [normalized, nil] if normalized + + # Mixed case or unknown methods are treated as '_OTHER' + ['_OTHER', method.to_s] end # Generates span name for stable semantic conventions 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 3e036e9d44..84a48defe7 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 @@ -14,24 +14,11 @@ 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] - normalized_method, original_method = HttpHelper.normalize_method(http_method || env.method) + normalized_method, original_method = HttpHelper.normalize_method(env.method) # Per semantic conventions, span name uses 'HTTP' when method is unknown span_name = HttpHelper.span_name_for_stable(normalized_method) diff --git a/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/middlewares/http_helper.rb b/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/middlewares/http_helper.rb index 64393b61e7..1818e23032 100644 --- a/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/middlewares/http_helper.rb +++ b/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/middlewares/http_helper.rb @@ -10,21 +10,36 @@ module Faraday module Middlewares # Utility module for HTTP-related helper methods module HttpHelper - # Known HTTP methods per semantic conventions (stable methods only) - # https://opentelemetry.io/docs/specs/semconv/http/http-spans/ - # Includes methods from RFC9110 and RFC5789 (PATCH) - # Note: QUERY method is excluded as it's still in Development status - KNOWN_METHODS = %w[ - CONNECT - DELETE - GET - HEAD - OPTIONS - PATCH - POST - PUT - TRACE - ].freeze + # 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 # Pre-computed span names for old semantic conventions to avoid allocations OLD_SPAN_NAMES = { @@ -39,19 +54,21 @@ module HttpHelper 'TRACE' => 'HTTP TRACE' }.freeze + private_constant :METHOD_CACHE, :OLD_SPAN_NAMES + module_function - # Normalizes an HTTP method per semantic conventions - # @param method [String, Symbol] the HTTP method to normalize - # @return [Array] normalized method and original if different + # Normalizes an HTTP method according to OpenTelemetry semantic conventions + # @param method [String, Symbol] The HTTP method to normalize + # @return [Array] A tuple of [normalized_method, original_method] + # where normalized_method is either a known method or '_OTHER', + # and original_method is the original value if it was normalized to '_OTHER', or nil def normalize_method(method) - method_str = method.is_a?(String) ? method.upcase : method.to_s.upcase + normalized = METHOD_CACHE[method] + return [normalized, nil] if normalized - if KNOWN_METHODS.include?(method_str) - [method_str, nil] - else - ['_OTHER', method_str] - end + # Mixed case or unknown methods are treated as '_OTHER' + ['_OTHER', method.to_s] end # Generates span name for stable semantic conventions 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 3dd0934c70..1d7994b5de 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 @@ -14,24 +14,11 @@ 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] - normalized_method, _original_method = HttpHelper.normalize_method(http_method || env.method) + normalized_method, _original_method = HttpHelper.normalize_method(env.method) # Per semantic conventions, span name uses 'HTTP' when method is unknown span_name = HttpHelper.span_name_for_old(normalized_method) 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 8488286a8b..8362042637 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 @@ -14,24 +14,11 @@ 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] - normalized_method, original_method = HttpHelper.normalize_method(http_method || env.method) + normalized_method, original_method = HttpHelper.normalize_method(env.method) span_name = HttpHelper.span_name_for_stable(normalized_method) config = Faraday::Instrumentation.instance.config 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 bd76fcda71..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 @@ -118,7 +118,7 @@ _(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.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' 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 445b9c6046..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 @@ -100,7 +100,7 @@ _(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.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' 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 a884ec8a70..e98117c6bf 100644 --- a/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/dup/client.rb +++ b/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/dup/client.rb @@ -19,8 +19,7 @@ module Client def perform(req, options) uri = req.uri - request_method = req.verb.to_s.upcase - normalized_method, original_method = HttpHelper.normalize_method(request_method) + normalized_method, original_method = HttpHelper.normalize_method(req.verb) span_name = create_span_name(normalized_method, uri.path) attributes = { diff --git a/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/http_helper.rb b/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/http_helper.rb index 3c577db2cd..5da910ac2b 100644 --- a/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/http_helper.rb +++ b/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/http_helper.rb @@ -10,8 +10,36 @@ module HTTP module Patches # Module for normalizing HTTP methods module HttpHelper - # List of known HTTP methods per OpenTelemetry semantic conventions - KNOWN_METHODS = %w[CONNECT DELETE GET HEAD OPTIONS PATCH POST PUT TRACE].freeze + # 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 # Pre-computed span names for old semantic conventions to avoid allocations OLD_SPAN_NAMES = { @@ -26,14 +54,19 @@ module HttpHelper 'TRACE' => 'HTTP TRACE' }.freeze - # Normalizes an HTTP method according to OpenTelemetry semantic conventions + private_constant :METHOD_CACHE, :OLD_SPAN_NAMES + + # Normalizes an HTTP method to uppercase per OpenTelemetry semantic conventions. # @param method [String, Symbol] The HTTP method to normalize # @return [Array] A tuple of [normalized_method, original_method] # where normalized_method is either a known method or '_OTHER', # and original_method is the uppercase original method if it was normalized to '_OTHER', or nil def self.normalize_method(method) - normalized = method.is_a?(String) ? method.upcase : method.to_s.upcase - KNOWN_METHODS.include?(normalized) ? [normalized, nil] : ['_OTHER', normalized] + normalized = METHOD_CACHE[method] + return [normalized, nil] if normalized + + # Mixed case or unknown methods are treated as '_OTHER' + ['_OTHER', method.to_s] end # Generates span name for stable semantic conventions 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 ace57f3d6e..bff95d1d68 100644 --- a/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/old/client.rb +++ b/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/old/client.rb @@ -19,8 +19,7 @@ module Client def perform(req, options) uri = req.uri - request_method = req.verb.to_s.upcase - normalized_method, _original_method = HttpHelper.normalize_method(request_method) + normalized_method, _original_method = HttpHelper.normalize_method(req.verb) span_name = create_span_name(normalized_method, uri.path) attributes = { 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 8fd958b6a8..7990567ea3 100644 --- a/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/stable/client.rb +++ b/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/stable/client.rb @@ -19,8 +19,7 @@ module Client def perform(req, options) uri = req.uri - request_method = req.verb.to_s.upcase - normalized_method, original_method = HttpHelper.normalize_method(request_method) + normalized_method, original_method = HttpHelper.normalize_method(req.verb) span_name = create_span_name(normalized_method, uri.path) attributes = { 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 2252c06aae..079984b201 100644 --- a/instrumentation/http/test/instrumentation/http/patches/dup/client_test.rb +++ b/instrumentation/http/test/instrumentation/http/patches/dup/client_test.rb @@ -261,7 +261,7 @@ _(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.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' 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 3e74728734..f2a28581b4 100644 --- a/instrumentation/http/test/instrumentation/http/patches/stable/client_test.rb +++ b/instrumentation/http/test/instrumentation/http/patches/stable/client_test.rb @@ -203,7 +203,7 @@ _(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.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' diff --git a/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/http_helper.rb b/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/http_helper.rb index 92ddae68a4..a7eb2bbee6 100644 --- a/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/http_helper.rb +++ b/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/http_helper.rb @@ -10,8 +10,36 @@ module HttpClient module Patches # Module for normalizing HTTP methods module HttpHelper - # List of known HTTP methods per OpenTelemetry semantic conventions - KNOWN_METHODS = %w[CONNECT DELETE GET HEAD OPTIONS PATCH POST PUT TRACE].freeze + # 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 # Pre-computed span names for old semantic conventions to avoid allocations OLD_SPAN_NAMES = { @@ -26,14 +54,19 @@ module HttpHelper 'TRACE' => 'HTTP TRACE' }.freeze + private_constant :METHOD_CACHE, :OLD_SPAN_NAMES + # Normalizes an HTTP method according to OpenTelemetry semantic conventions # @param method [String, Symbol] The HTTP method to normalize # @return [Array] A tuple of [normalized_method, original_method] # where normalized_method is either a known method or '_OTHER', # and original_method is the uppercase original method if it was normalized to '_OTHER', or nil def self.normalize_method(method) - normalized = method.is_a?(String) ? method.upcase : method.to_s.upcase - KNOWN_METHODS.include?(normalized) ? [normalized, nil] : ['_OTHER', normalized] + normalized = METHOD_CACHE[method] + return [normalized, nil] if normalized + + # Mixed case or unknown methods are treated as '_OTHER' + ['_OTHER', method.to_s] end # Generates span name for stable semantic conventions diff --git a/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/http_helper.rb b/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/http_helper.rb index dd7b99b188..616126613b 100644 --- a/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/http_helper.rb +++ b/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/http_helper.rb @@ -9,9 +9,36 @@ module Instrumentation module HTTPX # Utility module for normalizing HTTP methods according to OpenTelemetry semantic conventions module HttpHelper - # Standard HTTP methods as defined in the OpenTelemetry semantic conventions - # https://opentelemetry.io/docs/specs/semconv/http/http-spans/ - KNOWN_METHODS = %w[CONNECT DELETE GET HEAD OPTIONS PATCH POST PUT TRACE].freeze + # 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 # Pre-computed span names for old semantic conventions to avoid allocations OLD_SPAN_NAMES = { @@ -26,20 +53,19 @@ module HttpHelper 'TRACE' => 'HTTP TRACE' }.freeze - # Normalizes an HTTP method according to OpenTelemetry semantic conventions + private_constant :METHOD_CACHE, :OLD_SPAN_NAMES + + # Normalizes an HTTP method to uppercase per OpenTelemetry semantic conventions. # @param method [String, Symbol] The HTTP method to normalize # @return [Array] A tuple of [normalized_method, original_method] - # - For known methods: returns [uppercase_method, nil] - # - For unknown methods: returns ['_OTHER', uppercase_original_method] + # where normalized_method is either a known method or '_OTHER', + # and original_method is the original value if it was normalized to '_OTHER', or nil def self.normalize_method(method) - return [nil, nil] if method.nil? - - normalized = method.is_a?(String) ? method.upcase : method.to_s.upcase - if KNOWN_METHODS.include?(normalized) - [normalized, nil] - else - ['_OTHER', normalized] - end + normalized = METHOD_CACHE[method] + return [normalized, nil] if normalized + + # Mixed case or unknown methods are treated as '_OTHER' + ['_OTHER', method.to_s] end # Generates span name for stable semantic conventions diff --git a/instrumentation/net_http/lib/opentelemetry/instrumentation/net/http/patches/http_helper.rb b/instrumentation/net_http/lib/opentelemetry/instrumentation/net/http/patches/http_helper.rb index f1e100c4fc..263d2ce339 100644 --- a/instrumentation/net_http/lib/opentelemetry/instrumentation/net/http/patches/http_helper.rb +++ b/instrumentation/net_http/lib/opentelemetry/instrumentation/net/http/patches/http_helper.rb @@ -11,21 +11,36 @@ module HTTP module Patches # Utility module for HTTP-related helper methods module HttpHelper - # Known HTTP methods per semantic conventions (stable methods only) - # https://opentelemetry.io/docs/specs/semconv/http/http-spans/ - # Includes methods from RFC9110 and RFC5789 (PATCH) - # Note: QUERY method is excluded as it's still in Development status - KNOWN_METHODS = %w[ - CONNECT - DELETE - GET - HEAD - OPTIONS - PATCH - POST - PUT - TRACE - ].freeze + # 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 # Pre-computed span names for old semantic conventions to avoid allocations OLD_SPAN_NAMES = { @@ -40,19 +55,21 @@ module HttpHelper 'TRACE' => 'HTTP TRACE' }.freeze + private_constant :METHOD_CACHE, :OLD_SPAN_NAMES + module_function - # Normalizes an HTTP method per semantic conventions - # @param method [String] the HTTP method to normalize - # @return [Array] normalized method and original if different + # Normalizes an HTTP method according to OpenTelemetry semantic conventions + # @param method [String, Symbol] The HTTP method to normalize + # @return [Array] A tuple of [normalized_method, original_method] + # where normalized_method is either a known method or '_OTHER', + # and original_method is the original value if it was normalized to '_OTHER', or nil def normalize_method(method) - method_str = method.is_a?(String) ? method.upcase : method.to_s.upcase + normalized = METHOD_CACHE[method] + return [normalized, nil] if normalized - if KNOWN_METHODS.include?(method_str) - [method_str, nil] - else - ['_OTHER', method_str] - end + # Mixed case or unknown methods are treated as '_OTHER' + ['_OTHER', method.to_s] end # Generates span name for stable semantic conventions From f59066e5adfd55851ad973a484e972f9f8515c14 Mon Sep 17 00:00:00 2001 From: Ariel Valentin Date: Thu, 6 Nov 2025 20:40:32 -0600 Subject: [PATCH 5/6] squash: reduce feature envy --- .../opentelemetry/instrumentation/ethon.rb | 1 + .../instrumentation/ethon/http_helper.rb | 86 +++++++++++++++++ .../instrumentation/ethon/patches/dup/easy.rb | 17 ++-- .../ethon/patches/http_helper.rb | 89 ------------------ .../instrumentation/ethon/patches/old/easy.rb | 13 +-- .../ethon/patches/stable/easy.rb | 15 ++- .../opentelemetry/instrumentation/excon.rb | 1 + .../instrumentation/excon/http_helper.rb | 86 +++++++++++++++++ .../middlewares/dup/tracer_middleware.rb | 13 +-- .../middlewares/old/tracer_middleware.rb | 9 +- .../middlewares/stable/tracer_middleware.rb | 11 +-- .../opentelemetry/instrumentation/faraday.rb | 1 + .../instrumentation/faraday/http_helper.rb | 86 +++++++++++++++++ .../middlewares/dup/tracer_middleware.rb | 13 +-- .../faraday/middlewares/http_helper.rb | 91 ------------------ .../middlewares/old/tracer_middleware.rb | 11 +-- .../middlewares/stable/tracer_middleware.rb | 11 +-- .../lib/opentelemetry/instrumentation/http.rb | 1 + .../instrumentation/http/http_helper.rb | 86 +++++++++++++++++ .../http/patches/dup/client.rb | 19 ++-- .../http/patches/http_helper.rb | 89 ------------------ .../http/patches/old/client.rb | 15 ++- .../http/patches/stable/client.rb | 17 ++-- .../instrumentation/http_client.rb | 1 + .../http_client/http_helper.rb | 86 +++++++++++++++++ .../http_client/patches/dup/client.rb | 14 +-- .../http_client/patches/http_helper.rb | 89 ------------------ .../http_client/patches/old/client.rb | 10 +- .../http_client/patches/stable/client.rb | 11 +-- .../http_client/patches/dup/client_test.rb | 1 - .../http_client/patches/old/client_test.rb | 1 - .../http_client/patches/stable/client_test.rb | 1 - .../opentelemetry/instrumentation/httpx.rb | 1 + .../instrumentation/httpx/dup/plugin.rb | 13 +-- .../instrumentation/httpx/http_helper.rb | 47 +++++----- .../instrumentation/httpx/old/plugin.rb | 9 +- .../instrumentation/httpx/stable/plugin.rb | 11 +-- .../opentelemetry/instrumentation/net/http.rb | 1 + .../instrumentation/net/http}/http_helper.rb | 51 +++++----- .../net/http/patches/dup/instrumentation.rb | 13 +-- .../net/http/patches/http_helper.rb | 93 ------------------- .../net/http/patches/old/instrumentation.rb | 9 +- .../http/patches/stable/instrumentation.rb | 11 +-- 43 files changed, 586 insertions(+), 668 deletions(-) create mode 100644 instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/http_helper.rb delete mode 100644 instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/http_helper.rb create mode 100644 instrumentation/excon/lib/opentelemetry/instrumentation/excon/http_helper.rb create mode 100644 instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/http_helper.rb delete mode 100644 instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/middlewares/http_helper.rb create mode 100644 instrumentation/http/lib/opentelemetry/instrumentation/http/http_helper.rb delete mode 100644 instrumentation/http/lib/opentelemetry/instrumentation/http/patches/http_helper.rb create mode 100644 instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/http_helper.rb delete mode 100644 instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/http_helper.rb rename instrumentation/{excon/lib/opentelemetry/instrumentation/excon/middlewares => net_http/lib/opentelemetry/instrumentation/net/http}/http_helper.rb (58%) delete mode 100644 instrumentation/net_http/lib/opentelemetry/instrumentation/net/http/patches/http_helper.rb 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..cfa059e023 --- /dev/null +++ b/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/http_helper.rb @@ -0,0 +1,86 @@ +# 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, :normalized_method, :original_method, 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 + + # Pre-computed span names for old semantic conventions to avoid allocations + OLD_SPAN_NAMES = { + 'CONNECT' => 'HTTP CONNECT', + 'DELETE' => 'HTTP DELETE', + 'GET' => 'HTTP GET', + 'HEAD' => 'HTTP HEAD', + 'OPTIONS' => 'HTTP OPTIONS', + 'PATCH' => 'HTTP PATCH', + 'POST' => 'HTTP POST', + 'PUT' => 'HTTP PUT', + 'TRACE' => 'HTTP TRACE' + }.freeze + + private_constant :METHOD_CACHE, :OLD_SPAN_NAMES + + # Prepares all span data for the specified semantic convention in a single call + # @param method [String, Symbol] The HTTP method + # @param semconv [Symbol] The semantic convention to use (:stable or :old) + # @return [SpanCreationAttributes] struct containing span_name, normalized_method, and original_method + def self.span_attrs_for(method, semconv: :stable) + normalized = METHOD_CACHE[method] + if normalized + span_name = semconv == :old ? OLD_SPAN_NAMES[normalized] : normalized + SpanCreationAttributes.new( + span_name: span_name, + normalized_method: normalized, + original_method: nil + ) + else + SpanCreationAttributes.new( + span_name: 'HTTP', + normalized_method: '_OTHER', + original_method: method.to_s + ) + end + 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 cd0be97723..c44cc11fda 100644 --- a/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/dup/easy.rb +++ b/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/dup/easy.rb @@ -4,8 +4,6 @@ # # SPDX-License-Identifier: Apache-2.0 -require_relative '../http_helper' - module OpenTelemetry module Instrumentation module Ethon @@ -70,12 +68,11 @@ def reset end def otel_before_request - normalized_method, original_method = HttpHelper.normalize_method(@otel_method) - span_name = HttpHelper.span_name_for_stable(normalized_method) + span_data = HttpHelper.span_attrs_for(@otel_method) @otel_span = tracer.start_span( - span_name, - attributes: span_creation_attributes(normalized_method, original_method), + span_data.span_name, + attributes: span_creation_attributes(span_data), kind: :client ) @@ -92,12 +89,12 @@ def otel_span_started? private - def span_creation_attributes(normalized_method, original_method) + def span_creation_attributes(span_data) instrumentation_attrs = { - 'http.method' => normalized_method, - 'http.request.method' => normalized_method + 'http.method' => span_data.normalized_method, + 'http.request.method' => span_data.normalized_method } - instrumentation_attrs['http.request.method_original'] = original_method if original_method + instrumentation_attrs['http.request.method_original'] = span_data.original_method if span_data.original_method uri = _otel_cleanse_uri(url) if uri diff --git a/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/http_helper.rb b/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/http_helper.rb deleted file mode 100644 index d75be0a19f..0000000000 --- a/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/http_helper.rb +++ /dev/null @@ -1,89 +0,0 @@ -# frozen_string_literal: true - -# Copyright The OpenTelemetry Authors -# -# SPDX-License-Identifier: Apache-2.0 - -module OpenTelemetry - module Instrumentation - module Ethon - module Patches - # Helper module for HTTP method normalization - module HttpHelper - # 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 - - # Pre-computed span names for old semantic conventions to avoid allocations - OLD_SPAN_NAMES = { - 'CONNECT' => 'HTTP CONNECT', - 'DELETE' => 'HTTP DELETE', - 'GET' => 'HTTP GET', - 'HEAD' => 'HTTP HEAD', - 'OPTIONS' => 'HTTP OPTIONS', - 'PATCH' => 'HTTP PATCH', - 'POST' => 'HTTP POST', - 'PUT' => 'HTTP PUT', - 'TRACE' => 'HTTP TRACE' - }.freeze - - private_constant :METHOD_CACHE, :OLD_SPAN_NAMES - - # Normalizes an HTTP method to uppercase per OpenTelemetry semantic conventions. - # @param method [String, Symbol] The HTTP method to normalize - # @return [Array] A tuple of [normalized_method, original_method] - # where normalized_method is either a known method or '_OTHER', - # and original_method is the original value if it was normalized to '_OTHER', or nil - def self.normalize_method(method) - normalized = METHOD_CACHE[method] - return [normalized, nil] if normalized - - # Mixed case or unknown methods are treated as '_OTHER' - ['_OTHER', method.to_s] - end - - # Generates span name for stable semantic conventions - # @param normalized_method [String] the normalized HTTP method - # @return [String] the span name - def self.span_name_for_stable(normalized_method) - normalized_method == '_OTHER' ? 'HTTP' : normalized_method - end - - # Generates span name for old semantic conventions - # @param normalized_method [String] the normalized HTTP method - # @return [String] the span name - def self.span_name_for_old(normalized_method) - OLD_SPAN_NAMES.fetch(normalized_method, 'HTTP') - end - end - end - end - end -end 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 fd466acc41..99ffe28b5d 100644 --- a/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/old/easy.rb +++ b/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/old/easy.rb @@ -4,8 +4,6 @@ # # SPDX-License-Identifier: Apache-2.0 -require_relative '../http_helper' - module OpenTelemetry module Instrumentation module Ethon @@ -69,12 +67,11 @@ def reset end def otel_before_request - normalized_method, _original_method = HttpHelper.normalize_method(@otel_method) - span_name = HttpHelper.span_name_for_old(normalized_method) + span_data = HttpHelper.span_attrs_for(@otel_method, semconv: :old) @otel_span = tracer.start_span( - span_name, - attributes: span_creation_attributes(normalized_method), + span_data.span_name, + attributes: span_creation_attributes(span_data), kind: :client ) @@ -91,9 +88,9 @@ def otel_span_started? private - def span_creation_attributes(normalized_method) + def span_creation_attributes(span_data) instrumentation_attrs = { - 'http.method' => normalized_method + 'http.method' => span_data.normalized_method } uri = _otel_cleanse_uri(url) 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 c0f5c57354..627fa9e807 100644 --- a/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/stable/easy.rb +++ b/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/stable/easy.rb @@ -4,8 +4,6 @@ # # SPDX-License-Identifier: Apache-2.0 -require_relative '../http_helper' - module OpenTelemetry module Instrumentation module Ethon @@ -69,12 +67,11 @@ def reset end def otel_before_request - normalized_method, original_method = HttpHelper.normalize_method(@otel_method) - span_name = HttpHelper.span_name_for_stable(normalized_method) + span_data = HttpHelper.span_attrs_for(@otel_method) @otel_span = tracer.start_span( - span_name, - attributes: span_creation_attributes(normalized_method, original_method), + span_data.span_name, + attributes: span_creation_attributes(span_data), kind: :client ) @@ -91,11 +88,11 @@ def otel_span_started? private - def span_creation_attributes(normalized_method, original_method) + def span_creation_attributes(span_data) instrumentation_attrs = { - 'http.request.method' => normalized_method + 'http.request.method' => span_data.normalized_method } - instrumentation_attrs['http.request.method_original'] = original_method if original_method + instrumentation_attrs['http.request.method_original'] = span_data.original_method if span_data.original_method uri = _otel_cleanse_uri(url) if uri 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..17efb8665f --- /dev/null +++ b/instrumentation/excon/lib/opentelemetry/instrumentation/excon/http_helper.rb @@ -0,0 +1,86 @@ +# 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 attributes + SpanCreationAttributes = Struct.new(:span_name, :normalized_method, :original_method, 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 + + # Pre-computed span names for old semantic conventions to avoid allocations + OLD_SPAN_NAMES = { + 'CONNECT' => 'HTTP CONNECT', + 'DELETE' => 'HTTP DELETE', + 'GET' => 'HTTP GET', + 'HEAD' => 'HTTP HEAD', + 'OPTIONS' => 'HTTP OPTIONS', + 'PATCH' => 'HTTP PATCH', + 'POST' => 'HTTP POST', + 'PUT' => 'HTTP PUT', + 'TRACE' => 'HTTP TRACE' + }.freeze + + private_constant :METHOD_CACHE, :OLD_SPAN_NAMES + + # Prepares all span data for the specified semantic convention in a single call + # @param method [String, Symbol] The HTTP method + # @param semconv [Symbol] The semantic convention to use (:stable or :old) + # @return [SpanCreationAttributes] struct containing span_name, normalized_method, and original_method + def self.span_attrs_for(method, semconv: :stable) + normalized = METHOD_CACHE[method] + if normalized + span_name = semconv == :old ? OLD_SPAN_NAMES[normalized] : normalized + SpanCreationAttributes.new( + span_name: span_name, + normalized_method: normalized, + original_method: nil + ) + else + SpanCreationAttributes.new( + span_name: 'HTTP', + normalized_method: '_OTHER', + original_method: method.to_s + ) + end + 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 5711034390..0ccdfd8b08 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 @@ -4,8 +4,6 @@ # # SPDX-License-Identifier: Apache-2.0 -require_relative '../http_helper' - module OpenTelemetry module Instrumentation module Excon @@ -19,31 +17,30 @@ class TracerMiddleware < ::Excon::Middleware::Base def request_call(datum) return @stack.request_call(datum) if untraced?(datum) - normalized_method, original_method = HttpHelper.normalize_method(datum[:method]) - span_name = HttpHelper.span_name_for_stable(normalized_method) + span_data = HttpHelper.span_attrs_for(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 => normalized_method, + OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => span_data.normalized_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' => normalized_method, + 'http.request.method' => span_data.normalized_method, 'url.scheme' => datum[:scheme], 'url.path' => datum[:path], 'url.full' => cleansed_url, 'server.address' => datum[:hostname], 'server.port' => datum[:port] } - attributes['http.request.method_original'] = original_method if original_method + attributes['http.request.method_original'] = span_data.original_method if span_data.original_method 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(span_name, attributes: attributes, kind: :client) + 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 d3947e80cb..9ffa61543d 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 @@ -4,8 +4,6 @@ # # SPDX-License-Identifier: Apache-2.0 -require_relative '../http_helper' - module OpenTelemetry module Instrumentation module Excon @@ -19,12 +17,11 @@ class TracerMiddleware < ::Excon::Middleware::Base def request_call(datum) return @stack.request_call(datum) if untraced?(datum) - normalized_method, _original_method = HttpHelper.normalize_method(datum[:method]) - span_name = HttpHelper.span_name_for_old(normalized_method) + span_data = HttpHelper.span_attrs_for(datum[:method], semconv: :old) attributes = { OpenTelemetry::SemanticConventions::Trace::HTTP_HOST => datum[:host], - OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => normalized_method, + OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => span_data.normalized_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)), @@ -34,7 +31,7 @@ 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(span_name, attributes: attributes, kind: :client) + 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 3db9ba3026..b7c4263a15 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 @@ -4,8 +4,6 @@ # # SPDX-License-Identifier: Apache-2.0 -require_relative '../http_helper' - module OpenTelemetry module Instrumentation module Excon @@ -19,23 +17,22 @@ class TracerMiddleware < ::Excon::Middleware::Base def request_call(datum) return @stack.request_call(datum) if untraced?(datum) - normalized_method, original_method = HttpHelper.normalize_method(datum[:method]) - span_name = HttpHelper.span_name_for_stable(normalized_method) + span_data = HttpHelper.span_attrs_for(datum[:method]) attributes = { - 'http.request.method' => normalized_method, + 'http.request.method' => span_data.normalized_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] } - attributes['http.request.method_original'] = original_method if original_method + attributes['http.request.method_original'] = span_data.original_method if span_data.original_method 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(span_name, attributes: attributes, kind: :client) + 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/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..8f92a6aac0 --- /dev/null +++ b/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/http_helper.rb @@ -0,0 +1,86 @@ +# 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, :normalized_method, :original_method, 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 + + # Pre-computed span names for old semantic conventions to avoid allocations + OLD_SPAN_NAMES = { + 'CONNECT' => 'HTTP CONNECT', + 'DELETE' => 'HTTP DELETE', + 'GET' => 'HTTP GET', + 'HEAD' => 'HTTP HEAD', + 'OPTIONS' => 'HTTP OPTIONS', + 'PATCH' => 'HTTP PATCH', + 'POST' => 'HTTP POST', + 'PUT' => 'HTTP PUT', + 'TRACE' => 'HTTP TRACE' + }.freeze + + private_constant :METHOD_CACHE, :OLD_SPAN_NAMES + + # Prepares all span data for the specified semantic convention in a single call + # @param method [String, Symbol] The HTTP method + # @param semconv [Symbol] The semantic convention to use (:stable or :old) + # @return [SpanCreationAttributes] struct containing span_name, normalized_method, and original_method + def self.span_attrs_for(method, semconv: :stable) + normalized = METHOD_CACHE[method] + if normalized + span_name = semconv == :old ? OLD_SPAN_NAMES[normalized] : normalized + SpanCreationAttributes.new( + span_name: span_name, + normalized_method: normalized, + original_method: nil + ) + else + SpanCreationAttributes.new( + span_name: 'HTTP', + normalized_method: '_OTHER', + original_method: method.to_s + ) + end + 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 84a48defe7..4fb7285ee2 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 @@ -4,8 +4,6 @@ # # SPDX-License-Identifier: Apache-2.0 -require_relative '../http_helper' - module OpenTelemetry module Instrumentation module Faraday @@ -18,23 +16,20 @@ class TracerMiddleware < ::Faraday::Middleware HTTP_STATUS_SUCCESS_RANGE = (100..399) def call(env) - normalized_method, original_method = HttpHelper.normalize_method(env.method) - - # Per semantic conventions, span name uses 'HTTP' when method is unknown - span_name = HttpHelper.span_name_for_stable(normalized_method) + span_data = HttpHelper.span_attrs_for(env.method) config = Faraday::Instrumentation.instance.config attributes = span_creation_attributes( - http_method: normalized_method, - original_method: original_method, + http_method: span_data.normalized_method, + original_method: span_data.original_method, url: env.url, config: config ) OpenTelemetry::Common::HTTP::ClientContext.with_attributes(attributes) do |attrs, _| tracer.in_span( - span_name, 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) diff --git a/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/middlewares/http_helper.rb b/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/middlewares/http_helper.rb deleted file mode 100644 index 1818e23032..0000000000 --- a/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/middlewares/http_helper.rb +++ /dev/null @@ -1,91 +0,0 @@ -# frozen_string_literal: true - -# Copyright The OpenTelemetry Authors -# -# SPDX-License-Identifier: Apache-2.0 - -module OpenTelemetry - module Instrumentation - module Faraday - module Middlewares - # Utility module for HTTP-related helper methods - module HttpHelper - # 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 - - # Pre-computed span names for old semantic conventions to avoid allocations - OLD_SPAN_NAMES = { - 'CONNECT' => 'HTTP CONNECT', - 'DELETE' => 'HTTP DELETE', - 'GET' => 'HTTP GET', - 'HEAD' => 'HTTP HEAD', - 'OPTIONS' => 'HTTP OPTIONS', - 'PATCH' => 'HTTP PATCH', - 'POST' => 'HTTP POST', - 'PUT' => 'HTTP PUT', - 'TRACE' => 'HTTP TRACE' - }.freeze - - private_constant :METHOD_CACHE, :OLD_SPAN_NAMES - - module_function - - # Normalizes an HTTP method according to OpenTelemetry semantic conventions - # @param method [String, Symbol] The HTTP method to normalize - # @return [Array] A tuple of [normalized_method, original_method] - # where normalized_method is either a known method or '_OTHER', - # and original_method is the original value if it was normalized to '_OTHER', or nil - def normalize_method(method) - normalized = METHOD_CACHE[method] - return [normalized, nil] if normalized - - # Mixed case or unknown methods are treated as '_OTHER' - ['_OTHER', method.to_s] - end - - # Generates span name for stable semantic conventions - # @param normalized_method [String] the normalized HTTP method - # @return [String] the span name - def span_name_for_stable(normalized_method) - normalized_method == '_OTHER' ? 'HTTP' : normalized_method - end - - # Generates span name for old semantic conventions - # @param normalized_method [String] the normalized HTTP method - # @return [String] the span name - def span_name_for_old(normalized_method) - OLD_SPAN_NAMES.fetch(normalized_method, 'HTTP') - end - end - end - end - end -end 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 1d7994b5de..579a070371 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 @@ -4,8 +4,6 @@ # # SPDX-License-Identifier: Apache-2.0 -require_relative '../http_helper' - module OpenTelemetry module Instrumentation module Faraday @@ -18,20 +16,17 @@ class TracerMiddleware < ::Faraday::Middleware HTTP_STATUS_SUCCESS_RANGE = (100..399) def call(env) - normalized_method, _original_method = HttpHelper.normalize_method(env.method) - - # Per semantic conventions, span name uses 'HTTP' when method is unknown - span_name = HttpHelper.span_name_for_old(normalized_method) + span_data = HttpHelper.span_attrs_for(env.method, semconv: :old) config = Faraday::Instrumentation.instance.config attributes = span_creation_attributes( - http_method: normalized_method, url: env.url, config: config + http_method: span_data.normalized_method, url: env.url, config: config ) OpenTelemetry::Common::HTTP::ClientContext.with_attributes(attributes) do |attrs, _| tracer.in_span( - span_name, 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) 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 8362042637..3be9b4fd98 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 @@ -4,8 +4,6 @@ # # SPDX-License-Identifier: Apache-2.0 -require_relative '../http_helper' - module OpenTelemetry module Instrumentation module Faraday @@ -18,21 +16,20 @@ class TracerMiddleware < ::Faraday::Middleware HTTP_STATUS_SUCCESS_RANGE = (100..399) def call(env) - normalized_method, original_method = HttpHelper.normalize_method(env.method) - span_name = HttpHelper.span_name_for_stable(normalized_method) + span_data = HttpHelper.span_attrs_for(env.method) config = Faraday::Instrumentation.instance.config attributes = span_creation_attributes( - http_method: normalized_method, - original_method: original_method, + http_method: span_data.normalized_method, + original_method: span_data.original_method, url: env.url, config: config ) OpenTelemetry::Common::HTTP::ClientContext.with_attributes(attributes) do |attrs, _| tracer.in_span( - span_name, 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) 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..a80dacf8b9 --- /dev/null +++ b/instrumentation/http/lib/opentelemetry/instrumentation/http/http_helper.rb @@ -0,0 +1,86 @@ +# 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, :normalized_method, :original_method, 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 + + # Pre-computed span names for old semantic conventions to avoid allocations + OLD_SPAN_NAMES = { + 'CONNECT' => 'HTTP CONNECT', + 'DELETE' => 'HTTP DELETE', + 'GET' => 'HTTP GET', + 'HEAD' => 'HTTP HEAD', + 'OPTIONS' => 'HTTP OPTIONS', + 'PATCH' => 'HTTP PATCH', + 'POST' => 'HTTP POST', + 'PUT' => 'HTTP PUT', + 'TRACE' => 'HTTP TRACE' + }.freeze + + private_constant :METHOD_CACHE, :OLD_SPAN_NAMES + + # Prepares all span data for the specified semantic convention in a single call + # @param method [String, Symbol] The HTTP method + # @param semconv [Symbol] The semantic convention to use (:stable or :old) + # @return [SpanCreationAttributes] struct containing span_name, normalized_method, and original_method + def self.span_attrs_for(method, semconv: :stable) + normalized = METHOD_CACHE[method] + if normalized + span_name = semconv == :old ? OLD_SPAN_NAMES[normalized] : normalized + SpanCreationAttributes.new( + span_name: span_name, + normalized_method: normalized, + original_method: nil + ) + else + SpanCreationAttributes.new( + span_name: 'HTTP', + normalized_method: '_OTHER', + original_method: method.to_s + ) + end + 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 e98117c6bf..669e5362ab 100644 --- a/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/dup/client.rb +++ b/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/dup/client.rb @@ -4,8 +4,6 @@ # # SPDX-License-Identifier: Apache-2.0 -require_relative '../http_helper' - module OpenTelemetry module Instrumentation module HTTP @@ -18,27 +16,28 @@ module Client HTTP_STATUS_SUCCESS_RANGE = (100..399) def perform(req, options) + span_data = HttpHelper.span_attrs_for(req.verb) + uri = req.uri - normalized_method, original_method = HttpHelper.normalize_method(req.verb) - span_name = create_span_name(normalized_method, uri.path) + span_name = create_span_name(span_data, uri.path) attributes = { # old semconv - 'http.method' => normalized_method, + 'http.method' => span_data.normalized_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' => normalized_method, + 'http.request.method' => span_data.normalized_method, 'url.scheme' => uri.scheme, 'url.path' => uri.path, 'url.full' => "#{uri.scheme}://#{uri.host}", 'server.address' => uri.host, 'server.port' => uri.port } - attributes['http.request.method_original'] = original_method if original_method + attributes['http.request.method_original'] = span_data.original_method if span_data.original_method attributes['url.query'] = uri.query unless uri.query.nil? attributes.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) @@ -65,11 +64,11 @@ def annotate_span_with_response!(span, response) span.status = OpenTelemetry::Trace::Status.error unless HTTP_STATUS_SUCCESS_RANGE.cover?(status_code) end - def create_span_name(normalized_method, request_path) - default_span_name = HttpHelper.span_name_for_stable(normalized_method) + 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(normalized_method, request_path) + updated_span_name = implementation.call(span_data.normalized_method, request_path) updated_span_name.is_a?(String) ? updated_span_name : default_span_name else default_span_name diff --git a/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/http_helper.rb b/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/http_helper.rb deleted file mode 100644 index 5da910ac2b..0000000000 --- a/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/http_helper.rb +++ /dev/null @@ -1,89 +0,0 @@ -# frozen_string_literal: true - -# Copyright The OpenTelemetry Authors -# -# SPDX-License-Identifier: Apache-2.0 - -module OpenTelemetry - module Instrumentation - module HTTP - module Patches - # Module for normalizing HTTP methods - module HttpHelper - # 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 - - # Pre-computed span names for old semantic conventions to avoid allocations - OLD_SPAN_NAMES = { - 'CONNECT' => 'HTTP CONNECT', - 'DELETE' => 'HTTP DELETE', - 'GET' => 'HTTP GET', - 'HEAD' => 'HTTP HEAD', - 'OPTIONS' => 'HTTP OPTIONS', - 'PATCH' => 'HTTP PATCH', - 'POST' => 'HTTP POST', - 'PUT' => 'HTTP PUT', - 'TRACE' => 'HTTP TRACE' - }.freeze - - private_constant :METHOD_CACHE, :OLD_SPAN_NAMES - - # Normalizes an HTTP method to uppercase per OpenTelemetry semantic conventions. - # @param method [String, Symbol] The HTTP method to normalize - # @return [Array] A tuple of [normalized_method, original_method] - # where normalized_method is either a known method or '_OTHER', - # and original_method is the uppercase original method if it was normalized to '_OTHER', or nil - def self.normalize_method(method) - normalized = METHOD_CACHE[method] - return [normalized, nil] if normalized - - # Mixed case or unknown methods are treated as '_OTHER' - ['_OTHER', method.to_s] - end - - # Generates span name for stable semantic conventions - # @param normalized_method [String] the normalized HTTP method - # @return [String] the span name - def self.span_name_for_stable(normalized_method) - normalized_method == '_OTHER' ? 'HTTP' : normalized_method - end - - # Generates span name for old semantic conventions - # @param normalized_method [String] the normalized HTTP method - # @return [String] the span name - def self.span_name_for_old(normalized_method) - OLD_SPAN_NAMES.fetch(normalized_method, 'HTTP') - end - end - end - end - end -end 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 bff95d1d68..79bd1162a9 100644 --- a/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/old/client.rb +++ b/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/old/client.rb @@ -4,8 +4,6 @@ # # SPDX-License-Identifier: Apache-2.0 -require_relative '../http_helper' - module OpenTelemetry module Instrumentation module HTTP @@ -18,12 +16,13 @@ module Client HTTP_STATUS_SUCCESS_RANGE = (100..399) def perform(req, options) + span_data = HttpHelper.span_attrs_for(req.verb, semconv: :old) + uri = req.uri - normalized_method, _original_method = HttpHelper.normalize_method(req.verb) - span_name = create_span_name(normalized_method, uri.path) + span_name = create_span_name(span_data, uri.path) attributes = { - 'http.method' => normalized_method, + 'http.method' => span_data.normalized_method, 'http.scheme' => uri.scheme, 'http.target' => uri.path, 'http.url' => "#{uri.scheme}://#{uri.host}", @@ -53,11 +52,11 @@ def annotate_span_with_response!(span, response) span.status = OpenTelemetry::Trace::Status.error unless HTTP_STATUS_SUCCESS_RANGE.cover?(status_code) end - def create_span_name(normalized_method, request_path) - default_span_name = HttpHelper.span_name_for_old(normalized_method) + 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(normalized_method, request_path) + updated_span_name = implementation.call(span_data.normalized_method, request_path) updated_span_name.is_a?(String) ? updated_span_name : default_span_name else default_span_name 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 7990567ea3..ef97a8d8e5 100644 --- a/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/stable/client.rb +++ b/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/stable/client.rb @@ -4,8 +4,6 @@ # # SPDX-License-Identifier: Apache-2.0 -require_relative '../http_helper' - module OpenTelemetry module Instrumentation module HTTP @@ -18,19 +16,20 @@ module Client HTTP_STATUS_SUCCESS_RANGE = (100..399) def perform(req, options) + span_data = HttpHelper.span_attrs_for(req.verb) + uri = req.uri - normalized_method, original_method = HttpHelper.normalize_method(req.verb) - span_name = create_span_name(normalized_method, uri.path) + span_name = create_span_name(span_data, uri.path) attributes = { - 'http.request.method' => normalized_method, + 'http.request.method' => span_data.normalized_method, 'url.scheme' => uri.scheme, 'url.path' => uri.path, 'url.full' => "#{uri.scheme}://#{uri.host}", 'server.address' => uri.host, 'server.port' => uri.port } - attributes['http.request.method_original'] = original_method if original_method + attributes['http.request.method_original'] = span_data.original_method if span_data.original_method attributes['url.query'] = uri.query unless uri.query.nil? attributes.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) @@ -56,11 +55,11 @@ def annotate_span_with_response!(span, response) span.status = OpenTelemetry::Trace::Status.error unless HTTP_STATUS_SUCCESS_RANGE.cover?(status_code) end - def create_span_name(normalized_method, request_path) - default_span_name = HttpHelper.span_name_for_stable(normalized_method) + 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(normalized_method, request_path) + updated_span_name = implementation.call(span_data.normalized_method, request_path) updated_span_name.is_a?(String) ? updated_span_name : default_span_name else default_span_name 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..82c691ce54 --- /dev/null +++ b/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/http_helper.rb @@ -0,0 +1,86 @@ +# 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, :normalized_method, :original_method, 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 + + # Pre-computed span names for old semantic conventions to avoid allocations + OLD_SPAN_NAMES = { + 'CONNECT' => 'HTTP CONNECT', + 'DELETE' => 'HTTP DELETE', + 'GET' => 'HTTP GET', + 'HEAD' => 'HTTP HEAD', + 'OPTIONS' => 'HTTP OPTIONS', + 'PATCH' => 'HTTP PATCH', + 'POST' => 'HTTP POST', + 'PUT' => 'HTTP PUT', + 'TRACE' => 'HTTP TRACE' + }.freeze + + private_constant :METHOD_CACHE, :OLD_SPAN_NAMES + + # Prepares all span data for the specified semantic convention in a single call + # @param method [String, Symbol] The HTTP method + # @param semconv [Symbol] The semantic convention to use (:stable or :old) + # @return [SpanCreationAttributes] struct containing span_name, normalized_method, and original_method + def self.span_attrs_for(method, semconv: :stable) + normalized = METHOD_CACHE[method] + if normalized + span_name = semconv == :old ? OLD_SPAN_NAMES[normalized] : normalized + SpanCreationAttributes.new( + span_name: span_name, + normalized_method: normalized, + original_method: nil + ) + else + SpanCreationAttributes.new( + span_name: 'HTTP', + normalized_method: '_OTHER', + original_method: method.to_s + ) + end + 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 e4632cbca4..4a86bea131 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 @@ -4,8 +4,6 @@ # # SPDX-License-Identifier: Apache-2.0 -require_relative '../http_helper' - module OpenTelemetry module Instrumentation module HttpClient @@ -22,19 +20,17 @@ def do_get_block(req, proxy, conn, &) uri = req.header.request_uri url = "#{uri.scheme}://#{uri.host}" request_method = req.header.request_method - normalized_method, original_method = HttpHelper.normalize_method(request_method) - - span_name = HttpHelper.span_name_for_stable(normalized_method) + span_data = HttpHelper.span_attrs_for(request_method) attributes = { - 'http.method' => normalized_method, + 'http.method' => span_data.normalized_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' => normalized_method, + 'http.request.method' => span_data.normalized_method, 'url.scheme' => uri.scheme, 'url.path' => uri.path, 'url.full' => url, @@ -42,10 +38,10 @@ def do_get_block(req, proxy, conn, &) 'server.port' => uri.port }.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) - attributes['http.request.method_original'] = original_method if original_method + attributes['http.request.method_original'] = span_data.original_method if span_data.original_method attributes['url.query'] = uri.query unless uri.query.nil? - tracer.in_span(span_name, 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/http_helper.rb b/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/http_helper.rb deleted file mode 100644 index a7eb2bbee6..0000000000 --- a/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/http_helper.rb +++ /dev/null @@ -1,89 +0,0 @@ -# frozen_string_literal: true - -# Copyright The OpenTelemetry Authors -# -# SPDX-License-Identifier: Apache-2.0 - -module OpenTelemetry - module Instrumentation - module HttpClient - module Patches - # Module for normalizing HTTP methods - module HttpHelper - # 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 - - # Pre-computed span names for old semantic conventions to avoid allocations - OLD_SPAN_NAMES = { - 'CONNECT' => 'HTTP CONNECT', - 'DELETE' => 'HTTP DELETE', - 'GET' => 'HTTP GET', - 'HEAD' => 'HTTP HEAD', - 'OPTIONS' => 'HTTP OPTIONS', - 'PATCH' => 'HTTP PATCH', - 'POST' => 'HTTP POST', - 'PUT' => 'HTTP PUT', - 'TRACE' => 'HTTP TRACE' - }.freeze - - private_constant :METHOD_CACHE, :OLD_SPAN_NAMES - - # Normalizes an HTTP method according to OpenTelemetry semantic conventions - # @param method [String, Symbol] The HTTP method to normalize - # @return [Array] A tuple of [normalized_method, original_method] - # where normalized_method is either a known method or '_OTHER', - # and original_method is the uppercase original method if it was normalized to '_OTHER', or nil - def self.normalize_method(method) - normalized = METHOD_CACHE[method] - return [normalized, nil] if normalized - - # Mixed case or unknown methods are treated as '_OTHER' - ['_OTHER', method.to_s] - end - - # Generates span name for stable semantic conventions - # @param normalized_method [String] the normalized HTTP method - # @return [String] the span name - def self.span_name_for_stable(normalized_method) - normalized_method == '_OTHER' ? 'HTTP' : normalized_method - end - - # Generates span name for old semantic conventions - # @param normalized_method [String] the normalized HTTP method - # @return [String] the span name - def self.span_name_for_old(normalized_method) - OLD_SPAN_NAMES.fetch(normalized_method, 'HTTP') - end - end - end - end - end -end 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 ef061313d8..ecd141eca2 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 @@ -4,8 +4,6 @@ # # SPDX-License-Identifier: Apache-2.0 -require_relative '../http_helper' - module OpenTelemetry module Instrumentation module HttpClient @@ -22,12 +20,10 @@ def do_get_block(req, proxy, conn, &) uri = req.header.request_uri url = "#{uri.scheme}://#{uri.host}" request_method = req.header.request_method - normalized_method, _original_method = HttpHelper.normalize_method(request_method) - - span_name = HttpHelper.span_name_for_old(normalized_method) + span_data = HttpHelper.span_attrs_for(request_method, semconv: :old) attributes = { - 'http.method' => normalized_method, + 'http.method' => span_data.normalized_method, 'http.scheme' => uri.scheme, 'http.target' => uri.path, 'http.url' => url, @@ -35,7 +31,7 @@ def do_get_block(req, proxy, conn, &) 'net.peer.port' => uri.port }.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) - tracer.in_span(span_name, 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/stable/client.rb b/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/stable/client.rb index 44812e623a..58236a079f 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 @@ -4,8 +4,6 @@ # # SPDX-License-Identifier: Apache-2.0 -require_relative '../http_helper' - module OpenTelemetry module Instrumentation module HttpClient @@ -22,11 +20,10 @@ def do_get_block(req, proxy, conn, &) uri = req.header.request_uri url = "#{uri.scheme}://#{uri.host}" request_method = req.header.request_method - normalized_method, original_method = HttpHelper.normalize_method(request_method) - span_name = HttpHelper.span_name_for_stable(normalized_method) + span_data = HttpHelper.span_attrs_for(request_method) attributes = { - 'http.request.method' => normalized_method, + 'http.request.method' => span_data.normalized_method, 'url.scheme' => uri.scheme, 'url.path' => uri.path, 'url.full' => url, @@ -34,10 +31,10 @@ def do_get_block(req, proxy, conn, &) 'server.port' => uri.port }.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) - attributes['http.request.method_original'] = original_method if original_method + attributes['http.request.method_original'] = span_data.original_method if span_data.original_method attributes['url.query'] = uri.query unless uri.query.nil? - tracer.in_span(span_name, 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 1ec8dfe789..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 @@ -178,7 +178,6 @@ http = HTTPClient.new http.request(:purge, 'http://example.com/cache') - _(exporter.finished_spans.size).must_equal 1 _(span.name).must_equal 'HTTP' # old semantic conventions _(span.attributes['http.method']).must_equal '_OTHER' 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 210762c473..1de639f9d4 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 @@ -131,7 +131,6 @@ http = HTTPClient.new http.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 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 f9844a4f22..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 @@ -149,7 +149,6 @@ http = HTTPClient.new http.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' 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 b8cd3b58fd..141b561989 100644 --- a/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/dup/plugin.rb +++ b/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/dup/plugin.rb @@ -4,8 +4,6 @@ # # SPDX-License-Identifier: Apache-2.0 -require_relative '../http_helper' - module OpenTelemetry module Instrumentation module HTTPX @@ -74,20 +72,19 @@ def initialize_span(request, start_time = ::Time.now) verb = request.verb uri = request.uri - normalized_method, original_method = HttpHelper.normalize_method(verb) - span_name = HttpHelper.span_name_for_stable(normalized_method) + span_data = HttpHelper.span_attrs_for(verb) config = HTTPX::Instrumentation.instance.config attributes = { OpenTelemetry::SemanticConventions::Trace::HTTP_HOST => uri.host, - OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => normalized_method, + OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => span_data.normalized_method, 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' => normalized_method, + 'http.request.method' => span_data.normalized_method, 'url.scheme' => uri.scheme, 'url.path' => uri.path, 'url.full' => "#{uri.scheme}://#{uri.host}", @@ -95,12 +92,12 @@ def initialize_span(request, start_time = ::Time.now) 'server.port' => uri.port } - attributes['http.request.method_original'] = original_method if original_method + attributes['http.request.method_original'] = span_data.original_method if span_data.original_method 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) - span = tracer.start_span(span_name, 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 index 616126613b..6bb7b29221 100644 --- a/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/http_helper.rb +++ b/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/http_helper.rb @@ -8,7 +8,11 @@ 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, :normalized_method, :original_method, keyword_init: true) + # Pre-computed mapping to avoid string allocations during normalization METHOD_CACHE = { 'CONNECT' => 'CONNECT', @@ -55,31 +59,26 @@ module HttpHelper private_constant :METHOD_CACHE, :OLD_SPAN_NAMES - # Normalizes an HTTP method to uppercase per OpenTelemetry semantic conventions. - # @param method [String, Symbol] The HTTP method to normalize - # @return [Array] A tuple of [normalized_method, original_method] - # where normalized_method is either a known method or '_OTHER', - # and original_method is the original value if it was normalized to '_OTHER', or nil - def self.normalize_method(method) + # Prepares all span data for the specified semantic convention in a single call + # @param method [String, Symbol] The HTTP method + # @param semconv [Symbol] The semantic convention to use (:stable or :old) + # @return [SpanCreationAttributes] struct containing span_name, normalized_method, and original_method + def self.span_attrs_for(method, semconv: :stable) normalized = METHOD_CACHE[method] - return [normalized, nil] if normalized - - # Mixed case or unknown methods are treated as '_OTHER' - ['_OTHER', method.to_s] - end - - # Generates span name for stable semantic conventions - # @param normalized_method [String] the normalized HTTP method - # @return [String] the span name - def self.span_name_for_stable(normalized_method) - normalized_method == '_OTHER' ? 'HTTP' : normalized_method - end - - # Generates span name for old semantic conventions - # @param normalized_method [String] the normalized HTTP method - # @return [String] the span name - def self.span_name_for_old(normalized_method) - OLD_SPAN_NAMES.fetch(normalized_method, 'HTTP') + if normalized + span_name = semconv == :old ? OLD_SPAN_NAMES[normalized] : normalized + SpanCreationAttributes.new( + span_name: span_name, + normalized_method: normalized, + original_method: nil + ) + else + SpanCreationAttributes.new( + span_name: 'HTTP', + normalized_method: '_OTHER', + original_method: method.to_s + ) + 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 5d6a75bd4c..9d575a24a2 100644 --- a/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/old/plugin.rb +++ b/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/old/plugin.rb @@ -4,8 +4,6 @@ # # SPDX-License-Identifier: Apache-2.0 -require_relative '../http_helper' - module OpenTelemetry module Instrumentation module HTTPX @@ -73,14 +71,13 @@ def initialize_span(request, start_time = ::Time.now) verb = request.verb uri = request.uri - normalized_method, _original_method = HttpHelper.normalize_method(verb) - span_name = HttpHelper.span_name_for_old(normalized_method) + span_data = HttpHelper.span_attrs_for(verb, semconv: :old) config = HTTPX::Instrumentation.instance.config attributes = { OpenTelemetry::SemanticConventions::Trace::HTTP_HOST => uri.host, - OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => normalized_method, + OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => span_data.normalized_method, OpenTelemetry::SemanticConventions::Trace::HTTP_SCHEME => uri.scheme, OpenTelemetry::SemanticConventions::Trace::HTTP_TARGET => uri.path, OpenTelemetry::SemanticConventions::Trace::HTTP_URL => "#{uri.scheme}://#{uri.host}", @@ -91,7 +88,7 @@ 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) - span = tracer.start_span(span_name, 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 74975ce8db..b2ec2ce3d3 100644 --- a/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/stable/plugin.rb +++ b/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/stable/plugin.rb @@ -4,8 +4,6 @@ # # SPDX-License-Identifier: Apache-2.0 -require_relative '../http_helper' - module OpenTelemetry module Instrumentation module HTTPX @@ -73,25 +71,24 @@ def initialize_span(request, start_time = ::Time.now) verb = request.verb uri = request.uri - normalized_method, original_method = HttpHelper.normalize_method(verb) - span_name = HttpHelper.span_name_for_stable(normalized_method) + span_data = HttpHelper.span_attrs_for(verb) config = HTTPX::Instrumentation.instance.config attributes = { - 'http.request.method' => normalized_method, + 'http.request.method' => span_data.normalized_method, 'url.scheme' => uri.scheme, 'url.path' => uri.path, 'url.full' => "#{uri.scheme}://#{uri.host}", 'server.address' => uri.host, 'server.port' => uri.port } - attributes['http.request.method_original'] = original_method if original_method + attributes['http.request.method_original'] = span_data.original_method if span_data.original_method 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) - span = tracer.start_span(span_name, 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/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/excon/lib/opentelemetry/instrumentation/excon/middlewares/http_helper.rb b/instrumentation/net_http/lib/opentelemetry/instrumentation/net/http/http_helper.rb similarity index 58% rename from instrumentation/excon/lib/opentelemetry/instrumentation/excon/middlewares/http_helper.rb rename to instrumentation/net_http/lib/opentelemetry/instrumentation/net/http/http_helper.rb index 73ac6f71d1..717dcf1302 100644 --- a/instrumentation/excon/lib/opentelemetry/instrumentation/excon/middlewares/http_helper.rb +++ b/instrumentation/net_http/lib/opentelemetry/instrumentation/net/http/http_helper.rb @@ -6,10 +6,14 @@ module OpenTelemetry module Instrumentation - module Excon - module Middlewares + 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, :normalized_method, :original_method, keyword_init: true) + # Pre-computed mapping to avoid string allocations during normalization METHOD_CACHE = { 'CONNECT' => 'CONNECT', @@ -56,31 +60,26 @@ module HttpHelper private_constant :METHOD_CACHE, :OLD_SPAN_NAMES - # Normalizes an HTTP method to uppercase per OpenTelemetry semantic conventions. - # @param method [String, Symbol] The HTTP method to normalize - # @return [Array] A tuple of [normalized_method, original_method] - # where normalized_method is either a known method or '_OTHER', - # and original_method is the original value if it was normalized to '_OTHER', or nil - def self.normalize_method(method) + # Prepares all span data for the specified semantic convention in a single call + # @param method [String, Symbol] The HTTP method + # @param semconv [Symbol] The semantic convention to use (:stable or :old) + # @return [SpanCreationAttributes] struct containing span_name, normalized_method, and original_method + def self.span_attrs_for(method, semconv: :stable) normalized = METHOD_CACHE[method] - return [normalized, nil] if normalized - - # Mixed case or unknown methods are treated as '_OTHER' - ['_OTHER', method.to_s] - end - - # Generates span name for stable semantic conventions - # @param normalized_method [String] the normalized HTTP method - # @return [String] the span name - def self.span_name_for_stable(normalized_method) - normalized_method == '_OTHER' ? 'HTTP' : normalized_method - end - - # Generates span name for old semantic conventions - # @param normalized_method [String] the normalized HTTP method - # @return [String] the span name - def self.span_name_for_old(normalized_method) - OLD_SPAN_NAMES.fetch(normalized_method, 'HTTP') + if normalized + span_name = semconv == :old ? OLD_SPAN_NAMES[normalized] : normalized + SpanCreationAttributes.new( + span_name: span_name, + normalized_method: normalized, + original_method: nil + ) + else + SpanCreationAttributes.new( + span_name: 'HTTP', + normalized_method: '_OTHER', + original_method: method.to_s + ) + 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 310d88722a..f706c68943 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 @@ -4,8 +4,6 @@ # # SPDX-License-Identifier: Apache-2.0 -require_relative '../http_helper' - module OpenTelemetry module Instrumentation module Net @@ -25,21 +23,20 @@ def request(req, body = nil, &) return super if untraced? - normalized_method, original_method = HttpHelper.normalize_method(req.method) - span_name = HttpHelper.span_name_for_stable(normalized_method) + span_data = HttpHelper.span_attrs_for(req.method) attributes = { - OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => normalized_method, + OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => span_data.normalized_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' => normalized_method, + 'http.request.method' => span_data.normalized_method, 'url.scheme' => USE_SSL_TO_SCHEME[use_ssl?], 'server.address' => @address, 'server.port' => @port } - attributes['http.request.method_original'] = original_method if original_method + attributes['http.request.method_original'] = span_data.original_method if span_data.original_method path, query = split_path_and_query(req.path) attributes['url.path'] = path attributes['url.query'] = query if query @@ -47,7 +44,7 @@ def request(req, body = nil, &) attributes.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) tracer.in_span( - span_name, + span_data.span_name, attributes: attributes, kind: :client ) do |span| diff --git a/instrumentation/net_http/lib/opentelemetry/instrumentation/net/http/patches/http_helper.rb b/instrumentation/net_http/lib/opentelemetry/instrumentation/net/http/patches/http_helper.rb deleted file mode 100644 index 263d2ce339..0000000000 --- a/instrumentation/net_http/lib/opentelemetry/instrumentation/net/http/patches/http_helper.rb +++ /dev/null @@ -1,93 +0,0 @@ -# frozen_string_literal: true - -# Copyright The OpenTelemetry Authors -# -# SPDX-License-Identifier: Apache-2.0 - -module OpenTelemetry - module Instrumentation - module Net - module HTTP - module Patches - # Utility module for HTTP-related helper methods - module HttpHelper - # 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 - - # Pre-computed span names for old semantic conventions to avoid allocations - OLD_SPAN_NAMES = { - 'CONNECT' => 'HTTP CONNECT', - 'DELETE' => 'HTTP DELETE', - 'GET' => 'HTTP GET', - 'HEAD' => 'HTTP HEAD', - 'OPTIONS' => 'HTTP OPTIONS', - 'PATCH' => 'HTTP PATCH', - 'POST' => 'HTTP POST', - 'PUT' => 'HTTP PUT', - 'TRACE' => 'HTTP TRACE' - }.freeze - - private_constant :METHOD_CACHE, :OLD_SPAN_NAMES - - module_function - - # Normalizes an HTTP method according to OpenTelemetry semantic conventions - # @param method [String, Symbol] The HTTP method to normalize - # @return [Array] A tuple of [normalized_method, original_method] - # where normalized_method is either a known method or '_OTHER', - # and original_method is the original value if it was normalized to '_OTHER', or nil - def normalize_method(method) - normalized = METHOD_CACHE[method] - return [normalized, nil] if normalized - - # Mixed case or unknown methods are treated as '_OTHER' - ['_OTHER', method.to_s] - end - - # Generates span name for stable semantic conventions - # @param normalized_method [String] the normalized HTTP method - # @return [String] the span name - def span_name_for_stable(normalized_method) - normalized_method == '_OTHER' ? 'HTTP' : normalized_method - end - - # Generates span name for old semantic conventions - # @param normalized_method [String] the normalized HTTP method - # @return [String] the span name - def span_name_for_old(normalized_method) - OLD_SPAN_NAMES.fetch(normalized_method, 'HTTP') - end - end - end - end - end - end -end 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 d92acac0ca..7976d30d92 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 @@ -4,8 +4,6 @@ # # SPDX-License-Identifier: Apache-2.0 -require_relative '../http_helper' - module OpenTelemetry module Instrumentation module Net @@ -25,11 +23,10 @@ def request(req, body = nil, &) return super if untraced? - normalized_method, _original_method = HttpHelper.normalize_method(req.method) - span_name = HttpHelper.span_name_for_old(normalized_method) + span_data = HttpHelper.span_attrs_for(req.method, semconv: :old) attributes = { - OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => normalized_method, + OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => span_data.normalized_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, @@ -37,7 +34,7 @@ def request(req, body = nil, &) }.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) tracer.in_span( - span_name, + span_data.span_name, attributes: attributes, kind: :client ) do |span| 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 54ac0a8a0e..a8a2acf493 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 @@ -4,8 +4,6 @@ # # SPDX-License-Identifier: Apache-2.0 -require_relative '../http_helper' - module OpenTelemetry module Instrumentation module Net @@ -25,16 +23,15 @@ def request(req, body = nil, &) return super if untraced? - normalized_method, original_method = HttpHelper.normalize_method(req.method) - span_name = HttpHelper.span_name_for_stable(normalized_method) + span_data = HttpHelper.span_attrs_for(req.method) attributes = { - 'http.request.method' => normalized_method, + 'http.request.method' => span_data.normalized_method, 'url.scheme' => USE_SSL_TO_SCHEME[use_ssl?], 'server.address' => @address, 'server.port' => @port } - attributes['http.request.method_original'] = original_method if original_method + attributes['http.request.method_original'] = span_data.original_method if span_data.original_method path, query = split_path_and_query(req.path) attributes['url.path'] = path attributes['url.query'] = query if query @@ -42,7 +39,7 @@ def request(req, body = nil, &) attributes.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) tracer.in_span( - span_name, + span_data.span_name, attributes: attributes, kind: :client ) do |span| From 49c6dc0d0ca8a7764c4e82eee7679fafcd06d341 Mon Sep 17 00:00:00 2001 From: Ariel Valentin Date: Sun, 9 Nov 2025 15:21:39 -0600 Subject: [PATCH 6/6] feat: HTTP Semconv naming --- Rakefile | 3 +- instrumentation/all/Gemfile | 1 + .../instrumentation/ethon/http_helper.rb | 100 ++++++++++++----- .../instrumentation/ethon/patches/dup/easy.rb | 12 +-- .../instrumentation/ethon/patches/old/easy.rb | 10 +- .../ethon/patches/stable/easy.rb | 11 +- .../instrumentation/excon/http_helper.rb | 102 ++++++++++++------ .../middlewares/dup/tracer_middleware.rb | 7 +- .../middlewares/old/tracer_middleware.rb | 5 +- .../middlewares/stable/tracer_middleware.rb | 18 ++-- .../excon/patches/old/socket.rb | 2 +- .../excon/old/instrumentation_test.rb | 14 +-- .../instrumentation/faraday/http_helper.rb | 102 +++++++++++++----- .../middlewares/dup/tracer_middleware.rb | 19 +--- .../middlewares/old/tracer_middleware.rb | 14 +-- .../middlewares/stable/tracer_middleware.rb | 18 +--- .../middlewares/old/tracer_middleware_test.rb | 10 +- .../instrumentation/http/http_helper.rb | 100 ++++++++++++----- .../http/patches/dup/client.rb | 11 +- .../http/patches/old/client.rb | 9 +- .../http/patches/stable/client.rb | 22 ++-- .../http/patches/old/client_test.rb | 10 +- .../http_client/http_helper.rb | 100 ++++++++++++----- .../http_client/patches/dup/client.rb | 7 +- .../http_client/patches/old/client.rb | 6 +- .../http_client/patches/old/session.rb | 2 +- .../http_client/patches/stable/client.rb | 17 ++- .../http_client/patches/old/client_test.rb | 8 +- .../http_client/patches/old/session_test.rb | 2 +- .../instrumentation/httpx/dup/plugin.rb | 7 +- .../instrumentation/httpx/http_helper.rb | 100 ++++++++++++----- .../instrumentation/httpx/old/plugin.rb | 5 +- .../instrumentation/httpx/stable/plugin.rb | 6 +- .../test/instrumentation/old/plugin_test.rb | 8 +- .../instrumentation/net/http/http_helper.rb | 100 ++++++++++++----- .../net/http/patches/dup/instrumentation.rb | 24 ++--- .../net/http/patches/old/instrumentation.rb | 15 ++- .../http/patches/stable/instrumentation.rb | 14 +-- .../net/http/old/instrumentation_test.rb | 12 +-- 39 files changed, 630 insertions(+), 403 deletions(-) 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/http_helper.rb b/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/http_helper.rb index cfa059e023..a5fc210b1a 100644 --- a/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/http_helper.rb +++ b/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/http_helper.rb @@ -11,7 +11,7 @@ module Ethon # @api private module HttpHelper # Lightweight struct to hold span creation attributes - SpanCreationAttributes = Struct.new(:span_name, :normalized_method, :original_method, keyword_init: true) + SpanCreationAttributes = Struct.new(:span_name, :attributes, keyword_init: true) # Pre-computed mapping to avoid string allocations during normalization METHOD_CACHE = { @@ -44,41 +44,83 @@ module HttpHelper :trace => 'TRACE' }.freeze - # Pre-computed span names for old semantic conventions to avoid allocations - OLD_SPAN_NAMES = { - 'CONNECT' => 'HTTP CONNECT', - 'DELETE' => 'HTTP DELETE', - 'GET' => 'HTTP GET', - 'HEAD' => 'HTTP HEAD', - 'OPTIONS' => 'HTTP OPTIONS', - 'PATCH' => 'HTTP PATCH', - 'POST' => 'HTTP POST', - 'PUT' => 'HTTP PUT', - 'TRACE' => 'HTTP 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 - private_constant :METHOD_CACHE, :OLD_SPAN_NAMES + SpanCreationAttributes.new(span_name: span_name, attributes: attributes) + end - # Prepares all span data for the specified semantic convention in a single call + # Prepares span data using stable semantic conventions # @param method [String, Symbol] The HTTP method - # @param semconv [Symbol] The semantic convention to use (:stable or :old) - # @return [SpanCreationAttributes] struct containing span_name, normalized_method, and original_method - def self.span_attrs_for(method, semconv: :stable) + # @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 - span_name = semconv == :old ? OLD_SPAN_NAMES[normalized] : normalized - SpanCreationAttributes.new( - span_name: span_name, - normalized_method: normalized, - original_method: nil - ) + base_name = normalized + method_value = normalized + original = nil else - SpanCreationAttributes.new( - span_name: 'HTTP', - normalized_method: '_OTHER', - original_method: method.to_s - ) + 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 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 c44cc11fda..4bf519916c 100644 --- a/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/dup/easy.rb +++ b/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/dup/easy.rb @@ -68,7 +68,7 @@ def reset end def otel_before_request - span_data = HttpHelper.span_attrs_for(@otel_method) + span_data = HttpHelper.span_attrs_for_dup(@otel_method) @otel_span = tracer.start_span( span_data.span_name, @@ -90,11 +90,7 @@ def otel_span_started? private def span_creation_attributes(span_data) - instrumentation_attrs = { - 'http.method' => span_data.normalized_method, - 'http.request.method' => span_data.normalized_method - } - instrumentation_attrs['http.request.method_original'] = span_data.original_method if span_data.original_method + instrumentation_attrs = {} uri = _otel_cleanse_uri(url) if uri @@ -106,9 +102,7 @@ def span_creation_attributes(span_data) 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 99ffe28b5d..b84efb1c90 100644 --- a/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/old/easy.rb +++ b/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/old/easy.rb @@ -67,7 +67,7 @@ def reset end def otel_before_request - span_data = HttpHelper.span_attrs_for(@otel_method, semconv: :old) + span_data = HttpHelper.span_attrs_for_old(@otel_method) @otel_span = tracer.start_span( span_data.span_name, @@ -89,9 +89,7 @@ def otel_span_started? private def span_creation_attributes(span_data) - instrumentation_attrs = { - 'http.method' => span_data.normalized_method - } + instrumentation_attrs = {} uri = _otel_cleanse_uri(url) if uri @@ -101,9 +99,7 @@ def span_creation_attributes(span_data) 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 627fa9e807..d87cf20588 100644 --- a/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/stable/easy.rb +++ b/instrumentation/ethon/lib/opentelemetry/instrumentation/ethon/patches/stable/easy.rb @@ -67,7 +67,7 @@ def reset end def otel_before_request - span_data = HttpHelper.span_attrs_for(@otel_method) + span_data = HttpHelper.span_attrs_for_stable(@otel_method) @otel_span = tracer.start_span( span_data.span_name, @@ -89,10 +89,7 @@ def otel_span_started? private def span_creation_attributes(span_data) - instrumentation_attrs = { - 'http.request.method' => span_data.normalized_method - } - instrumentation_attrs['http.request.method_original'] = span_data.original_method if span_data.original_method + instrumentation_attrs = {} uri = _otel_cleanse_uri(url) if uri @@ -102,9 +99,7 @@ def span_creation_attributes(span_data) 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/excon/lib/opentelemetry/instrumentation/excon/http_helper.rb b/instrumentation/excon/lib/opentelemetry/instrumentation/excon/http_helper.rb index 17efb8665f..639ea5aff0 100644 --- a/instrumentation/excon/lib/opentelemetry/instrumentation/excon/http_helper.rb +++ b/instrumentation/excon/lib/opentelemetry/instrumentation/excon/http_helper.rb @@ -10,8 +10,8 @@ module Excon # Utility module for HTTP-related helper methods # @api private module HttpHelper - # Lightweight struct to hold span creation attributes - SpanCreationAttributes = Struct.new(:span_name, :normalized_method, :original_method, keyword_init: true) + # 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 = { @@ -44,41 +44,83 @@ module HttpHelper :trace => 'TRACE' }.freeze - # Pre-computed span names for old semantic conventions to avoid allocations - OLD_SPAN_NAMES = { - 'CONNECT' => 'HTTP CONNECT', - 'DELETE' => 'HTTP DELETE', - 'GET' => 'HTTP GET', - 'HEAD' => 'HTTP HEAD', - 'OPTIONS' => 'HTTP OPTIONS', - 'PATCH' => 'HTTP PATCH', - 'POST' => 'HTTP POST', - 'PUT' => 'HTTP PUT', - 'TRACE' => 'HTTP 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 - private_constant :METHOD_CACHE, :OLD_SPAN_NAMES + SpanCreationAttributes.new(span_name: span_name, attributes: attributes) + end - # Prepares all span data for the specified semantic convention in a single call + # Prepares span data using stable semantic conventions # @param method [String, Symbol] The HTTP method - # @param semconv [Symbol] The semantic convention to use (:stable or :old) - # @return [SpanCreationAttributes] struct containing span_name, normalized_method, and original_method - def self.span_attrs_for(method, semconv: :stable) + # @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 - span_name = semconv == :old ? OLD_SPAN_NAMES[normalized] : normalized - SpanCreationAttributes.new( - span_name: span_name, - normalized_method: normalized, - original_method: nil - ) + base_name = normalized + method_value = normalized + original = nil else - SpanCreationAttributes.new( - span_name: 'HTTP', - normalized_method: '_OTHER', - original_method: method.to_s - ) + 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 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 0ccdfd8b08..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 @@ -17,29 +17,26 @@ class TracerMiddleware < ::Excon::Middleware::Base def request_call(datum) return @stack.request_call(datum) if untraced?(datum) - span_data = HttpHelper.span_attrs_for(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 => span_data.normalized_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' => span_data.normalized_method, 'url.scheme' => datum[:scheme], 'url.path' => datum[:path], 'url.full' => cleansed_url, 'server.address' => datum[:hostname], 'server.port' => datum[:port] } - attributes['http.request.method_original'] = span_data.original_method if span_data.original_method 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) + 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 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 9ffa61543d..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 @@ -17,11 +17,10 @@ class TracerMiddleware < ::Excon::Middleware::Base def request_call(datum) return @stack.request_call(datum) if untraced?(datum) - span_data = HttpHelper.span_attrs_for(datum[:method], semconv: :old) + span_data = HttpHelper.span_attrs_for_old(datum[:method]) attributes = { OpenTelemetry::SemanticConventions::Trace::HTTP_HOST => datum[:host], - OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => span_data.normalized_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)), @@ -30,7 +29,7 @@ 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) + 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 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 b7c4263a15..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 @@ -17,21 +17,17 @@ class TracerMiddleware < ::Excon::Middleware::Base def request_call(datum) return @stack.request_call(datum) if untraced?(datum) - span_data = HttpHelper.span_attrs_for(datum[:method]) + span_data = HttpHelper.span_attrs_for_stable(datum[:method]) - attributes = { - 'http.request.method' => span_data.normalized_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] - } - attributes['http.request.method_original'] = span_data.original_method if span_data.original_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) + 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 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/old/instrumentation_test.rb b/instrumentation/excon/test/opentelemetry/instrumentation/excon/old/instrumentation_test.rb index f5f493527b..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' @@ -93,7 +93,7 @@ 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' @@ -113,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' @@ -143,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' @@ -224,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 @@ -309,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) @@ -340,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/faraday/lib/opentelemetry/instrumentation/faraday/http_helper.rb b/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/http_helper.rb index 8f92a6aac0..9cd2d55056 100644 --- a/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/http_helper.rb +++ b/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/http_helper.rb @@ -11,7 +11,7 @@ module Faraday # @api private module HttpHelper # Lightweight struct to hold span creation attributes - SpanCreationAttributes = Struct.new(:span_name, :normalized_method, :original_method, keyword_init: true) + SpanCreationAttributes = Struct.new(:span_name, :attributes, keyword_init: true) # Pre-computed mapping to avoid string allocations during normalization METHOD_CACHE = { @@ -44,41 +44,85 @@ module HttpHelper :trace => 'TRACE' }.freeze - # Pre-computed span names for old semantic conventions to avoid allocations - OLD_SPAN_NAMES = { - 'CONNECT' => 'HTTP CONNECT', - 'DELETE' => 'HTTP DELETE', - 'GET' => 'HTTP GET', - 'HEAD' => 'HTTP HEAD', - 'OPTIONS' => 'HTTP OPTIONS', - 'PATCH' => 'HTTP PATCH', - 'POST' => 'HTTP POST', - 'PUT' => 'HTTP PUT', - 'TRACE' => 'HTTP 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 - private_constant :METHOD_CACHE, :OLD_SPAN_NAMES + # 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 - # Prepares all span data for the specified semantic convention in a single call + 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 - # @param semconv [Symbol] The semantic convention to use (:stable or :old) - # @return [SpanCreationAttributes] struct containing span_name, normalized_method, and original_method - def self.span_attrs_for(method, semconv: :stable) + # @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 - span_name = semconv == :old ? OLD_SPAN_NAMES[normalized] : normalized - SpanCreationAttributes.new( - span_name: span_name, - normalized_method: normalized, - original_method: nil - ) + base_name = normalized + method_value = normalized + original = nil else - SpanCreationAttributes.new( - span_name: 'HTTP', - normalized_method: '_OTHER', - original_method: method.to_s - ) + 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 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 4fb7285ee2..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 @@ -16,16 +16,12 @@ class TracerMiddleware < ::Faraday::Middleware HTTP_STATUS_SUCCESS_RANGE = (100..399) def call(env) - span_data = HttpHelper.span_attrs_for(env.method) + span_data = HttpHelper.span_attrs_for_dup(env.method) config = Faraday::Instrumentation.instance.config - attributes = span_creation_attributes( - http_method: span_data.normalized_method, - original_method: span_data.original_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( @@ -50,25 +46,20 @@ def call(env) private - def span_creation_attributes(http_method:, original_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 } - attrs['http.request.method_original'] = original_method if original_method if url.host attrs['net.peer.name'] = url.host attrs['server.address'] = url.host 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 579a070371..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 @@ -16,13 +16,12 @@ class TracerMiddleware < ::Faraday::Middleware HTTP_STATUS_SUCCESS_RANGE = (100..399) def call(env) - span_data = HttpHelper.span_attrs_for(env.method, semconv: :old) + span_data = HttpHelper.span_attrs_for_old(env.method) config = Faraday::Instrumentation.instance.config - attributes = span_creation_attributes( - http_method: span_data.normalized_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( @@ -47,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 3be9b4fd98..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 @@ -16,16 +16,12 @@ class TracerMiddleware < ::Faraday::Middleware HTTP_STATUS_SUCCESS_RANGE = (100..399) def call(env) - span_data = HttpHelper.span_attrs_for(env.method) + span_data = HttpHelper.span_attrs_for_stable(env.method) config = Faraday::Instrumentation.instance.config - attributes = span_creation_attributes( - http_method: span_data.normalized_method, - original_method: span_data.original_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( @@ -50,19 +46,15 @@ def call(env) private - def span_creation_attributes(http_method:, original_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['http.request.method_original'] = original_method if original_method 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/old/tracer_middleware_test.rb b/instrumentation/faraday/test/opentelemetry/instrumentation/faraday/middlewares/old/tracer_middleware_test.rb index ac0ddc8c67..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' @@ -116,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' @@ -206,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/http/lib/opentelemetry/instrumentation/http/http_helper.rb b/instrumentation/http/lib/opentelemetry/instrumentation/http/http_helper.rb index a80dacf8b9..d9305e812d 100644 --- a/instrumentation/http/lib/opentelemetry/instrumentation/http/http_helper.rb +++ b/instrumentation/http/lib/opentelemetry/instrumentation/http/http_helper.rb @@ -11,7 +11,7 @@ module HTTP # @api private module HttpHelper # Lightweight struct to hold span creation attributes - SpanCreationAttributes = Struct.new(:span_name, :normalized_method, :original_method, keyword_init: true) + SpanCreationAttributes = Struct.new(:span_name, :attributes, keyword_init: true) # Pre-computed mapping to avoid string allocations during normalization METHOD_CACHE = { @@ -44,41 +44,83 @@ module HttpHelper :trace => 'TRACE' }.freeze - # Pre-computed span names for old semantic conventions to avoid allocations - OLD_SPAN_NAMES = { - 'CONNECT' => 'HTTP CONNECT', - 'DELETE' => 'HTTP DELETE', - 'GET' => 'HTTP GET', - 'HEAD' => 'HTTP HEAD', - 'OPTIONS' => 'HTTP OPTIONS', - 'PATCH' => 'HTTP PATCH', - 'POST' => 'HTTP POST', - 'PUT' => 'HTTP PUT', - 'TRACE' => 'HTTP 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 - private_constant :METHOD_CACHE, :OLD_SPAN_NAMES + SpanCreationAttributes.new(span_name: span_name, attributes: attributes) + end - # Prepares all span data for the specified semantic convention in a single call + # Prepares span data using stable semantic conventions # @param method [String, Symbol] The HTTP method - # @param semconv [Symbol] The semantic convention to use (:stable or :old) - # @return [SpanCreationAttributes] struct containing span_name, normalized_method, and original_method - def self.span_attrs_for(method, semconv: :stable) + # @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 - span_name = semconv == :old ? OLD_SPAN_NAMES[normalized] : normalized - SpanCreationAttributes.new( - span_name: span_name, - normalized_method: normalized, - original_method: nil - ) + base_name = normalized + method_value = normalized + original = nil else - SpanCreationAttributes.new( - span_name: 'HTTP', - normalized_method: '_OTHER', - original_method: method.to_s - ) + 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 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 669e5362ab..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,30 +16,27 @@ module Client HTTP_STATUS_SUCCESS_RANGE = (100..399) def perform(req, options) - span_data = HttpHelper.span_attrs_for(req.verb) + span_data = HttpHelper.span_attrs_for_dup(req.verb) uri = req.uri span_name = create_span_name(span_data, uri.path) attributes = { # old semconv - 'http.method' => span_data.normalized_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' => span_data.normalized_method, 'url.scheme' => uri.scheme, 'url.path' => uri.path, 'url.full' => "#{uri.scheme}://#{uri.host}", 'server.address' => uri.host, 'server.port' => uri.port } - attributes['http.request.method_original'] = span_data.original_method if span_data.original_method 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) @@ -68,7 +65,9 @@ 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(span_data.normalized_method, request_path) + # 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 default_span_name 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 79bd1162a9..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,19 +16,18 @@ module Client HTTP_STATUS_SUCCESS_RANGE = (100..399) def perform(req, options) - span_data = HttpHelper.span_attrs_for(req.verb, semconv: :old) + span_data = HttpHelper.span_attrs_for_old(req.verb) uri = req.uri span_name = create_span_name(span_data, uri.path) attributes = { - 'http.method' => span_data.normalized_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) @@ -56,7 +55,9 @@ 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(span_data.normalized_method, request_path) + # 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 default_span_name 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 ef97a8d8e5..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,22 +16,18 @@ module Client HTTP_STATUS_SUCCESS_RANGE = (100..399) def perform(req, options) - span_data = HttpHelper.span_attrs_for(req.verb) + span_data = HttpHelper.span_attrs_for_stable(req.verb) uri = req.uri span_name = create_span_name(span_data, uri.path) - attributes = { - 'http.request.method' => span_data.normalized_method, - 'url.scheme' => uri.scheme, - 'url.path' => uri.path, - 'url.full' => "#{uri.scheme}://#{uri.host}", - 'server.address' => uri.host, - 'server.port' => uri.port - } - attributes['http.request.method_original'] = span_data.original_method if span_data.original_method + 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) @@ -59,7 +55,9 @@ 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(span_data.normalized_method, request_path) + # 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 default_span_name 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 292e4e91c0..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 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 index 82c691ce54..036439eae3 100644 --- a/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/http_helper.rb +++ b/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/http_helper.rb @@ -11,7 +11,7 @@ module HttpClient # @api private module HttpHelper # Lightweight struct to hold span creation attributes - SpanCreationAttributes = Struct.new(:span_name, :normalized_method, :original_method, keyword_init: true) + SpanCreationAttributes = Struct.new(:span_name, :attributes, keyword_init: true) # Pre-computed mapping to avoid string allocations during normalization METHOD_CACHE = { @@ -44,41 +44,83 @@ module HttpHelper :trace => 'TRACE' }.freeze - # Pre-computed span names for old semantic conventions to avoid allocations - OLD_SPAN_NAMES = { - 'CONNECT' => 'HTTP CONNECT', - 'DELETE' => 'HTTP DELETE', - 'GET' => 'HTTP GET', - 'HEAD' => 'HTTP HEAD', - 'OPTIONS' => 'HTTP OPTIONS', - 'PATCH' => 'HTTP PATCH', - 'POST' => 'HTTP POST', - 'PUT' => 'HTTP PUT', - 'TRACE' => 'HTTP 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 - private_constant :METHOD_CACHE, :OLD_SPAN_NAMES + SpanCreationAttributes.new(span_name: span_name, attributes: attributes) + end - # Prepares all span data for the specified semantic convention in a single call + # Prepares span data using stable semantic conventions # @param method [String, Symbol] The HTTP method - # @param semconv [Symbol] The semantic convention to use (:stable or :old) - # @return [SpanCreationAttributes] struct containing span_name, normalized_method, and original_method - def self.span_attrs_for(method, semconv: :stable) + # @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 - span_name = semconv == :old ? OLD_SPAN_NAMES[normalized] : normalized - SpanCreationAttributes.new( - span_name: span_name, - normalized_method: normalized, - original_method: nil - ) + base_name = normalized + method_value = normalized + original = nil else - SpanCreationAttributes.new( - span_name: 'HTTP', - normalized_method: '_OTHER', - original_method: method.to_s - ) + 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 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 4a86bea131..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,25 +20,22 @@ 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(request_method) + span_data = HttpHelper.span_attrs_for_dup(request_method) attributes = { - 'http.method' => span_data.normalized_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' => span_data.normalized_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['http.request.method_original'] = span_data.original_method if span_data.original_method attributes['url.query'] = uri.query unless uri.query.nil? tracer.in_span(span_data.span_name, attributes: attributes, kind: :client) do |span| 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 ecd141eca2..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 @@ -20,16 +20,16 @@ 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(request_method, semconv: :old) + + span_data = HttpHelper.span_attrs_for_old(request_method) attributes = { - 'http.method' => span_data.normalized_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(span_data.span_name, attributes: attributes, kind: :client) do |span| OpenTelemetry.propagation.inject(req.header) 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 58236a079f..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 @@ -20,18 +20,15 @@ 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(request_method) - attributes = { - 'http.request.method' => span_data.normalized_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['http.request.method_original'] = span_data.original_method if span_data.original_method attributes['url.query'] = uri.query unless uri.query.nil? tracer.in_span(span_data.span_name, attributes: attributes, kind: :client) do |span| 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 1de639f9d4..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 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/httpx/lib/opentelemetry/instrumentation/httpx/dup/plugin.rb b/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/dup/plugin.rb index 141b561989..dc5292d53f 100644 --- a/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/dup/plugin.rb +++ b/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/dup/plugin.rb @@ -72,19 +72,17 @@ def initialize_span(request, start_time = ::Time.now) verb = request.verb uri = request.uri - span_data = HttpHelper.span_attrs_for(verb) + 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 => span_data.normalized_method, 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' => span_data.normalized_method, 'url.scheme' => uri.scheme, 'url.path' => uri.path, 'url.full' => "#{uri.scheme}://#{uri.host}", @@ -92,10 +90,9 @@ def initialize_span(request, start_time = ::Time.now) 'server.port' => uri.port } - attributes['http.request.method_original'] = span_data.original_method if span_data.original_method 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(span_data.span_name, attributes: attributes, kind: :client, start_timestamp: start_time) diff --git a/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/http_helper.rb b/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/http_helper.rb index 6bb7b29221..52b25a4a5e 100644 --- a/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/http_helper.rb +++ b/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/http_helper.rb @@ -11,7 +11,7 @@ module HTTPX # @api private module HttpHelper # Lightweight struct to hold span creation attributes - SpanCreationAttributes = Struct.new(:span_name, :normalized_method, :original_method, keyword_init: true) + SpanCreationAttributes = Struct.new(:span_name, :attributes, keyword_init: true) # Pre-computed mapping to avoid string allocations during normalization METHOD_CACHE = { @@ -44,41 +44,83 @@ module HttpHelper :trace => 'TRACE' }.freeze - # Pre-computed span names for old semantic conventions to avoid allocations - OLD_SPAN_NAMES = { - 'CONNECT' => 'HTTP CONNECT', - 'DELETE' => 'HTTP DELETE', - 'GET' => 'HTTP GET', - 'HEAD' => 'HTTP HEAD', - 'OPTIONS' => 'HTTP OPTIONS', - 'PATCH' => 'HTTP PATCH', - 'POST' => 'HTTP POST', - 'PUT' => 'HTTP PUT', - 'TRACE' => 'HTTP 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 - private_constant :METHOD_CACHE, :OLD_SPAN_NAMES + SpanCreationAttributes.new(span_name: span_name, attributes: attributes) + end - # Prepares all span data for the specified semantic convention in a single call + # Prepares span data using stable semantic conventions # @param method [String, Symbol] The HTTP method - # @param semconv [Symbol] The semantic convention to use (:stable or :old) - # @return [SpanCreationAttributes] struct containing span_name, normalized_method, and original_method - def self.span_attrs_for(method, semconv: :stable) + # @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 - span_name = semconv == :old ? OLD_SPAN_NAMES[normalized] : normalized - SpanCreationAttributes.new( - span_name: span_name, - normalized_method: normalized, - original_method: nil - ) + base_name = normalized + method_value = normalized + original = nil else - SpanCreationAttributes.new( - span_name: 'HTTP', - normalized_method: '_OTHER', - original_method: method.to_s - ) + 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 diff --git a/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/old/plugin.rb b/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/old/plugin.rb index 9d575a24a2..3f04b09c0c 100644 --- a/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/old/plugin.rb +++ b/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/old/plugin.rb @@ -71,13 +71,12 @@ def initialize_span(request, start_time = ::Time.now) verb = request.verb uri = request.uri - span_data = HttpHelper.span_attrs_for(verb, semconv: :old) + 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 => span_data.normalized_method, OpenTelemetry::SemanticConventions::Trace::HTTP_SCHEME => uri.scheme, OpenTelemetry::SemanticConventions::Trace::HTTP_TARGET => uri.path, OpenTelemetry::SemanticConventions::Trace::HTTP_URL => "#{uri.scheme}://#{uri.host}", @@ -86,7 +85,7 @@ 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(span_data.span_name, attributes: attributes, kind: :client, start_timestamp: start_time) diff --git a/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/stable/plugin.rb b/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/stable/plugin.rb index b2ec2ce3d3..cfc6db9454 100644 --- a/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/stable/plugin.rb +++ b/instrumentation/httpx/lib/opentelemetry/instrumentation/httpx/stable/plugin.rb @@ -71,22 +71,20 @@ def initialize_span(request, start_time = ::Time.now) verb = request.verb uri = request.uri - span_data = HttpHelper.span_attrs_for(verb) + span_data = HttpHelper.span_attrs_for_stable(verb) config = HTTPX::Instrumentation.instance.config attributes = { - 'http.request.method' => span_data.normalized_method, 'url.scheme' => uri.scheme, 'url.path' => uri.path, 'url.full' => "#{uri.scheme}://#{uri.host}", 'server.address' => uri.host, 'server.port' => uri.port } - attributes['http.request.method_original'] = span_data.original_method if span_data.original_method 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(span_data.span_name, attributes: attributes, kind: :client, start_timestamp: start_time) diff --git a/instrumentation/httpx/test/instrumentation/old/plugin_test.rb b/instrumentation/httpx/test/instrumentation/old/plugin_test.rb index b4fe220a19..8431546a40 100644 --- a/instrumentation/httpx/test/instrumentation/old/plugin_test.rb +++ b/instrumentation/httpx/test/instrumentation/old/plugin_test.rb @@ -57,7 +57,7 @@ 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' @@ -74,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' @@ -93,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' @@ -121,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/net_http/lib/opentelemetry/instrumentation/net/http/http_helper.rb b/instrumentation/net_http/lib/opentelemetry/instrumentation/net/http/http_helper.rb index 717dcf1302..3b221fe06a 100644 --- a/instrumentation/net_http/lib/opentelemetry/instrumentation/net/http/http_helper.rb +++ b/instrumentation/net_http/lib/opentelemetry/instrumentation/net/http/http_helper.rb @@ -12,7 +12,7 @@ module HTTP # @api private module HttpHelper # Lightweight struct to hold span creation attributes - SpanCreationAttributes = Struct.new(:span_name, :normalized_method, :original_method, keyword_init: true) + SpanCreationAttributes = Struct.new(:span_name, :attributes, keyword_init: true) # Pre-computed mapping to avoid string allocations during normalization METHOD_CACHE = { @@ -45,41 +45,83 @@ module HttpHelper :trace => 'TRACE' }.freeze - # Pre-computed span names for old semantic conventions to avoid allocations - OLD_SPAN_NAMES = { - 'CONNECT' => 'HTTP CONNECT', - 'DELETE' => 'HTTP DELETE', - 'GET' => 'HTTP GET', - 'HEAD' => 'HTTP HEAD', - 'OPTIONS' => 'HTTP OPTIONS', - 'PATCH' => 'HTTP PATCH', - 'POST' => 'HTTP POST', - 'PUT' => 'HTTP PUT', - 'TRACE' => 'HTTP 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 - private_constant :METHOD_CACHE, :OLD_SPAN_NAMES + SpanCreationAttributes.new(span_name: span_name, attributes: attributes) + end - # Prepares all span data for the specified semantic convention in a single call + # Prepares span data using stable semantic conventions # @param method [String, Symbol] The HTTP method - # @param semconv [Symbol] The semantic convention to use (:stable or :old) - # @return [SpanCreationAttributes] struct containing span_name, normalized_method, and original_method - def self.span_attrs_for(method, semconv: :stable) + # @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 - span_name = semconv == :old ? OLD_SPAN_NAMES[normalized] : normalized - SpanCreationAttributes.new( - span_name: span_name, - normalized_method: normalized, - original_method: nil - ) + base_name = normalized + method_value = normalized + original = nil else - SpanCreationAttributes.new( - span_name: 'HTTP', - normalized_method: '_OTHER', - original_method: method.to_s - ) + 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 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 f706c68943..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,19 @@ def request(req, body = nil, &) return super if untraced? - span_data = HttpHelper.span_attrs_for(req.method) - - attributes = { - OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => span_data.normalized_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' => span_data.normalized_method, - 'url.scheme' => USE_SSL_TO_SCHEME[use_ssl?], - 'server.address' => @address, - 'server.port' => @port - } - attributes['http.request.method_original'] = span_data.original_method if span_data.original_method + 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( span_data.span_name, 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 7976d30d92..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 @@ -23,15 +23,12 @@ def request(req, body = nil, &) return super if untraced? - span_data = HttpHelper.span_attrs_for(req.method, semconv: :old) + span_data = HttpHelper.span_attrs_for_old(req.method) - attributes = { - OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => span_data.normalized_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) + 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( span_data.span_name, @@ -65,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 a8a2acf493..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,16 @@ def request(req, body = nil, &) return super if untraced? - span_data = HttpHelper.span_attrs_for(req.method) + span_data = HttpHelper.span_attrs_for_stable(req.method) - attributes = { - 'http.request.method' => span_data.normalized_method, - 'url.scheme' => USE_SSL_TO_SCHEME[use_ssl?], - 'server.address' => @address, - 'server.port' => @port - } - attributes['http.request.method_original'] = span_data.original_method if span_data.original_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( span_data.span_name, 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 133559602f..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 @@ -106,7 +106,7 @@ 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 @@ -132,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 @@ -187,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 @@ -305,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)