Skip to content

Commit ce222ed

Browse files
committed
feat(boto3): add error, retry and response attribute enrichment
1 parent aca29ac commit ce222ed

1 file changed

Lines changed: 115 additions & 7 deletions

File tree

‎sentry_sdk/integrations/boto3/_instrumentation.py‎

Lines changed: 115 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from urllib.parse import urlsplit
33

44
from botocore.awsrequest import AWSRequest
5+
from botocore.exceptions import ClientError
56
from botocore.response import StreamingBody
67

78
import sentry_sdk
@@ -33,6 +34,7 @@
3334
def _set_span_attributes(
3435
span: "Union[Span, StreamedSpan]", attributes: "Attributes"
3536
) -> None:
37+
"""Will be removed in the major."""
3638
if isinstance(span, StreamedSpan):
3739
span.set_attributes(attributes)
3840
return
@@ -85,6 +87,95 @@ def _get_client_attributes(
8587
return attributes
8688

8789

90+
def _get_response_attributes(response: "Any") -> "Attributes":
91+
if not isinstance(response, dict):
92+
return {}
93+
94+
metadata = response.get("ResponseMetadata")
95+
if not isinstance(metadata, dict):
96+
return {}
97+
attributes: "Attributes" = {}
98+
99+
# botocore injects HTTP status into `ResponseMetadata` after parsing.
100+
# https://github.com/boto/botocore/blob/develop/botocore/parsers.py#L273-L284
101+
status_code = metadata.get("HTTPStatusCode")
102+
if isinstance(status_code, int) and 100 <= status_code <= 599:
103+
attributes[SPANDATA.HTTP_STATUS_CODE] = status_code
104+
105+
retry_attempts = metadata.get("RetryAttempts")
106+
# botocore represents retries as `attempts - 1`; OTel suggests "if and only if", so skip zero.
107+
# https://github.com/boto/botocore/blob/develop/botocore/endpoint.py#L221-L229
108+
# https://opentelemetry.io/docs/specs/semconv/http/http-spans/#http-client-span
109+
if (
110+
isinstance(retry_attempts, int)
111+
# avoid emitting `resend_count=True`.
112+
and not isinstance(retry_attempts, bool)
113+
and retry_attempts > 0
114+
):
115+
attributes[SPANDATA.HTTP_REQUEST_RESEND_COUNT] = retry_attempts
116+
117+
headers = metadata.get("HTTPHeaders")
118+
if not isinstance(headers, dict):
119+
headers = {}
120+
121+
request_id = metadata.get("RequestId")
122+
if not isinstance(request_id, str) or not request_id:
123+
request_id = next(
124+
(
125+
value
126+
for value in (
127+
headers.get("x-amzn-requestid"),
128+
headers.get("x-amzn-request-id"),
129+
headers.get("x-amz-request-id"),
130+
)
131+
if isinstance(value, str) and value
132+
),
133+
None,
134+
)
135+
if isinstance(request_id, str) and request_id:
136+
attributes[SPANDATA.AWS_REQUEST_ID] = request_id
137+
138+
# S3's `HostId` is the extended request ID returned in `x-amz-id-2`.
139+
# https://docs.aws.amazon.com/AmazonS3/latest/developerguide/get-request-ids.html
140+
extended_request_id = metadata.get("HostId")
141+
if not isinstance(extended_request_id, str) or not extended_request_id:
142+
extended_request_id = headers.get("x-amz-id-2")
143+
if isinstance(extended_request_id, str) and extended_request_id:
144+
attributes[SPANDATA.AWS_EXTENDED_REQUEST_ID] = extended_request_id
145+
146+
return attributes
147+
148+
149+
def _get_error_type(exception: "BaseException") -> str:
150+
if isinstance(exception, ClientError):
151+
# `ClientError` wraps AWS service errors; `Error.Code` identifies the
152+
# actual service error, e.g. `AccessDeniedException`.
153+
# https://docs.aws.amazon.com/boto3/latest/guide/error-handling.html
154+
error = exception.response.get("Error")
155+
if isinstance(error, dict):
156+
error_code = error.get("Code")
157+
if isinstance(error_code, str) and error_code:
158+
return error_code
159+
160+
# failures before a service response have no AWS error code.
161+
# https://opentelemetry.io/docs/specs/semconv/rpc/rpc-spans/
162+
exception_type = type(exception)
163+
exception_name = exception_type.__qualname__
164+
exception_module = exception_type.__module__
165+
if exception_module not in ("builtins", "__builtins__"):
166+
return "%s.%s" % (exception_module, exception_name)
167+
return exception_name
168+
169+
170+
def _get_error_attributes(exception: "BaseException") -> "Attributes":
171+
attributes: "Attributes" = {}
172+
if isinstance(exception, ClientError):
173+
attributes.update(_get_response_attributes(exception.response))
174+
175+
attributes[SPANDATA.ERROR_TYPE] = _get_error_type(exception)
176+
return attributes
177+
178+
88179
def _start_client_span(
89180
ctx: "AwsCallContext",
90181
) -> "Optional[Union[Span, StreamedSpan]]":
@@ -129,11 +220,18 @@ def _finish_client_span(
129220
span: "Union[Span, StreamedSpan]",
130221
parsed: "Dict[str, Any]",
131222
) -> None:
132-
span.__exit__(None, None, None)
133-
134223
if isinstance(span, NoOpStreamedSpan):
224+
span.__exit__(None, None, None)
135225
return
136226

227+
attributes: "Attributes" = {}
228+
with capture_internal_exceptions():
229+
attributes = _get_response_attributes(parsed)
230+
231+
with capture_internal_exceptions():
232+
_set_span_attributes(span, attributes)
233+
span.__exit__(None, None, None)
234+
137235
body = parsed.get("Body")
138236
if not isinstance(body, StreamingBody):
139237
return
@@ -171,11 +269,15 @@ def sentry_streaming_body_read(*args: "Any", **kwargs: "Any") -> bytes:
171269
streaming_span.finish()
172270
return ret
173271
except Exception as exc:
174-
if isinstance(streaming_span, StreamedSpan):
175-
streaming_span.__exit__(type(exc), exc, exc.__traceback__)
176-
else:
177-
streaming_span.set_status(SPANSTATUS.INTERNAL_ERROR)
178-
streaming_span.finish()
272+
with capture_internal_exceptions():
273+
_set_span_attributes(streaming_span, _get_error_attributes(exc))
274+
275+
with capture_internal_exceptions():
276+
if isinstance(streaming_span, StreamedSpan):
277+
streaming_span.__exit__(type(exc), exc, exc.__traceback__)
278+
else:
279+
streaming_span.set_status(SPANSTATUS.INTERNAL_ERROR)
280+
streaming_span.finish()
179281
raise
180282

181283
body.read = sentry_streaming_body_read # type: ignore
@@ -194,6 +296,12 @@ def _finish_client_span_with_error(
194296
span: "Union[Span, StreamedSpan]",
195297
exception: "BaseException",
196298
) -> None:
299+
attributes: "Attributes" = {}
300+
with capture_internal_exceptions():
301+
attributes = _get_error_attributes(exception)
302+
303+
with capture_internal_exceptions():
304+
_set_span_attributes(span, attributes)
197305
span.__exit__(type(exception), exception, exception.__traceback__)
198306

199307

0 commit comments

Comments
 (0)