Skip to content

Commit

Permalink
Observability Support (#3092)
Browse files Browse the repository at this point in the history
  • Loading branch information
jterapin authored Sep 3, 2024
1 parent a7bfec2 commit dafe8c5
Show file tree
Hide file tree
Showing 30 changed files with 1,462 additions and 25 deletions.
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ group :test do
gem 'addressable'
gem 'cucumber'
gem 'webmock'

gem 'multipart-post'
gem 'rspec'
gem 'opentelemetry-sdk'
end
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ def default_plugins
'Aws::Plugins::ChecksumAlgorithm' => "#{core_plugins}/checksum_algorithm.rb",
'Aws::Plugins::RequestCompression' => "#{core_plugins}/request_compression.rb",
'Aws::Plugins::DefaultsMode' => "#{core_plugins}/defaults_mode.rb",
'Aws::Plugins::RecursionDetection' => "#{core_plugins}/recursion_detection.rb"
'Aws::Plugins::RecursionDetection' => "#{core_plugins}/recursion_detection.rb",
'Aws::Plugins::Telemetry' => "#{core_plugins}/telemetry.rb"
}
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,18 @@ module {{module_name}}
# @api private
def build_request(operation_name, params = {})
handlers = @handlers.for(operation_name)
tracer = config.telemetry_provider.tracer_provider.tracer(
Aws::Telemetry.module_to_tracer_name('{{module_name}}')
)
context = Seahorse::Client::RequestContext.new(
operation_name: operation_name,
operation: config.api.operation(operation_name),
client: self,
params: params,
http_response: Seahorse::Client::Http::AsyncResponse.new,
config: config)
config: config,
tracer: tracer
)
context[:gem_name] = '{{gem_name}}'
context[:gem_version] = '{{gem_version}}'
Seahorse::Client::Request.new(handlers, context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,18 @@ module {{module_name}}
end
end
{{/authorizer?}}
tracer = config.telemetry_provider.tracer_provider.tracer(
Aws::Telemetry.module_to_tracer_name('{{module_name}}')
)
context = Seahorse::Client::RequestContext.new(
operation_name: operation_name,
operation: config.api.operation(operation_name),{{#authorizer?}}
authorizer: authorizer,{{/authorizer?}}
client: self,
params: params,
config: config)
config: config,
tracer: tracer
)
context[:gem_name] = '{{gem_name}}'
context[:gem_version] = '{{gem_version}}'
Seahorse::Client::Request.new(handlers, context)
Expand Down
5 changes: 5 additions & 0 deletions build_tools/customizations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -107,13 +107,16 @@ def dynamodb_example_deep_transform(subsegment, keys)
end

api('ImportExport') do |api|
api['metadata']['serviceId'] ||= 'importexport'

api['operations'].each do |_, operation|
operation['http']['requestUri'] = '/'
end
end

%w(Lambda LambdaPreview).each do |svc_name|
api(svc_name) do |api|
api['metadata']['serviceId'] ||= 'Lambda Preview' if svc_name == 'LambdaPreview'
api['shapes']['Timestamp']['type'] = 'timestamp'
end

Expand Down Expand Up @@ -220,6 +223,8 @@ def dynamodb_example_deep_transform(subsegment, keys)
# uses both flattened and locationName. Query protocol is supposed to
# ignore location name (xmlName) when flattened (xmlFlattened) is used.
api('SimpleDB') do |api|
api['metadata']['serviceId'] ||= 'SimpleDB'

api['shapes'].each do |_, shape|
next unless shape['type'] == 'structure'

Expand Down
4 changes: 2 additions & 2 deletions build_tools/services.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ class ServiceEnumerator
MANIFEST_PATH = File.expand_path('../../services.json', __FILE__)

# Minimum `aws-sdk-core` version for new gem builds
MINIMUM_CORE_VERSION = "3.201.0"
MINIMUM_CORE_VERSION = "3.203.0"

# Minimum `aws-sdk-core` version for new S3 gem builds
MINIMUM_CORE_VERSION_S3 = "3.201.0"
MINIMUM_CORE_VERSION_S3 = "3.203.0"

EVENTSTREAM_PLUGIN = "Aws::Plugins::EventStreamConfiguration"

Expand Down
2 changes: 2 additions & 0 deletions gems/aws-sdk-core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
Unreleased Changes
------------------

* Feature - Add support for Observability which includes a configuration, `telemetry_provider` and an OpenTelemetry-based telemetry provider.

3.202.2 (2024-08-30)
------------------

Expand Down
14 changes: 2 additions & 12 deletions gems/aws-sdk-core/lib/aws-sdk-core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
require_relative 'aws-sdk-core/deprecations'

# credential providers

require_relative 'aws-sdk-core/credential_provider'
require_relative 'aws-sdk-core/refreshing_credentials'
require_relative 'aws-sdk-core/assume_role_credentials'
Expand All @@ -30,7 +29,6 @@
require_relative 'aws-sdk-core/plugins/bearer_authorization'

# client modules

require_relative 'aws-sdk-core/client_stubs'
require_relative 'aws-sdk-core/async_client_stubs'
require_relative 'aws-sdk-core/eager_loader'
Expand All @@ -45,24 +43,20 @@
require_relative 'aws-sdk-core/util'

# resource classes

require_relative 'aws-sdk-core/resources/collection'

# logging

require_relative 'aws-sdk-core/log/formatter'
require_relative 'aws-sdk-core/log/param_filter'
require_relative 'aws-sdk-core/log/param_formatter'

# stubbing

require_relative 'aws-sdk-core/stubbing/empty_stub'
require_relative 'aws-sdk-core/stubbing/data_applicator'
require_relative 'aws-sdk-core/stubbing/stub_data'
require_relative 'aws-sdk-core/stubbing/xml_error'

# stubbing protocols

require_relative 'aws-sdk-core/stubbing/protocols/json'
require_relative 'aws-sdk-core/stubbing/protocols/rest'
require_relative 'aws-sdk-core/stubbing/protocols/rest_json'
Expand All @@ -73,7 +67,6 @@
require_relative 'aws-sdk-core/stubbing/protocols/api_gateway'

# protocols

require_relative 'aws-sdk-core/error_handler'
require_relative 'aws-sdk-core/rest'
require_relative 'aws-sdk-core/xml'
Expand All @@ -82,21 +75,18 @@
require_relative 'aws-sdk-core/rpc_v2'

# event stream

require_relative 'aws-sdk-core/binary'
require_relative 'aws-sdk-core/event_emitter'

# endpoint discovery

require_relative 'aws-sdk-core/endpoint_cache'

# client metrics

# client metrics / telemetry
require_relative 'aws-sdk-core/client_side_monitoring/request_metrics'
require_relative 'aws-sdk-core/client_side_monitoring/publisher'
require_relative 'aws-sdk-core/telemetry'

# utilities

require_relative 'aws-sdk-core/arn'
require_relative 'aws-sdk-core/arn_parser'
require_relative 'aws-sdk-core/ec2_metadata'
Expand Down
31 changes: 29 additions & 2 deletions gems/aws-sdk-core/lib/aws-sdk-core/plugins/stub_responses.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ def after_initialize(client)
class Handler < Seahorse::Client::Handler

def call(context)
span_wrapper(context) do
stub_responses(context)
end
end

private

def stub_responses(context)
stub = context.client.next_stub(context)
resp = Seahorse::Client::Response.new(context: context)
async_mode = context.client.is_a? Seahorse::Client::AsyncBase
Expand All @@ -58,8 +66,15 @@ def call(context)
apply_stub(stub, resp, async_mode)
end

async_mode ? Seahorse::Client::AsyncResponse.new(
context: context, stream: context[:input_event_stream_handler].event_emitter.stream, sync_queue: Queue.new) : resp
if async_mode
Seahorse::Client::AsyncResponse.new(
context: context,
stream: context[:input_event_stream_handler].event_emitter.stream,
sync_queue: Queue.new
)
else
resp
end
end

def apply_stub(stub, response, async_mode = false)
Expand Down Expand Up @@ -99,6 +114,18 @@ def signal_http(stub, http_resp, async_mode = false)
http_resp.signal_done
end

def span_wrapper(context, &block)
context.tracer.in_span(
'Handler.StubResponses',
attributes: Aws::Telemetry.http_request_attrs(context)
) do |span|
block.call.tap do
span.add_attributes(
Aws::Telemetry.http_response_attrs(context)
)
end
end
end
end
end
end
Expand Down
75 changes: 75 additions & 0 deletions gems/aws-sdk-core/lib/aws-sdk-core/plugins/telemetry.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# frozen_string_literal: true

module Aws
module Plugins
# @api private
class Telemetry < Seahorse::Client::Plugin
option(
:telemetry_provider,
default: Aws::Telemetry::NoOpTelemetryProvider,
doc_type: Aws::Telemetry::TelemetryProviderBase,
rbs_type: Aws::Telemetry::TelemetryProviderBase,
docstring: <<-DOCS) do |_cfg|
Allows you to provide a telemetry provider, which is used to
emit telemetry data. By default, uses `NoOpTelemetryProvider` which
will not record or emit any telemetry data. The SDK supports the
following telemetry providers:
* OpenTelemetry (OTel) - To use the OTel provider, install and require the
`opentelemetry-sdk` gem and then, pass in an instance of a
`Aws::Telemetry::OTelProvider` for telemetry provider.
DOCS
Aws::Telemetry::NoOpTelemetryProvider.new
end

def after_initialize(client)
validate_telemetry_provider(client.config)
end

def validate_telemetry_provider(config)
unless config.telemetry_provider.is_a?(Aws::Telemetry::TelemetryProviderBase)
raise ArgumentError,
'Must provide a telemetry provider for the '\
'`telemetry_provider` configuration option.'
end
end

class Handler < Seahorse::Client::Handler
def call(context)
span_wrapper(context) { @handler.call(context) }
end

private

def span_wrapper(context, &block)
service_id = service_id(context)
attributes = {
'rpc.system' => 'aws-api',
'rpc.service' => service_id,
'rpc.method' => context.operation.name,
'code.function' => context.operation_name.to_s,
'code.namespace' => 'Aws::Plugins::Telemetry'
}
context.tracer.in_span(
parent_span_name(context, service_id),
attributes: attributes,
kind: Aws::Telemetry::SpanKind::CLIENT,
&block
)
end

def service_id(context)
context.config.api.metadata['serviceId'] ||
context.config.api.metadata['serviceAbbreviation'] ||
context.config.api.metadata['serviceFullName']
end

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

handler(Handler, step: :initialize, priority: 99)
end
end
end
78 changes: 78 additions & 0 deletions gems/aws-sdk-core/lib/aws-sdk-core/telemetry.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# frozen_string_literal: true

require_relative 'telemetry/base'
require_relative 'telemetry/no_op'
require_relative 'telemetry/otel'
require_relative 'telemetry/span_kind'
require_relative 'telemetry/span_status'

module Aws
# Observability is the extent to which a system's current state can be
# inferred from the data it emits. The data emitted is commonly referred
# as Telemetry. The AWS SDK for Ruby currently supports traces as
# a telemetry signal.
#
# A telemetry provider is used to emit telemetry data. By default, the
# {NoOpTelemetryProvider} will not record or emit any telemetry data.
# The SDK currently supports OpenTelemetry (OTel) as a provider. See
# {OTelProvider} for more information.
#
# If a provider isn't supported, you can implement your own provider by
# inheriting the following base classes and implementing the interfaces
# defined:
# * {TelemetryProviderBase}
# * {ContextManagerBase}
# * {TracerProviderBase}
# * {TracerBase}
# * {SpanBase}
module Telemetry
class << self
# @api private
def module_to_tracer_name(module_name)
"#{module_name.gsub('::', '.')}.client".downcase
end

# @api private
def http_request_attrs(context)
{
'http.method' => context.http_request.http_method,
'net.protocol.name' => 'http'
}.tap do |h|
h['net.protocol.version'] =
if context.client.is_a? Seahorse::Client::AsyncBase
'2'
else
Net::HTTP::HTTPVersion
end

unless context.config.stub_responses
h['net.peer.name'] = context.http_request.endpoint.host
h['net.peer.port'] = context.http_request.endpoint.port.to_s
end

if context.http_request.headers.key?('Content-Length')
h['http.request_content_length'] =
context.http_request.headers['Content-Length']
end
end
end

# @api private
def http_response_attrs(context)
{
'http.status_code' => context.http_response.status_code.to_s
}.tap do |h|
if context.http_response.headers.key?('Content-Length')
h['http.response_content_length'] =
context.http_response.headers['Content-Length']
end

if context.http_response.headers.key?('x-amz-request-id')
h['aws.request_id'] =
context.http_response.headers['x-amz-request-id']
end
end
end
end
end
end
Loading

0 comments on commit dafe8c5

Please sign in to comment.