Skip to content

Commit

Permalink
feat: Integration with V3 telemetry provider (#1186)
Browse files Browse the repository at this point in the history
* feat: Integration with V3 telemetry provider

* Fix CI failure

* Fix CI failure - attempt 2

* Fix CI failure - attempt 3

* Fix CI failure - attempt 4

* Add minor cleanup

* Address feedbacks

* Add handler helper to contain shared methods

* Add config documentation

* Add minor update

* Update based on feedbacks

---------

Co-authored-by: Kayla Reopelle <[email protected]>
  • Loading branch information
jterapin and kaylareopelle authored Oct 7, 2024
1 parent 745c333 commit a71d905
Show file tree
Hide file tree
Showing 12 changed files with 853 additions and 315 deletions.
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

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
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)
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
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

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

0 comments on commit a71d905

Please sign in to comment.