11from typing import TYPE_CHECKING
2+ from urllib .parse import urlsplit
23
34import sentry_sdk
45from sentry_sdk .consts import OP , SPANDATA , SPANSTATUS
56from sentry_sdk .integrations import DidNotEnable
6- from sentry_sdk .integrations .boto3 .consts import IDENTIFIER , ORIGIN
7+ from sentry_sdk .integrations .boto3 .consts import (
8+ AWS_RPC_SYSTEM_NAME ,
9+ DEFAULT_PORTS ,
10+ IDENTIFIER ,
11+ ORIGIN ,
12+ )
713from sentry_sdk .traces import NoOpStreamedSpan , StreamedSpan
814from sentry_sdk .tracing import BAGGAGE_HEADER_NAME , Span
915from sentry_sdk .tracing_utils import (
3137 raise DidNotEnable ("botocore not installed" )
3238
3339
40+ def _set_span_attributes (
41+ span : "Union[Span, StreamedSpan]" , attributes : "Attributes"
42+ ) -> None :
43+ if isinstance (span , StreamedSpan ):
44+ span .set_attributes (attributes )
45+ return
46+
47+ for key , value in attributes .items ():
48+ span .set_data (key , value )
49+
50+
51+ def _get_server_attributes (endpoint_url : "Optional[str]" ) -> "Attributes" :
52+ if not endpoint_url :
53+ return {}
54+
55+ try :
56+ parsed_url = urlsplit (endpoint_url )
57+ if parsed_url .scheme not in DEFAULT_PORTS or not parsed_url .hostname :
58+ return {}
59+
60+ # `server.port` is only defined together with `server.address`.
61+ # Infer the effective port when the configured HTTP(S) endpoint omits it.
62+ # https://opentelemetry.io/docs/specs/semconv/rpc/rpc-spans/
63+ return {
64+ SPANDATA .SERVER_ADDRESS : parsed_url .hostname ,
65+ SPANDATA .SERVER_PORT : parsed_url .port or DEFAULT_PORTS [parsed_url .scheme ],
66+ }
67+
68+ except (TypeError , UnicodeError , ValueError ):
69+ # invalid client metadata must not prevent the AWS call from running.
70+ return {}
71+
72+
73+ def _get_client_attributes (
74+ ctx : "AwsCallContext" ,
75+ ) -> "Attributes" :
76+ attributes : "Attributes" = {}
77+
78+ # `rpc.service` is deprecated in OTel, but js still uses it.
79+ if ctx .service_id :
80+ attributes [SPANDATA .RPC_SERVICE ] = ctx .service_id
81+
82+ if ctx .region_name :
83+ attributes [SPANDATA .CLOUD_REGION ] = ctx .region_name
84+
85+ attributes .update (_get_server_attributes (ctx .endpoint_url ))
86+ return attributes
87+
88+
3489def _start_client_span (
3590 ctx : "AwsCallContext" ,
3691) -> "Optional[Union[Span, StreamedSpan]]" :
@@ -43,17 +98,27 @@ def _start_client_span(
4398 # e.g. "aws.unknown.GetObject"
4499 service_name = ctx .service_id_hyphenized or "unknown"
45100 span_name = f"aws.{ service_name } .{ ctx .operation_name } "
101+ attributes : "Attributes" = {
102+ SPANDATA .RPC_METHOD : ctx .operation_name ,
103+ SPANDATA .RPC_SYSTEM_NAME : AWS_RPC_SYSTEM_NAME ,
104+ }
105+ with capture_internal_exceptions ():
106+ attributes .update (_get_client_attributes (ctx ))
107+ span_op = OP .HTTP_CLIENT
108+ span_origin = ORIGIN
46109
47110 if has_span_streaming_enabled (client .options ):
48111 if sentry_sdk .traces .get_current_span () is None :
49112 return None
50113
51- attributes : "Attributes" = {
52- SPANDATA .SENTRY_OP : OP .HTTP_CLIENT ,
53- SPANDATA .SENTRY_ORIGIN : ORIGIN ,
54- }
55- if ctx .service_id :
56- attributes [SPANDATA .RPC_METHOD ] = f"{ ctx .service_id } /{ ctx .operation_name } "
114+ # `start_span()` evaluates `ignore_spans` against the initial attributes.
115+ # https://opentelemetry.io/docs/specs/semconv/rpc/rpc-spans/#rpc-client-span
116+ attributes .update (
117+ {
118+ SPANDATA .SENTRY_OP : span_op ,
119+ SPANDATA .SENTRY_ORIGIN : span_origin ,
120+ }
121+ )
57122 return sentry_sdk .traces .start_span (
58123 name = span_name ,
59124 attributes = attributes ,
@@ -64,9 +129,11 @@ def _start_client_span(
64129
65130 span = sentry_sdk .start_span (
66131 name = span_name ,
67- op = OP . HTTP_CLIENT ,
68- origin = ORIGIN ,
132+ op = span_op ,
133+ origin = span_origin ,
69134 )
135+ with capture_internal_exceptions ():
136+ _set_span_attributes (span , attributes )
70137 with capture_internal_exceptions ():
71138 if ctx .service_id_hyphenized :
72139 span .set_tag ("aws.service_id" , ctx .service_id_hyphenized )
@@ -112,8 +179,8 @@ def _instrument_streaming_body(
112179 # unrelated new spans attach to the stream span since it's the current span.
113180 active = False ,
114181 attributes = {
115- "sentry.op" : OP .HTTP_CLIENT_STREAM ,
116- "sentry.origin" : ORIGIN ,
182+ SPANDATA . SENTRY_OP : OP .HTTP_CLIENT_STREAM ,
183+ SPANDATA . SENTRY_ORIGIN : ORIGIN ,
117184 },
118185 )
119186 else :
@@ -211,6 +278,9 @@ def _set_request_attributes(
211278 with capture_internal_exceptions ():
212279 parsed_url = parse_url (request .url , sanitize = False )
213280
281+ # overwrite server attributes when actual request URL is resolved.
282+ _set_span_attributes (span , _get_server_attributes (request .url ))
283+
214284 if isinstance (span , StreamedSpan ):
215285 span .set_attributes (get_url_attributes (client , parsed_url ))
216286 if request .method is not None :
@@ -264,10 +334,9 @@ def _sentry_request_created(
264334 fresh `AWSRequest` on every retry.
265335 https://github.com/boto/botocore/blob/f9195c79ea2bf46350dd320d2a0bf3db7da0b460/botocore/endpoint.py#L178-L202
266336 """
267- from sentry_sdk .integrations .boto3 import Boto3Integration
268337
269338 client = sentry_sdk .get_client ()
270- if client .get_integration (Boto3Integration ) is None :
339+ if client .get_integration ("boto3" ) is None :
271340 return
272341
273342 with capture_internal_exceptions ():
@@ -294,10 +363,8 @@ def _sentry_request_created(
294363def _sentry_before_sign (
295364 request : "AWSRequest" , signature_version : "Any" , ** kwargs : "Any"
296365) -> None :
297- from sentry_sdk .integrations .boto3 import Boto3Integration
298-
299366 client = sentry_sdk .get_client ()
300- if client .get_integration (Boto3Integration ) is None :
367+ if client .get_integration ("boto3" ) is None :
301368 return
302369
303370 with capture_internal_exceptions ():
0 commit comments