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
# 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
# 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
# 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
# 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
# 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
# File lib/honeycomb/integrations/aws.rb, line 187 def handle_redirect(span, context) span.add_field "aws.region", context[:redirect_region] end
# 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
# File lib/honeycomb/integrations/aws.rb, line 201 def handle_response(context) on_headers(context) on_error(context) on_done(context) end
# 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
# 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
# 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
# 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
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
# 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
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.
# 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