22from urllib .parse import urlsplit
33
44from botocore .awsrequest import AWSRequest
5+ from botocore .exceptions import ClientError
56from botocore .response import StreamingBody
67
78import sentry_sdk
3334def _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+
88179def _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