Skip to content
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

feat: Integration with V3 telemetry provider #1186

Merged
57 changes: 13 additions & 44 deletions instrumentation/aws_sdk/Appraisals
Original file line number Diff line number Diff line change
Expand Up @@ -4,58 +4,27 @@
#
# SPDX-License-Identifier: Apache-2.0

appraise 'aws-sdk-3.1' do
gem 'aws-sdk', '~> 3.1'
appraise 'aws-sdk-3' do
gem 'aws-sdk-core', '~> 3'
gem 'aws-sdk-lambda', '~> 1'
gem 'aws-sdk-dynamodb', '~> 1'
gem 'aws-sdk-sns', '~> 1'
gem 'aws-sdk-sqs', '~> 1'
end
jterapin marked this conversation as resolved.
Show resolved Hide resolved

appraise 'aws-sdk-3.0' do
gem 'aws-sdk', '~> 3.0'
# pre-Observability support in V3 SDK
appraise 'aws-sdk-3.202' do
gem 'aws-sdk-core', '~> 3.202'
gem 'aws-sdk-lambda', '~> 1.127'
gem 'aws-sdk-dynamodb', '~> 1.118'
gem 'aws-sdk-sns', '~> 1.82'
gem 'aws-sdk-sqs', '~> 1.80'
end

appraise 'aws-sdk-2.11' do
gem 'aws-sdk', '~> 2.11'
end

appraise 'aws-sdk-2.10' do
gem 'aws-sdk', '~> 2.10'
end

appraise 'aws-sdk-2.9' do
gem 'aws-sdk', '~> 2.9'
end

appraise 'aws-sdk-2.8' do
gem 'aws-sdk', '~> 2.8'
end

appraise 'aws-sdk-2.7' do
gem 'aws-sdk', '~> 2.7'
end

appraise 'aws-sdk-2.6' do
gem 'aws-sdk', '~> 2.6'
end

appraise 'aws-sdk-2.5' do
gem 'aws-sdk', '~> 2.5'
end

appraise 'aws-sdk-2.4' do
gem 'aws-sdk', '~> 2.4'
end

appraise 'aws-sdk-2.3' do
gem 'aws-sdk', '~> 2.3'
end

appraise 'aws-sdk-2.2' do
gem 'aws-sdk', '~> 2.2'
end

appraise 'aws-sdk-2.1' do
gem 'aws-sdk', '~> 2.1'
end

appraise 'aws-sdk-2.0' do
gem 'aws-sdk', '~> 2.0'
end
24 changes: 24 additions & 0 deletions instrumentation/aws_sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,30 @@ OpenTelemetry::SDK.configure do |c|
c.use_all
end
```
### Configuration options
jterapin marked this conversation as resolved.
Show resolved Hide resolved
This instrumentation offers the following configuration options:
* `:inject_messaging_context` (default: `false`): When set to `true`, adds context key/value
to Message Attributes for SQS/SNS messages.
* `suppress_internal_instrumentation` (default: `false`): When set to `true`, any spans with
span kind of `internal` are suppressed from traces.

## Integration with SDK V3's Telemetry support
AWS SDK for Ruby V3 added support for Observability which includes a new configuration,
`telemetry_provider` and an OpenTelemetry-based telemetry provider. Only applies to
AWS service gems released after 2024-09-03.

Using later versions of these gems will give more details on the internal spans.
See below for example usage:
```ruby
# configures the OpenTelemetry SDK with instrumentation defaults
OpenTelemetry::SDK.configure do |c|
c.use 'OpenTelemetry::Instrumentation::AwsSdk'
end

# create open-telemetry provider and pass to client config
otel_provider = Aws::Telemetry::OTelProvider.new
client = Aws::S3::Client.new(telemetry_provider: otel_provider)
```

## Example

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,23 @@
module OpenTelemetry
module Instrumentation
module AwsSdk
# Generates Spans for all interactions with AwsSdk
# This handler supports specifically supports V2 and V3
# prior to Observability support released on 2024-09-03.
class Handler < Seahorse::Client::Handler
def call(context)
return super unless context

service_id = service_name(context)
jterapin marked this conversation as resolved.
Show resolved Hide resolved
operation = context.operation&.name
client_method = "#{service_id}.#{operation}"
service_id = HandlerHelper.service_id(context, legacy: true)
client_method = HandlerHelper.client_method(service_id, context)

tracer.in_span(
span_name(context, client_method, service_id),
attributes: attributes(context, client_method, service_id, operation),
kind: span_kind(client_method, service_id)
HandlerHelper.span_name(context, client_method, service_id, legacy: true),
attributes: HandlerHelper.span_attributes(context, client_method, service_id, legacy: true),
kind: HandlerHelper.span_kind(client_method, service_id)
) do |span|
if instrumentation_config[:inject_messaging_context] &&
%w[SQS SNS].include?(service_id)
MessagingHelper.inject_context(context, client_method)
end
MessagingHelper.inject_context_if_supported(context, client_method, service_id)

if instrumentation_config[:suppress_internal_instrumentation]
if HandlerHelper.instrumentation_config[:suppress_internal_instrumentation]
OpenTelemetry::Common::Utilities.untraced { super }
else
super
Expand All @@ -49,47 +46,6 @@ def call(context)
def tracer
AwsSdk::Instrumentation.instance.tracer
end

def instrumentation_config
AwsSdk::Instrumentation.instance.config
end

def service_name(context)
# Support aws-sdk v2.0.x, which 'metadata' has a setter method only
return context.client.class.to_s.split('::')[1] if ::Seahorse::Model::Api.instance_method(:metadata).parameters.length.positive?

context.client.class.api.metadata['serviceId'] || context.client.class.to_s.split('::')[1]
end

def span_kind(client_method, service_id)
case service_id
when 'SQS', 'SNS'
MessagingHelper.span_kind(client_method)
else
OpenTelemetry::Trace::SpanKind::CLIENT
end
end

def span_name(context, client_method, service_id)
case service_id
when 'SQS', 'SNS'
MessagingHelper.legacy_span_name(context, client_method)
else
client_method
end
end

def attributes(context, client_method, service_id, operation)
{
'aws.region' => context.config.region,
OpenTelemetry::SemanticConventions::Trace::RPC_SYSTEM => 'aws-api',
OpenTelemetry::SemanticConventions::Trace::RPC_METHOD => operation,
OpenTelemetry::SemanticConventions::Trace::RPC_SERVICE => service_id
}.tap do |attrs|
attrs[SemanticConventions::Trace::DB_SYSTEM] = 'dynamodb' if service_id == 'DynamoDB'
MessagingHelper.apply_span_attributes(context, attrs, client_method, service_id) if %w[SQS SNS].include?(service_id)
end
end
end

# A Seahorse::Client::Plugin that enables instrumentation for all AWS services
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# frozen_string_literal: true

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

module OpenTelemetry
module Instrumentation
module AwsSdk
# Utility module that contains shared methods between AwsSdk and Telemetry handlers
module HandlerHelper
kaylareopelle marked this conversation as resolved.
Show resolved Hide resolved
class << self
def instrumentation_config
AwsSdk::Instrumentation.instance.config
end

def client_method(service_id, context)
"#{service_id}.#{context.operation.name}".delete(' ')
end

def span_attributes(context, client_method, service_id, legacy: false)
{
'aws.region' => context.config.region,
OpenTelemetry::SemanticConventions::Trace::CODE_FUNCTION => context.operation_name.to_s,
OpenTelemetry::SemanticConventions::Trace::CODE_NAMESPACE => 'Aws::Plugins::Telemetry',
OpenTelemetry::SemanticConventions::Trace::RPC_METHOD => context.operation.name,
OpenTelemetry::SemanticConventions::Trace::RPC_SERVICE => service_id,
OpenTelemetry::SemanticConventions::Trace::RPC_SYSTEM => 'aws-api'
}.tap do |attrs|
attrs[OpenTelemetry::SemanticConventions::Trace::CODE_NAMESPACE] = 'Aws::Plugins::AwsSdk' if legacy
attrs[SemanticConventions::Trace::DB_SYSTEM] = 'dynamodb' if service_id == 'DynamoDB'

MessagingHelper.apply_span_attributes(context, attrs, client_method, service_id) if MessagingHelper::SUPPORTED_SERVICES.include?(service_id)
end
end

def span_kind(client_method, service_id)
case service_id
when *MessagingHelper::SUPPORTED_SERVICES
MessagingHelper.span_kind(client_method)
else
OpenTelemetry::Trace::SpanKind::CLIENT
end
end

def span_name(context, client_method, service_id, legacy: false)
case service_id
when *MessagingHelper::SUPPORTED_SERVICES
if legacy
MessagingHelper.legacy_span_name(context, client_method)
else
MessagingHelper.span_name(context, client_method)
end
else
client_method
end
end

def service_id(context, legacy: false)
if legacy
legacy_service_id(context)
else
context.config.api.metadata['serviceId'] ||
context.config.api.metadata['serviceAbbreviation'] ||
context.config.api.metadata['serviceFullName']
end
end

private

def legacy_service_id(context)
# Support aws-sdk v2.0.x, which 'metadata' has a setter method only
return context.client.class.to_s.split('::')[1] if ::Seahorse::Model::Api.instance_method(:metadata).parameters.length.positive?

context.client.class.api.metadata['serviceId'] || context.client.class.to_s.split('::')[1]
end
end
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,38 @@
module OpenTelemetry
module Instrumentation
module AwsSdk
# Instrumentation class that detects and installs the AwsSdk instrumentation
# The `OpenTelemetry::Instrumentation::AwsSdk::Instrumentation` class contains
# logic to detect and install the AwsSdk instrumentation.
#
# ## Configuration keys and options
#
# ### `:inject_messaging_context`
#
# Allows adding of context key/value to Message Attributes for SQS/SNS messages.
#
# - `false` **(default)** - Context key/value will not be added.
# - `true` - Context key/value will be added.
#
# ### `:suppress_internal_instrumentation`
#
# Disables tracing of spans of `internal` span kind.
#
# - `false` **(default)** - Internal spans are traced.
# - `true` - Internal spans are not traced.
#
# @example An explicit default configuration
# OpenTelemetry::SDK.configure do |c|
# c.use 'OpenTelemetry::Instrumentation::AwsSdk', {
# inject_messaging_context: false,
# suppress_internal_instrumentation: false
# }
# end
class Instrumentation < OpenTelemetry::Instrumentation::Base
MINIMUM_VERSION = Gem::Version.new('2.0.0')

install do |_config|
require_dependencies
patch_telemetry_plugin if telemetry_plugin?
add_plugins(Seahorse::Client::Base, *loaded_service_clients)
end

Expand Down Expand Up @@ -41,12 +67,34 @@ def gem_version

def require_dependencies
require_relative 'handler'
require_relative 'handler_helper'
require_relative 'message_attributes'
require_relative 'messaging_helper'
require_relative 'patches/telemetry'
end

def add_plugins(*targets)
targets.each { |klass| klass.add_plugin(AwsSdk::Plugin) }
targets.each do |klass|
next if supports_telemetry_plugin?(klass)

klass.add_plugin(AwsSdk::Plugin)
end
end

def supports_telemetry_plugin?(klass)
telemetry_plugin? &&
klass.plugins.include?(Aws::Plugins::Telemetry)
end

def telemetry_plugin?
::Aws::Plugins.const_defined?(:Telemetry)
end

# Patches AWS SDK V3's telemetry plugin for integration
# This patch supports configuration set by this gem and
# additional span attributes that was not provided by the plugin
def patch_telemetry_plugin
::Aws::Plugins::Telemetry::Handler.prepend(Patches::Handler)
end

def loaded_service_clients
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,18 @@ module Instrumentation
module AwsSdk
# An utility class to help SQS/SNS-related span attributes/context injection
class MessagingHelper
SUPPORTED_SERVICES = %w[SQS SNS].freeze
class << self
SQS_SEND_MESSAGE = 'SQS.SendMessage'
SQS_SEND_MESSAGE_BATCH = 'SQS.SendMessageBatch'
SQS_RECEIVE_MESSAGE = 'SQS.ReceiveMessage'
SNS_PUBLISH = 'SNS.Publish'
SEND_MESSAGE_CLIENT_METHODS = [SQS_SEND_MESSAGE, SQS_SEND_MESSAGE_BATCH, SNS_PUBLISH].freeze

def supported_services
SUPPORTED_SERVICES
end
jterapin marked this conversation as resolved.
Show resolved Hide resolved

def queue_name(context)
topic_arn = context.params[:topic_arn]
target_arn = context.params[:target_arn]
Expand All @@ -34,6 +39,17 @@ def queue_name(context)
'unknown'
end

def span_name(context, client_method)
case client_method
when SQS_SEND_MESSAGE, SQS_SEND_MESSAGE_BATCH, SNS_PUBLISH
"#{client_method}.#{queue_name(context)}.Publish"
when SQS_RECEIVE_MESSAGE
"#{client_method}.#{queue_name(context)}.Receive"
else
client_method
end
end

def legacy_span_name(context, client_method)
case client_method
when SQS_SEND_MESSAGE, SQS_SEND_MESSAGE_BATCH, SNS_PUBLISH
Expand Down Expand Up @@ -65,6 +81,13 @@ def span_kind(client_method)
end
end

def inject_context_if_supported(context, client_method, service_id)
if HandlerHelper.instrumentation_config[:inject_messaging_context] &&
SUPPORTED_SERVICES.include?(service_id)
inject_context(context, client_method)
end
end

def inject_context(context, client_method)
return unless SEND_MESSAGE_CLIENT_METHODS.include?(client_method)

Expand Down
Loading
Loading