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/dup/instrumentation_test.rb b/instrumentation/excon/test/opentelemetry/instrumentation/excon/dup/instrumentation_test.rb index cbdcaa1ef1..6e53dfc412 100644 --- a/instrumentation/excon/test/opentelemetry/instrumentation/excon/dup/instrumentation_test.rb +++ b/instrumentation/excon/test/opentelemetry/instrumentation/excon/dup/instrumentation_test.rb @@ -24,6 +24,7 @@ stub_request(:get, 'http://example.com/success?hello=there').to_return(status: 200) stub_request(:get, 'http://example.com/failure').to_return(status: 500) stub_request(:get, 'http://example.com/timeout').to_timeout + stub_request(:get, 'http://example.com/users/123').to_return(status: 200) # this is currently a noop but this will future proof the test @orig_propagation = OpenTelemetry.propagation @@ -404,6 +405,24 @@ _(exporter.finished_spans.size).must_equal(0) end end + + it 'uses url.template in span name when present in client context' do + client_context_attrs = { 'url.template' => '/users/{id}' } + OpenTelemetry::Common::HTTP::ClientContext.with_attributes(client_context_attrs) do + Excon.get('http://example.com/users/123') + end + + _(exporter.finished_spans.size).must_equal 1 + _(span.name).must_equal 'GET /users/{id}' + _(span.attributes['http.method']).must_equal 'GET' + _(span.attributes['http.request.method']).must_equal 'GET' + _(span.attributes['url.template']).must_equal '/users/{id}' + assert_requested( + :get, + 'http://example.com/users/123', + headers: { 'Traceparent' => "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" } + ) + end end def assert_http_spans(scheme: 'http', host: 'localhost', port: nil, target: '/', exception: nil) 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/excon/test/opentelemetry/instrumentation/excon/stable/instrumentation_test.rb b/instrumentation/excon/test/opentelemetry/instrumentation/excon/stable/instrumentation_test.rb index bd27a5e7d4..41b01a76a4 100644 --- a/instrumentation/excon/test/opentelemetry/instrumentation/excon/stable/instrumentation_test.rb +++ b/instrumentation/excon/test/opentelemetry/instrumentation/excon/stable/instrumentation_test.rb @@ -24,6 +24,7 @@ stub_request(:get, 'http://example.com/success?hello=there').to_return(status: 200) stub_request(:get, 'http://example.com/failure').to_return(status: 500) stub_request(:get, 'http://example.com/timeout').to_timeout + stub_request(:get, 'http://example.com/users/123').to_return(status: 200) # this is currently a noop but this will future proof the test @orig_propagation = OpenTelemetry.propagation @@ -349,6 +350,23 @@ _(exporter.finished_spans.size).must_equal(0) end end + + it 'uses url.template in span name when present in client context' do + client_context_attrs = { 'url.template' => '/users/{id}' } + OpenTelemetry::Common::HTTP::ClientContext.with_attributes(client_context_attrs) do + Excon.get('http://example.com/users/123') + end + + _(exporter.finished_spans.size).must_equal 1 + _(span.name).must_equal 'GET /users/{id}' + _(span.attributes['http.request.method']).must_equal 'GET' + _(span.attributes['url.template']).must_equal '/users/{id}' + assert_requested( + :get, + 'http://example.com/users/123', + headers: { 'Traceparent' => "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" } + ) + end end def assert_http_spans(scheme: 'http', host: 'localhost', port: nil, target: '/', exception: nil) 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/dup/tracer_middleware_test.rb b/instrumentation/faraday/test/opentelemetry/instrumentation/faraday/middlewares/dup/tracer_middleware_test.rb index 73c96de86c..a8afa068b4 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 @@ -279,5 +279,30 @@ _(tracers).must_equal 1 end end + + describe 'url.template in span name' do + let(:client) do + Faraday.new('http://example.com') do |builder| + builder.adapter(:test) do |stub| + stub.get('/users/123') { |_| [200, {}, 'OK'] } + end + end + end + + it 'uses url.template in span name when present in client context' do + client_context_attrs = { 'url.template' => '/users/{id}' } + response = OpenTelemetry::Common::HTTP::ClientContext.with_attributes(client_context_attrs) do + client.get('/users/123') + end + + _(span.name).must_equal 'GET /users/{id}' + _(span.attributes['http.method']).must_equal 'GET' + _(span.attributes['http.request.method']).must_equal 'GET' + _(span.attributes['url.template']).must_equal '/users/{id}' + _(response.env.request_headers['Traceparent']).must_equal( + "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" + ) + end + end end end 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/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 37bd67c020..ed8bc05ba0 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 @@ -250,5 +250,29 @@ _(tracers).must_equal 1 end end + + describe 'url.template in span name' do + let(:client) do + Faraday.new('http://example.com') do |builder| + builder.adapter(:test) do |stub| + stub.get('/users/123') { |_| [200, {}, 'OK'] } + end + end + end + + it 'uses url.template in span name when present in client context' do + client_context_attrs = { 'url.template' => '/users/{id}' } + response = OpenTelemetry::Common::HTTP::ClientContext.with_attributes(client_context_attrs) do + client.get('/users/123') + end + + _(span.name).must_equal 'GET /users/{id}' + _(span.attributes['http.request.method']).must_equal 'GET' + _(span.attributes['url.template']).must_equal '/users/{id}' + _(response.env.request_headers['Traceparent']).must_equal( + "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" + ) + end + end end end 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/dup/client_test.rb b/instrumentation/http/test/instrumentation/http/patches/dup/client_test.rb index 079984b201..9eb6bd48b2 100644 --- a/instrumentation/http/test/instrumentation/http/patches/dup/client_test.rb +++ b/instrumentation/http/test/instrumentation/http/patches/dup/client_test.rb @@ -35,6 +35,7 @@ stub_request(:get, 'http://example.com/success?hello=there').to_return(status: 200) stub_request(:post, 'http://example.com/failure').to_return(status: 500) stub_request(:get, 'https://example.com/timeout').to_timeout + stub_request(:get, 'http://example.com/users/123').to_return(status: 200) end after do @@ -272,5 +273,23 @@ headers: { 'Traceparent' => "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" } ) end + + it 'uses url.template in span name when present in client context' do + client_context_attrs = { 'url.template' => '/users/{id}' } + OpenTelemetry::Common::HTTP::ClientContext.with_attributes(client_context_attrs) do + HTTP.get('http://example.com/users/123') + end + + _(exporter.finished_spans.size).must_equal 1 + _(span.name).must_equal 'GET /users/{id}' + _(span.attributes['http.method']).must_equal 'GET' + _(span.attributes['http.request.method']).must_equal 'GET' + _(span.attributes['url.template']).must_equal '/users/{id}' + assert_requested( + :get, + 'http://example.com/users/123', + 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 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/test/instrumentation/http/patches/stable/client_test.rb b/instrumentation/http/test/instrumentation/http/patches/stable/client_test.rb index f2a28581b4..a1755bed3d 100644 --- a/instrumentation/http/test/instrumentation/http/patches/stable/client_test.rb +++ b/instrumentation/http/test/instrumentation/http/patches/stable/client_test.rb @@ -35,6 +35,7 @@ stub_request(:get, 'http://example.com/success?hello=there').to_return(status: 200) stub_request(:post, 'http://example.com/failure').to_return(status: 500) stub_request(:get, 'https://example.com/timeout').to_timeout + stub_request(:get, 'http://example.com/users/123').to_return(status: 200) end after do @@ -214,5 +215,22 @@ headers: { 'Traceparent' => "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" } ) end + + it 'uses url.template in span name when present in client context' do + client_context_attrs = { 'url.template' => '/users/{id}' } + OpenTelemetry::Common::HTTP::ClientContext.with_attributes(client_context_attrs) do + HTTP.get('http://example.com/users/123') + end + + _(exporter.finished_spans.size).must_equal 1 + _(span.name).must_equal 'GET /users/{id}' + _(span.attributes['http.request.method']).must_equal 'GET' + _(span.attributes['url.template']).must_equal '/users/{id}' + assert_requested( + :get, + 'http://example.com/users/123', + 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/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/dup/instrumentation_test.rb b/instrumentation/net_http/test/opentelemetry/instrumentation/net/http/dup/instrumentation_test.rb index 4be38b26e8..3c774a685f 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 @@ -23,6 +23,7 @@ stub_request(:get, 'http://example.com/success?hello=there').to_return(status: 200) stub_request(:post, 'http://example.com/failure').to_return(status: 500) stub_request(:get, 'https://example.com/timeout').to_timeout + stub_request(:get, 'http://example.com/users/123').to_return(status: 200) # this is currently a noop but this will future proof the test @orig_propagation = OpenTelemetry.propagation @@ -381,6 +382,24 @@ def fake_socket.close; end WebMock.disable_net_connect! end + it 'uses url.template in span name when present in client context' do + client_context_attrs = { 'url.template' => '/users/{id}' } + OpenTelemetry::Common::HTTP::ClientContext.with_attributes(client_context_attrs) do + Net::HTTP.get('example.com', '/users/123') + end + + _(exporter.finished_spans.size).must_equal 1 + _(span.name).must_equal 'GET /users/{id}' + _(span.attributes['http.method']).must_equal 'GET' + _(span.attributes['http.request.method']).must_equal 'GET' + _(span.attributes['url.template']).must_equal '/users/{id}' + assert_requested( + :get, + 'http://example.com/users/123', + headers: { 'Traceparent' => "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" } + ) + end + it 'emits a "connect" span when connecting through an non-ssl proxy' do WebMock.allow_net_connect! 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) 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 62b5332f4d..7840796b26 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 @@ -23,6 +23,7 @@ stub_request(:get, 'http://example.com/success?hello=there').to_return(status: 200) stub_request(:post, 'http://example.com/failure').to_return(status: 500) stub_request(:get, 'https://example.com/timeout').to_timeout + stub_request(:get, 'http://example.com/users/123').to_return(status: 200) # this is currently a noop but this will future proof the test @orig_propagation = OpenTelemetry.propagation @@ -355,5 +356,22 @@ def fake_socket.close; end ensure WebMock.disable_net_connect! end + + it 'uses url.template in span name when present in client context' do + client_context_attrs = { 'url.template' => '/users/{id}' } + OpenTelemetry::Common::HTTP::ClientContext.with_attributes(client_context_attrs) do + Net::HTTP.get('example.com', '/users/123') + end + + _(exporter.finished_spans.size).must_equal 1 + _(span.name).must_equal 'GET /users/{id}' + _(span.attributes['http.request.method']).must_equal 'GET' + _(span.attributes['url.template']).must_equal '/users/{id}' + assert_requested( + :get, + 'http://example.com/users/123', + headers: { 'Traceparent' => "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" } + ) + end end end