Skip to content

Commit fb587da

Browse files
feat(boto3): Support span streaming (#6193)
Replace deprecated `http.method` and `url` attributes with `http.request.method` and `url.full` attributes in the streaming lifecycle mode. Use `url.query` and `url.fragment` instead of `http.query` and `http.fragment` in the streaming mode. Combine the undocumented `aws.service_id` and `aws.operation_name` attributes into the `rpc.method` attribute in the streaming lifecycle path.
1 parent bc380bf commit fb587da

3 files changed

Lines changed: 398 additions & 139 deletions

File tree

sentry_sdk/consts.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -893,6 +893,12 @@ class SPANDATA:
893893
Example: "5249fbada8d5416482c2f6e47e337372"
894894
"""
895895

896+
RPC_METHOD = "rpc.method"
897+
"""
898+
The fully-qualified logical name of the method from the RPC interface perspective.
899+
Example: "com.example.ExampleService/exampleMethod"
900+
"""
901+
896902
SERVER_ADDRESS = "server.address"
897903
"""
898904
Name of the database host.

sentry_sdk/integrations/boto3.py

Lines changed: 82 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
from sentry_sdk.consts import OP, SPANDATA
55
from sentry_sdk.integrations import _check_minimum_version, Integration, DidNotEnable
66
from sentry_sdk.tracing import Span
7+
from sentry_sdk.traces import StreamedSpan
8+
from sentry_sdk.tracing_utils import has_span_streaming_enabled
79
from sentry_sdk.utils import (
810
capture_internal_exceptions,
9-
ensure_integration_enabled,
1011
parse_url,
1112
parse_version,
1213
)
@@ -18,6 +19,9 @@
1819
from typing import Dict
1920
from typing import Optional
2021
from typing import Type
22+
from typing import Union
23+
24+
from botocore.model import ServiceId
2125

2226
try:
2327
from botocore import __version__ as BOTOCORE_VERSION
@@ -44,7 +48,7 @@ def sentry_patched_init(
4448
) -> None:
4549
orig_init(self, *args, **kwargs)
4650
meta = self.meta
47-
service_id = meta.service_model.service_id.hyphenize()
51+
service_id = meta.service_model.service_id
4852
meta.events.register(
4953
"request-created",
5054
partial(_sentry_request_created, service_id=service_id),
@@ -55,27 +59,52 @@ def sentry_patched_init(
5559
BaseClient.__init__ = sentry_patched_init # type: ignore
5660

5761

58-
@ensure_integration_enabled(Boto3Integration)
5962
def _sentry_request_created(
60-
service_id: str, request: "AWSRequest", operation_name: str, **kwargs: "Any"
63+
service_id: "ServiceId", request: "AWSRequest", operation_name: str, **kwargs: "Any"
6164
) -> None:
62-
description = "aws.%s.%s" % (service_id, operation_name)
63-
span = sentry_sdk.start_span(
64-
op=OP.HTTP_CLIENT,
65-
name=description,
66-
origin=Boto3Integration.origin,
67-
)
68-
69-
if request.url is not None:
70-
with capture_internal_exceptions():
71-
parsed_url = parse_url(request.url, sanitize=False)
72-
span.set_data("aws.request.url", parsed_url.url)
73-
span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query)
74-
span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment)
75-
76-
span.set_tag("aws.service_id", service_id)
77-
span.set_tag("aws.operation_name", operation_name)
78-
span.set_data(SPANDATA.HTTP_METHOD, request.method)
65+
description = "aws.%s.%s" % (service_id.hyphenize(), operation_name)
66+
67+
client = sentry_sdk.get_client()
68+
if client.get_integration(Boto3Integration) is None:
69+
return
70+
71+
is_span_streaming_enabled = has_span_streaming_enabled(client.options)
72+
span: "Union[Span, StreamedSpan]"
73+
if is_span_streaming_enabled:
74+
span = sentry_sdk.traces.start_span(
75+
name=description,
76+
attributes={
77+
"sentry.op": OP.HTTP_CLIENT,
78+
"sentry.origin": Boto3Integration.origin,
79+
SPANDATA.RPC_METHOD: f"{service_id}/{operation_name}",
80+
},
81+
)
82+
if request.url is not None:
83+
with capture_internal_exceptions():
84+
parsed_url = parse_url(request.url, sanitize=False)
85+
span.set_attribute(SPANDATA.URL_FULL, parsed_url.url)
86+
span.set_attribute(SPANDATA.URL_QUERY, parsed_url.query)
87+
span.set_attribute(SPANDATA.URL_FRAGMENT, parsed_url.fragment)
88+
89+
if request.method is not None:
90+
span.set_attribute(SPANDATA.HTTP_REQUEST_METHOD, request.method)
91+
else:
92+
span = sentry_sdk.start_span(
93+
op=OP.HTTP_CLIENT,
94+
name=description,
95+
origin=Boto3Integration.origin,
96+
)
97+
98+
if request.url is not None:
99+
with capture_internal_exceptions():
100+
parsed_url = parse_url(request.url, sanitize=False)
101+
span.set_data("aws.request.url", parsed_url.url)
102+
span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query)
103+
span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment)
104+
105+
span.set_tag("aws.service_id", service_id.hyphenize())
106+
span.set_tag("aws.operation_name", operation_name)
107+
span.set_data(SPANDATA.HTTP_METHOD, request.method)
79108

80109
# We do it in order for subsequent http calls/retries be
81110
# attached to this span.
@@ -89,7 +118,7 @@ def _sentry_request_created(
89118
def _sentry_after_call(
90119
context: "Dict[str, Any]", parsed: "Dict[str, Any]", **kwargs: "Any"
91120
) -> None:
92-
span: "Optional[Span]" = context.pop("_sentrysdk_span", None)
121+
span: "Optional[Union[Span, StreamedSpan]]" = context.pop("_sentrysdk_span", None)
93122

94123
# Span could be absent if the integration is disabled.
95124
if span is None:
@@ -100,29 +129,51 @@ def _sentry_after_call(
100129
if not isinstance(body, StreamingBody):
101130
return
102131

103-
streaming_span = span.start_child(
104-
op=OP.HTTP_CLIENT_STREAM,
105-
name=span.description,
106-
origin=Boto3Integration.origin,
107-
)
132+
streaming_span: "Union[Span, StreamedSpan]"
133+
if isinstance(span, StreamedSpan):
134+
streaming_span = sentry_sdk.traces.start_span(
135+
name=span.name,
136+
parent_span=span,
137+
attributes={
138+
"sentry.op": OP.HTTP_CLIENT_STREAM,
139+
"sentry.origin": Boto3Integration.origin,
140+
},
141+
)
142+
else:
143+
streaming_span = span.start_child(
144+
op=OP.HTTP_CLIENT_STREAM,
145+
name=span.description,
146+
origin=Boto3Integration.origin,
147+
)
108148

109149
orig_read = body.read
110150
orig_close = body.close
111151

112152
def sentry_streaming_body_read(*args: "Any", **kwargs: "Any") -> bytes:
113153
try:
114154
ret = orig_read(*args, **kwargs)
115-
if not ret:
155+
if ret:
156+
return ret
157+
158+
if isinstance(streaming_span, StreamedSpan):
159+
streaming_span.end()
160+
else:
116161
streaming_span.finish()
117162
return ret
118163
except Exception:
119-
streaming_span.finish()
164+
if isinstance(streaming_span, StreamedSpan):
165+
streaming_span.end()
166+
else:
167+
streaming_span.finish()
120168
raise
121169

122170
body.read = sentry_streaming_body_read # type: ignore
123171

124172
def sentry_streaming_body_close(*args: "Any", **kwargs: "Any") -> None:
125-
streaming_span.finish()
173+
if isinstance(streaming_span, StreamedSpan):
174+
streaming_span.end()
175+
else:
176+
streaming_span.finish()
126177
orig_close(*args, **kwargs)
127178

128179
body.close = sentry_streaming_body_close # type: ignore
@@ -131,7 +182,7 @@ def sentry_streaming_body_close(*args: "Any", **kwargs: "Any") -> None:
131182
def _sentry_after_call_error(
132183
context: "Dict[str, Any]", exception: "Type[BaseException]", **kwargs: "Any"
133184
) -> None:
134-
span: "Optional[Span]" = context.pop("_sentrysdk_span", None)
185+
span: "Optional[Union[Span, StreamedSpan]]" = context.pop("_sentrysdk_span", None)
135186

136187
# Span could be absent if the integration is disabled.
137188
if span is None:

0 commit comments

Comments
 (0)