Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions sentry_sdk/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,18 @@ class SPANDATA:
Example: ["Token limit exceeded"]
"""

AWS_EXTENDED_REQUEST_ID = "aws.extended_request_id"
"""
The AWS extended request ID as returned in the response headers.
Example: "wzHcyEWfmOGDIE5QOhTAqFDoDWP3y8IUvpNINCwL9N4TEHbUw0/gZJ+VZTmCNCWR7fezEN3eCiQ="
"""

AWS_REQUEST_ID = "aws.request_id"
"""
The AWS request ID as returned in the response headers.
Example: "79b9da39-b7ae-508a-a6bc-864b2829c622"
"""

CACHE_HIT = "cache.hit"
"""
A boolean indicating whether the requested data was found in the cache.
Expand Down Expand Up @@ -547,6 +559,12 @@ class SPANDATA:
Example: my_user
"""

ERROR_TYPE = "error.type"
"""
Describes a class of error the operation ended with.
Example: "timeout"
"""

GEN_AI_AGENT_NAME = "gen_ai.agent.name"
"""
The name of the agent being used.
Expand Down Expand Up @@ -886,6 +904,12 @@ class SPANDATA:
Example: GET
"""

HTTP_REQUEST_RESEND_COUNT = "http.request.resend_count"
"""
The ordinal number of request resending attempt (for any reason, including redirects).
Example: 2
"""

HTTP_ROUTE = "http.route"
"""
The matched route, that is, the path template used to match the request.
Expand Down
13 changes: 12 additions & 1 deletion sentry_sdk/integrations/boto3/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@
from sentry_sdk.integrations.boto3._context import AwsCallContext
from sentry_sdk.integrations.boto3._instrumentation import (
_finish_span,
_get_error_attributes,
_get_response_attributes,
_instrument_streaming_body,
_sentry_before_sign,
_sentry_request_created,
_set_span_attributes,
_start_client_span,
)
from sentry_sdk.traces import NoOpStreamedSpan, StreamedSpan
Expand Down Expand Up @@ -87,7 +90,15 @@ def sentry_patched_make_api_call(

try:
with span_ctx:
parsed = orig_make_api_call(self, operation_name, api_params)
try:
parsed = orig_make_api_call(self, operation_name, api_params)
except BaseException as error:
with capture_internal_exceptions():
_set_span_attributes(span, _get_error_attributes(error))
raise
else:
with capture_internal_exceptions():
_set_span_attributes(span, _get_response_attributes(parsed))
except BaseException as error:
# finish `StreamedSpan` explicitly; static spans are finished by
# their context manager.
Expand Down
102 changes: 102 additions & 0 deletions sentry_sdk/integrations/boto3/_instrumentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

try:
from botocore.awsrequest import AWSRequest
from botocore.exceptions import ClientError
from botocore.response import StreamingBody
except ImportError:
raise DidNotEnable("botocore not installed")
Expand All @@ -38,6 +39,7 @@
def _set_span_attributes(
span: "Union[Span, StreamedSpan]", attributes: "Attributes"
) -> None:
"""Will be removed in the major."""
if isinstance(span, StreamedSpan):
span.set_attributes(attributes)
return
Expand Down Expand Up @@ -89,6 +91,99 @@ def _get_client_attributes(
return attributes


def _get_response_attributes(response: "Any") -> "Attributes":
if not isinstance(response, dict):
return {}

metadata = response.get("ResponseMetadata")
if not isinstance(metadata, dict):
return {}
attributes: "Attributes" = {}

# botocore injects HTTP status into `ResponseMetadata` after parsing.
# https://github.com/boto/botocore/blob/develop/botocore/parsers.py#L273-L284
status_code = metadata.get("HTTPStatusCode")
if isinstance(status_code, int) and 100 <= status_code <= 599:
attributes[SPANDATA.HTTP_STATUS_CODE] = status_code

retry_attempts = metadata.get("RetryAttempts")
# botocore represents retries as `attempts - 1`; OTel suggests "if and only if", so skip zero.
# https://github.com/boto/botocore/blob/develop/botocore/endpoint.py#L221-L229
# https://opentelemetry.io/docs/specs/semconv/http/http-spans/#http-client-span
if (
isinstance(retry_attempts, int)
# avoid emitting `resend_count=True`.
and not isinstance(retry_attempts, bool)
and retry_attempts > 0
):
attributes[SPANDATA.HTTP_REQUEST_RESEND_COUNT] = retry_attempts

headers = metadata.get("HTTPHeaders")
if not isinstance(headers, dict):
headers = {}

request_id = next(
(
value
for value in (
metadata.get("RequestId"),
headers.get("x-amzn-requestid"),
headers.get("x-amzn-request-id"),
headers.get("x-amz-request-id"),
)
if isinstance(value, str) and value
),
None,
)
if request_id is not None:
attributes[SPANDATA.AWS_REQUEST_ID] = request_id

# S3's `HostId` is the extended request ID returned in `x-amz-id-2`.
# https://docs.aws.amazon.com/AmazonS3/latest/developerguide/get-request-ids.html
extended_request_id = next(
(
value
for value in (metadata.get("HostId"), headers.get("x-amz-id-2"))
if isinstance(value, str) and value
),
None,
)
if extended_request_id is not None:
attributes[SPANDATA.AWS_EXTENDED_REQUEST_ID] = extended_request_id

return attributes


def _get_error_type(exception: "BaseException") -> str:
if isinstance(exception, ClientError):
# `ClientError` wraps AWS service errors; `Error.Code` identifies the
# actual service error, e.g. `AccessDeniedException`.
# https://docs.aws.amazon.com/boto3/latest/guide/error-handling.html
error = exception.response.get("Error")
if isinstance(error, dict):
error_code = error.get("Code")
if isinstance(error_code, str) and error_code:
return error_code

# failures before a service response have no AWS error code.
# https://opentelemetry.io/docs/specs/semconv/rpc/rpc-spans/
exception_type = type(exception)
exception_name = exception_type.__qualname__
exception_module = exception_type.__module__
if exception_module not in ("builtins", "__builtins__"):
return "%s.%s" % (exception_module, exception_name)
return exception_name


def _get_error_attributes(exception: "BaseException") -> "Attributes":
attributes: "Attributes" = {}
if isinstance(exception, ClientError):
attributes.update(_get_response_attributes(exception.response))

attributes[SPANDATA.ERROR_TYPE] = _get_error_type(exception)
return attributes


def _start_client_span(
ctx: "AwsCallContext",
) -> "Optional[Union[Span, StreamedSpan]]":
Expand Down Expand Up @@ -200,6 +295,13 @@ def finish(error: "Optional[BaseException]" = None) -> None:
return

finished = True
if error is not None:
with capture_internal_exceptions():
attributes = _get_error_attributes(error)
_set_span_attributes(streaming_span, attributes)
if isinstance(span, StreamedSpan):
_set_span_attributes(span, attributes)

_finish_span(streaming_span, error)
if isinstance(span, StreamedSpan):
_finish_span(span, error)
Expand Down
Loading
Loading