66"""
77
88import uuid
9+ import warnings
10+ from datetime import datetime , timedelta , timezone
911from enum import Enum
1012from typing import TYPE_CHECKING
1113
1214import sentry_sdk
1315from sentry_sdk .tracing_utils import Baggage
14- from sentry_sdk .utils import format_attribute , logger
16+ from sentry_sdk .utils import (
17+ capture_internal_exceptions ,
18+ format_attribute ,
19+ logger ,
20+ nanosecond_time ,
21+ should_be_treated_as_error ,
22+ )
1523
1624if TYPE_CHECKING :
1725 from typing import Any , Callable , Optional , ParamSpec , TypeVar , Union
@@ -182,8 +190,13 @@ class StreamedSpan:
182190 "_parent_span_id" ,
183191 "_segment" ,
184192 "_parent_sampled" ,
193+ "_start_timestamp" ,
194+ "_start_timestamp_monotonic_ns" ,
195+ "_finished" ,
196+ "_timestamp" ,
185197 "_status" ,
186198 "_scope" ,
199+ "_previous_span_on_scope" ,
187200 "_baggage" ,
188201 )
189202
@@ -216,11 +229,24 @@ def __init__(
216229 self ._parent_sampled = parent_sampled
217230 self ._baggage = baggage
218231
232+ self ._start_timestamp = datetime .now (timezone .utc )
233+ self ._timestamp : "Optional[datetime]" = None
234+ self ._finished : bool = False
235+
236+ try :
237+ # profiling depends on this value and requires that
238+ # it is measured in nanoseconds
239+ self ._start_timestamp_monotonic_ns = nanosecond_time ()
240+ except AttributeError :
241+ pass
242+
219243 self ._span_id : "Optional[str]" = None
220244
221245 self ._status = SpanStatus .OK .value
222246 self .set_attribute ("sentry.span.source" , SegmentSource .CUSTOM .value )
223247
248+ self ._start ()
249+
224250 def __repr__ (self ) -> str :
225251 return (
226252 f"<{ self .__class__ .__name__ } ("
@@ -237,7 +263,77 @@ def __enter__(self) -> "StreamedSpan":
237263 def __exit__ (
238264 self , ty : "Optional[Any]" , value : "Optional[Any]" , tb : "Optional[Any]"
239265 ) -> None :
240- pass
266+ if value is not None and should_be_treated_as_error (ty , value ):
267+ self .status = SpanStatus .ERROR
268+
269+ self ._end ()
270+
271+ def end (self , end_timestamp : "Optional[Union[float, datetime]]" = None ) -> None :
272+ """
273+ Finish this span and queue it for sending.
274+
275+ :param end_timestamp: End timestamp to use instead of current time.
276+ :type end_timestamp: "Optional[Union[float, datetime]]"
277+ """
278+ try :
279+ if end_timestamp and self ._timestamp is None :
280+ if isinstance (end_timestamp , float ):
281+ end_timestamp = datetime .fromtimestamp (end_timestamp , timezone .utc )
282+ self ._timestamp = end_timestamp
283+ except AttributeError :
284+ logger .debug (f"Failed to set end_timestamp: { end_timestamp } " )
285+
286+ self ._end ()
287+
288+ def finish (self , end_timestamp : "Optional[Union[float, datetime]]" = None ) -> None :
289+ warnings .warn (
290+ "span.finish() is deprecated. Use span.end() instead." ,
291+ stacklevel = 2 ,
292+ category = DeprecationWarning ,
293+ )
294+
295+ self .end (end_timestamp )
296+
297+ def _start (self ) -> None :
298+ if self ._active :
299+ old_span = self ._scope .span
300+ self ._scope .span = self
301+ self ._previous_span_on_scope = old_span
302+
303+ def _end (self ) -> None :
304+ if self ._finished is True :
305+ # This span is already finished, ignore.
306+ return
307+
308+ # Detach from scope
309+ if self ._active :
310+ with capture_internal_exceptions ():
311+ old_span = self ._previous_span_on_scope
312+ del self ._previous_span_on_scope
313+ self ._scope .span = old_span
314+
315+ client = sentry_sdk .get_client ()
316+ if not client .is_active ():
317+ return
318+
319+ # Set attributes from the segment
320+ self .set_attribute ("sentry.segment.id" , self ._segment .span_id )
321+ self .set_attribute ("sentry.segment.name" , self ._segment .name )
322+
323+ # Set the end timestamp if not set yet (e.g. via span.end(<timestamp>))
324+ if self ._timestamp is None :
325+ try :
326+ elapsed = nanosecond_time () - self ._start_timestamp_monotonic_ns
327+ self ._timestamp = self ._start_timestamp + timedelta (
328+ microseconds = elapsed / 1000
329+ )
330+ except AttributeError :
331+ self ._timestamp = datetime .now (timezone .utc )
332+
333+ self ._finished = True
334+
335+ # Finally, queue the span for sending to Sentry
336+ self ._scope ._capture_span (self )
241337
242338 def get_attributes (self ) -> "Attributes" :
243339 return self ._attributes
@@ -302,10 +398,27 @@ def trace_id(self) -> str:
302398 def sampled (self ) -> "Optional[bool]" :
303399 return True
304400
401+ @property
402+ def start_timestamp (self ) -> "Optional[datetime]" :
403+ return self ._start_timestamp
404+
405+ @property
406+ def timestamp (self ) -> "Optional[datetime]" :
407+ return self ._timestamp
408+
305409
306410class NoOpStreamedSpan (StreamedSpan ):
307- def __init__ (self ) -> None :
308- pass
411+ __slots__ = (
412+ "_scope" ,
413+ "_previous_span_on_scope" ,
414+ )
415+
416+ def __init__ (
417+ self ,
418+ scope : "Optional[sentry_sdk.Scope]" = None ,
419+ ) -> None :
420+ self ._scope = scope # type: ignore[assignment]
421+ self ._start ()
309422
310423 def __repr__ (self ) -> str :
311424 return f"<{ self .__class__ .__name__ } (sampled={ self .sampled } )>"
@@ -316,7 +429,24 @@ def __enter__(self) -> "NoOpStreamedSpan":
316429 def __exit__ (
317430 self , ty : "Optional[Any]" , value : "Optional[Any]" , tb : "Optional[Any]"
318431 ) -> None :
319- pass
432+ self ._end ()
433+
434+ def _start (self ) -> None :
435+ if self ._scope is None :
436+ return self
437+
438+ old_span = self ._scope .span
439+ self ._scope .span = self
440+ self ._previous_span_on_scope = old_span
441+
442+ def _end (self ) -> None :
443+ if self ._scope is None :
444+ return
445+
446+ with capture_internal_exceptions ():
447+ old_span = self ._previous_span_on_scope
448+ del self ._previous_span_on_scope
449+ self ._scope .span = old_span
320450
321451 def get_attributes (self ) -> "Attributes" :
322452 return {}
0 commit comments