diff --git a/instrumentation/http_client/Appraisals b/instrumentation/http_client/Appraisals index 326ebd94c3..1602d8d722 100644 --- a/instrumentation/http_client/Appraisals +++ b/instrumentation/http_client/Appraisals @@ -4,6 +4,15 @@ # # SPDX-License-Identifier: Apache-2.0 -appraise 'httpclient-2.8' do - gem 'httpclient', '~> 2.8.0' +# To faclitate HTTP semantic convention stability migration, we are using +# appraisal to test the different semantic convention modes along with different +# HTTP 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 "httpclient-2.8-#{mode}" do + gem 'httpclient', '~> 2.8.0' + end end diff --git a/instrumentation/http_client/README.md b/instrumentation/http_client/README.md index 126ee790b1..8ffdba4038 100644 --- a/instrumentation/http_client/README.md +++ b/instrumentation/http_client/README.md @@ -52,3 +52,19 @@ The `opentelemetry-instrumentation-http_client` gem is distributed under the Apa [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 HttpClient 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, HttpClient instrumentation code comes in three patch versions: `dup`, `old`, and `stable`. These versions are identical except for the attributes they send. Any changes to HttpClient 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/http_client/lib/opentelemetry/instrumentation/http_client/instrumentation.rb b/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/instrumentation.rb index 8b57fd2671..ea22bcde4a 100644 --- a/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/instrumentation.rb +++ b/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/instrumentation.rb @@ -12,8 +12,9 @@ module HttpClient # The Instrumentation class contains logic to detect and install the HttpClient instrumentation class Instrumentation < OpenTelemetry::Instrumentation::Base install do |_config| - require_dependencies - patch + patch_type = determine_semconv + send(:"require_dependencies_#{patch_type}") + send(:"patch_#{patch_type}") end present do @@ -22,14 +23,47 @@ class Instrumentation < OpenTelemetry::Instrumentation::Base private - def patch - ::HTTPClient.prepend(Patches::Client) - ::HTTPClient::Session.prepend(Patches::Session) + 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 patch_dup + ::HTTPClient.prepend(Patches::Dup::Client) + ::HTTPClient::Session.prepend(Patches::Dup::Session) + end + + def patch_old + ::HTTPClient.prepend(Patches::Old::Client) + ::HTTPClient::Session.prepend(Patches::Old::Session) + end + + def patch_stable + ::HTTPClient.prepend(Patches::Stable::Client) + ::HTTPClient::Session.prepend(Patches::Stable::Session) + end + + def require_dependencies_dup + require_relative 'patches/dup/client' + require_relative 'patches/dup/session' + end + + def require_dependencies_old + require_relative 'patches/old/client' + require_relative 'patches/old/session' end - def require_dependencies - require_relative 'patches/client' - require_relative 'patches/session' + def require_dependencies_stable + require_relative 'patches/stable/client' + require_relative 'patches/stable/session' end end end diff --git a/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/client.rb b/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/client.rb deleted file mode 100644 index d66e21b964..0000000000 --- a/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/client.rb +++ /dev/null @@ -1,58 +0,0 @@ -# frozen_string_literal: true - -# Copyright The OpenTelemetry Authors -# -# SPDX-License-Identifier: Apache-2.0 - -module OpenTelemetry - module Instrumentation - module HttpClient - module Patches - # Module to prepend to HTTPClient for instrumentation - module Client - # Constant for the HTTP status range - HTTP_STATUS_SUCCESS_RANGE = (100..399) - - private - - def do_get_block(req, proxy, conn, &) - uri = req.header.request_uri - url = "#{uri.scheme}://#{uri.host}" - request_method = req.header.request_method - - attributes = { - 'http.method' => request_method, - 'http.scheme' => uri.scheme, - 'http.target' => uri.path, - 'http.url' => url, - 'net.peer.name' => uri.host, - 'net.peer.port' => uri.port - }.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) - - tracer.in_span("HTTP #{request_method}", attributes: attributes, kind: :client) do |span| - OpenTelemetry.propagation.inject(req.header) - super.tap do - response = conn.pop - annotate_span_with_response!(span, response) - conn.push response - end - end - end - - def annotate_span_with_response!(span, response) - return unless response&.status_code - - status_code = response.status_code.to_i - - span.set_attribute('http.status_code', status_code) - span.status = OpenTelemetry::Trace::Status.error unless HTTP_STATUS_SUCCESS_RANGE.cover?(status_code) - end - - def tracer - HttpClient::Instrumentation.instance.tracer - end - end - end - end - end -end diff --git a/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/dup/client.rb b/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/dup/client.rb new file mode 100644 index 0000000000..6113e8e5ea --- /dev/null +++ b/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/dup/client.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Instrumentation + module HttpClient + module Patches + module Dup + # Module to prepend to HTTPClient for instrumentation + module Client + # Constant for the HTTP status range + HTTP_STATUS_SUCCESS_RANGE = (100..399) + + private + + def do_get_block(req, proxy, conn, &) + uri = req.header.request_uri + url = "#{uri.scheme}://#{uri.host}" + request_method = req.header.request_method + + attributes = { + 'http.method' => request_method, + 'http.scheme' => uri.scheme, + 'http.target' => uri.path, + 'http.url' => url, + 'net.peer.name' => uri.host, + 'net.peer.port' => uri.port, + # stable semantic conventions + 'http.request.method' => request_method, + 'url.scheme' => uri.scheme, + 'url.path' => uri.path, + 'url.full' => url, + 'server.address' => uri.host, + 'server.port' => uri.port + }.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) + + attributes['url.query'] = uri.query unless uri.query.nil? + + tracer.in_span(request_method, attributes: attributes, kind: :client) do |span| + OpenTelemetry.propagation.inject(req.header) + super.tap do + response = conn.pop + annotate_span_with_response!(span, response) + conn.push response + end + end + end + + def annotate_span_with_response!(span, response) + return unless response&.status_code + + status_code = response.status_code.to_i + + span.set_attribute('http.status_code', status_code) + span.set_attribute('http.response.status_code', status_code) + span.status = OpenTelemetry::Trace::Status.error unless HTTP_STATUS_SUCCESS_RANGE.cover?(status_code) + end + + def tracer + HttpClient::Instrumentation.instance.tracer + end + end + end + end + end + end +end diff --git a/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/dup/session.rb b/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/dup/session.rb new file mode 100644 index 0000000000..faa633e73b --- /dev/null +++ b/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/dup/session.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Instrumentation + module HttpClient + module Patches + module Dup + # Module to prepend to HTTPClient::Session for instrumentation + module Session + def connect + site = @proxy || @dest + url = site.addr + + attributes = { 'http.url' => url, 'url.full' => url }.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) + tracer.in_span('CONNECT', attributes: attributes) do + super + end + end + + private + + def tracer + HttpClient::Instrumentation.instance.tracer + end + end + end + end + end + end +end diff --git a/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/old/client.rb b/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/old/client.rb new file mode 100644 index 0000000000..d6d15e6594 --- /dev/null +++ b/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/old/client.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Instrumentation + module HttpClient + module Patches + module Old + # Module to prepend to HTTPClient for instrumentation + module Client + # Constant for the HTTP status range + HTTP_STATUS_SUCCESS_RANGE = (100..399) + + private + + def do_get_block(req, proxy, conn, &) + uri = req.header.request_uri + url = "#{uri.scheme}://#{uri.host}" + request_method = req.header.request_method + + attributes = { + 'http.method' => request_method, + 'http.scheme' => uri.scheme, + 'http.target' => uri.path, + 'http.url' => url, + 'net.peer.name' => uri.host, + 'net.peer.port' => uri.port + }.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) + + tracer.in_span("HTTP #{request_method}", attributes: attributes, kind: :client) do |span| + OpenTelemetry.propagation.inject(req.header) + super.tap do + response = conn.pop + annotate_span_with_response!(span, response) + conn.push response + end + end + end + + def annotate_span_with_response!(span, response) + return unless response&.status_code + + status_code = response.status_code.to_i + + span.set_attribute('http.status_code', status_code) + span.status = OpenTelemetry::Trace::Status.error unless HTTP_STATUS_SUCCESS_RANGE.cover?(status_code) + end + + def tracer + HttpClient::Instrumentation.instance.tracer + end + end + end + end + end + end +end 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 new file mode 100644 index 0000000000..7309f0a4d9 --- /dev/null +++ b/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/old/session.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Instrumentation + module HttpClient + module Patches + module Old + # Module to prepend to HTTPClient::Session for instrumentation + module Session + def connect + site = @proxy || @dest + url = site.addr + + attributes = { 'http.url' => url }.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) + tracer.in_span('HTTP CONNECT', attributes: attributes) do + super + end + end + + private + + def tracer + HttpClient::Instrumentation.instance.tracer + end + end + end + end + end + end +end diff --git a/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/session.rb b/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/session.rb deleted file mode 100644 index 7604dff4c4..0000000000 --- a/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/session.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -# Copyright The OpenTelemetry Authors -# -# SPDX-License-Identifier: Apache-2.0 - -module OpenTelemetry - module Instrumentation - module HttpClient - module Patches - # Module to prepend to HTTPClient::Session for instrumentation - module Session - def connect - site = @proxy || @dest - url = site.addr - - attributes = { 'http.url' => url }.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) - tracer.in_span('HTTP CONNECT', attributes: attributes) do - super - end - end - - private - - def tracer - HttpClient::Instrumentation.instance.tracer - end - end - end - end - 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 new file mode 100644 index 0000000000..df2bee0846 --- /dev/null +++ b/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/stable/client.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Instrumentation + module HttpClient + module Patches + module Stable + # Module to prepend to HTTPClient for instrumentation + module Client + # Constant for the HTTP status range + HTTP_STATUS_SUCCESS_RANGE = (100..399) + + private + + def do_get_block(req, proxy, conn, &) + uri = req.header.request_uri + url = "#{uri.scheme}://#{uri.host}" + request_method = req.header.request_method + + attributes = { + 'http.request.method' => request_method, + 'url.scheme' => uri.scheme, + 'url.path' => uri.path, + 'url.full' => url, + 'server.address' => uri.host, + 'server.port' => uri.port + }.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) + + attributes['url.query'] = uri.query unless uri.query.nil? + + tracer.in_span(request_method, attributes: attributes, kind: :client) do |span| + OpenTelemetry.propagation.inject(req.header) + super.tap do + response = conn.pop + annotate_span_with_response!(span, response) + conn.push response + end + end + end + + def annotate_span_with_response!(span, response) + return unless response&.status_code + + status_code = response.status_code.to_i + + span.set_attribute('http.response.status_code', status_code) + span.status = OpenTelemetry::Trace::Status.error unless HTTP_STATUS_SUCCESS_RANGE.cover?(status_code) + end + + def tracer + HttpClient::Instrumentation.instance.tracer + end + end + end + end + end + end +end diff --git a/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/stable/session.rb b/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/stable/session.rb new file mode 100644 index 0000000000..ea4c84389c --- /dev/null +++ b/instrumentation/http_client/lib/opentelemetry/instrumentation/http_client/patches/stable/session.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Instrumentation + module HttpClient + module Patches + module Stable + # Module to prepend to HTTPClient::Session for instrumentation + module Session + def connect + site = @proxy || @dest + url = site.addr + + attributes = { 'url.full' => url }.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) + tracer.in_span('CONNECT', attributes: attributes) do + super + end + end + + private + + def tracer + HttpClient::Instrumentation.instance.tracer + end + end + end + end + end + end +end diff --git a/instrumentation/http_client/test/instrumentation/http_client/instrumentation_test.rb b/instrumentation/http_client/test/instrumentation/http_client/instrumentation_test.rb index 424f078e43..7acceda421 100644 --- a/instrumentation/http_client/test/instrumentation/http_client/instrumentation_test.rb +++ b/instrumentation/http_client/test/instrumentation/http_client/instrumentation_test.rb @@ -11,6 +11,8 @@ describe OpenTelemetry::Instrumentation::HttpClient do let(:instrumentation) { OpenTelemetry::Instrumentation::HttpClient::Instrumentation.instance } + before { skip unless ENV['BUNDLE_GEMFILE'].include?('old') } + it 'has #name' do _(instrumentation.name).must_equal 'OpenTelemetry::Instrumentation::HttpClient' end diff --git a/instrumentation/http_client/test/instrumentation/http_client/patches/dup/client_test.rb b/instrumentation/http_client/test/instrumentation/http_client/patches/dup/client_test.rb new file mode 100644 index 0000000000..3ed03d46c6 --- /dev/null +++ b/instrumentation/http_client/test/instrumentation/http_client/patches/dup/client_test.rb @@ -0,0 +1,176 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +require_relative '../../../../../lib/opentelemetry/instrumentation/http_client' +require_relative '../../../../../lib/opentelemetry/instrumentation/http_client/patches/dup/client' + +describe OpenTelemetry::Instrumentation::HttpClient::Patches::Dup::Client do + let(:instrumentation) { OpenTelemetry::Instrumentation::HttpClient::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 + @orig_propagation = OpenTelemetry.propagation + propagator = OpenTelemetry::Trace::Propagation::TraceContext.text_map_propagator + OpenTelemetry.propagation = propagator + instrumentation.install({}) + stub_request(:get, 'http://example.com/success').to_return(status: 200) + 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 + end + + after do + # Force re-install of instrumentation + instrumentation.instance_variable_set(:@installed, false) + + OpenTelemetry.propagation = @orig_propagation + end + + describe '#do_request' do + it 'traces a simple request' do + http = HTTPClient.new + http.receive_timeout = 1 + http.get('http://example.com/success') + + _(exporter.finished_spans.size).must_equal(1) + _(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 + _(span.attributes['http.target']).must_equal '/success' + _(span.attributes['net.peer.name']).must_equal 'example.com' + _(span.attributes['net.peer.port']).must_equal 80 + # stable semantic conventions + _(span.attributes['http.request.method']).must_equal 'GET' + _(span.attributes['url.scheme']).must_equal 'http' + _(span.attributes['http.response.status_code']).must_equal 200 + _(span.attributes['url.path']).must_equal '/success' + _(span.attributes['server.address']).must_equal 'example.com' + _(span.attributes['server.port']).must_equal 80 + 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 + http = HTTPClient.new + http.receive_timeout = 1 + http.post('http://example.com/failure') + + _(exporter.finished_spans.size).must_equal 1 + _(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 + _(span.attributes['http.target']).must_equal '/failure' + _(span.attributes['net.peer.name']).must_equal 'example.com' + _(span.attributes['net.peer.port']).must_equal 80 + # stable semantic conventions + _(span.attributes['http.request.method']).must_equal 'POST' + _(span.attributes['url.scheme']).must_equal 'http' + _(span.attributes['http.response.status_code']).must_equal 500 + _(span.attributes['url.path']).must_equal '/failure' + _(span.attributes['server.address']).must_equal 'example.com' + _(span.attributes['server.port']).must_equal 80 + assert_requested( + :post, + 'http://example.com/failure', + headers: { 'Traceparent' => "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" } + ) + end + + it 'after request timeout' do + expect do + http = HTTPClient.new + http.receive_timeout = 1 + http.get('https://example.com/timeout') + end.must_raise HTTPClient::TimeoutError + + _(exporter.finished_spans.size).must_equal 1 + _(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 + _(span.attributes['http.target']).must_equal '/timeout' + _(span.attributes['net.peer.name']).must_equal 'example.com' + _(span.attributes['net.peer.port']).must_equal 443 + # stable semantic coventions + _(span.attributes['http.request.method']).must_equal 'GET' + _(span.attributes['url.scheme']).must_equal 'https' + _(span.attributes['http.response.status_code']).must_be_nil + _(span.attributes['url.path']).must_equal '/timeout' + _(span.attributes['server.address']).must_equal 'example.com' + _(span.attributes['server.port']).must_equal 443 + _(span.status.code).must_equal( + OpenTelemetry::Trace::Status::ERROR + ) + _(span.status.description).must_equal( + 'Unhandled exception of type: HTTPClient::TimeoutError' + ) + assert_requested( + :get, + 'https://example.com/timeout', + headers: { 'Traceparent' => "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" } + ) + end + + it 'merges http client attributes' do + OpenTelemetry::Common::HTTP::ClientContext.with_attributes('peer.service' => 'foo') do + http = HTTPClient.new + http.receive_timeout = 1 + http.get('http://example.com/success') + end + + _(exporter.finished_spans.size).must_equal 1 + _(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 + _(span.attributes['http.target']).must_equal '/success' + _(span.attributes['net.peer.name']).must_equal 'example.com' + _(span.attributes['net.peer.port']).must_equal 80 + _(span.attributes['peer.service']).must_equal 'foo' + # stable semantic conventions + _(span.attributes['http.request.method']).must_equal 'GET' + _(span.attributes['url.scheme']).must_equal 'http' + _(span.attributes['http.response.status_code']).must_equal 200 + _(span.attributes['url.path']).must_equal '/success' + _(span.attributes['server.address']).must_equal 'example.com' + _(span.attributes['server.port']).must_equal 80 + _(span.attributes['peer.service']).must_equal 'foo' + assert_requested( + :get, + 'http://example.com/success', + headers: { 'Traceparent' => "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" } + ) + end + + it 'merges http query attribute when present' do + http = HTTPClient.new + http.receive_timeout = 1 + http.get('http://example.com/success?hello=there') + + _(exporter.finished_spans.size).must_equal 1 + _(span.name).must_equal 'GET' + _(span.attributes['url.path']).must_equal '/success' + _(span.attributes['url.query']).must_equal 'hello=there' + assert_requested( + :get, + 'http://example.com/success?hello=there', + headers: { 'Traceparent' => "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" } + ) + end + end +end diff --git a/instrumentation/http_client/test/instrumentation/http_client/patches/dup/session_test.rb b/instrumentation/http_client/test/instrumentation/http_client/patches/dup/session_test.rb new file mode 100644 index 0000000000..f9bb1e6be0 --- /dev/null +++ b/instrumentation/http_client/test/instrumentation/http_client/patches/dup/session_test.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +require_relative '../../../../../lib/opentelemetry/instrumentation/http_client' +require_relative '../../../../../lib/opentelemetry/instrumentation/http_client/patches/dup/session' + +describe OpenTelemetry::Instrumentation::HttpClient::Patches::Dup::Session do + let(:instrumentation) { OpenTelemetry::Instrumentation::HttpClient::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 + instrumentation.install({}) + end + + # Force re-install of instrumentation + after { instrumentation.instance_variable_set(:@installed, false) } + + describe '#connect' do + it 'emits span on connect' do + WebMock.allow_net_connect! + TCPServer.open('localhost', 0) do |server| + Thread.start { server.accept } + port = server.addr[1] + + assert_raises(HTTPClient::ReceiveTimeoutError) do + http = HTTPClient.new + http.receive_timeout = 0.01 + http.get("http://username:password@localhost:#{port}/example") + end + end + + _(exporter.finished_spans.size).must_equal(2) + _(span.name).must_equal 'CONNECT' + _(span.attributes['http.url']).must_match(%r{http://localhost:}) + _(span.attributes['url.full']).must_match(%r{http://localhost:}) + ensure + WebMock.disable_net_connect! + end + end +end diff --git a/instrumentation/http_client/test/instrumentation/http_client/patches/client_test.rb b/instrumentation/http_client/test/instrumentation/http_client/patches/old/client_test.rb similarity index 93% rename from instrumentation/http_client/test/instrumentation/http_client/patches/client_test.rb rename to instrumentation/http_client/test/instrumentation/http_client/patches/old/client_test.rb index d71d2555c5..b008283091 100644 --- a/instrumentation/http_client/test/instrumentation/http_client/patches/client_test.rb +++ b/instrumentation/http_client/test/instrumentation/http_client/patches/old/client_test.rb @@ -6,15 +6,17 @@ require 'test_helper' -require_relative '../../../../lib/opentelemetry/instrumentation/http_client' -require_relative '../../../../lib/opentelemetry/instrumentation/http_client/patches/client' +require_relative '../../../../../lib/opentelemetry/instrumentation/http_client' +require_relative '../../../../../lib/opentelemetry/instrumentation/http_client/patches/old/client' -describe OpenTelemetry::Instrumentation::HttpClient::Patches::Client do +describe OpenTelemetry::Instrumentation::HttpClient::Patches::Old::Client do let(:instrumentation) { OpenTelemetry::Instrumentation::HttpClient::Instrumentation.instance } let(:exporter) { EXPORTER } let(:span) { exporter.finished_spans.first } before do + skip unless ENV['BUNDLE_GEMFILE'].include?('old') + exporter.reset @orig_propagation = OpenTelemetry.propagation propagator = OpenTelemetry::Trace::Propagation::TraceContext.text_map_propagator diff --git a/instrumentation/http_client/test/instrumentation/http_client/patches/session_test.rb b/instrumentation/http_client/test/instrumentation/http_client/patches/old/session_test.rb similarity index 78% rename from instrumentation/http_client/test/instrumentation/http_client/patches/session_test.rb rename to instrumentation/http_client/test/instrumentation/http_client/patches/old/session_test.rb index d38bd7a721..73fadbd686 100644 --- a/instrumentation/http_client/test/instrumentation/http_client/patches/session_test.rb +++ b/instrumentation/http_client/test/instrumentation/http_client/patches/old/session_test.rb @@ -6,15 +6,17 @@ require 'test_helper' -require_relative '../../../../lib/opentelemetry/instrumentation/http_client' -require_relative '../../../../lib/opentelemetry/instrumentation/http_client/patches/session' +require_relative '../../../../../lib/opentelemetry/instrumentation/http_client' +require_relative '../../../../../lib/opentelemetry/instrumentation/http_client/patches/old/session' -describe OpenTelemetry::Instrumentation::HttpClient::Patches::Session do +describe OpenTelemetry::Instrumentation::HttpClient::Patches::Old::Session do let(:instrumentation) { OpenTelemetry::Instrumentation::HttpClient::Instrumentation.instance } let(:exporter) { EXPORTER } let(:span) { exporter.finished_spans.first } before do + skip unless ENV['BUNDLE_GEMFILE'].include?('old') + exporter.reset instrumentation.install({}) end diff --git a/instrumentation/http_client/test/instrumentation/http_client/patches/stable/client_test.rb b/instrumentation/http_client/test/instrumentation/http_client/patches/stable/client_test.rb new file mode 100644 index 0000000000..4fc41e6590 --- /dev/null +++ b/instrumentation/http_client/test/instrumentation/http_client/patches/stable/client_test.rb @@ -0,0 +1,147 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +require_relative '../../../../../lib/opentelemetry/instrumentation/http_client' +require_relative '../../../../../lib/opentelemetry/instrumentation/http_client/patches/stable/client' + +describe OpenTelemetry::Instrumentation::HttpClient::Patches::Stable::Client do + let(:instrumentation) { OpenTelemetry::Instrumentation::HttpClient::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 + @orig_propagation = OpenTelemetry.propagation + propagator = OpenTelemetry::Trace::Propagation::TraceContext.text_map_propagator + OpenTelemetry.propagation = propagator + instrumentation.install({}) + stub_request(:get, 'http://example.com/success').to_return(status: 200) + 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 + end + + after do + # Force re-install of instrumentation + instrumentation.instance_variable_set(:@installed, false) + + OpenTelemetry.propagation = @orig_propagation + end + + describe '#do_request' do + it 'traces a simple request' do + http = HTTPClient.new + http.receive_timeout = 1 + http.get('http://example.com/success') + + _(exporter.finished_spans.size).must_equal(1) + _(span.name).must_equal 'GET' + _(span.attributes['http.request.method']).must_equal 'GET' + _(span.attributes['url.scheme']).must_equal 'http' + _(span.attributes['http.response.status_code']).must_equal 200 + _(span.attributes['url.path']).must_equal '/success' + _(span.attributes['server.address']).must_equal 'example.com' + _(span.attributes['server.port']).must_equal 80 + 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 + http = HTTPClient.new + http.receive_timeout = 1 + http.post('http://example.com/failure') + + _(exporter.finished_spans.size).must_equal 1 + _(span.name).must_equal 'POST' + _(span.attributes['http.request.method']).must_equal 'POST' + _(span.attributes['url.scheme']).must_equal 'http' + _(span.attributes['http.response.status_code']).must_equal 500 + _(span.attributes['url.path']).must_equal '/failure' + _(span.attributes['server.address']).must_equal 'example.com' + _(span.attributes['server.port']).must_equal 80 + assert_requested( + :post, + 'http://example.com/failure', + headers: { 'Traceparent' => "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" } + ) + end + + it 'after request timeout' do + expect do + http = HTTPClient.new + http.receive_timeout = 1 + http.get('https://example.com/timeout') + end.must_raise HTTPClient::TimeoutError + + _(exporter.finished_spans.size).must_equal 1 + _(span.name).must_equal 'GET' + _(span.attributes['http.request.method']).must_equal 'GET' + _(span.attributes['url.scheme']).must_equal 'https' + _(span.attributes['http.response.status_code']).must_be_nil + _(span.attributes['url.path']).must_equal '/timeout' + _(span.attributes['server.address']).must_equal 'example.com' + _(span.attributes['server.port']).must_equal 443 + _(span.status.code).must_equal( + OpenTelemetry::Trace::Status::ERROR + ) + _(span.status.description).must_equal( + 'Unhandled exception of type: HTTPClient::TimeoutError' + ) + assert_requested( + :get, + 'https://example.com/timeout', + headers: { 'Traceparent' => "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" } + ) + end + + it 'merges http client attributes' do + OpenTelemetry::Common::HTTP::ClientContext.with_attributes('peer.service' => 'foo') do + http = HTTPClient.new + http.receive_timeout = 1 + http.get('http://example.com/success') + end + + _(exporter.finished_spans.size).must_equal 1 + _(span.name).must_equal 'GET' + _(span.attributes['http.request.method']).must_equal 'GET' + _(span.attributes['url.scheme']).must_equal 'http' + _(span.attributes['http.response.status_code']).must_equal 200 + _(span.attributes['url.path']).must_equal '/success' + _(span.attributes['server.address']).must_equal 'example.com' + _(span.attributes['server.port']).must_equal 80 + _(span.attributes['peer.service']).must_equal 'foo' + assert_requested( + :get, + 'http://example.com/success', + headers: { 'Traceparent' => "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" } + ) + end + + it 'merges http query attribute when present' do + http = HTTPClient.new + http.receive_timeout = 1 + http.get('http://example.com/success?hello=there') + + _(exporter.finished_spans.size).must_equal 1 + _(span.name).must_equal 'GET' + _(span.attributes['url.path']).must_equal '/success' + _(span.attributes['url.query']).must_equal 'hello=there' + assert_requested( + :get, + 'http://example.com/success?hello=there', + headers: { 'Traceparent' => "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" } + ) + end + end +end diff --git a/instrumentation/http_client/test/instrumentation/http_client/patches/stable/session_test.rb b/instrumentation/http_client/test/instrumentation/http_client/patches/stable/session_test.rb new file mode 100644 index 0000000000..749c9eb4f9 --- /dev/null +++ b/instrumentation/http_client/test/instrumentation/http_client/patches/stable/session_test.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +require_relative '../../../../../lib/opentelemetry/instrumentation/http_client' +require_relative '../../../../../lib/opentelemetry/instrumentation/http_client/patches/stable/session' + +describe OpenTelemetry::Instrumentation::HttpClient::Patches::Stable::Session do + let(:instrumentation) { OpenTelemetry::Instrumentation::HttpClient::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 + instrumentation.install({}) + end + + # Force re-install of instrumentation + after { instrumentation.instance_variable_set(:@installed, false) } + + describe '#connect' do + it 'emits span on connect' do + WebMock.allow_net_connect! + TCPServer.open('localhost', 0) do |server| + Thread.start { server.accept } + port = server.addr[1] + + assert_raises(HTTPClient::ReceiveTimeoutError) do + http = HTTPClient.new + http.receive_timeout = 0.01 + http.get("http://username:password@localhost:#{port}/example") + end + end + + _(exporter.finished_spans.size).must_equal(2) + _(span.name).must_equal 'CONNECT' + _(span.attributes['url.full']).must_match(%r{http://localhost:}) + ensure + WebMock.disable_net_connect! + end + end +end