diff --git a/instrumentation/faraday/Appraisals b/instrumentation/faraday/Appraisals index 4ed32e3428..594dee720b 100644 --- a/instrumentation/faraday/Appraisals +++ b/instrumentation/faraday/Appraisals @@ -4,12 +4,22 @@ # # SPDX-License-Identifier: Apache-2.0 -%w[1.0 2.0].each do |version| - appraise "faraday-#{version}" do - gem 'faraday', "~> #{version}" +# 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/ + +versions = %w[1.0 2.0] +semconv_stability = %w[old stable dup] + +semconv_stability.each do |mode| + versions.each do |version| + appraise "faraday-#{version}-#{mode}" do + gem 'faraday', "~> #{version}" + end end -end -appraise 'faraday-latest' do - gem 'faraday' + appraise "faraday-latest-#{mode}" do + gem 'faraday' + end end diff --git a/instrumentation/faraday/README.md b/instrumentation/faraday/README.md index 88b77cad23..a8d7b589b5 100644 --- a/instrumentation/faraday/README.md +++ b/instrumentation/faraday/README.md @@ -59,3 +59,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/). diff --git a/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/instrumentation.rb b/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/instrumentation.rb index 6aa08328ad..779778069a 100644 --- a/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/instrumentation.rb +++ b/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/instrumentation.rb @@ -13,9 +13,10 @@ class Instrumentation < OpenTelemetry::Instrumentation::Base MINIMUM_VERSION = Gem::Version.new('1.0') install do |_config| - require_dependencies - register_tracer_middleware - use_middleware_by_default + patch_type = determine_semconv + send(:"require_dependencies_#{patch_type}") + send(:"register_tracer_middleware_#{patch_type}") + send(:"use_middleware_by_default_#{patch_type}") end compatible do @@ -36,19 +37,62 @@ def gem_version Gem::Version.new(::Faraday::VERSION) end - def require_dependencies - require_relative 'middlewares/tracer_middleware' - require_relative '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 require_dependencies_dup + require_relative 'middlewares/dup/tracer_middleware' + require_relative 'patches/dup/connection' + end + + def require_dependencies_old + require_relative 'middlewares/old/tracer_middleware' + require_relative 'patches/old/connection' end - def register_tracer_middleware + def require_dependencies_stable + require_relative 'middlewares/stable/tracer_middleware' + require_relative 'patches/stable/connection' + end + + def register_tracer_middleware_dup ::Faraday::Middleware.register_middleware( - open_telemetry: Middlewares::TracerMiddleware + open_telemetry: Middlewares::Dup::TracerMiddleware ) end - def use_middleware_by_default - ::Faraday::Connection.prepend(Patches::Connection) + def register_tracer_middleware_old + ::Faraday::Middleware.register_middleware( + open_telemetry: Middlewares::Old::TracerMiddleware + ) + end + + def register_tracer_middleware_stable + ::Faraday::Middleware.register_middleware( + open_telemetry: Middlewares::Stable::TracerMiddleware + ) + end + + def use_middleware_by_default_dup + ::Faraday::Connection.prepend(Patches::Dup::Connection) + end + + def use_middleware_by_default_old + ::Faraday::Connection.prepend(Patches::Old::Connection) + end + + def use_middleware_by_default_stable + ::Faraday::Connection.prepend(Patches::Stable::Connection) end end end diff --git a/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/middlewares/dup/tracer_middleware.rb b/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/middlewares/dup/tracer_middleware.rb new file mode 100644 index 0000000000..b1de1d7521 --- /dev/null +++ b/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/middlewares/dup/tracer_middleware.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Instrumentation + module Faraday + module Middlewares + module Dup + # TracerMiddleware propagates context and instruments Faraday requests + # by way of its middleware system + class TracerMiddleware < ::Faraday::Middleware + HTTP_METHODS_SYMBOL_TO_STRING = { + connect: 'CONNECT', + delete: 'DELETE', + get: 'GET', + head: 'HEAD', + options: 'OPTIONS', + patch: 'PATCH', + post: 'POST', + put: 'PUT', + trace: 'TRACE' + }.freeze + + # Constant for the HTTP status range + HTTP_STATUS_SUCCESS_RANGE = (100..399) + + def call(env) + http_method = HTTP_METHODS_SYMBOL_TO_STRING[env.method] + config = Faraday::Instrumentation.instance.config + + attributes = span_creation_attributes( + http_method: http_method, url: env.url, config: config + ) + + OpenTelemetry::Common::HTTP::ClientContext.with_attributes(attributes) do |attrs, _| + tracer.in_span( + http_method, attributes: attrs, kind: config.fetch(:span_kind) + ) do |span| + OpenTelemetry.propagation.inject(env.request_headers) + + if config[:enable_internal_instrumentation] == false + OpenTelemetry::Common::Utilities.untraced do + app.call(env).on_complete { |resp| trace_response(span, resp.status) } + end + else + app.call(env).on_complete { |resp| trace_response(span, resp.status) } + end + rescue ::Faraday::Error => e + trace_response(span, e.response[:status]) if e.response + + raise + end + end + end + + private + + def span_creation_attributes(http_method:, url:, config:) + cleansed_url = OpenTelemetry::Common::Utilities.cleanse_url(url.to_s) + attrs = { + 'http.method' => http_method, + 'http.request.method' => http_method, + 'http.url' => cleansed_url, + 'url.full' => cleansed_url, + 'faraday.adapter.name' => app.class.name + } + if url.host + attrs['net.peer.name'] = url.host + attrs['server.address'] = url.host + end + attrs['peer.service'] = config[:peer_service] if config[:peer_service] + + attrs.merge!( + OpenTelemetry::Common::HTTP::ClientContext.attributes + ) + end + + # Versions prior to 1.0 do not define an accessor for app + attr_reader :app if Gem::Version.new(Faraday::VERSION) < Gem::Version.new('1.0.0') + + def tracer + Faraday::Instrumentation.instance.tracer + end + + def trace_response(span, status) + span.set_attribute('http.status_code', status) + span.set_attribute('http.response.status_code', status) + span.status = OpenTelemetry::Trace::Status.error unless HTTP_STATUS_SUCCESS_RANGE.cover?(status.to_i) + end + end + end + end + end + end +end diff --git a/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/middlewares/old/tracer_middleware.rb b/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/middlewares/old/tracer_middleware.rb new file mode 100644 index 0000000000..7b263a2f9e --- /dev/null +++ b/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/middlewares/old/tracer_middleware.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Instrumentation + module Faraday + module Middlewares + module Old + # TracerMiddleware propagates context and instruments Faraday requests + # by way of its middleware system + class TracerMiddleware < ::Faraday::Middleware + HTTP_METHODS_SYMBOL_TO_STRING = { + connect: 'CONNECT', + delete: 'DELETE', + get: 'GET', + head: 'HEAD', + options: 'OPTIONS', + patch: 'PATCH', + post: 'POST', + put: 'PUT', + trace: 'TRACE' + }.freeze + + # Constant for the HTTP status range + HTTP_STATUS_SUCCESS_RANGE = (100..399) + + def call(env) + http_method = HTTP_METHODS_SYMBOL_TO_STRING[env.method] + config = Faraday::Instrumentation.instance.config + + attributes = span_creation_attributes( + http_method: http_method, url: env.url, config: config + ) + + OpenTelemetry::Common::HTTP::ClientContext.with_attributes(attributes) do |attrs, _| + tracer.in_span( + "HTTP #{http_method}", attributes: attrs, kind: config.fetch(:span_kind) + ) do |span| + OpenTelemetry.propagation.inject(env.request_headers) + + if config[:enable_internal_instrumentation] == false + OpenTelemetry::Common::Utilities.untraced do + app.call(env).on_complete { |resp| trace_response(span, resp.status) } + end + else + app.call(env).on_complete { |resp| trace_response(span, resp.status) } + end + rescue ::Faraday::Error => e + trace_response(span, e.response[:status]) if e.response + + raise + end + end + end + + private + + def span_creation_attributes(http_method:, url:, config:) + attrs = { + 'http.method' => http_method, + 'http.url' => OpenTelemetry::Common::Utilities.cleanse_url(url.to_s), + 'faraday.adapter.name' => app.class.name + } + attrs['net.peer.name'] = url.host if url.host + attrs['peer.service'] = config[:peer_service] if config[:peer_service] + + attrs.merge!( + OpenTelemetry::Common::HTTP::ClientContext.attributes + ) + end + + # Versions prior to 1.0 do not define an accessor for app + attr_reader :app if Gem::Version.new(Faraday::VERSION) < Gem::Version.new('1.0.0') + + def tracer + Faraday::Instrumentation.instance.tracer + end + + def trace_response(span, status) + span.set_attribute('http.status_code', status) + span.status = OpenTelemetry::Trace::Status.error unless HTTP_STATUS_SUCCESS_RANGE.cover?(status.to_i) + end + end + end + end + end + end +end diff --git a/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/middlewares/stable/tracer_middleware.rb b/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/middlewares/stable/tracer_middleware.rb new file mode 100644 index 0000000000..4f169e27d6 --- /dev/null +++ b/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/middlewares/stable/tracer_middleware.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Instrumentation + module Faraday + module Middlewares + module Stable + # TracerMiddleware propagates context and instruments Faraday requests + # by way of its middleware system + class TracerMiddleware < ::Faraday::Middleware + HTTP_METHODS_SYMBOL_TO_STRING = { + connect: 'CONNECT', + delete: 'DELETE', + get: 'GET', + head: 'HEAD', + options: 'OPTIONS', + patch: 'PATCH', + post: 'POST', + put: 'PUT', + trace: 'TRACE' + }.freeze + + # Constant for the HTTP status range + HTTP_STATUS_SUCCESS_RANGE = (100..399) + + def call(env) + http_method = HTTP_METHODS_SYMBOL_TO_STRING[env.method] + config = Faraday::Instrumentation.instance.config + + attributes = span_creation_attributes( + http_method: http_method, url: env.url, config: config + ) + + OpenTelemetry::Common::HTTP::ClientContext.with_attributes(attributes) do |attrs, _| + tracer.in_span( + http_method, attributes: attrs, kind: config.fetch(:span_kind) + ) do |span| + OpenTelemetry.propagation.inject(env.request_headers) + + if config[:enable_internal_instrumentation] == false + OpenTelemetry::Common::Utilities.untraced do + app.call(env).on_complete { |resp| trace_response(span, resp.status) } + end + else + app.call(env).on_complete { |resp| trace_response(span, resp.status) } + end + rescue ::Faraday::Error => e + trace_response(span, e.response[:status]) if e.response + + raise + end + end + end + + private + + def span_creation_attributes(http_method:, url:, config:) + attrs = { + 'http.request.method' => http_method, + 'url.full' => OpenTelemetry::Common::Utilities.cleanse_url(url.to_s), + 'faraday.adapter.name' => app.class.name + } + attrs['server.address'] = url.host if url.host + attrs['peer.service'] = config[:peer_service] if config[:peer_service] + + attrs.merge!( + OpenTelemetry::Common::HTTP::ClientContext.attributes + ) + end + + # Versions prior to 1.0 do not define an accessor for app + attr_reader :app if Gem::Version.new(Faraday::VERSION) < Gem::Version.new('1.0.0') + + def tracer + Faraday::Instrumentation.instance.tracer + end + + def trace_response(span, status) + span.set_attribute('http.response.status_code', status) + span.status = OpenTelemetry::Trace::Status.error unless HTTP_STATUS_SUCCESS_RANGE.cover?(status.to_i) + end + end + end + end + end + end +end diff --git a/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/middlewares/tracer_middleware.rb b/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/middlewares/tracer_middleware.rb deleted file mode 100644 index ac4b298431..0000000000 --- a/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/middlewares/tracer_middleware.rb +++ /dev/null @@ -1,89 +0,0 @@ -# frozen_string_literal: true - -# Copyright The OpenTelemetry Authors -# -# SPDX-License-Identifier: Apache-2.0 - -module OpenTelemetry - module Instrumentation - module Faraday - module Middlewares - # TracerMiddleware propagates context and instruments Faraday requests - # by way of its middleware system - class TracerMiddleware < ::Faraday::Middleware - HTTP_METHODS_SYMBOL_TO_STRING = { - connect: 'CONNECT', - delete: 'DELETE', - get: 'GET', - head: 'HEAD', - options: 'OPTIONS', - patch: 'PATCH', - post: 'POST', - put: 'PUT', - trace: 'TRACE' - }.freeze - - # Constant for the HTTP status range - HTTP_STATUS_SUCCESS_RANGE = (100..399) - - def call(env) - http_method = HTTP_METHODS_SYMBOL_TO_STRING[env.method] - config = Faraday::Instrumentation.instance.config - - attributes = span_creation_attributes( - http_method: http_method, url: env.url, config: config - ) - - OpenTelemetry::Common::HTTP::ClientContext.with_attributes(attributes) do |attrs, _| - tracer.in_span( - "HTTP #{http_method}", attributes: attrs, kind: config.fetch(:span_kind) - ) do |span| - OpenTelemetry.propagation.inject(env.request_headers) - - if config[:enable_internal_instrumentation] == false - OpenTelemetry::Common::Utilities.untraced do - app.call(env).on_complete { |resp| trace_response(span, resp.status) } - end - else - app.call(env).on_complete { |resp| trace_response(span, resp.status) } - end - rescue ::Faraday::Error => e - trace_response(span, e.response[:status]) if e.response - - raise - end - end - end - - private - - def span_creation_attributes(http_method:, url:, config:) - attrs = { - 'http.method' => http_method, - 'http.url' => OpenTelemetry::Common::Utilities.cleanse_url(url.to_s), - 'faraday.adapter.name' => app.class.name - } - attrs['net.peer.name'] = url.host if url.host - attrs['peer.service'] = config[:peer_service] if config[:peer_service] - - attrs.merge!( - OpenTelemetry::Common::HTTP::ClientContext.attributes - ) - end - - # Versions prior to 1.0 do not define an accessor for app - attr_reader :app if Gem::Version.new(Faraday::VERSION) < Gem::Version.new('1.0.0') - - def tracer - Faraday::Instrumentation.instance.tracer - end - - def trace_response(span, status) - span.set_attribute('http.status_code', status) - span.status = OpenTelemetry::Trace::Status.error unless HTTP_STATUS_SUCCESS_RANGE.cover?(status.to_i) - end - end - end - end - end -end diff --git a/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/patches/connection.rb b/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/patches/connection.rb deleted file mode 100644 index 1819488ad0..0000000000 --- a/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/patches/connection.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -# Copyright The OpenTelemetry Authors -# -# SPDX-License-Identifier: Apache-2.0 - -module OpenTelemetry - module Instrumentation - module Faraday - module Patches - # Module to be prepended to force Faraday to use the middleware by - # default so the user doesn't have to call `use` for every connection. - module Connection - # Wraps Faraday::Connection#initialize: - # https://github.com/lostisland/faraday/blob/ff9dc1d1219a1bbdba95a9a4cf5d135b97247ee2/lib/faraday/connection.rb#L62-L92 - def initialize(...) - super.tap do - use(:open_telemetry) unless builder.handlers.any? do |handler| - handler.klass == Middlewares::TracerMiddleware - end - end - end - end - end - end - end -end diff --git a/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/patches/dup/connection.rb b/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/patches/dup/connection.rb new file mode 100644 index 0000000000..621ca05962 --- /dev/null +++ b/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/patches/dup/connection.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Instrumentation + module Faraday + module Patches + module Dup + # Module to be prepended to force Faraday to use the middleware by + # default so the user doesn't have to call `use` for every connection. + module Connection + # Wraps Faraday::Connection#initialize: + # https://github.com/lostisland/faraday/blob/ff9dc1d1219a1bbdba95a9a4cf5d135b97247ee2/lib/faraday/connection.rb#L62-L92 + def initialize(...) + super.tap do + use(:open_telemetry) unless builder.handlers.any? do |handler| + handler.klass == Middlewares::Dup::TracerMiddleware + end + end + end + end + end + end + end + end +end diff --git a/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/patches/old/connection.rb b/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/patches/old/connection.rb new file mode 100644 index 0000000000..dbe067900a --- /dev/null +++ b/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/patches/old/connection.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Instrumentation + module Faraday + module Patches + module Old + # Module to be prepended to force Faraday to use the middleware by + # default so the user doesn't have to call `use` for every connection. + module Connection + # Wraps Faraday::Connection#initialize: + # https://github.com/lostisland/faraday/blob/ff9dc1d1219a1bbdba95a9a4cf5d135b97247ee2/lib/faraday/connection.rb#L62-L92 + def initialize(...) + super.tap do + use(:open_telemetry) unless builder.handlers.any? do |handler| + handler.klass == Middlewares::Old::TracerMiddleware + end + end + end + end + end + end + end + end +end diff --git a/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/patches/stable/connection.rb b/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/patches/stable/connection.rb new file mode 100644 index 0000000000..8cb5a880bc --- /dev/null +++ b/instrumentation/faraday/lib/opentelemetry/instrumentation/faraday/patches/stable/connection.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Instrumentation + module Faraday + module Patches + module Stable + # Module to be prepended to force Faraday to use the middleware by + # default so the user doesn't have to call `use` for every connection. + module Connection + # Wraps Faraday::Connection#initialize: + # https://github.com/lostisland/faraday/blob/ff9dc1d1219a1bbdba95a9a4cf5d135b97247ee2/lib/faraday/connection.rb#L62-L92 + def initialize(...) + super.tap do + use(:open_telemetry) unless builder.handlers.any? do |handler| + handler.klass == Middlewares::Stable::TracerMiddleware + end + end + end + end + end + end + end + end +end diff --git a/instrumentation/faraday/test/opentelemetry/instrumentation/faraday/middlewares/dup/tracer_middleware_test.rb b/instrumentation/faraday/test/opentelemetry/instrumentation/faraday/middlewares/dup/tracer_middleware_test.rb new file mode 100644 index 0000000000..99348cf31b --- /dev/null +++ b/instrumentation/faraday/test/opentelemetry/instrumentation/faraday/middlewares/dup/tracer_middleware_test.rb @@ -0,0 +1,252 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +# require Instrumentation so .install method is found: +require_relative '../../../../../../lib/opentelemetry/instrumentation/faraday/middlewares/dup/tracer_middleware' + +describe OpenTelemetry::Instrumentation::Faraday::Middlewares::Dup::TracerMiddleware do + let(:instrumentation) { OpenTelemetry::Instrumentation::Faraday::Instrumentation.instance } + let(:exporter) { EXPORTER } + let(:span) { exporter.finished_spans.first } + + let(:client) do + Faraday.new('http://username:password@example.com') do |builder| + builder.adapter(:test) do |stub| + stub.get('/success') { |_| [200, {}, 'OK'] } + stub.get('/failure') { |_| [500, {}, 'OK'] } + stub.get('/not_found') { |_| [404, {}, 'OK'] } + stub.get('/show-shared-attributes') { |_| [200, {}, OpenTelemetry::Common::HTTP::ClientContext.attributes.to_json] } + end + end + end + + before do + skip unless ENV['BUNDLE_GEMFILE'].include?('dup') + + ENV['OTEL_SEMCONV_STABILITY_OPT_IN'] = 'http/dup' + exporter.reset + + # 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 + instrumentation.instance_variable_set(:@installed, false) + instrumentation.install + end + + after do + OpenTelemetry.propagation = @orig_propagation + end + + describe 'first span' do + describe 'given a client with a base url' do + it 'has http 200 attributes' do + response = client.get('/success') + + _(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' + _(span.attributes['net.peer.name']).must_equal 'example.com' + _(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' + _(span.attributes['server.address']).must_equal 'example.com' + _(response.env.request_headers['Traceparent']).must_equal( + "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" + ) + end + + it 'has http.status_code 404' do + response = client.get('/not_found') + + _(span.name).must_equal 'GET' + _(span.attributes['http.method']).must_equal 'GET' + _(span.attributes['http.status_code']).must_equal 404 + _(span.attributes['http.url']).must_equal 'http://example.com/not_found' + _(span.attributes['net.peer.name']).must_equal 'example.com' + _(span.attributes['http.request.method']).must_equal 'GET' + _(span.attributes['http.response.status_code']).must_equal 404 + _(span.attributes['url.full']).must_equal 'http://example.com/not_found' + _(span.attributes['server.address']).must_equal 'example.com' + _(response.env.request_headers['Traceparent']).must_equal( + "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" + ) + end + + it 'has http.status_code 500' do + response = client.get('/failure') + + _(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' + _(span.attributes['net.peer.name']).must_equal 'example.com' + _(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' + _(span.attributes['server.address']).must_equal 'example.com' + _(response.env.request_headers['Traceparent']).must_equal( + "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" + ) + end + + it 'merges http client attributes' do + client_context_attrs = { + 'test.attribute' => 'test.value', 'http.method' => 'OVERRIDE', 'http.request.method' => 'OVERRIDE' + } + response = OpenTelemetry::Common::HTTP::ClientContext.with_attributes(client_context_attrs) do + client.get('/success') + end + + _(span.name).must_equal 'GET' + _(span.attributes['http.method']).must_equal 'OVERRIDE' + _(span.attributes['http.status_code']).must_equal 200 + _(span.attributes['http.url']).must_equal 'http://example.com/success' + _(span.attributes['net.peer.name']).must_equal 'example.com' + _(span.attributes['test.attribute']).must_equal 'test.value' + _(span.attributes['http.request.method']).must_equal 'OVERRIDE' + _(span.attributes['http.response.status_code']).must_equal 200 + _(span.attributes['url.full']).must_equal 'http://example.com/success' + _(span.attributes['server.address']).must_equal 'example.com' + _(response.env.request_headers['Traceparent']).must_equal( + "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" + ) + end + + it 'ammends attributes to client context' do + response = client.get('/show-shared-attributes') + shared_attributes = JSON.parse(response.body) + expected_attributes = { + 'http.method' => 'GET', 'http.url' => 'http://example.com/show-shared-attributes', + 'http.request.method' => 'GET', 'url.full' => 'http://example.com/show-shared-attributes', + 'faraday.adapter.name' => 'Faraday::Adapter::Test', + 'net.peer.name' => 'example.com', + 'server.address' => 'example.com' + } + + _(shared_attributes).must_equal expected_attributes + end + + it 'accepts peer service name from config' do + instrumentation.instance_variable_set(:@installed, false) + instrumentation.install(peer_service: 'example:faraday') + + client.get('/success') + + _(span.attributes['peer.service']).must_equal 'example:faraday' + end + + it 'defaults to span kind client' do + instrumentation.instance_variable_set(:@installed, false) + instrumentation.install + + client.get('/success') + + _(span.kind).must_equal :client + end + + it 'allows overriding the span kind to internal' do + instrumentation.instance_variable_set(:@installed, false) + instrumentation.install(span_kind: :internal) + + client.get('/success') + + _(span.kind).must_equal :internal + end + + it 'reports the name of the configured adapter' do + instrumentation.instance_variable_set(:@installed, false) + instrumentation.install + + client.get('/success') + + _(span.attributes.fetch('faraday.adapter.name')).must_equal Faraday::Adapter::Test.name + 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 + client.get('/success') + end + + _(span.attributes['peer.service']).must_equal 'example:custom' + end + + it 'does not leak authentication credentials' do + client.run_request(:get, 'http://username:password@example.com/success', nil, {}) + + _(span.attributes['http.url']).must_equal 'http://example.com/success' + end + end + + describe 'given a client without a base url' do + let(:client) do + Faraday.new do |builder| + builder.adapter(:test) do |stub| + stub.get('/success') { |_| [200, {}, 'OK'] } + end + end + end + + it 'omits missing attributes' do + response = client.get('/success') + + _(span.name).must_equal 'GET' + _(span.attributes['http.method']).must_equal 'GET' + _(span.attributes['http.status_code']).must_equal 200 + _(span.attributes['http.url']).must_equal 'http:/success' + _(span.attributes).wont_include('net.peer.name') + _(span.attributes['http.request.method']).must_equal 'GET' + _(span.attributes['http.response.status_code']).must_equal 200 + _(span.attributes['url.full']).must_equal 'http:/success' + _(span.attributes).wont_include('server.address') + _(response.env.request_headers['Traceparent']).must_equal( + "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" + ) + end + end + + describe 'when faraday raises an error' do + let(:client) do + Faraday.new do |builder| + builder.response :raise_error + builder.adapter(:test) do |stub| + stub.get('/not_found') { |_| [404, {}, 'NOT FOUND'] } + end + end + end + + it 'adds response attributes' do + assert_raises Faraday::Error do + client.get('/not_found') + end + + _(span.attributes['http.status_code']).must_equal 404 + _(span.attributes['http.response.status_code']).must_equal 404 + _(span.status.code).must_equal OpenTelemetry::Trace::Status::ERROR + end + end + + describe 'when explicitly adding the tracer middleware' do + let(:client) do + Faraday.new do |builder| + builder.use :open_telemetry + end + end + + it 'only adds the middleware once' do + tracers = client.builder.handlers.count(OpenTelemetry::Instrumentation::Faraday::Middlewares::Dup::TracerMiddleware) + _(tracers).must_equal 1 + end + end + end +end diff --git a/instrumentation/faraday/test/opentelemetry/instrumentation/faraday/middlewares/tracer_middleware_test.rb b/instrumentation/faraday/test/opentelemetry/instrumentation/faraday/middlewares/old/tracer_middleware_test.rb similarity index 96% rename from instrumentation/faraday/test/opentelemetry/instrumentation/faraday/middlewares/tracer_middleware_test.rb rename to instrumentation/faraday/test/opentelemetry/instrumentation/faraday/middlewares/old/tracer_middleware_test.rb index 67aab661f9..4f88b61b0c 100644 --- a/instrumentation/faraday/test/opentelemetry/instrumentation/faraday/middlewares/tracer_middleware_test.rb +++ b/instrumentation/faraday/test/opentelemetry/instrumentation/faraday/middlewares/old/tracer_middleware_test.rb @@ -7,9 +7,9 @@ require 'test_helper' # require Instrumentation so .install method is found: -require_relative '../../../../../lib/opentelemetry/instrumentation/faraday/middlewares/tracer_middleware' +require_relative '../../../../../../lib/opentelemetry/instrumentation/faraday/middlewares/old/tracer_middleware' -describe OpenTelemetry::Instrumentation::Faraday::Middlewares::TracerMiddleware do +describe OpenTelemetry::Instrumentation::Faraday::Middlewares::Old::TracerMiddleware do let(:instrumentation) { OpenTelemetry::Instrumentation::Faraday::Instrumentation.instance } let(:exporter) { EXPORTER } let(:span) { exporter.finished_spans.first } @@ -26,6 +26,8 @@ end before do + skip unless ENV['BUNDLE_GEMFILE'].include?('old') + exporter.reset # this is currently a noop but this will future proof the test @@ -218,7 +220,7 @@ end it 'only adds the middleware once' do - tracers = client.builder.handlers.count(OpenTelemetry::Instrumentation::Faraday::Middlewares::TracerMiddleware) + tracers = client.builder.handlers.count(OpenTelemetry::Instrumentation::Faraday::Middlewares::Old::TracerMiddleware) _(tracers).must_equal 1 end end diff --git a/instrumentation/faraday/test/opentelemetry/instrumentation/faraday/middlewares/stable/tracer_middleware_test.rb b/instrumentation/faraday/test/opentelemetry/instrumentation/faraday/middlewares/stable/tracer_middleware_test.rb new file mode 100644 index 0000000000..5435518d95 --- /dev/null +++ b/instrumentation/faraday/test/opentelemetry/instrumentation/faraday/middlewares/stable/tracer_middleware_test.rb @@ -0,0 +1,229 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +# require Instrumentation so .install method is found: +require_relative '../../../../../../lib/opentelemetry/instrumentation/faraday/middlewares/stable/tracer_middleware' + +describe OpenTelemetry::Instrumentation::Faraday::Middlewares::Stable::TracerMiddleware do + let(:instrumentation) { OpenTelemetry::Instrumentation::Faraday::Instrumentation.instance } + let(:exporter) { EXPORTER } + let(:span) { exporter.finished_spans.first } + + let(:client) do + Faraday.new('http://username:password@example.com') do |builder| + builder.adapter(:test) do |stub| + stub.get('/success') { |_| [200, {}, 'OK'] } + stub.get('/failure') { |_| [500, {}, 'OK'] } + stub.get('/not_found') { |_| [404, {}, 'OK'] } + stub.get('/show-shared-attributes') { |_| [200, {}, OpenTelemetry::Common::HTTP::ClientContext.attributes.to_json] } + end + end + end + + before do + skip unless ENV['BUNDLE_GEMFILE'].include?('stable') + + ENV['OTEL_SEMCONV_STABILITY_OPT_IN'] = 'http' + exporter.reset + + # 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 + instrumentation.instance_variable_set(:@installed, false) + instrumentation.install + end + + after do + OpenTelemetry.propagation = @orig_propagation + end + + describe 'first span' do + describe 'given a client with a base url' do + it 'has http 200 attributes' do + response = client.get('/success') + + _(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' + _(span.attributes['server.address']).must_equal 'example.com' + _(response.env.request_headers['Traceparent']).must_equal( + "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" + ) + end + + it 'has http.response.status_code 404' do + response = client.get('/not_found') + + _(span.name).must_equal 'GET' + _(span.attributes['http.request.method']).must_equal 'GET' + _(span.attributes['http.response.status_code']).must_equal 404 + _(span.attributes['url.full']).must_equal 'http://example.com/not_found' + _(span.attributes['server.address']).must_equal 'example.com' + _(response.env.request_headers['Traceparent']).must_equal( + "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" + ) + end + + it 'has http.response.status_code 500' do + response = client.get('/failure') + + _(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' + _(span.attributes['server.address']).must_equal 'example.com' + _(response.env.request_headers['Traceparent']).must_equal( + "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" + ) + end + + it 'merges http client attributes' do + client_context_attrs = { + 'test.attribute' => 'test.value', 'http.request.method' => 'OVERRIDE' + } + response = OpenTelemetry::Common::HTTP::ClientContext.with_attributes(client_context_attrs) do + client.get('/success') + end + + _(span.name).must_equal 'GET' + _(span.attributes['http.request.method']).must_equal 'OVERRIDE' + _(span.attributes['http.response.status_code']).must_equal 200 + _(span.attributes['url.full']).must_equal 'http://example.com/success' + _(span.attributes['server.address']).must_equal 'example.com' + _(span.attributes['test.attribute']).must_equal 'test.value' + _(response.env.request_headers['Traceparent']).must_equal( + "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" + ) + end + + it 'ammends attributes to client context' do + response = client.get('/show-shared-attributes') + shared_attributes = JSON.parse(response.body) + expected_attributes = { + 'http.request.method' => 'GET', 'url.full' => 'http://example.com/show-shared-attributes', + 'faraday.adapter.name' => 'Faraday::Adapter::Test', + 'server.address' => 'example.com' + } + + _(shared_attributes).must_equal expected_attributes + end + + it 'accepts peer service name from config' do + instrumentation.instance_variable_set(:@installed, false) + instrumentation.install(peer_service: 'example:faraday') + + client.get('/success') + + _(span.attributes['peer.service']).must_equal 'example:faraday' + end + + it 'defaults to span kind client' do + instrumentation.instance_variable_set(:@installed, false) + instrumentation.install + + client.get('/success') + + _(span.kind).must_equal :client + end + + it 'allows overriding the span kind to internal' do + instrumentation.instance_variable_set(:@installed, false) + instrumentation.install(span_kind: :internal) + + client.get('/success') + + _(span.kind).must_equal :internal + end + + it 'reports the name of the configured adapter' do + instrumentation.instance_variable_set(:@installed, false) + instrumentation.install + + client.get('/success') + + _(span.attributes.fetch('faraday.adapter.name')).must_equal Faraday::Adapter::Test.name + 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 + client.get('/success') + end + + _(span.attributes['peer.service']).must_equal 'example:custom' + end + + it 'does not leak authentication credentials' do + client.run_request(:get, 'http://username:password@example.com/success', nil, {}) + + _(span.attributes['url.full']).must_equal 'http://example.com/success' + end + end + + describe 'given a client without a base url' do + let(:client) do + Faraday.new do |builder| + builder.adapter(:test) do |stub| + stub.get('/success') { |_| [200, {}, 'OK'] } + end + end + end + + it 'omits missing attributes' do + response = client.get('/success') + + _(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:/success' + _(span.attributes).wont_include('server.address') + _(response.env.request_headers['Traceparent']).must_equal( + "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" + ) + end + end + + describe 'when faraday raises an error' do + let(:client) do + Faraday.new do |builder| + builder.response :raise_error + builder.adapter(:test) do |stub| + stub.get('/not_found') { |_| [404, {}, 'NOT FOUND'] } + end + end + end + + it 'adds response attributes' do + assert_raises Faraday::Error do + client.get('/not_found') + end + + _(span.attributes['http.response.status_code']).must_equal 404 + _(span.status.code).must_equal OpenTelemetry::Trace::Status::ERROR + end + end + + describe 'when explicitly adding the tracer middleware' do + let(:client) do + Faraday.new do |builder| + builder.use :open_telemetry + end + end + + it 'only adds the middleware once' do + tracers = client.builder.handlers.count(OpenTelemetry::Instrumentation::Faraday::Middlewares::Stable::TracerMiddleware) + _(tracers).must_equal 1 + end + end + end +end diff --git a/instrumentation/faraday/test/opentelemetry/instrumentation/faraday_test.rb b/instrumentation/faraday/test/opentelemetry/instrumentation/faraday_test.rb index eabf86e05f..d4cace38f6 100644 --- a/instrumentation/faraday/test/opentelemetry/instrumentation/faraday_test.rb +++ b/instrumentation/faraday/test/opentelemetry/instrumentation/faraday_test.rb @@ -11,6 +11,8 @@ let(:exporter) { EXPORTER } before do + skip unless ENV['BUNDLE_GEMFILE'].include?('old') + instrumentation.install exporter.reset end