Skip to content

Commit a5e41f5

Browse files
committed
feat(boto3): improve boto3 integration
1 parent c622458 commit a5e41f5

6 files changed

Lines changed: 289 additions & 24 deletions

File tree

‎sentry_sdk/consts.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,12 @@ class SPANDATA:
414414
Example: "10.1.2.80"
415415
"""
416416

417+
CLOUD_REGION = "cloud.region"
418+
"""
419+
The geographical region the resource is running.
420+
Example: "us-east-1"
421+
"""
422+
417423
CODE_FILEPATH = "code.filepath"
418424
"""
419425
.. deprecated::
@@ -977,12 +983,24 @@ class SPANDATA:
977983
Example: "com.example.ExampleService/exampleMethod"
978984
"""
979985

986+
RPC_SERVICE = "rpc.service"
987+
"""
988+
The full (logical) name of the service being called, including its package name, if applicable.
989+
Example: "myService.BestService"
990+
"""
991+
980992
RPC_RESPONSE_STATUS_CODE = "rpc.response.status_code"
981993
"""
982994
Status code of the RPC returned by the RPC server or generated by the client.
983995
Example: "DEADLINE_EXCEEDED"
984996
"""
985997

998+
RPC_SYSTEM_NAME = "rpc.system.name"
999+
"""
1000+
A string identifying the remoting system.
1001+
Example: "aws-api"
1002+
"""
1003+
9861004
SERVER_ADDRESS = "server.address"
9871005
"""
9881006
Name of the database host.

‎sentry_sdk/integrations/boto3/_client.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ def sentry_patched_make_api_call(
9191
if client.get_integration(Boto3Integration) is None:
9292
return orig_make_api_call(self, operation_name, api_params)
9393

94-
ctx = AwsCallContext(operation_name)
94+
ctx = AwsCallContext(operation_name, api_params)
9595

9696
# add optional metadata to context.
9797
with capture_internal_exceptions():

‎sentry_sdk/integrations/boto3/_context.py‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from sentry_sdk.utils import capture_internal_exceptions
55

66
if TYPE_CHECKING:
7-
from typing import Any, Optional
7+
from typing import Any, Optional, Dict
88

99
try:
1010
from botocore.client import BaseClient
@@ -14,15 +14,27 @@
1414

1515
class AwsCallContext:
1616
__slots__ = (
17+
"service_name",
1718
"service_id",
1819
"service_id_hyphenized",
1920
"operation_name",
21+
"region_name",
22+
"endpoint_url",
23+
"params",
2024
)
2125

22-
def __init__(self, operation_name: str) -> None:
26+
def __init__(self, operation_name: str, params: "Any") -> None:
2327
self.operation_name: str = operation_name
28+
self.params: "Dict[str, Any]" = {}
29+
self.service_name: "Optional[str]" = None
2430
self.service_id: "Optional[str]" = None
2531
self.service_id_hyphenized: "Optional[str]" = None
32+
self.region_name: "Optional[str]" = None
33+
self.endpoint_url: "Optional[str]" = None
34+
35+
if isinstance(params, dict):
36+
with capture_internal_exceptions():
37+
self.params = dict(params)
2638

2739
def add_metadata(self, client: "BaseClient") -> None:
2840
def _get_attr(obj: "Any", name: str) -> "Any":
@@ -35,10 +47,16 @@ def _get_attr(obj: "Any", name: str) -> "Any":
3547
client_meta = _get_attr(client, "meta")
3648
service_model = _get_attr(client_meta, "service_model")
3749

50+
# botocore's internal identifier, e.g. `apigateway`.
51+
self.service_name = _get_attr(service_model, "service_name")
52+
3853
# modeled AWS service identity used in span names, e.g. `API Gateway`.
3954
service_id = _get_attr(service_model, "service_id")
4055
if service_id is not None:
4156
with capture_internal_exceptions():
4257
self.service_id = str(service_id)
4358
with capture_internal_exceptions():
4459
self.service_id_hyphenized = service_id.hyphenize()
60+
61+
self.region_name = _get_attr(client_meta, "region_name")
62+
self.endpoint_url = _get_attr(client_meta, "endpoint_url")

‎sentry_sdk/integrations/boto3/_instrumentation.py‎

Lines changed: 83 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from typing import TYPE_CHECKING
2+
from urllib.parse import urlsplit
23

34
import sentry_sdk
45
from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS
@@ -31,30 +32,95 @@
3132
raise DidNotEnable("botocore not installed")
3233

3334

35+
_AWS_RPC_SYSTEM_NAME = "aws-api"
36+
37+
38+
def _set_span_attributes(
39+
span: "Union[Span, StreamedSpan]", attributes: "Attributes"
40+
) -> None:
41+
if isinstance(span, StreamedSpan):
42+
span.set_attributes(attributes)
43+
return
44+
45+
for key, value in attributes.items():
46+
span.set_data(key, value)
47+
48+
49+
def _get_server_attributes(endpoint_url: "Optional[str]") -> "Attributes":
50+
if not endpoint_url:
51+
return {}
52+
53+
default_ports = {
54+
"http": 80,
55+
"https": 443,
56+
}
57+
58+
try:
59+
parsed_url = urlsplit(endpoint_url)
60+
if parsed_url.scheme not in default_ports or not parsed_url.hostname:
61+
return {}
62+
63+
# `server.port` is only defined together with `server.address`.
64+
# Infer the effective port when the configured HTTP(S) endpoint omits it.
65+
# https://opentelemetry.io/docs/specs/semconv/rpc/rpc-spans/
66+
return {
67+
SPANDATA.SERVER_ADDRESS: parsed_url.hostname,
68+
SPANDATA.SERVER_PORT: parsed_url.port or default_ports[parsed_url.scheme],
69+
}
70+
71+
except (TypeError, UnicodeError, ValueError):
72+
# Invalid client metadata must not prevent the AWS call from running.
73+
return {}
74+
75+
76+
def _get_client_attributes(
77+
ctx: "AwsCallContext",
78+
) -> "Attributes":
79+
attributes: "Attributes" = {}
80+
81+
# `rpc.service` is deprecated in OTel, but js still uses it.
82+
if ctx.service_id:
83+
attributes[SPANDATA.RPC_SERVICE] = ctx.service_id
84+
85+
if ctx.region_name:
86+
attributes[SPANDATA.CLOUD_REGION] = ctx.region_name
87+
88+
attributes.update(_get_server_attributes(ctx.endpoint_url))
89+
return attributes
90+
91+
3492
def _start_client_span(
3593
ctx: "AwsCallContext",
3694
) -> "Optional[Union[Span, StreamedSpan]]":
37-
from sentry_sdk.integrations.boto3 import Boto3Integration
38-
3995
client = sentry_sdk.get_client()
40-
if client.get_integration(Boto3Integration) is None:
96+
if client.get_integration("boto3") is None:
4197
return None
4298

4399
# use unknown if `service_id_hyphenized` so span name can still be created.
44100
# e.g. "aws.unkown.GetObject"
45101
service_name = ctx.service_id_hyphenized or "unknown"
46102
span_name = f"aws.{service_name}.{ctx.operation_name}"
103+
attributes: "Attributes" = {
104+
SPANDATA.RPC_METHOD: ctx.operation_name,
105+
SPANDATA.RPC_SYSTEM_NAME: _AWS_RPC_SYSTEM_NAME,
106+
}
107+
with capture_internal_exceptions():
108+
attributes.update(_get_client_attributes(ctx))
109+
span_op = OP.HTTP_CLIENT
110+
span_origin = ORIGIN
47111

48112
if has_span_streaming_enabled(client.options):
49113
if sentry_sdk.traces.get_current_span() is None:
50114
return None
51115

52-
attributes: "Attributes" = {
53-
SPANDATA.SENTRY_OP: OP.HTTP_CLIENT,
54-
SPANDATA.SENTRY_ORIGIN: ORIGIN,
55-
}
56-
if ctx.service_id:
57-
attributes[SPANDATA.RPC_METHOD] = f"{ctx.service_id}/{ctx.operation_name}"
116+
# `start_span()` evaluates `ignore_spans` against the initial attributes.
117+
# https://opentelemetry.io/docs/specs/semconv/rpc/rpc-spans/#rpc-client-span
118+
attributes.update(
119+
{
120+
SPANDATA.SENTRY_OP: span_op,
121+
SPANDATA.SENTRY_ORIGIN: span_origin,
122+
}
123+
)
58124
return sentry_sdk.traces.start_span(
59125
name=span_name,
60126
attributes=attributes,
@@ -65,9 +131,11 @@ def _start_client_span(
65131

66132
span = sentry_sdk.start_span(
67133
name=span_name,
68-
op=OP.HTTP_CLIENT,
69-
origin=ORIGIN,
134+
op=span_op,
135+
origin=span_origin,
70136
)
137+
with capture_internal_exceptions():
138+
_set_span_attributes(span, attributes)
71139
with capture_internal_exceptions():
72140
if ctx.service_id_hyphenized:
73141
span.set_tag("aws.service_id", ctx.service_id_hyphenized)
@@ -113,8 +181,8 @@ def _instrument_streaming_body(
113181
# unrelated new spans attach to the stream span since it's the current span.
114182
active=False,
115183
attributes={
116-
"sentry.op": OP.HTTP_CLIENT_STREAM,
117-
"sentry.origin": ORIGIN,
184+
SPANDATA.SENTRY_OP: OP.HTTP_CLIENT_STREAM,
185+
SPANDATA.SENTRY_ORIGIN: ORIGIN,
118186
},
119187
)
120188
else:
@@ -265,10 +333,9 @@ def _sentry_request_created(
265333
fresh `AWSRequest` on every retry.
266334
https://github.com/boto/botocore/blob/f9195c79ea2bf46350dd320d2a0bf3db7da0b460/botocore/endpoint.py#L178-L202
267335
"""
268-
from sentry_sdk.integrations.boto3 import Boto3Integration
269336

270337
client = sentry_sdk.get_client()
271-
if client.get_integration(Boto3Integration) is None:
338+
if client.get_integration("boto3") is None:
272339
return
273340

274341
with capture_internal_exceptions():
@@ -295,10 +362,8 @@ def _sentry_request_created(
295362
def _sentry_before_sign(
296363
request: "AWSRequest", signature_version: "Any", **kwargs: "Any"
297364
) -> None:
298-
from sentry_sdk.integrations.boto3 import Boto3Integration
299-
300365
client = sentry_sdk.get_client()
301-
if client.get_integration(Boto3Integration) is None:
366+
if client.get_integration("boto3") is None:
302367
return
303368

304369
with capture_internal_exceptions():

0 commit comments

Comments
 (0)