Skip to content

Commit 0a7eae8

Browse files
committed
ref: Allow to start and finish StreamedSpans
1 parent 656ef2e commit 0a7eae8

1 file changed

Lines changed: 135 additions & 5 deletions

File tree

sentry_sdk/traces.py

Lines changed: 135 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,20 @@
66
"""
77

88
import uuid
9+
import warnings
10+
from datetime import datetime, timedelta, timezone
911
from enum import Enum
1012
from typing import TYPE_CHECKING
1113

1214
import sentry_sdk
1315
from 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

1624
if 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

306410
class 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

Comments
 (0)