From 37fed931dbfbbe8be702dafb7b9287d6b1731932 Mon Sep 17 00:00:00 2001 From: Hannah Ramadan Date: Wed, 18 Jun 2025 10:20:04 -0700 Subject: [PATCH 1/4] feat: rest-client HTTP semantic convention stability migration --- instrumentation/restclient/Appraisals | 19 ++- instrumentation/restclient/README.md | 16 +++ .../restclient/instrumentation.rb | 42 +++++- .../restclient/patches/dup/request.rb | 79 +++++++++++ .../restclient/patches/old/request.rb | 75 +++++++++++ .../restclient/patches/request.rb | 73 ---------- .../restclient/patches/stable/request.rb | 75 +++++++++++ .../restclient/dup/instrumentation_test.rb | 126 ++++++++++++++++++ .../{ => old}/instrumentation_test.rb | 6 +- .../restclient/stable/instrumentation_test.rb | 117 ++++++++++++++++ 10 files changed, 542 insertions(+), 86 deletions(-) create mode 100644 instrumentation/restclient/lib/opentelemetry/instrumentation/restclient/patches/dup/request.rb create mode 100644 instrumentation/restclient/lib/opentelemetry/instrumentation/restclient/patches/old/request.rb delete mode 100644 instrumentation/restclient/lib/opentelemetry/instrumentation/restclient/patches/request.rb create mode 100644 instrumentation/restclient/lib/opentelemetry/instrumentation/restclient/patches/stable/request.rb create mode 100644 instrumentation/restclient/test/opentelemetry/instrumentation/restclient/dup/instrumentation_test.rb rename instrumentation/restclient/test/opentelemetry/instrumentation/restclient/{ => old}/instrumentation_test.rb (94%) create mode 100644 instrumentation/restclient/test/opentelemetry/instrumentation/restclient/stable/instrumentation_test.rb diff --git a/instrumentation/restclient/Appraisals b/instrumentation/restclient/Appraisals index 5a7c3ae064..649adb4c58 100644 --- a/instrumentation/restclient/Appraisals +++ b/instrumentation/restclient/Appraisals @@ -1,9 +1,18 @@ # frozen_string_literal: true -appraise 'rest-client-2.1' do - gem 'rest-client', '~> 2.1.0' -end +# To faclitate HTTP semantic convention stability migration, we are using +# appraisal to test the different semantic convention modes along with different +# gem versions. For more information on the semantic convention modes, see: +# https://opentelemetry.io/docs/specs/semconv/non-normative/http-migration/ + +semconv_stability = %w[dup stable old] + +semconv_stability.each do |mode| + appraise "rest-client-2.1_#{mode}" do + gem 'rest-client', '~> 2.1.0' + end -appraise 'rest-client-2.0' do - gem 'rest-client', '~> 2.0.0' + appraise "rest-client-2.0_#{mode}" do + gem 'rest-client', '~> 2.0.0' + end end diff --git a/instrumentation/restclient/README.md b/instrumentation/restclient/README.md index 398aae4383..443a84de3c 100644 --- a/instrumentation/restclient/README.md +++ b/instrumentation/restclient/README.md @@ -48,3 +48,19 @@ Apache 2.0 license. See [LICENSE][license-github] for more information. [community-meetings]: https://github.com/open-telemetry/community#community-meetings [slack-channel]: https://cloud-native.slack.com/archives/C01NWKKMKMY [discussions-url]: https://github.com/open-telemetry/opentelemetry-ruby/discussions + +## HTTP semantic convention stability + +In the OpenTelemetry ecosystem, HTTP semantic conventions have now reached a stable state. However, the initial Faraday instrumentation was introduced before this stability was achieved, which resulted in HTTP attributes being based on an older version of the semantic conventions. + +To facilitate the migration to stable semantic conventions, you can use the `OTEL_SEMCONV_STABILITY_OPT_IN` environment variable. This variable allows you to opt-in to the new stable conventions, ensuring compatibility and future-proofing your instrumentation. + +When setting the value for `OTEL_SEMCONV_STABILITY_OPT_IN`, you can specify which conventions you wish to adopt: + +- `http` - Emits the stable HTTP and networking conventions and ceases emitting the old conventions previously emitted by the instrumentation. +- `http/dup` - Emits both the old and stable HTTP and networking conventions, enabling a phased rollout of the stable semantic conventions. +- Default behavior (in the absence of either value) is to continue emitting the old HTTP and networking conventions the instrumentation previously emitted. + +During the transition from old to stable conventions, Faraday instrumentation code comes in three patch versions: `dup`, `old`, and `stable`. These versions are identical except for the attributes they send. Any changes to Faraday instrumentation should consider all three patches. + +For additional information on migration, please refer to our [documentation](https://opentelemetry.io/docs/specs/semconv/non-normative/http-migration/). \ No newline at end of file diff --git a/instrumentation/restclient/lib/opentelemetry/instrumentation/restclient/instrumentation.rb b/instrumentation/restclient/lib/opentelemetry/instrumentation/restclient/instrumentation.rb index 15d3c10ece..d980e26a5b 100644 --- a/instrumentation/restclient/lib/opentelemetry/instrumentation/restclient/instrumentation.rb +++ b/instrumentation/restclient/lib/opentelemetry/instrumentation/restclient/instrumentation.rb @@ -11,8 +11,9 @@ module RestClient # instrumentation class Instrumentation < OpenTelemetry::Instrumentation::Base install do |_config| - require_dependencies - patch_request + patch_type = determine_semconv + send(:"require_dependencies_#{patch_type}") + send(:"patch_request_#{patch_type}") end present do @@ -23,12 +24,41 @@ class Instrumentation < OpenTelemetry::Instrumentation::Base private - def require_dependencies - require_relative 'patches/request' + def determine_semconv + stability_opt_in = ENV.fetch('OTEL_SEMCONV_STABILITY_OPT_IN', '') + values = stability_opt_in.split(',').map(&:strip) + + if values.include?('http/dup') + 'dup' + elsif values.include?('http') + 'stable' + else + 'old' + end + end + + def require_dependencies_dup + require_relative 'patches/dup/request' + end + + def require_dependencies_stable + require_relative 'patches/stable/request' + end + + def require_dependencies_old + require_relative 'patches/old/request' + end + + def patch_request_dup + ::RestClient::Request.prepend(Patches::Dup::Request) + end + + def patch_request_stable + ::RestClient::Request.prepend(Patches::Stable::Request) end - def patch_request - ::RestClient::Request.prepend(Patches::Request) + def patch_request_old + ::RestClient::Request.prepend(Patches::Old::Request) end end end diff --git a/instrumentation/restclient/lib/opentelemetry/instrumentation/restclient/patches/dup/request.rb b/instrumentation/restclient/lib/opentelemetry/instrumentation/restclient/patches/dup/request.rb new file mode 100644 index 0000000000..293ce7162f --- /dev/null +++ b/instrumentation/restclient/lib/opentelemetry/instrumentation/restclient/patches/dup/request.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Instrumentation + module RestClient + module Patches + module Dup + # Module to prepend to RestClient::Request for instrumentation + module Request + # Constant for the HTTP status range + HTTP_STATUS_SUCCESS_RANGE = (100..399) + + def execute(&) + trace_request do |_span| + super + end + end + + private + + def create_request_span + http_method = method.upcase + instrumentation_attrs = { + 'http.method' => http_method.to_s, + 'http.request.method' => http_method.to_s, + 'http.url' => OpenTelemetry::Common::Utilities.cleanse_url(url), + 'url.full' => OpenTelemetry::Common::Utilities.cleanse_url(url) + } + instrumentation_config = RestClient::Instrumentation.instance.config + instrumentation_attrs['peer.service'] = instrumentation_config[:peer_service] if instrumentation_config[:peer_service] + span = tracer.start_span( + "HTTP #{http_method}", + attributes: instrumentation_attrs.merge( + OpenTelemetry::Common::HTTP::ClientContext.attributes + ), + kind: :client + ) + + OpenTelemetry::Trace.with_span(span) do + OpenTelemetry.propagation.inject(processed_headers) + end + + span + end + + def trace_request + span = create_request_span + + yield(span).tap do |response| + # Verify return value is a response. + # If so, add additional attributes. + if response.is_a?(::RestClient::Response) + span.set_attribute('http.status_code', response.code) + span.set_attribute('http.response.status_code', response.code) + span.status = OpenTelemetry::Trace::Status.error unless HTTP_STATUS_SUCCESS_RANGE.cover?(response.code.to_i) + end + end + rescue ::RestClient::ExceptionWithResponse => e + span.set_attribute('http.status_code', e.http_code) + span.set_attribute('http.response.status_code', e.http_code) + span.status = OpenTelemetry::Trace::Status.error unless HTTP_STATUS_SUCCESS_RANGE.cover?(e.http_code.to_i) + raise e + ensure + span.finish + end + + def tracer + RestClient::Instrumentation.instance.tracer + end + end + end + end + end + end +end diff --git a/instrumentation/restclient/lib/opentelemetry/instrumentation/restclient/patches/old/request.rb b/instrumentation/restclient/lib/opentelemetry/instrumentation/restclient/patches/old/request.rb new file mode 100644 index 0000000000..2c447f4091 --- /dev/null +++ b/instrumentation/restclient/lib/opentelemetry/instrumentation/restclient/patches/old/request.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Instrumentation + module RestClient + module Patches + module Old + # Module to prepend to RestClient::Request for instrumentation + module Request + # Constant for the HTTP status range + HTTP_STATUS_SUCCESS_RANGE = (100..399) + + def execute(&) + trace_request do |_span| + super + end + end + + private + + def create_request_span + http_method = method.upcase + instrumentation_attrs = { + 'http.method' => http_method.to_s, + 'http.url' => OpenTelemetry::Common::Utilities.cleanse_url(url) + } + instrumentation_config = RestClient::Instrumentation.instance.config + instrumentation_attrs['peer.service'] = instrumentation_config[:peer_service] if instrumentation_config[:peer_service] + span = tracer.start_span( + "HTTP #{http_method}", + attributes: instrumentation_attrs.merge( + OpenTelemetry::Common::HTTP::ClientContext.attributes + ), + kind: :client + ) + + OpenTelemetry::Trace.with_span(span) do + OpenTelemetry.propagation.inject(processed_headers) + end + + span + end + + def trace_request + span = create_request_span + + yield(span).tap do |response| + # Verify return value is a response. + # If so, add additional attributes. + if response.is_a?(::RestClient::Response) + span.set_attribute('http.status_code', response.code) + span.status = OpenTelemetry::Trace::Status.error unless HTTP_STATUS_SUCCESS_RANGE.cover?(response.code.to_i) + end + end + rescue ::RestClient::ExceptionWithResponse => e + span.set_attribute('http.status_code', e.http_code) + span.status = OpenTelemetry::Trace::Status.error unless HTTP_STATUS_SUCCESS_RANGE.cover?(e.http_code.to_i) + raise e + ensure + span.finish + end + + def tracer + RestClient::Instrumentation.instance.tracer + end + end + end + end + end + end +end diff --git a/instrumentation/restclient/lib/opentelemetry/instrumentation/restclient/patches/request.rb b/instrumentation/restclient/lib/opentelemetry/instrumentation/restclient/patches/request.rb deleted file mode 100644 index 94b74ccfef..0000000000 --- a/instrumentation/restclient/lib/opentelemetry/instrumentation/restclient/patches/request.rb +++ /dev/null @@ -1,73 +0,0 @@ -# frozen_string_literal: true - -# Copyright The OpenTelemetry Authors -# -# SPDX-License-Identifier: Apache-2.0 - -module OpenTelemetry - module Instrumentation - module RestClient - module Patches - # Module to prepend to RestClient::Request for instrumentation - module Request - # Constant for the HTTP status range - HTTP_STATUS_SUCCESS_RANGE = (100..399) - - def execute(&) - trace_request do |_span| - super - end - end - - private - - def create_request_span - http_method = method.upcase - instrumentation_attrs = { - 'http.method' => http_method.to_s, - 'http.url' => OpenTelemetry::Common::Utilities.cleanse_url(url) - } - instrumentation_config = RestClient::Instrumentation.instance.config - instrumentation_attrs['peer.service'] = instrumentation_config[:peer_service] if instrumentation_config[:peer_service] - span = tracer.start_span( - "HTTP #{http_method}", - attributes: instrumentation_attrs.merge( - OpenTelemetry::Common::HTTP::ClientContext.attributes - ), - kind: :client - ) - - OpenTelemetry::Trace.with_span(span) do - OpenTelemetry.propagation.inject(processed_headers) - end - - span - end - - def trace_request - span = create_request_span - - yield(span).tap do |response| - # Verify return value is a response. - # If so, add additional attributes. - if response.is_a?(::RestClient::Response) - span.set_attribute('http.status_code', response.code) - span.status = OpenTelemetry::Trace::Status.error unless HTTP_STATUS_SUCCESS_RANGE.cover?(response.code.to_i) - end - end - rescue ::RestClient::ExceptionWithResponse => e - span.set_attribute('http.status_code', e.http_code) - span.status = OpenTelemetry::Trace::Status.error unless HTTP_STATUS_SUCCESS_RANGE.cover?(e.http_code.to_i) - raise e - ensure - span.finish - end - - def tracer - RestClient::Instrumentation.instance.tracer - end - end - end - end - end -end diff --git a/instrumentation/restclient/lib/opentelemetry/instrumentation/restclient/patches/stable/request.rb b/instrumentation/restclient/lib/opentelemetry/instrumentation/restclient/patches/stable/request.rb new file mode 100644 index 0000000000..d062224ebb --- /dev/null +++ b/instrumentation/restclient/lib/opentelemetry/instrumentation/restclient/patches/stable/request.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Instrumentation + module RestClient + module Patches + module Stable + # Module to prepend to RestClient::Request for instrumentation + module Request + # Constant for the HTTP status range + HTTP_STATUS_SUCCESS_RANGE = (100..399) + + def execute(&) + trace_request do |_span| + super + end + end + + private + + def create_request_span + http_method = method.upcase + instrumentation_attrs = { + 'http.request.method' => http_method.to_s, + 'url.full' => OpenTelemetry::Common::Utilities.cleanse_url(url) + } + instrumentation_config = RestClient::Instrumentation.instance.config + instrumentation_attrs['peer.service'] = instrumentation_config[:peer_service] if instrumentation_config[:peer_service] + span = tracer.start_span( + "HTTP #{http_method}", + attributes: instrumentation_attrs.merge( + OpenTelemetry::Common::HTTP::ClientContext.attributes + ), + kind: :client + ) + + OpenTelemetry::Trace.with_span(span) do + OpenTelemetry.propagation.inject(processed_headers) + end + + span + end + + def trace_request + span = create_request_span + + yield(span).tap do |response| + # Verify return value is a response. + # If so, add additional attributes. + if response.is_a?(::RestClient::Response) + span.set_attribute('http.response.status_code', response.code) + span.status = OpenTelemetry::Trace::Status.error unless HTTP_STATUS_SUCCESS_RANGE.cover?(response.code.to_i) + end + end + rescue ::RestClient::ExceptionWithResponse => e + span.set_attribute('http.response.status_code', e.http_code) + span.status = OpenTelemetry::Trace::Status.error unless HTTP_STATUS_SUCCESS_RANGE.cover?(e.http_code.to_i) + raise e + ensure + span.finish + end + + def tracer + RestClient::Instrumentation.instance.tracer + end + end + end + end + end + end +end diff --git a/instrumentation/restclient/test/opentelemetry/instrumentation/restclient/dup/instrumentation_test.rb b/instrumentation/restclient/test/opentelemetry/instrumentation/restclient/dup/instrumentation_test.rb new file mode 100644 index 0000000000..484c4345bb --- /dev/null +++ b/instrumentation/restclient/test/opentelemetry/instrumentation/restclient/dup/instrumentation_test.rb @@ -0,0 +1,126 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +require_relative '../../../../../lib/opentelemetry/instrumentation/restclient' +require_relative '../../../../../lib/opentelemetry/instrumentation/restclient/patches/old/request' + +describe OpenTelemetry::Instrumentation::RestClient::Instrumentation do + let(:instrumentation) { OpenTelemetry::Instrumentation::RestClient::Instrumentation.instance } + let(:exporter) { EXPORTER } + let(:span) { exporter.finished_spans.first } + + before do + skip unless ENV['BUNDLE_GEMFILE'].include?('dup') + + ENV['OTEL_SEMCONV_STABILITY_OPT_IN'] = 'http/dup' + exporter.reset + stub_request(:get, 'http://example.com/success').to_return(status: 200) + stub_request(:get, 'http://example.com/failure').to_return(status: 500) + + # this is currently a noop but this will future proof the test + @orig_propagation = OpenTelemetry.propagation + propagator = OpenTelemetry::Trace::Propagation::TraceContext.text_map_propagator + OpenTelemetry.propagation = propagator + end + + after do + # Force re-install of instrumentation + instrumentation.instance_variable_set(:@installed, false) + + OpenTelemetry.propagation = @orig_propagation + end + + describe 'tracing' do + before do + instrumentation.install + end + + it 'before request' do + _(exporter.finished_spans.size).must_equal 0 + end + + it 'after request with success code' do + RestClient.get('http://username:password@example.com/success') + + _(exporter.finished_spans.size).must_equal 1 + _(span.name).must_equal 'HTTP 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' + _(span.attributes['http.request.method']).must_equal 'GET' + _(span.attributes['http.response.status_code']).must_equal 200 + _(span.attributes['url.full']).must_equal 'http://example.com/success' + assert_requested( + :get, + 'http://example.com/success', + headers: { 'Traceparent' => "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" } + ) + end + + it 'after request with failure code' do + expect do + RestClient.get('http://username:password@example.com/failure') + end.must_raise RestClient::InternalServerError + + _(exporter.finished_spans.size).must_equal 1 + _(span.name).must_equal 'HTTP 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' + _(span.attributes['http.request.method']).must_equal 'GET' + _(span.attributes['http.response.status_code']).must_equal 500 + _(span.attributes['url.full']).must_equal 'http://example.com/failure' + assert_requested( + :get, + 'http://example.com/failure', + headers: { 'Traceparent' => "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" } + ) + end + + it 'merges HTTP client context' do + client_context_attrs = { + 'test.attribute' => 'test.value', 'http.method' => 'OVERRIDE', 'http.request.method' => 'OVERRIDE' + } + OpenTelemetry::Common::HTTP::ClientContext.with_attributes(client_context_attrs) do + RestClient.get('http://username:password@example.com/success') + end + + _(span.attributes['http.method']).must_equal 'OVERRIDE' + _(span.attributes['test.attribute']).must_equal 'test.value' + _(span.attributes['http.url']).must_equal 'http://example.com/success' + _(span.attributes['http.request.method']).must_equal 'OVERRIDE' + _(span.attributes['test.attribute']).must_equal 'test.value' + _(span.attributes['url.full']).must_equal 'http://example.com/success' + end + + it 'accepts peer service name from config' do + instrumentation.instance_variable_set(:@installed, false) + instrumentation.install(peer_service: 'example:faraday') + + RestClient.get('http://example.com/success') + _(span.attributes['peer.service']).must_equal 'example:faraday' + end + + it 'prioritizes context attributes over config for peer service name' do + instrumentation.instance_variable_set(:@installed, false) + instrumentation.install(peer_service: 'example:faraday') + + client_context_attrs = { 'peer.service' => 'example:custom' } + OpenTelemetry::Common::HTTP::ClientContext.with_attributes(client_context_attrs) do + RestClient.get('http://example.com/success') + end + _(span.attributes['peer.service']).must_equal 'example:custom' + end + + it 'creates valid http method span attribute when method is a Symbol' do + RestClient::Request.execute(method: :get, url: 'http://username:password@example.com/success') + + _(span.attributes['http.method']).must_equal 'GET' + end + end +end diff --git a/instrumentation/restclient/test/opentelemetry/instrumentation/restclient/instrumentation_test.rb b/instrumentation/restclient/test/opentelemetry/instrumentation/restclient/old/instrumentation_test.rb similarity index 94% rename from instrumentation/restclient/test/opentelemetry/instrumentation/restclient/instrumentation_test.rb rename to instrumentation/restclient/test/opentelemetry/instrumentation/restclient/old/instrumentation_test.rb index 4f467ad5c0..778677cc49 100644 --- a/instrumentation/restclient/test/opentelemetry/instrumentation/restclient/instrumentation_test.rb +++ b/instrumentation/restclient/test/opentelemetry/instrumentation/restclient/old/instrumentation_test.rb @@ -6,8 +6,8 @@ require 'test_helper' -require_relative '../../../../lib/opentelemetry/instrumentation/restclient' -require_relative '../../../../lib/opentelemetry/instrumentation/restclient/patches/request' +require_relative '../../../../../lib/opentelemetry/instrumentation/restclient' +require_relative '../../../../../lib/opentelemetry/instrumentation/restclient/patches/old/request' describe OpenTelemetry::Instrumentation::RestClient::Instrumentation do let(:instrumentation) { OpenTelemetry::Instrumentation::RestClient::Instrumentation.instance } @@ -15,6 +15,8 @@ let(:span) { exporter.finished_spans.first } before do + skip unless ENV['BUNDLE_GEMFILE'].include?('old') + exporter.reset stub_request(:get, 'http://example.com/success').to_return(status: 200) stub_request(:get, 'http://example.com/failure').to_return(status: 500) diff --git a/instrumentation/restclient/test/opentelemetry/instrumentation/restclient/stable/instrumentation_test.rb b/instrumentation/restclient/test/opentelemetry/instrumentation/restclient/stable/instrumentation_test.rb new file mode 100644 index 0000000000..dea6844cc7 --- /dev/null +++ b/instrumentation/restclient/test/opentelemetry/instrumentation/restclient/stable/instrumentation_test.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +require_relative '../../../../../lib/opentelemetry/instrumentation/restclient' +require_relative '../../../../../lib/opentelemetry/instrumentation/restclient/patches/old/request' + +describe OpenTelemetry::Instrumentation::RestClient::Instrumentation do + let(:instrumentation) { OpenTelemetry::Instrumentation::RestClient::Instrumentation.instance } + let(:exporter) { EXPORTER } + let(:span) { exporter.finished_spans.first } + + before do + skip unless ENV['BUNDLE_GEMFILE'].include?('stable') + + ENV['OTEL_SEMCONV_STABILITY_OPT_IN'] = 'http' + exporter.reset + stub_request(:get, 'http://example.com/success').to_return(status: 200) + stub_request(:get, 'http://example.com/failure').to_return(status: 500) + + # this is currently a noop but this will future proof the test + @orig_propagation = OpenTelemetry.propagation + propagator = OpenTelemetry::Trace::Propagation::TraceContext.text_map_propagator + OpenTelemetry.propagation = propagator + end + + after do + # Force re-install of instrumentation + instrumentation.instance_variable_set(:@installed, false) + + OpenTelemetry.propagation = @orig_propagation + end + + describe 'tracing' do + before do + instrumentation.install + end + + it 'before request' do + _(exporter.finished_spans.size).must_equal 0 + end + + it 'after request with success code' do + RestClient.get('http://username:password@example.com/success') + + _(exporter.finished_spans.size).must_equal 1 + _(span.name).must_equal 'HTTP GET' + _(span.attributes['http.request.method']).must_equal 'GET' + _(span.attributes['http.response.status_code']).must_equal 200 + _(span.attributes['url.full']).must_equal 'http://example.com/success' + assert_requested( + :get, + 'http://example.com/success', + headers: { 'Traceparent' => "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" } + ) + end + + it 'after request with failure code' do + expect do + RestClient.get('http://username:password@example.com/failure') + end.must_raise RestClient::InternalServerError + + _(exporter.finished_spans.size).must_equal 1 + _(span.name).must_equal 'HTTP GET' + _(span.attributes['http.request.method']).must_equal 'GET' + _(span.attributes['http.response.status_code']).must_equal 500 + _(span.attributes['url.full']).must_equal 'http://example.com/failure' + assert_requested( + :get, + 'http://example.com/failure', + headers: { 'Traceparent' => "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" } + ) + end + + it 'merges HTTP client context' do + client_context_attrs = { + 'test.attribute' => 'test.value', 'http.request.method' => 'OVERRIDE' + } + OpenTelemetry::Common::HTTP::ClientContext.with_attributes(client_context_attrs) do + RestClient.get('http://username:password@example.com/success') + end + + _(span.attributes['http.request.method']).must_equal 'OVERRIDE' + _(span.attributes['test.attribute']).must_equal 'test.value' + _(span.attributes['url.full']).must_equal 'http://example.com/success' + end + + it 'accepts peer service name from config' do + instrumentation.instance_variable_set(:@installed, false) + instrumentation.install(peer_service: 'example:faraday') + + RestClient.get('http://example.com/success') + _(span.attributes['peer.service']).must_equal 'example:faraday' + end + + it 'prioritizes context attributes over config for peer service name' do + instrumentation.instance_variable_set(:@installed, false) + instrumentation.install(peer_service: 'example:faraday') + + client_context_attrs = { 'peer.service' => 'example:custom' } + OpenTelemetry::Common::HTTP::ClientContext.with_attributes(client_context_attrs) do + RestClient.get('http://example.com/success') + end + _(span.attributes['peer.service']).must_equal 'example:custom' + end + + it 'creates valid http method span attribute when method is a Symbol' do + RestClient::Request.execute(method: :get, url: 'http://username:password@example.com/success') + + _(span.attributes['http.request.method']).must_equal 'GET' + end + end +end From dd68f8dd048b17cafb8b4871d39a4acf8ea32287 Mon Sep 17 00:00:00 2001 From: Hannah Ramadan Date: Wed, 18 Jun 2025 11:37:29 -0700 Subject: [PATCH 2/4] fix: rubocop whitespace fix --- .../opentelemetry/instrumentation/restclient/instrumentation.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instrumentation/restclient/lib/opentelemetry/instrumentation/restclient/instrumentation.rb b/instrumentation/restclient/lib/opentelemetry/instrumentation/restclient/instrumentation.rb index d980e26a5b..3ca0d13bce 100644 --- a/instrumentation/restclient/lib/opentelemetry/instrumentation/restclient/instrumentation.rb +++ b/instrumentation/restclient/lib/opentelemetry/instrumentation/restclient/instrumentation.rb @@ -13,7 +13,7 @@ class Instrumentation < OpenTelemetry::Instrumentation::Base install do |_config| patch_type = determine_semconv send(:"require_dependencies_#{patch_type}") - send(:"patch_request_#{patch_type}") + send(:"patch_request_#{patch_type}") end present do From c710c7b19684cecf592f146c8849723b73b6f6f1 Mon Sep 17 00:00:00 2001 From: Hannah Ramadan Date: Mon, 23 Jun 2025 09:27:58 -0700 Subject: [PATCH 3/4] feat: update span name --- .../instrumentation/restclient/patches/dup/request.rb | 2 +- .../instrumentation/restclient/patches/stable/request.rb | 2 +- .../instrumentation/restclient/dup/instrumentation_test.rb | 4 ++-- .../instrumentation/restclient/stable/instrumentation_test.rb | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/instrumentation/restclient/lib/opentelemetry/instrumentation/restclient/patches/dup/request.rb b/instrumentation/restclient/lib/opentelemetry/instrumentation/restclient/patches/dup/request.rb index 293ce7162f..5c12bcc88a 100644 --- a/instrumentation/restclient/lib/opentelemetry/instrumentation/restclient/patches/dup/request.rb +++ b/instrumentation/restclient/lib/opentelemetry/instrumentation/restclient/patches/dup/request.rb @@ -33,7 +33,7 @@ def create_request_span instrumentation_config = RestClient::Instrumentation.instance.config instrumentation_attrs['peer.service'] = instrumentation_config[:peer_service] if instrumentation_config[:peer_service] span = tracer.start_span( - "HTTP #{http_method}", + http_method.to_s, attributes: instrumentation_attrs.merge( OpenTelemetry::Common::HTTP::ClientContext.attributes ), diff --git a/instrumentation/restclient/lib/opentelemetry/instrumentation/restclient/patches/stable/request.rb b/instrumentation/restclient/lib/opentelemetry/instrumentation/restclient/patches/stable/request.rb index d062224ebb..9a36fdce04 100644 --- a/instrumentation/restclient/lib/opentelemetry/instrumentation/restclient/patches/stable/request.rb +++ b/instrumentation/restclient/lib/opentelemetry/instrumentation/restclient/patches/stable/request.rb @@ -31,7 +31,7 @@ def create_request_span instrumentation_config = RestClient::Instrumentation.instance.config instrumentation_attrs['peer.service'] = instrumentation_config[:peer_service] if instrumentation_config[:peer_service] span = tracer.start_span( - "HTTP #{http_method}", + http_method.to_s, attributes: instrumentation_attrs.merge( OpenTelemetry::Common::HTTP::ClientContext.attributes ), diff --git a/instrumentation/restclient/test/opentelemetry/instrumentation/restclient/dup/instrumentation_test.rb b/instrumentation/restclient/test/opentelemetry/instrumentation/restclient/dup/instrumentation_test.rb index 484c4345bb..3cb6ef9de1 100644 --- a/instrumentation/restclient/test/opentelemetry/instrumentation/restclient/dup/instrumentation_test.rb +++ b/instrumentation/restclient/test/opentelemetry/instrumentation/restclient/dup/instrumentation_test.rb @@ -48,7 +48,7 @@ RestClient.get('http://username:password@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.url']).must_equal 'http://example.com/success' @@ -68,7 +68,7 @@ end.must_raise RestClient::InternalServerError _(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.url']).must_equal 'http://example.com/failure' diff --git a/instrumentation/restclient/test/opentelemetry/instrumentation/restclient/stable/instrumentation_test.rb b/instrumentation/restclient/test/opentelemetry/instrumentation/restclient/stable/instrumentation_test.rb index dea6844cc7..eb3f3bd58f 100644 --- a/instrumentation/restclient/test/opentelemetry/instrumentation/restclient/stable/instrumentation_test.rb +++ b/instrumentation/restclient/test/opentelemetry/instrumentation/restclient/stable/instrumentation_test.rb @@ -48,7 +48,7 @@ RestClient.get('http://username:password@example.com/success') _(exporter.finished_spans.size).must_equal 1 - _(span.name).must_equal 'HTTP GET' + _(span.name).must_equal 'GET' _(span.attributes['http.request.method']).must_equal 'GET' _(span.attributes['http.response.status_code']).must_equal 200 _(span.attributes['url.full']).must_equal 'http://example.com/success' @@ -65,7 +65,7 @@ end.must_raise RestClient::InternalServerError _(exporter.finished_spans.size).must_equal 1 - _(span.name).must_equal 'HTTP GET' + _(span.name).must_equal 'GET' _(span.attributes['http.request.method']).must_equal 'GET' _(span.attributes['http.response.status_code']).must_equal 500 _(span.attributes['url.full']).must_equal 'http://example.com/failure' From 1278cc56f088eea4a2f366c62903db1c509f4ae4 Mon Sep 17 00:00:00 2001 From: Hannah Ramadan <76922290+hannahramadan@users.noreply.github.com> Date: Mon, 28 Jul 2025 11:21:21 -0700 Subject: [PATCH 4/4] Update instrumentation/restclient/README.md Co-authored-by: Kayla Reopelle <87386821+kaylareopelle@users.noreply.github.com> --- instrumentation/restclient/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instrumentation/restclient/README.md b/instrumentation/restclient/README.md index 443a84de3c..f1176e2f4c 100644 --- a/instrumentation/restclient/README.md +++ b/instrumentation/restclient/README.md @@ -61,6 +61,6 @@ When setting the value for `OTEL_SEMCONV_STABILITY_OPT_IN`, you can specify whic - `http/dup` - Emits both the old and stable HTTP and networking conventions, enabling a phased rollout of the stable semantic conventions. - Default behavior (in the absence of either value) is to continue emitting the old HTTP and networking conventions the instrumentation previously emitted. -During the transition from old to stable conventions, Faraday instrumentation code comes in three patch versions: `dup`, `old`, and `stable`. These versions are identical except for the attributes they send. Any changes to Faraday instrumentation should consider all three patches. +During the transition from old to stable conventions, RestClient instrumentation code comes in three patch versions: `dup`, `old`, and `stable`. These versions are identical except for the attributes they send. Any changes to RestClient instrumentation should consider all three patches. For additional information on migration, please refer to our [documentation](https://opentelemetry.io/docs/specs/semconv/non-normative/http-migration/). \ No newline at end of file