Skip to content

feat: excon HTTP semantic convention stability migration #1569

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

22 changes: 16 additions & 6 deletions instrumentation/excon/Appraisals
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,22 @@

# add more tests for excon

%w[0.71 0.109].each do |version|
appraise "excon-#{version}" do
gem 'excon', "~> #{version}.0"
# 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[0.71 0.109]
semconv_stability = %w[dup stable old]

semconv_stability.each do |mode|
versions.each do |version|
appraise "excon-#{version}-#{mode}" do
gem 'excon', "~> #{version}.0"
end
end
end

appraise 'excon-latest' do
gem 'excon'
appraise "excon-latest-#{mode}" do
gem 'excon'
end
end
17 changes: 17 additions & 0 deletions instrumentation/excon/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,20 @@ The `opentelemetry-instrumentation-all` 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 Excon 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, Excon instrumentation code comes in three patch versions: `dup`, `old`, and `stable`. These versions are identical except for the attributes they send. Any changes to Excon 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/).
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ class Instrumentation < OpenTelemetry::Instrumentation::Base
include OpenTelemetry::Instrumentation::Concerns::UntracedHosts

install do |_config|
require_dependencies
add_middleware
patch
patch_type = determine_semconv
send(:"require_dependencies_#{patch_type}")
send(:"add_middleware_#{patch_type}")
send(:"patch_#{patch_type}")
end

present do
Expand All @@ -28,17 +29,56 @@ class Instrumentation < OpenTelemetry::Instrumentation::Base

private

def require_dependencies
require_relative 'middlewares/tracer_middleware'
require_relative 'patches/socket'
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/socket'
end

def require_dependencies_stable
require_relative 'middlewares/stable/tracer_middleware'
require_relative 'patches/stable/socket'
end

def require_dependencies_old
require_relative 'middlewares/old/tracer_middleware'
require_relative 'patches/old/socket'
end

def add_middleware_dup
::Excon.defaults[:middlewares] = Middlewares::Dup::TracerMiddleware.around_default_stack
end

def add_middleware_stable
::Excon.defaults[:middlewares] = Middlewares::Stable::TracerMiddleware.around_default_stack
end

def add_middleware_old
::Excon.defaults[:middlewares] = Middlewares::Old::TracerMiddleware.around_default_stack
end

def patch_dup
::Excon::Socket.prepend(Patches::Dup::Socket)
end

def add_middleware
::Excon.defaults[:middlewares] = Middlewares::TracerMiddleware.around_default_stack
def patch_stable
::Excon::Socket.prepend(Patches::Stable::Socket)
end

def patch
::Excon::Socket.prepend(Patches::Socket)
def patch_old
::Excon::Socket.prepend(Patches::Old::Socket)
end
end
end
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# frozen_string_literal: true

# Copyright The OpenTelemetry Authors
#
# SPDX-License-Identifier: Apache-2.0

module OpenTelemetry
module Instrumentation
module Excon
module Middlewares
module Dup
# Excon middleware for instrumentation
class TracerMiddleware < ::Excon::Middleware::Base
HTTP_METHODS_TO_UPPERCASE = %w[connect delete get head options patch post put trace].each_with_object({}) do |method, hash|
uppercase_method = method.upcase
hash[method] = uppercase_method
hash[method.to_sym] = uppercase_method
hash[uppercase_method] = uppercase_method
end.freeze

HTTP_METHODS_TO_SPAN_NAMES = HTTP_METHODS_TO_UPPERCASE.values.each_with_object({}) do |uppercase_method, hash|
hash[uppercase_method] ||= uppercase_method
end.freeze

# Constant for the HTTP status range
HTTP_STATUS_SUCCESS_RANGE = (100..399)

def request_call(datum)
return @stack.request_call(datum) if untraced?(datum)

http_method = HTTP_METHODS_TO_UPPERCASE[datum[:method]]
cleansed_url = OpenTelemetry::Common::Utilities.cleanse_url(::Excon::Utils.request_uri(datum))
attributes = {
OpenTelemetry::SemanticConventions::Trace::HTTP_HOST => datum[:host],
OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => http_method,
OpenTelemetry::SemanticConventions::Trace::HTTP_SCHEME => datum[:scheme],
OpenTelemetry::SemanticConventions::Trace::HTTP_TARGET => datum[:path],
OpenTelemetry::SemanticConventions::Trace::HTTP_URL => cleansed_url,
OpenTelemetry::SemanticConventions::Trace::NET_PEER_NAME => datum[:hostname],
OpenTelemetry::SemanticConventions::Trace::NET_PEER_PORT => datum[:port],
'http.request.method' => http_method,
'url.scheme' => datum[:scheme],
'url.path' => datum[:path],
'url.full' => cleansed_url,
'server.address' => datum[:hostname],
'server.port' => datum[:port]
}
attributes['url.query'] = datum[:query] if datum[:query]
peer_service = Excon::Instrumentation.instance.config[:peer_service]
attributes[OpenTelemetry::SemanticConventions::Trace::PEER_SERVICE] = peer_service if peer_service
attributes.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes)
span = tracer.start_span(HTTP_METHODS_TO_SPAN_NAMES[http_method], attributes: attributes, kind: :client)
ctx = OpenTelemetry::Trace.context_with_span(span)
datum[:otel_span] = span
datum[:otel_token] = OpenTelemetry::Context.attach(ctx)
OpenTelemetry.propagation.inject(datum[:headers])
@stack.request_call(datum)
end

def response_call(datum)
@stack.response_call(datum).tap do |d|
handle_response(d)
end
end

def error_call(datum)
handle_response(datum)
@stack.error_call(datum)
end

# Returns a copy of the default stack with the trace middleware injected
def self.around_default_stack
::Excon.defaults[:middlewares].dup.tap do |default_stack|
# If the default stack contains a version of the trace middleware already...
existing_trace_middleware = default_stack.find { |m| m <= TracerMiddleware }
default_stack.delete(existing_trace_middleware) if existing_trace_middleware
# Inject after the ResponseParser middleware
response_middleware_index = default_stack.index(::Excon::Middleware::ResponseParser).to_i
default_stack.insert(response_middleware_index + 1, self)
end
end

private

def handle_response(datum)
datum.delete(:otel_span)&.tap do |span|
return unless span.recording?

if datum.key?(:response)
response = datum[:response]
span.set_attribute(OpenTelemetry::SemanticConventions::Trace::HTTP_STATUS_CODE, response[:status])
span.set_attribute('http.response.status_code', response[:status])
span.status = OpenTelemetry::Trace::Status.error unless HTTP_STATUS_SUCCESS_RANGE.cover?(response[:status].to_i)
end

if datum.key?(:error)
span.status = OpenTelemetry::Trace::Status.error('Request has failed')
span.record_exception(datum[:error])
end

span.finish
OpenTelemetry::Context.detach(datum.delete(:otel_token)) if datum.include?(:otel_token)
end
rescue StandardError => e
OpenTelemetry.handle_error(e)
end

def tracer
Excon::Instrumentation.instance.tracer
end

def untraced?(datum)
datum.key?(:otel_span) || Excon::Instrumentation.instance.untraced?(datum[:host])
end
end
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# frozen_string_literal: true

# Copyright The OpenTelemetry Authors
#
# SPDX-License-Identifier: Apache-2.0

module OpenTelemetry
module Instrumentation
module Excon
module Middlewares
module Old
# Excon middleware for instrumentation
class TracerMiddleware < ::Excon::Middleware::Base
HTTP_METHODS_TO_UPPERCASE = %w[connect delete get head options patch post put trace].each_with_object({}) do |method, hash|
uppercase_method = method.upcase
hash[method] = uppercase_method
hash[method.to_sym] = uppercase_method
hash[uppercase_method] = uppercase_method
end.freeze

HTTP_METHODS_TO_SPAN_NAMES = HTTP_METHODS_TO_UPPERCASE.values.each_with_object({}) do |uppercase_method, hash|
hash[uppercase_method] ||= "HTTP #{uppercase_method}"
end.freeze

# Constant for the HTTP status range
HTTP_STATUS_SUCCESS_RANGE = (100..399)

def request_call(datum)
return @stack.request_call(datum) if untraced?(datum)

http_method = HTTP_METHODS_TO_UPPERCASE[datum[:method]]
attributes = {
OpenTelemetry::SemanticConventions::Trace::HTTP_HOST => datum[:host],
OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => http_method,
OpenTelemetry::SemanticConventions::Trace::HTTP_SCHEME => datum[:scheme],
OpenTelemetry::SemanticConventions::Trace::HTTP_TARGET => datum[:path],
OpenTelemetry::SemanticConventions::Trace::HTTP_URL => OpenTelemetry::Common::Utilities.cleanse_url(::Excon::Utils.request_uri(datum)),
OpenTelemetry::SemanticConventions::Trace::NET_PEER_NAME => datum[:hostname],
OpenTelemetry::SemanticConventions::Trace::NET_PEER_PORT => datum[:port]
}
peer_service = Excon::Instrumentation.instance.config[:peer_service]
attributes[OpenTelemetry::SemanticConventions::Trace::PEER_SERVICE] = peer_service if peer_service
attributes.merge!(OpenTelemetry::Common::HTTP::ClientContext.attributes)
span = tracer.start_span(HTTP_METHODS_TO_SPAN_NAMES[http_method], attributes: attributes, kind: :client)
ctx = OpenTelemetry::Trace.context_with_span(span)
datum[:otel_span] = span
datum[:otel_token] = OpenTelemetry::Context.attach(ctx)
OpenTelemetry.propagation.inject(datum[:headers])
@stack.request_call(datum)
end

def response_call(datum)
@stack.response_call(datum).tap do |d|
handle_response(d)
end
end

def error_call(datum)
handle_response(datum)
@stack.error_call(datum)
end

# Returns a copy of the default stack with the trace middleware injected
def self.around_default_stack
::Excon.defaults[:middlewares].dup.tap do |default_stack|
# If the default stack contains a version of the trace middleware already...
existing_trace_middleware = default_stack.find { |m| m <= TracerMiddleware }
default_stack.delete(existing_trace_middleware) if existing_trace_middleware
# Inject after the ResponseParser middleware
response_middleware_index = default_stack.index(::Excon::Middleware::ResponseParser).to_i
default_stack.insert(response_middleware_index + 1, self)
end
end

private

def handle_response(datum)
datum.delete(:otel_span)&.tap do |span|
return unless span.recording?

if datum.key?(:response)
response = datum[:response]
span.set_attribute(OpenTelemetry::SemanticConventions::Trace::HTTP_STATUS_CODE, response[:status])
span.status = OpenTelemetry::Trace::Status.error unless HTTP_STATUS_SUCCESS_RANGE.cover?(response[:status].to_i)
end

if datum.key?(:error)
span.status = OpenTelemetry::Trace::Status.error('Request has failed')
span.record_exception(datum[:error])
end

span.finish
OpenTelemetry::Context.detach(datum.delete(:otel_token)) if datum.include?(:otel_token)
end
rescue StandardError => e
OpenTelemetry.handle_error(e)
end

def tracer
Excon::Instrumentation.instance.tracer
end

def untraced?(datum)
datum.key?(:otel_span) || Excon::Instrumentation.instance.untraced?(datum[:host])
end
end
end
end
end
end
end
Loading
Loading