diff --git a/instrumentation/http/Appraisals b/instrumentation/http/Appraisals index 5a6c74f7a4..3e908a481d 100644 --- a/instrumentation/http/Appraisals +++ b/instrumentation/http/Appraisals @@ -4,10 +4,19 @@ # # SPDX-License-Identifier: Apache-2.0 -appraise 'http-4.4' do - gem 'http', '~> 4.4.0' -end +# 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 "http-4.4.0-#{mode}" do + gem 'http', '~> 4.4.0' + end -appraise 'http-3.3.0' do - gem 'http', '~> 3.3.0' + appraise "http-3.3.0-#{mode}" do + gem 'http', '~> 3.3.0' + end end diff --git a/instrumentation/http/README.md b/instrumentation/http/README.md index 2c4512abd7..9ae2296a1d 100644 --- a/instrumentation/http/README.md +++ b/instrumentation/http/README.md @@ -64,3 +64,19 @@ The `opentelemetry-instrumentation-http` gem is distributed under the Apache 2.0 [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 HTTP 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, HTTP instrumentation code comes in three patch versions: `dup`, `old`, and `stable`. These versions are identical except for the attributes they send. Any changes to HTTP 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/). diff --git a/instrumentation/http/lib/opentelemetry/instrumentation/http/instrumentation.rb b/instrumentation/http/lib/opentelemetry/instrumentation/http/instrumentation.rb index dfe3951bfc..a7d228cfd3 100644 --- a/instrumentation/http/lib/opentelemetry/instrumentation/http/instrumentation.rb +++ b/instrumentation/http/lib/opentelemetry/instrumentation/http/instrumentation.rb @@ -10,8 +10,9 @@ module HTTP # The Instrumentation class contains logic to detect and install the Http 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 @@ -20,14 +21,47 @@ class Instrumentation < OpenTelemetry::Instrumentation::Base option :span_name_formatter, default: nil, validate: :callable - def patch - ::HTTP::Client.prepend(Patches::Client) - ::HTTP::Connection.prepend(Patches::Connection) + 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_old + ::HTTP::Client.prepend(Patches::Old::Client) + ::HTTP::Connection.prepend(Patches::Old::Connection) + end + + def patch_dup + ::HTTP::Client.prepend(Patches::Dup::Client) + ::HTTP::Connection.prepend(Patches::Dup::Connection) + end + + def patch_stable + ::HTTP::Client.prepend(Patches::Stable::Client) + ::HTTP::Connection.prepend(Patches::Stable::Connection) + end + + def require_dependencies_dup + require_relative 'patches/dup/client' + require_relative 'patches/dup/connection' + end + + def require_dependencies_old + require_relative 'patches/old/client' + require_relative 'patches/old/connection' end - def require_dependencies - require_relative 'patches/client' - require_relative 'patches/connection' + def require_dependencies_stable + require_relative 'patches/stable/client' + require_relative 'patches/stable/connection' end end end diff --git a/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/client.rb b/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/client.rb deleted file mode 100644 index 1d98cb5c11..0000000000 --- a/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/client.rb +++ /dev/null @@ -1,70 +0,0 @@ -# frozen_string_literal: true - -# Copyright The OpenTelemetry Authors -# -# SPDX-License-Identifier: Apache-2.0 - -module OpenTelemetry - module Instrumentation - module HTTP - module Patches - # Module to prepend to HTTP::Client for instrumentation - module Client - # Constant for the HTTP status range - HTTP_STATUS_SUCCESS_RANGE = (100..399) - - def perform(req, options) - uri = req.uri - request_method = req.verb.to_s.upcase - span_name = create_request_span_name(request_method, uri.path) - - attributes = { - 'http.method' => request_method, - 'http.scheme' => uri.scheme, - 'http.target' => uri.path, - 'http.url' => "#{uri.scheme}://#{uri.host}", - 'net.peer.name' => uri.host, - 'net.peer.port' => uri.port - }.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) - - tracer.in_span(span_name, attributes: attributes, kind: :client) do |span| - OpenTelemetry.propagation.inject(req.headers) - super.tap do |response| - annotate_span_with_response!(span, response) - end - end - end - - private - - def config - OpenTelemetry::Instrumentation::HTTP::Instrumentation.instance.config - end - - def annotate_span_with_response!(span, response) - return unless response&.status - - status_code = response.status.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 create_request_span_name(request_method, request_path) - if (implementation = config[:span_name_formatter]) - updated_span_name = implementation.call(request_method, request_path) - updated_span_name.is_a?(String) ? updated_span_name : "HTTP #{request_method}" - else - "HTTP #{request_method}" - end - rescue StandardError - "HTTP #{request_method}" - end - - def tracer - HTTP::Instrumentation.instance.tracer - end - end - end - end - end -end diff --git a/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/connection.rb b/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/connection.rb deleted file mode 100644 index 6e162886fa..0000000000 --- a/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/connection.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -# Copyright The OpenTelemetry Authors -# -# SPDX-License-Identifier: Apache-2.0 - -module OpenTelemetry - module Instrumentation - module HTTP - module Patches - # Module to prepend to HTTP::Connection for instrumentation - module Connection - def initialize(req, options) - attributes = { - 'net.peer.name' => req.uri.host, - 'net.peer.port' => req.uri.port - }.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) - - tracer.in_span('HTTP CONNECT', attributes: attributes) do - super - end - end - - private - - def tracer - HTTP::Instrumentation.instance.tracer - end - end - end - end - end -end diff --git a/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/dup/client.rb b/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/dup/client.rb new file mode 100644 index 0000000000..1df4e43be9 --- /dev/null +++ b/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/dup/client.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Instrumentation + module HTTP + module Patches + # Module using old and stable HTTP semantic conventions + module Dup + # Module to prepend to HTTP::Client for instrumentation + module Client + # Constant for the HTTP status range + HTTP_STATUS_SUCCESS_RANGE = (100..399) + + def perform(req, options) + uri = req.uri + request_method = req.verb.to_s.upcase + span_name = create_request_span_name(request_method, uri.path) + + attributes = { + # old semconv + 'http.method' => request_method, + 'http.scheme' => uri.scheme, + 'http.target' => uri.path, + 'http.url' => "#{uri.scheme}://#{uri.host}", + 'net.peer.name' => uri.host, + 'net.peer.port' => uri.port, + # stable semconv + 'http.request.method' => request_method, + 'url.scheme' => uri.scheme, + 'url.path' => uri.path, + 'url.full' => "#{uri.scheme}://#{uri.host}", + 'server.address' => uri.host, + 'server.port' => uri.port + } + attributes['url.query'] = uri.query unless uri.query.nil? + attributes.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) + + tracer.in_span(span_name, attributes: attributes, kind: :client) do |span| + OpenTelemetry.propagation.inject(req.headers) + super.tap do |response| + annotate_span_with_response!(span, response) + end + end + end + + private + + def config + OpenTelemetry::Instrumentation::HTTP::Instrumentation.instance.config + end + + def annotate_span_with_response!(span, response) + return unless response&.status + + status_code = response.status.to_i + span.set_attribute('http.status_code', status_code) # old semconv + span.set_attribute('http.response.status_code', status_code) # stable semconv + span.status = OpenTelemetry::Trace::Status.error unless HTTP_STATUS_SUCCESS_RANGE.cover?(status_code) + end + + def create_request_span_name(request_method, request_path) + if (implementation = config[:span_name_formatter]) + updated_span_name = implementation.call(request_method, request_path) + updated_span_name.is_a?(String) ? updated_span_name : "HTTP #{request_method}" + else + "HTTP #{request_method}" + end + rescue StandardError + "HTTP #{request_method}" + end + + def tracer + HTTP::Instrumentation.instance.tracer + end + end + end + end + end + end +end diff --git a/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/dup/connection.rb b/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/dup/connection.rb new file mode 100644 index 0000000000..f807f59de8 --- /dev/null +++ b/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/dup/connection.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Instrumentation + module HTTP + module Patches + # Module using old and stable HTTP semantic conventions + module Dup + # Module to prepend to HTTP::Connection for instrumentation + module Connection + def initialize(req, options) + attributes = { + # old semconv + 'net.peer.name' => req.uri.host, + 'net.peer.port' => req.uri.port, + # stable semconv + 'server.address' => req.uri.host, + 'server.port' => req.uri.port + }.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) + + tracer.in_span('HTTP CONNECT', attributes: attributes) do + super + end + end + + private + + def tracer + HTTP::Instrumentation.instance.tracer + end + end + end + end + end + end +end diff --git a/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/old/client.rb b/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/old/client.rb new file mode 100644 index 0000000000..356a7caee9 --- /dev/null +++ b/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/old/client.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Instrumentation + module HTTP + module Patches + # Module using old HTTP semantic conventions + module Old + # Module to prepend to HTTP::Client for instrumentation + module Client + # Constant for the HTTP status range + HTTP_STATUS_SUCCESS_RANGE = (100..399) + + def perform(req, options) + uri = req.uri + request_method = req.verb.to_s.upcase + span_name = create_request_span_name(request_method, uri.path) + + attributes = { + 'http.method' => request_method, + 'http.scheme' => uri.scheme, + 'http.target' => uri.path, + 'http.url' => "#{uri.scheme}://#{uri.host}", + 'net.peer.name' => uri.host, + 'net.peer.port' => uri.port + }.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) + + tracer.in_span(span_name, attributes: attributes, kind: :client) do |span| + OpenTelemetry.propagation.inject(req.headers) + super.tap do |response| + annotate_span_with_response!(span, response) + end + end + end + + private + + def config + OpenTelemetry::Instrumentation::HTTP::Instrumentation.instance.config + end + + def annotate_span_with_response!(span, response) + return unless response&.status + + status_code = response.status.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 create_request_span_name(request_method, request_path) + if (implementation = config[:span_name_formatter]) + updated_span_name = implementation.call(request_method, request_path) + updated_span_name.is_a?(String) ? updated_span_name : "HTTP #{request_method}" + else + "HTTP #{request_method}" + end + rescue StandardError + "HTTP #{request_method}" + end + + def tracer + HTTP::Instrumentation.instance.tracer + end + end + end + end + end + end +end diff --git a/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/old/connection.rb b/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/old/connection.rb new file mode 100644 index 0000000000..b4f3b09bff --- /dev/null +++ b/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/old/connection.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Instrumentation + module HTTP + module Patches + # Module using old HTTP semantic conventions + module Old + # Module to prepend to HTTP::Connection for instrumentation + module Connection + def initialize(req, options) + attributes = { + 'net.peer.name' => req.uri.host, + 'net.peer.port' => req.uri.port + }.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) + + tracer.in_span('HTTP CONNECT', attributes: attributes) do + super + end + end + + private + + def tracer + HTTP::Instrumentation.instance.tracer + end + end + end + end + end + end +end diff --git a/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/stable/client.rb b/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/stable/client.rb new file mode 100644 index 0000000000..dda234b5c9 --- /dev/null +++ b/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/stable/client.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Instrumentation + module HTTP + module Patches + # Module using stable HTTP semantic conventions + module Stable + # Module to prepend to HTTP::Client for instrumentation + module Client + # Constant for the HTTP status range + HTTP_STATUS_SUCCESS_RANGE = (100..399) + + def perform(req, options) + uri = req.uri + request_method = req.verb.to_s.upcase + span_name = create_request_span_name(request_method, uri.path) + + attributes = { + 'http.request.method' => request_method, + 'url.scheme' => uri.scheme, + 'url.path' => uri.path, + 'url.full' => "#{uri.scheme}://#{uri.host}", + 'server.address' => uri.host, + 'server.port' => uri.port + } + attributes['url.query'] = uri.query unless uri.query.nil? + attributes.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) + + tracer.in_span(span_name, attributes: attributes, kind: :client) do |span| + OpenTelemetry.propagation.inject(req.headers) + super.tap do |response| + annotate_span_with_response!(span, response) + end + end + end + + private + + def config + OpenTelemetry::Instrumentation::HTTP::Instrumentation.instance.config + end + + def annotate_span_with_response!(span, response) + return unless response&.status + + status_code = response.status.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 create_request_span_name(request_method, request_path) + if (implementation = config[:span_name_formatter]) + updated_span_name = implementation.call(request_method, request_path) + updated_span_name.is_a?(String) ? updated_span_name : "HTTP #{request_method}" + else + "HTTP #{request_method}" + end + rescue StandardError + "HTTP #{request_method}" + end + + def tracer + HTTP::Instrumentation.instance.tracer + end + end + end + end + end + end +end diff --git a/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/stable/connection.rb b/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/stable/connection.rb new file mode 100644 index 0000000000..2797844b5b --- /dev/null +++ b/instrumentation/http/lib/opentelemetry/instrumentation/http/patches/stable/connection.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Instrumentation + module HTTP + module Patches + # Module using stable HTTP semantic conventions + module Stable + # Module to prepend to HTTP::Connection for instrumentation + module Connection + def initialize(req, options) + attributes = { + 'server.address' => req.uri.host, + 'server.port' => req.uri.port + }.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes) + + tracer.in_span('HTTP CONNECT', attributes: attributes) do + super + end + end + + private + + def tracer + HTTP::Instrumentation.instance.tracer + end + end + end + end + end + end +end diff --git a/instrumentation/http/test/instrumentation/http/instrumentation_test.rb b/instrumentation/http/test/instrumentation/http/instrumentation_test.rb index 302ac483d4..3bc4ca26cc 100644 --- a/instrumentation/http/test/instrumentation/http/instrumentation_test.rb +++ b/instrumentation/http/test/instrumentation/http/instrumentation_test.rb @@ -9,6 +9,8 @@ require_relative '../../../lib/opentelemetry/instrumentation/http' describe OpenTelemetry::Instrumentation::HTTP do + before { skip unless ENV['BUNDLE_GEMFILE'].include?('old') } + let(:instrumentation) { OpenTelemetry::Instrumentation::HTTP::Instrumentation.instance } it 'has #name' do @@ -36,4 +38,30 @@ _(instrumentation.install({})).must_equal(true) end end + + describe 'determine_semconv' do + it 'returns "dup" when OTEL_SEMCONV_STABILITY_OPT_IN includes other configs' do + OpenTelemetry::TestHelpers.with_env('OTEL_SEMCONV_STABILITY_OPT_IN' => 'http/dup, database') do + _(instrumentation.determine_semconv).must_equal('dup') + end + end + + it 'returns "dup" when OTEL_SEMCONV_STABILITY_OPT_IN includes both http/dup and http' do + OpenTelemetry::TestHelpers.with_env('OTEL_SEMCONV_STABILITY_OPT_IN' => 'http/dup, http') do + _(instrumentation.determine_semconv).must_equal('dup') + end + end + + it 'returns "stable" when OTEL_SEMCONV_STABILITY_OPT_IN is http' do + OpenTelemetry::TestHelpers.with_env('OTEL_SEMCONV_STABILITY_OPT_IN' => 'http') do + _(instrumentation.determine_semconv).must_equal('stable') + end + end + + it 'returns "old" when OTEL_SEMCONV_STABILITY_OPT_IN is empty' do + OpenTelemetry::TestHelpers.with_env('OTEL_SEMCONV_STABILITY_OPT_IN' => '') do + _(instrumentation.determine_semconv).must_equal('old') + end + end + end end diff --git a/instrumentation/http/test/instrumentation/http/patches/dup/client_test.rb b/instrumentation/http/test/instrumentation/http/patches/dup/client_test.rb new file mode 100644 index 0000000000..ad6916b2ca --- /dev/null +++ b/instrumentation/http/test/instrumentation/http/patches/dup/client_test.rb @@ -0,0 +1,250 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +require_relative '../../../../../lib/opentelemetry/instrumentation/http' +require_relative '../../../../../lib/opentelemetry/instrumentation/http/patches/dup/client' + +describe OpenTelemetry::Instrumentation::HTTP::Patches::Dup::Client do + let(:instrumentation) { OpenTelemetry::Instrumentation::HTTP::Instrumentation.instance } + let(:exporter) { EXPORTER } + let(:span) { exporter.finished_spans.first } + let(:config) do + { + span_name_formatter: span_name_formatter + } + end + let(:span_name_formatter) { nil } + + 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 + # simulate a fresh install: + instrumentation.instance_variable_set(:@installed, false) + instrumentation.install(config) + 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 + ENV.delete('OTEL_SEMCONV_STABILITY_OPT_IN') + # Force re-install of instrumentation + instrumentation.instance_variable_set(:@installed, false) + OpenTelemetry.propagation = @orig_propagation + end + + describe '#perform' do + it 'traces a simple request' do + HTTP.get('http://example.com/success') + _(exporter.finished_spans.size).must_equal(1) + _(span.name).must_equal 'HTTP GET' + # Old semantic conventions + _(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['url.full']).must_equal 'http://example.com' + _(span.attributes['server.port']).must_equal 80 + _(span.attributes['url.query']).must_be_nil + + 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.post('http://example.com/failure') + + _(exporter.finished_spans.size).must_equal 1 + _(span.name).must_equal 'HTTP POST' + # Old semantic conventions + _(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['url.full']).must_equal 'http://example.com' + _(span.attributes['server.port']).must_equal 80 + _(span.attributes['url.query']).must_be_nil + 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.get('https://example.com/timeout') + end.must_raise HTTP::TimeoutError + + _(exporter.finished_spans.size).must_equal 1 + _(span.name).must_equal 'HTTP GET' + # Old semantic conventions + _(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 conventions + _(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['url.full']).must_equal 'https://example.com' + _(span.attributes['server.port']).must_equal 443 + _(span.attributes['url.query']).must_be_nil + _(span.status.code).must_equal( + OpenTelemetry::Trace::Status::ERROR + ) + _(span.status.description).must_equal( + 'Unhandled exception of type: HTTP::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.get('http://example.com/success?hello=there') + end + + _(exporter.finished_spans.size).must_equal 1 + _(span.name).must_equal 'HTTP GET' + # Old semantic conventions + _(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['url.full']).must_equal 'http://example.com' + _(span.attributes['server.port']).must_equal 80 + _(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 + + describe 'when span_name_formatter specified' do + let(:span_name_formatter) do + # demonstrate simple addition of path and string to span name: + lambda { |request_method, request_path| + "HTTP #{request_method} #{request_path} miniswan" + } + end + + it 'enriches the span' do + OpenTelemetry::Common::HTTP::ClientContext.with_attributes('peer.service' => 'foo') do + HTTP.get('http://example.com/success') + end + + _(exporter.finished_spans.size).must_equal 1 + _(span.name).must_equal 'HTTP GET /success miniswan' + # Old semantic conventions + _(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['url.full']).must_equal 'http://example.com' + _(span.attributes['server.port']).must_equal 80 + _(span.attributes['url.query']).must_be_nil + assert_requested( + :get, + 'http://example.com/success', + headers: { 'Traceparent' => "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" } + ) + end + end + describe 'when span_formatter specified and it errors' do + let(:span_name_formatter) do + # demonstrate simple addition of path and string to span name: + lambda { |_request_method, _request_path| + raise 'Something Bad' + } + end + + it 'provides a sane default' do + OpenTelemetry::Common::HTTP::ClientContext.with_attributes('peer.service' => 'foo') do + HTTP.get('http://example.com/success') + end + + _(exporter.finished_spans.size).must_equal 1 + _(span.name).must_equal 'HTTP GET' + # Old semantic conventions + _(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['url.full']).must_equal 'http://example.com' + _(span.attributes['server.port']).must_equal 80 + _(span.attributes['url.query']).must_be_nil + assert_requested( + :get, + 'http://example.com/success', + headers: { 'Traceparent' => "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" } + ) + end + end + end +end diff --git a/instrumentation/http/test/instrumentation/http/patches/dup/connection_test.rb b/instrumentation/http/test/instrumentation/http/patches/dup/connection_test.rb new file mode 100644 index 0000000000..9e17123da9 --- /dev/null +++ b/instrumentation/http/test/instrumentation/http/patches/dup/connection_test.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +require_relative '../../../../../lib/opentelemetry/instrumentation/http' +require_relative '../../../../../lib/opentelemetry/instrumentation/http/patches/dup/connection' + +describe OpenTelemetry::Instrumentation::HTTP::Patches::Dup::Connection do + let(:instrumentation) { OpenTelemetry::Instrumentation::HTTP::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 do + ENV.delete('OTEL_SEMCONV_STABILITY_OPT_IN') + instrumentation.instance_variable_set(:@installed, false) + end + + 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(HTTP::TimeoutError) do + HTTP.timeout(connect: 0.1, write: 0.1, read: 0.1).get("http://localhost:#{port}/example") + end + end + + _(exporter.finished_spans.size).must_equal(2) + _(span.name).must_equal 'HTTP CONNECT' + # Old semantic conventions + _(span.attributes['net.peer.name']).must_equal('localhost') + _(span.attributes['net.peer.port']).wont_be_nil + # Stable semantic conventions + _(span.attributes['server.address']).must_equal('localhost') + _(span.attributes['server.port']).wont_be_nil + ensure + WebMock.disable_net_connect! + end + end +end diff --git a/instrumentation/http/test/instrumentation/http/patches/client_test.rb b/instrumentation/http/test/instrumentation/http/patches/old/client_test.rb similarity index 95% rename from instrumentation/http/test/instrumentation/http/patches/client_test.rb rename to instrumentation/http/test/instrumentation/http/patches/old/client_test.rb index 8d62a5b07c..6579dc8b5d 100644 --- a/instrumentation/http/test/instrumentation/http/patches/client_test.rb +++ b/instrumentation/http/test/instrumentation/http/patches/old/client_test.rb @@ -6,10 +6,10 @@ require 'test_helper' -require_relative '../../../../lib/opentelemetry/instrumentation/http' -require_relative '../../../../lib/opentelemetry/instrumentation/http/patches/client' +require_relative '../../../../../lib/opentelemetry/instrumentation/http' +require_relative '../../../../../lib/opentelemetry/instrumentation/http/patches/old/client' -describe OpenTelemetry::Instrumentation::HTTP::Patches::Client do +describe OpenTelemetry::Instrumentation::HTTP::Patches::Old::Client do let(:instrumentation) { OpenTelemetry::Instrumentation::HTTP::Instrumentation.instance } let(:exporter) { EXPORTER } let(:span) { exporter.finished_spans.first } @@ -21,6 +21,8 @@ let(:span_name_formatter) { nil } 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/test/instrumentation/http/patches/connection_test.rb b/instrumentation/http/test/instrumentation/http/patches/old/connection.rb similarity index 79% rename from instrumentation/http/test/instrumentation/http/patches/connection_test.rb rename to instrumentation/http/test/instrumentation/http/patches/old/connection.rb index fc0b2b2a49..89ed18b02f 100644 --- a/instrumentation/http/test/instrumentation/http/patches/connection_test.rb +++ b/instrumentation/http/test/instrumentation/http/patches/old/connection.rb @@ -6,15 +6,17 @@ require 'test_helper' -require_relative '../../../../lib/opentelemetry/instrumentation/http' -require_relative '../../../../lib/opentelemetry/instrumentation/http/patches/connection' +require_relative '../../../../../lib/opentelemetry/instrumentation/http' +require_relative '../../../../../lib/opentelemetry/instrumentation/http/patches/old/connection' -describe OpenTelemetry::Instrumentation::HTTP::Patches::Connection do +describe OpenTelemetry::Instrumentation::HTTP::Patches::Old::Connection do let(:instrumentation) { OpenTelemetry::Instrumentation::HTTP::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/test/instrumentation/http/patches/stable/client_test.rb b/instrumentation/http/test/instrumentation/http/patches/stable/client_test.rb new file mode 100644 index 0000000000..337b14e7b1 --- /dev/null +++ b/instrumentation/http/test/instrumentation/http/patches/stable/client_test.rb @@ -0,0 +1,199 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +require_relative '../../../../../lib/opentelemetry/instrumentation/http' +require_relative '../../../../../lib/opentelemetry/instrumentation/http/patches/stable/client' + +describe OpenTelemetry::Instrumentation::HTTP::Patches::Stable::Client do + let(:instrumentation) { OpenTelemetry::Instrumentation::HTTP::Instrumentation.instance } + let(:exporter) { EXPORTER } + let(:span) { exporter.finished_spans.first } + let(:config) do + { + span_name_formatter: span_name_formatter + } + end + let(:span_name_formatter) { nil } + + 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 + # simulate a fresh install: + instrumentation.instance_variable_set(:@installed, false) + instrumentation.install(config) + 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 + ENV.delete('OTEL_SEMCONV_STABILITY_OPT_IN') + # Force re-install of instrumentation + instrumentation.instance_variable_set(:@installed, false) + + OpenTelemetry.propagation = @orig_propagation + end + + describe '#perform' do + it 'traces a simple request' do + HTTP.get('http://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['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['url.full']).must_equal 'http://example.com' + _(span.attributes['server.port']).must_equal 80 + _(span.attributes['url.query']).must_be_nil + 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.post('http://example.com/failure') + + _(exporter.finished_spans.size).must_equal 1 + _(span.name).must_equal 'HTTP 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['url.full']).must_equal 'http://example.com' + _(span.attributes['server.port']).must_equal 80 + _(span.attributes['url.query']).must_be_nil + 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.get('https://example.com/timeout') + end.must_raise HTTP::TimeoutError + + _(exporter.finished_spans.size).must_equal 1 + _(span.name).must_equal 'HTTP 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['url.full']).must_equal 'https://example.com' + _(span.attributes['server.port']).must_equal 443 + _(span.attributes['url.query']).must_be_nil + _(span.status.code).must_equal( + OpenTelemetry::Trace::Status::ERROR + ) + _(span.status.description).must_equal( + 'Unhandled exception of type: HTTP::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.get('http://example.com/success?hello=there') + end + + _(exporter.finished_spans.size).must_equal 1 + _(span.name).must_equal 'HTTP 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['url.full']).must_equal 'http://example.com' + _(span.attributes['server.port']).must_equal 80 + _(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 + + describe 'when span_name_formatter specified' do + let(:span_name_formatter) do + # demonstrate simple addition of path and string to span name: + lambda { |request_method, request_path| + "HTTP #{request_method} #{request_path} miniswan" + } + end + + it 'enriches the span' do + OpenTelemetry::Common::HTTP::ClientContext.with_attributes('peer.service' => 'foo') do + HTTP.get('http://example.com/success') + end + + _(exporter.finished_spans.size).must_equal 1 + _(span.name).must_equal 'HTTP GET /success miniswan' + _(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['url.full']).must_equal 'http://example.com' + _(span.attributes['server.port']).must_equal 80 + _(span.attributes['url.query']).must_be_nil + assert_requested( + :get, + 'http://example.com/success', + headers: { 'Traceparent' => "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" } + ) + end + end + describe 'when span_formatter specified and it errors' do + let(:span_name_formatter) do + # demonstrate simple addition of path and string to span name: + lambda { |_request_method, _request_path| + raise 'Something Bad' + } + end + + it 'provides a sane default' do + OpenTelemetry::Common::HTTP::ClientContext.with_attributes('peer.service' => 'foo') do + HTTP.get('http://example.com/success') + end + + _(exporter.finished_spans.size).must_equal 1 + _(span.name).must_equal 'HTTP 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['url.full']).must_equal 'http://example.com' + _(span.attributes['server.port']).must_equal 80 + _(span.attributes['url.query']).must_be_nil + assert_requested( + :get, + 'http://example.com/success', + headers: { 'Traceparent' => "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" } + ) + end + end + end +end diff --git a/instrumentation/http/test/instrumentation/http/patches/stable/connection.rb b/instrumentation/http/test/instrumentation/http/patches/stable/connection.rb new file mode 100644 index 0000000000..1f6285bb3d --- /dev/null +++ b/instrumentation/http/test/instrumentation/http/patches/stable/connection.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +require_relative '../../../../../lib/opentelemetry/instrumentation/http' +require_relative '../../../../../lib/opentelemetry/instrumentation/http/patches/stable/connection' + +describe OpenTelemetry::Instrumentation::HTTP::Patches::Stable::Connection do + let(:instrumentation) { OpenTelemetry::Instrumentation::HTTP::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 do + ENV.delete('OTEL_SEMCONV_STABILITY_OPT_IN') + instrumentation.instance_variable_set(:@installed, false) + end + + 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(HTTP::TimeoutError) do + HTTP.timeout(connect: 0.1, write: 0.1, read: 0.1).get("http://localhost:#{port}/example") + end + end + + _(exporter.finished_spans.size).must_equal(2) + _(span.name).must_equal 'HTTP CONNECT' + _(span.attributes['server.address']).must_equal('localhost') + _(span.attributes['server.port']).wont_be_nil + ensure + WebMock.disable_net_connect! + end + end +end