class Honeycomb::Aws::ApiHandler

An AWS plugin handler that creates spans around API calls.

Each aws-sdk client provides a one-to-one mapping of methods to API operations. However, this doesn't mean that each method results in only one HTTP request to Amazon's servers. There may be request errors, retries, redirects, etc. So whereas {SdkHandler} wraps the logical operation in a span, {ApiHandler} wraps the individual API requests in separate child spans.

{Plugin} accomplishes this by adding {ApiHandler} as close to sending as possible, before the client retries requests, follows redirects, or even parses out response errors. That way, a new span is created for every literal HTTP request. But it also means we have to take care to propagate error information to the span correctly, since the stock AWS error handlers are upstream from this one.

@see github.com/aws/aws-sdk-ruby/blob/767a96db5cb98424a78249dca3f0be802148372e/gems/aws-sdk-s3/lib/aws-sdk-s3/plugins/s3_signer.rb#L36 @see github.com/aws/aws-sdk-ruby/blob/767a96db5cb98424a78249dca3f0be802148372e/gems/aws-sdk-core/lib/aws-sdk-core/plugins/client_metrics_send_plugin.rb#L9-L11 @see github.com/aws/aws-sdk-ruby/blob/97b28ccf18558fc908fd56f52741cf3329de9869/gems/aws-sdk-core/lib/seahorse/client/plugins/raise_response_errors.rb

Public Instance Methods

call(context) click to toggle source
# File lib/honeycomb/integrations/aws.rb, line 153
def call(context)
  context.config.honeycomb_client.start_span(name: "aws-api") do |span|
    instrument(span, context)
    @handler.call(context)
  end
end

Private Instance Methods

add_api_error_fields(span, error) click to toggle source
# File lib/honeycomb/integrations/aws.rb, line 241
def add_api_error_fields(span, error)
  span.add_field "response.error", error.code
  span.add_field "response.error_detail", error.message
end
add_aws_api_fields(span, context) click to toggle source
# File lib/honeycomb/integrations/aws.rb, line 174
def add_aws_api_fields(span, context)
  span.add context[:honeycomb_aws_sdk_data]
  span.add_field "aws.attempt", context.retries + 1
  add_credentials(span, context) if context.config.credentials
  handle_redirect(span, context) if context[:redirect_region]
end
add_credentials(span, context) click to toggle source
# File lib/honeycomb/integrations/aws.rb, line 181
def add_credentials(span, context)
  credentials = context.config.credentials.credentials
  span.add_field "aws.access_key_id", credentials.access_key_id
  span.add_field "aws.session_token", credentials.session_token
end
add_request_fields(span, context) click to toggle source
# File lib/honeycomb/integrations/aws.rb, line 191
def add_request_fields(span, context)
  request = context.http_request
  span.add_field "request.method", request.http_method
  span.add_field "request.scheme", request.endpoint.scheme
  span.add_field "request.host", request.endpoint.host
  span.add_field "request.path", request.endpoint.path
  span.add_field "request.query", request.endpoint.query
  span.add_field "request.user_agent", request.headers["user-agent"]
end
handle_redirect(span, context) click to toggle source
# File lib/honeycomb/integrations/aws.rb, line 187
def handle_redirect(span, context)
  span.add_field "aws.region", context[:redirect_region]
end
handle_request(context) click to toggle source
# File lib/honeycomb/integrations/aws.rb, line 168
def handle_request(context)
  span = context[:honeycomb_aws_api_span]
  add_aws_api_fields(span, context)
  add_request_fields(span, context)
end
handle_response(context) click to toggle source
# File lib/honeycomb/integrations/aws.rb, line 201
def handle_response(context)
  on_headers(context)
  on_error(context)
  on_done(context)
end
instrument(span, context) click to toggle source
# File lib/honeycomb/integrations/aws.rb, line 162
def instrument(span, context)
  context[:honeycomb_aws_api_span] = span
  handle_request(context)
  handle_response(context)
end
on_done(context) click to toggle source
# File lib/honeycomb/integrations/aws.rb, line 228
def on_done(context)
  context.http_response.on_done(300..599) do
    process_api_error(context)
    process_s3_region(context)
  end
end
on_error(context) click to toggle source
# File lib/honeycomb/integrations/aws.rb, line 220
def on_error(context)
  context.http_response.on_error do |error|
    span = context[:honeycomb_aws_api_span]
    span.add_field "response.error", error.class.name
    span.add_field "response.error_detail", error.message
  end
end
on_headers(context) click to toggle source
# File lib/honeycomb/integrations/aws.rb, line 207
def on_headers(context)
  context.http_response.on_headers do |status_code, headers|
    span = context[:honeycomb_aws_api_span]
    span.add_field "response.status_code", status_code
    headers.each do |header, value|
      if header.start_with?("x-amz-", "x-amzn-")
        field = "response.#{header.tr('-', '_')}"
        span.add_field(field, value)
      end
    end
  end
end
parse_api_error_from(context) click to toggle source

Runs a limited subset of response parsing for AWS-specific errors.

Because XML/JSON error handlers are inserted at priority 50 of the :sign step, they're upstream from {ApiHandler} (at priority 39), so we won't have access to the error saved in Seahorse::Client::Response yet. We only have the error in the Seahorse::Client::Http::Response object. But Seahorse::Client::NetHttp::Handler only triggers an HTTP response error for rescued exceptions (e.g., timeouts). We might still get back successful HTTP 3xx, 4xx, or 5xx responses that should be interpreted as aws-api errors.

So we have to duplicate the logic of either Aws::Xml::ErrorHandler or Aws::Json::ErrorHandler depending on which one is being used by the current client. We can determine this by their “protocol” metadata.

Note that there are still a few straggling errors that might occur from HTTP 2xx responses. Since those aren't really API call failures, we won't worry about parsing them out for the aws-api span. Once the upstream handlers process those errors, they'll be propagated to the aws-sdk span anyway (since {SdkHandler} will actually have access to the Seahorse::Client::Response#error).

@see github.com/aws/aws-sdk-ruby/blob/d0c5f6e5a3e83eeda2d1c81f5dd80e5ac562a6dc/gems/aws-sdk-core/lib/aws-sdk-core/client_stubs.rb#L298-L307 @see github.com/aws/aws-sdk-ruby/tree/b0ade445ce18b24c53a4548074b214e732b8b627/gems/aws-sdk-core/lib/aws-sdk-core/plugins/protocols @see github.com/aws/aws-sdk-ruby/blob/354d36792e47f2e81b4889f322928e848e062818/gems/aws-sdk-s3/lib/aws-sdk-s3/plugins/http_200_errors.rb @see github.com/aws/aws-sdk-ruby/blob/354d36792e47f2e81b4889f322928e848e062818/gems/aws-sdk-dynamodb/lib/aws-sdk-dynamodb/plugins/crc32_validation.rb

# File lib/honeycomb/integrations/aws.rb, line 272
def parse_api_error_from(context)
  case context.config.api.metadata["protocol"]
  when "query", "rest-xml", "ec2"
    XmlError.new(context)
  when "json", "rest-json"
    JsonError.new(context)
  end
end
process_api_error(context) click to toggle source
# File lib/honeycomb/integrations/aws.rb, line 235
def process_api_error(context)
  span = context[:honeycomb_aws_api_span]
  error = parse_api_error_from(context)
  add_api_error_fields(span, error) if error
end
process_s3_region(context) click to toggle source

Propagates S3 region redirect information to the next aws-api span.

When the AWS S3 client is configured with the wrong region, Amazon responds to API requests with an HTTP 400 indicating the correct region for the bucket.

This error is normally caught upstream by stock plugins that trigger a new API request with the right region, which will create another aws-api span after this one. However, since the aws.region field set by {#add_aws_api_fields} comes from the aws-sdk configuration, its value would continue being wrong in the next aws-api span. Instead, we want this span to have the wrong region (that triggered the error) and the next span to have the right region (which won't come from the config).

To update aws.region to the right value, {#handle_redirect} looks for a value stashed in the Seahorse::Client::Context#metadata. This is set by aws-sdk v3 (via the aws-sdk-s3 gem) but not by aws-sdk v2. So, we have to duplicate some of the upstream v3 logic in order to propagate the redirected region in the v2 case. We only do this in the v2 case in the hopes that eventually we don't have to maintain the duplicated logic.

@see github.com/aws/aws-sdk-ruby/blob/379d338406873b0f4b53f118c83fe40761e297ab/gems/aws-sdk-s3/lib/aws-sdk-s3/plugins/s3_signer.rb#L151

# File lib/honeycomb/integrations/aws.rb, line 331
def process_s3_region(context)
  return unless SDK_VERSION.start_with?("2.")

  redirect = S3Redirect.new(context)
  context[:redirect_region] = redirect.region if redirect.happening?
end