11from typing import TYPE_CHECKING
22
33import sentry_sdk
4- from sentry_sdk .consts import OP , SPANDATA
4+ from sentry_sdk .consts import OP , SPANDATA , SPANSTATUS
55from sentry_sdk .integrations import DidNotEnable
66from sentry_sdk .integrations .boto3 import Boto3Integration
7- from sentry_sdk .traces import StreamedSpan
7+ from sentry_sdk .traces import NoOpStreamedSpan , StreamedSpan
88from sentry_sdk .tracing import BAGGAGE_HEADER_NAME , Span
99from sentry_sdk .tracing_utils import (
1010 add_http_breadcrumb ,
@@ -160,26 +160,39 @@ def _replace_header(request: "AWSRequest", key: str, value: str) -> None:
160160 )
161161
162162
163- def _sentry_after_call (
164- context : "Dict[str, Any]" , parsed : "Dict[str, Any]" , ** kwargs : "Any"
163+ def _finish_span (
164+ span : "Union[Span, StreamedSpan]" ,
165+ error : "Optional[BaseException]" = None ,
165166) -> None :
166- span : "Optional[Union[Span, StreamedSpan]]" = context .pop ("_sentrysdk_span" , None )
167+ with capture_internal_exceptions ():
168+ if not isinstance (span , StreamedSpan ):
169+ if error is not None :
170+ span .set_status (SPANSTATUS .INTERNAL_ERROR )
171+ span .finish ()
172+ return
167173
168- # Span could be absent if the integration is disabled.
169- if span is None :
170- return
174+ if error is None :
175+ span .end ()
176+ else :
177+ span .__exit__ (type (error ), error , error .__traceback__ )
171178
172- span .__exit__ (None , None , None )
179+
180+ def _instrument_streaming_body (
181+ span : "Union[Span, StreamedSpan]" , parsed : "Dict[str, Any]"
182+ ) -> bool :
183+ if isinstance (span , NoOpStreamedSpan ):
184+ return False
173185
174186 body = parsed .get ("Body" )
175187 if not isinstance (body , StreamingBody ):
176- return
188+ return False
177189
178190 streaming_span : "Union[Span, StreamedSpan]"
179191 if isinstance (span , StreamedSpan ):
180192 streaming_span = sentry_sdk .traces .start_span (
181193 name = span .name ,
182194 parent_span = span ,
195+ active = False ,
183196 attributes = {
184197 "sentry.op" : OP .HTTP_CLIENT_STREAM ,
185198 "sentry.origin" : Boto3Integration .origin ,
@@ -194,35 +207,86 @@ def _sentry_after_call(
194207
195208 orig_read = body .read
196209 orig_close = body .close
210+ raw_stream = body ._raw_stream # type: ignore[attr-defined]
211+ orig_raw_close = raw_stream .close
212+ finished = False
213+
214+ def finish (error : "Optional[BaseException]" = None ) -> None :
215+ nonlocal finished
216+ if finished :
217+ return
218+
219+ finished = True
220+ _finish_span (streaming_span , error )
221+
222+ def content_length_reached () -> bool :
223+ content_length = getattr (body , "_content_length" , None )
224+ amount_read = getattr (body , "_amount_read" , None )
225+ return (
226+ content_length is not None
227+ and amount_read is not None
228+ and amount_read >= int (content_length )
229+ )
197230
198231 def sentry_streaming_body_read (* args : "Any" , ** kwargs : "Any" ) -> bytes :
199232 try :
200233 ret = orig_read (* args , ** kwargs )
201- if ret :
202- return ret
203-
204- if isinstance (streaming_span , StreamedSpan ):
205- streaming_span .end ()
206- else :
207- streaming_span .finish ()
234+ with capture_internal_exceptions ():
235+ amount = args [0 ] if args else kwargs .get ("amt" )
236+ if (
237+ amount is None
238+ or amount < 0
239+ or (amount > 0 and not ret )
240+ or content_length_reached ()
241+ ):
242+ finish ()
208243 return ret
209- except Exception :
210- if isinstance (streaming_span , StreamedSpan ):
211- streaming_span .end ()
212- else :
213- streaming_span .finish ()
244+ except BaseException as error :
245+ finish (error )
214246 raise
215247
216- body .read = sentry_streaming_body_read # type: ignore
217-
218248 def sentry_streaming_body_close (* args : "Any" , ** kwargs : "Any" ) -> None :
219- if isinstance (streaming_span , StreamedSpan ):
220- streaming_span .end ()
221- else :
222- streaming_span .finish ()
223- orig_close (* args , ** kwargs )
249+ try :
250+ orig_close (* args , ** kwargs )
251+ finish ()
252+ except BaseException as error :
253+ finish (error )
254+ raise
255+
256+ def sentry_raw_stream_close (* args : "Any" , ** kwargs : "Any" ) -> None :
257+ try :
258+ orig_raw_close (* args , ** kwargs )
259+ finish ()
260+ except BaseException as error :
261+ finish (error )
262+ raise
263+
264+ try :
265+ # StreamingBody.__exit__ closes `_raw_stream` directly, bypassing
266+ # StreamingBody.close(), so both levels need to be instrumented.
267+ raw_stream .close = sentry_raw_stream_close
268+ body .read = sentry_streaming_body_read # type: ignore
269+ body .close = sentry_streaming_body_close # type: ignore
270+ except Exception :
271+ finish ()
272+ raise
273+
274+ return True
275+
276+
277+ def _sentry_after_call (
278+ context : "Dict[str, Any]" , parsed : "Dict[str, Any]" , ** kwargs : "Any"
279+ ) -> None :
280+ span : "Optional[Union[Span, StreamedSpan]]" = context .pop ("_sentrysdk_span" , None )
281+
282+ # Span could be absent if the integration is disabled.
283+ if span is None :
284+ return
224285
225- body .close = sentry_streaming_body_close # type: ignore
286+ span .__exit__ (None , None , None )
287+
288+ with capture_internal_exceptions ():
289+ _instrument_streaming_body (span , parsed )
226290
227291
228292def _sentry_after_call_error (
0 commit comments