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 .consts import IDENTIFIER , ORIGIN
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 ,
@@ -162,26 +162,44 @@ def _replace_header(request: "AWSRequest", key: str, value: str) -> None:
162162 )
163163
164164
165- def _sentry_after_call (
166- context : "Dict[str, Any]" , parsed : "Dict[str, Any]" , ** kwargs : "Any"
165+ def _finish_span (
166+ span : "Union[Span, StreamedSpan]" ,
167+ error : "Optional[BaseException]" = None ,
167168) -> None :
168- span : "Optional[Union[Span, StreamedSpan]]" = context .pop ("_sentrysdk_span" , None )
169+ with capture_internal_exceptions ():
170+ if not isinstance (span , StreamedSpan ):
171+ if error is not None :
172+ span .set_status (SPANSTATUS .INTERNAL_ERROR )
173+ span .finish ()
174+ return
169175
170- # Span could be absent if the integration is disabled.
171- if span is None :
172- return
176+ if error is None :
177+ span .end ()
178+ else :
179+ span .__exit__ (type (error ), error , error .__traceback__ )
173180
174- span .__exit__ (None , None , None )
181+
182+ def _instrument_streaming_body (
183+ span : "Union[Span, StreamedSpan]" , parsed : "Dict[str, Any]"
184+ ) -> bool :
185+ if isinstance (span , NoOpStreamedSpan ):
186+ return False
175187
176188 body = parsed .get ("Body" )
177189 if not isinstance (body , StreamingBody ):
178- return
190+ return False
179191
180192 streaming_span : "Union[Span, StreamedSpan]"
181193 if isinstance (span , StreamedSpan ):
182194 streaming_span = sentry_sdk .traces .start_span (
183195 name = span .name ,
196+ # `parent_span` is set explicitly to the boto span.
184197 parent_span = span ,
198+ # avoid making the streaming span the current span on the scope since the application might
199+ # keep `StreamingBody` open before reading it. Otherwise: 1. when the streamingspan ends it
200+ # could restore the parent span on the scope, breaking the parent-child relation of newly
201+ # created spans; 2. newly created spans would be attached to the streaming span.
202+ active = False ,
185203 attributes = {
186204 "sentry.op" : OP .HTTP_CLIENT_STREAM ,
187205 "sentry.origin" : ORIGIN ,
@@ -196,35 +214,92 @@ def _sentry_after_call(
196214
197215 orig_read = body .read
198216 orig_close = body .close
217+ raw_stream = body ._raw_stream # type: ignore[attr-defined]
218+ orig_raw_close = raw_stream .close
219+ finished = False
220+ read_in_progress = False
221+
222+ def finish_span (error : "Optional[BaseException]" = None ) -> None :
223+ nonlocal finished
224+ if finished :
225+ return
226+
227+ finished = True
228+ _finish_span (streaming_span , error )
229+
230+ def content_length_reached () -> bool :
231+ content_length = getattr (body , "_content_length" , None )
232+ amount_read = getattr (body , "_amount_read" , None )
233+ return (
234+ content_length is not None
235+ and amount_read is not None
236+ and amount_read >= int (content_length )
237+ )
199238
200239 def sentry_streaming_body_read (* args : "Any" , ** kwargs : "Any" ) -> bytes :
240+ nonlocal read_in_progress
241+ read_in_progress = True
201242 try :
202- ret = orig_read (* args , ** kwargs )
203- if ret :
204- return ret
205-
206- if isinstance (streaming_span , StreamedSpan ):
207- streaming_span .end ()
208- else :
209- streaming_span .finish ()
210- return ret
211- except Exception :
212- if isinstance (streaming_span , StreamedSpan ):
213- streaming_span .end ()
214- else :
215- streaming_span .finish ()
243+ read_return_value = orig_read (* args , ** kwargs )
244+ with capture_internal_exceptions ():
245+ amount_of_bytes_requested = args [0 ] if args else kwargs .get ("amt" )
246+ if (
247+ amount_of_bytes_requested is None
248+ or amount_of_bytes_requested < 0
249+ or (amount_of_bytes_requested > 0 and not read_return_value )
250+ or content_length_reached ()
251+ ):
252+ finish_span ()
253+ return read_return_value
254+ except BaseException as error :
255+ finish_span (error )
216256 raise
217-
218- body . read = sentry_streaming_body_read # type: ignore
257+ finally :
258+ read_in_progress = False
219259
220260 def sentry_streaming_body_close (* args : "Any" , ** kwargs : "Any" ) -> None :
221- if isinstance (streaming_span , StreamedSpan ):
222- streaming_span .end ()
223- else :
224- streaming_span .finish ()
225- orig_close (* args , ** kwargs )
261+ try :
262+ orig_close (* args , ** kwargs )
263+ finish_span ()
264+ except BaseException as error :
265+ finish_span (error )
266+ raise
267+
268+ def sentry_raw_stream_close (* args : "Any" , ** kwargs : "Any" ) -> None :
269+ try :
270+ orig_raw_close (* args , ** kwargs )
271+ if not read_in_progress :
272+ finish_span ()
273+ except BaseException as error :
274+ finish_span (error )
275+ raise
276+
277+ try :
278+ # StreamingBody.__exit__ closes `_raw_stream` directly, bypassing
279+ # StreamingBody.close(), so both levels need to be instrumented.
280+ raw_stream .close = sentry_raw_stream_close
281+ body .read = sentry_streaming_body_read # type: ignore
282+ body .close = sentry_streaming_body_close # type: ignore
283+ except Exception :
284+ finish_span ()
285+ raise
226286
227- body .close = sentry_streaming_body_close # type: ignore
287+ return True
288+
289+
290+ def _sentry_after_call (
291+ context : "Dict[str, Any]" , parsed : "Dict[str, Any]" , ** kwargs : "Any"
292+ ) -> None :
293+ span : "Optional[Union[Span, StreamedSpan]]" = context .pop ("_sentrysdk_span" , None )
294+
295+ # Span could be absent if the integration is disabled.
296+ if span is None :
297+ return
298+
299+ span .__exit__ (None , None , None )
300+
301+ with capture_internal_exceptions ():
302+ _instrument_streaming_body (span , parsed )
228303
229304
230305def _sentry_after_call_error (
0 commit comments