Skip to content

Commit 3f786a9

Browse files
feat(boto3): Add common OTel AWS client attributes (#7481)
### Description Implements #7475 by adding common OTel attributes to boto client spans. ### Changes - add the following attributes: `rpc.system.name`, `rpc.service`, `rpc.method`, `cloud.region`, `server.address`, `server.port`. - set `rpc.system.name` to `aws-api` - changing span name to `Service.Operation` (now it's `aws.<service>.<Operation>`), e.g. `S3.HeadObject` following OTel is a breaking change and will be done in major. - use modeled AWS service for `rpc.service`; not supported anymore by OTel, but JS keeps this too. - use boto operation name for `rpc.method`. - extend `AwsCallContext` to carry extra client metadata for instrumentation. #### Issues Resolves #7475
1 parent 9837c88 commit 3f786a9

7 files changed

Lines changed: 298 additions & 22 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, Dict, Optional
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 & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
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
56
from 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+
)
713
from sentry_sdk.traces import NoOpStreamedSpan, StreamedSpan
814
from sentry_sdk.tracing import BAGGAGE_HEADER_NAME, Span
915
from sentry_sdk.tracing_utils import (
@@ -31,6 +37,55 @@
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+
3489
def _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(
294363
def _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():
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,12 @@
11
IDENTIFIER = "boto3"
22
ORIGIN = f"auto.http.{IDENTIFIER}"
3+
4+
# value is used by `rpc.system` (deprecated in OTel, but we still support it for now) and `rpc.system.name`
5+
# https://opentelemetry.io/docs/specs/semconv/cloud-providers/aws-sdk/#aws-sdk-spans
6+
AWS_RPC_SYSTEM_NAME = "aws-api"
7+
8+
# default ports for HTTP and HTTPS
9+
DEFAULT_PORTS = {
10+
"http": 80,
11+
"https": 443,
12+
}

0 commit comments

Comments
 (0)