Skip to content

Commit 1fbcd86

Browse files
committed
fix(boto3): finish streaming spans before legacy boto spans
1 parent 3b565e8 commit 1fbcd86

3 files changed

Lines changed: 84 additions & 35 deletions

File tree

‎sentry_sdk/integrations/boto3/_client.py‎

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,30 @@
2626

2727

2828
@contextmanager
29-
def _activate_client_span(span: "StreamedSpan") -> "Iterator[StreamedSpan]":
29+
def _activate_client_span(
30+
span: "Union[Span, StreamedSpan]",
31+
) -> "Iterator[Union[Span, StreamedSpan]]":
3032
"""Temporarily activate an inactive boto span without ending it."""
3133
if isinstance(span, NoOpStreamedSpan):
3234
yield span
3335
return
3436

3537
scope = sentry_sdk.get_current_scope()
36-
previous_span = scope.streamed_span
38+
if not isinstance(span, StreamedSpan):
39+
previous_span = scope.span
40+
scope.span = span
41+
try:
42+
yield span
43+
finally:
44+
scope.span = previous_span
45+
return
46+
47+
previous_streamed_span = scope.streamed_span
3748
scope.streamed_span = span
3849
try:
3950
yield span
4051
finally:
41-
scope.streamed_span = previous_span
52+
scope.streamed_span = previous_streamed_span
4253

4354

4455
def _patch_botocore_client() -> None:
@@ -82,26 +93,21 @@ def sentry_patched_make_api_call(
8293
return orig_make_api_call(self, operation_name, api_params)
8394

8495
# activate without finishing; a streaming response may outlive the call.
85-
span_ctx = (
86-
_activate_client_span(span) if isinstance(span, StreamedSpan) else span
87-
)
96+
span_ctx = _activate_client_span(span)
8897

8998
try:
9099
with span_ctx:
91100
parsed = orig_make_api_call(self, operation_name, api_params)
92101
except BaseException as error:
93-
# finish `StreamedSpan` explicitly; static spans are finished by
94-
# their context manager.
95-
if isinstance(span, StreamedSpan):
96-
_finish_span(span, error)
102+
_finish_span(span, error)
97103
raise
98104

99105
streaming_body_instrumented = False
100106
with capture_internal_exceptions():
101107
streaming_body_instrumented = _instrument_streaming_body(span, parsed)
102108

103109
# `StreamingBody`s finish their span when consumed or closed.
104-
if isinstance(span, StreamedSpan) and not streaming_body_instrumented:
110+
if not streaming_body_instrumented:
105111
_finish_span(span)
106112
return parsed
107113

‎sentry_sdk/integrations/boto3/_instrumentation.py‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,7 @@ def finish_span(error: "Optional[BaseException]" = None) -> None:
134134

135135
finished = True
136136
_finish_span(streaming_span, error)
137-
if isinstance(span, StreamedSpan):
138-
_finish_span(span, error)
137+
_finish_span(span, error)
139138

140139
def content_length_reached() -> bool:
141140
content_length = getattr(body, "_content_length", None)

‎tests/integrations/boto3/test_client.py‎

Lines changed: 66 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
from sentry_sdk.integrations.boto3._instrumentation import _instrument_streaming_body
1515
from sentry_sdk.integrations.boto3.consts import ORIGIN
1616
from sentry_sdk.integrations.stdlib import StdlibIntegration
17+
from sentry_sdk.traces import StreamedSpan
18+
from sentry_sdk.tracing import Span
1719
from tests.integrations.boto3.aws_mock import Body
1820

1921
session = boto3.Session( # type: ignore[attr-defined]
@@ -53,15 +55,17 @@ def log_message(self, *args):
5355
"consume",
5456
["read", "read_exact", "context", "close"],
5557
)
58+
@pytest.mark.parametrize("span_streaming", [True, False])
5659
def test_streaming_span_order_and_scope(
5760
sentry_init,
5861
capture_items,
5962
streaming_s3_server,
6063
consume,
64+
span_streaming,
6165
):
6266
sentry_init(
6367
traces_sample_rate=1.0,
64-
trace_lifecycle="stream",
68+
trace_lifecycle="stream" if span_streaming else "static",
6569
default_integrations=False,
6670
integrations=[Boto3Integration(), StdlibIntegration()],
6771
server_name="",
@@ -75,11 +79,31 @@ def test_streaming_span_order_and_scope(
7579
s3={"addressing_style": "path"},
7680
),
7781
)
78-
items = capture_items("span")
82+
request_client_spans = []
7983

80-
with sentry_sdk.traces.start_span(name="parent") as parent: # type: ignore[attr-defined]
84+
def record_client_span(request, **kwargs):
85+
request_client_spans.append(request.context["_sentrysdk_span"])
86+
87+
client.meta.events.register("request-created", record_client_span)
88+
items = capture_items()
89+
90+
parent_context = (
91+
sentry_sdk.traces.start_span(name="parent") # type: ignore[attr-defined]
92+
if span_streaming
93+
else sentry_sdk.start_transaction(name="parent")
94+
)
95+
with parent_context as parent:
8196
body = client.get_object(Bucket="bucket", Key="key")["Body"]
82-
assert sentry_sdk.traces.get_current_span() is parent # type: ignore[attr-defined]
97+
assert len(request_client_spans) == 1
98+
request_client_span = request_client_spans[0]
99+
if span_streaming:
100+
assert isinstance(request_client_span, StreamedSpan)
101+
assert request_client_span.end_timestamp is None
102+
assert sentry_sdk.traces.get_current_span() is parent # type: ignore[attr-defined]
103+
else:
104+
assert isinstance(request_client_span, Span)
105+
assert not isinstance(request_client_span, StreamedSpan)
106+
assert request_client_span.timestamp is None
83107

84108
if consume == "read":
85109
assert body.read() == b"x"
@@ -94,34 +118,54 @@ def test_streaming_span_order_and_scope(
94118
else:
95119
body.close()
96120

97-
assert sentry_sdk.traces.get_current_span() is parent # type: ignore[attr-defined]
121+
if span_streaming:
122+
assert request_client_span.end_timestamp is not None
123+
assert sentry_sdk.traces.get_current_span() is parent # type: ignore[attr-defined]
98124

99-
probe = sentry_sdk.traces.start_span(name="probe") # type: ignore[attr-defined]
100-
assert probe._parent_span_id == parent.span_id
101-
probe.end()
125+
probe = sentry_sdk.traces.start_span(name="probe") # type: ignore[attr-defined]
126+
assert probe._parent_span_id == parent.span_id
127+
probe.end()
102128

103-
body.close()
104-
assert sentry_sdk.traces.get_current_span() is parent # type: ignore[attr-defined]
129+
body.close()
130+
assert sentry_sdk.traces.get_current_span() is parent # type: ignore[attr-defined]
131+
else:
132+
assert request_client_span.timestamp is not None
105133

106134
sentry_sdk.flush()
107-
spans = [item.payload for item in items]
135+
if span_streaming:
136+
spans = [item.payload for item in items]
137+
else:
138+
transaction = next(item.payload for item in items if item.type == "transaction")
139+
spans = transaction["spans"]
108140
client_spans = [
109141
span
110142
for span in spans
111-
if span["name"] == "aws.s3.GetObject"
112-
and span["attributes"].get(SPANDATA.SENTRY_ORIGIN) == ORIGIN
113-
and span["attributes"].get(SPANDATA.SENTRY_OP) == OP.HTTP_CLIENT
143+
if span.get("name", span.get("description")) == "aws.s3.GetObject"
144+
and (
145+
span["attributes"].get(SPANDATA.SENTRY_ORIGIN) == ORIGIN
146+
and span["attributes"].get(SPANDATA.SENTRY_OP) == OP.HTTP_CLIENT
147+
if span_streaming
148+
else span["origin"] == ORIGIN and span["op"] == OP.HTTP_CLIENT
149+
)
114150
]
115151
http_spans = [
116152
span
117153
for span in spans
118-
if span["attributes"].get(SPANDATA.SENTRY_ORIGIN) == "auto.http.stdlib.httplib"
154+
if (
155+
span["attributes"].get(SPANDATA.SENTRY_ORIGIN) == "auto.http.stdlib.httplib"
156+
if span_streaming
157+
else span["origin"] == "auto.http.stdlib.httplib"
158+
)
119159
]
120160
stream_spans = [
121161
span
122162
for span in spans
123-
if span["name"] == "aws.s3.GetObject"
124-
and span["attributes"].get(SPANDATA.SENTRY_OP) == OP.HTTP_CLIENT_STREAM
163+
if span.get("name", span.get("description")) == "aws.s3.GetObject"
164+
and (
165+
span["attributes"].get(SPANDATA.SENTRY_OP) == OP.HTTP_CLIENT_STREAM
166+
if span_streaming
167+
else span["op"] == OP.HTTP_CLIENT_STREAM
168+
)
125169
]
126170
assert len(client_spans) == 1
127171
assert len(http_spans) == 1
@@ -132,10 +176,12 @@ def test_streaming_span_order_and_scope(
132176

133177
assert http_span["parent_span_id"] == client_span["span_id"]
134178
assert stream_span["parent_span_id"] == client_span["span_id"]
179+
assert client_span["span_id"] == request_client_span.span_id
180+
end_timestamp = "end_timestamp" if span_streaming else "timestamp"
135181
assert client_span["start_timestamp"] <= http_span["start_timestamp"]
136182
assert http_span["start_timestamp"] <= stream_span["start_timestamp"]
137-
assert http_span["end_timestamp"] <= stream_span["end_timestamp"]
138-
assert stream_span["end_timestamp"] <= client_span["end_timestamp"]
183+
assert http_span[end_timestamp] <= stream_span[end_timestamp]
184+
assert stream_span[end_timestamp] <= client_span[end_timestamp]
139185

140186

141187
@pytest.mark.parametrize("span_streaming", [True, False])
@@ -437,7 +483,5 @@ def invoke_client_method_and_read_body():
437483
client_spans = spans_by_op.get(OP.HTTP_CLIENT, [])
438484
stream_spans = spans_by_op.get(OP.HTTP_CLIENT_STREAM, [])
439485

440-
assert len(client_spans) == 1
441-
if span_streaming:
442-
_assert_one_failed_span(client_spans, span_streaming=True)
486+
_assert_one_failed_span(client_spans, span_streaming)
443487
_assert_one_failed_span(stream_spans, span_streaming)

0 commit comments

Comments
 (0)