862 lines
26 KiB
Python
862 lines
26 KiB
Python
"""
|
|
EXPERIMENTAL. Do not use in production.
|
|
|
|
The API in this file is only meant to be used in span streaming mode.
|
|
|
|
You can enable span streaming mode via
|
|
sentry_sdk.init(trace_lifecycle="stream").
|
|
"""
|
|
|
|
import sys
|
|
import uuid
|
|
import warnings
|
|
from datetime import datetime, timedelta, timezone
|
|
from enum import Enum
|
|
from typing import TYPE_CHECKING
|
|
|
|
import sentry_sdk
|
|
from sentry_sdk.consts import SPANDATA
|
|
from sentry_sdk.profiler.continuous_profiler import (
|
|
get_profiler_id,
|
|
try_autostart_continuous_profiler,
|
|
try_profile_lifecycle_trace_start,
|
|
)
|
|
from sentry_sdk.tracing_utils import Baggage
|
|
from sentry_sdk.utils import (
|
|
capture_internal_exceptions,
|
|
format_attribute,
|
|
get_current_thread_meta,
|
|
logger,
|
|
nanosecond_time,
|
|
should_be_treated_as_error,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
from typing import (
|
|
Any,
|
|
Callable,
|
|
Iterator,
|
|
Optional,
|
|
ParamSpec,
|
|
TypeVar,
|
|
Union,
|
|
overload,
|
|
)
|
|
|
|
from sentry_sdk._types import Attributes, AttributeValue, SpanJSON
|
|
from sentry_sdk.profiler.continuous_profiler import ContinuousProfile
|
|
|
|
P = ParamSpec("P")
|
|
R = TypeVar("R")
|
|
|
|
|
|
BAGGAGE_HEADER_NAME = "baggage"
|
|
SENTRY_TRACE_HEADER_NAME = "sentry-trace"
|
|
|
|
|
|
class SpanStatus(str, Enum):
|
|
OK = "ok"
|
|
ERROR = "error"
|
|
|
|
def __str__(self) -> str:
|
|
return self.value
|
|
|
|
|
|
_VALID_SPAN_STATUSES = frozenset(e.value for e in SpanStatus)
|
|
|
|
|
|
# Segment source, see
|
|
# https://getsentry.github.io/sentry-conventions/generated/attributes/sentry.html#sentryspansource
|
|
class SegmentSource(str, Enum):
|
|
COMPONENT = "component"
|
|
CUSTOM = "custom"
|
|
ROUTE = "route"
|
|
TASK = "task"
|
|
URL = "url"
|
|
VIEW = "view"
|
|
|
|
def __str__(self) -> str:
|
|
return self.value
|
|
|
|
|
|
# These are typically high cardinality and the server hates them
|
|
LOW_QUALITY_SEGMENT_SOURCES = [
|
|
SegmentSource.URL,
|
|
]
|
|
|
|
|
|
SOURCE_FOR_STYLE = {
|
|
"endpoint": SegmentSource.COMPONENT,
|
|
"function_name": SegmentSource.COMPONENT,
|
|
"handler_name": SegmentSource.COMPONENT,
|
|
"method_and_path_pattern": SegmentSource.ROUTE,
|
|
"path": SegmentSource.URL,
|
|
"route_name": SegmentSource.COMPONENT,
|
|
"route_pattern": SegmentSource.ROUTE,
|
|
"uri_template": SegmentSource.ROUTE,
|
|
"url": SegmentSource.ROUTE,
|
|
}
|
|
|
|
|
|
# Sentinel value for an unset parent_span to be able to distinguish it from
|
|
# a None set by the user
|
|
_DEFAULT_PARENT_SPAN = object()
|
|
|
|
|
|
def start_span(
|
|
name: str,
|
|
attributes: "Optional[Attributes]" = None,
|
|
parent_span: "Optional[StreamedSpan]" = _DEFAULT_PARENT_SPAN, # type: ignore[assignment]
|
|
active: bool = True,
|
|
) -> "StreamedSpan":
|
|
"""
|
|
Start a span.
|
|
|
|
EXPERIMENTAL. Use sentry_sdk.start_transaction() and sentry_sdk.start_span()
|
|
instead.
|
|
|
|
The span's parent, unless provided explicitly via the `parent_span` argument,
|
|
will be the current active span, if any. If there is none, this span will
|
|
become the root of a new span tree. If you explicitly want this span to be
|
|
top-level without a parent, set `parent_span=None`.
|
|
|
|
`start_span()` can either be used as context manager or you can use the span
|
|
object it returns and explicitly end it via `span.end()`. The following is
|
|
equivalent:
|
|
|
|
```python
|
|
import sentry_sdk
|
|
|
|
with sentry_sdk.traces.start_span(name="My Span"):
|
|
# do something
|
|
|
|
# The span automatically finishes once the `with` block is exited
|
|
```
|
|
|
|
```python
|
|
import sentry_sdk
|
|
|
|
span = sentry_sdk.traces.start_span(name="My Span")
|
|
# do something
|
|
span.end()
|
|
```
|
|
|
|
To continue a trace from another service, call
|
|
`sentry_sdk.traces.continue_trace()` prior to creating a top-level span.
|
|
|
|
:param name: The name to identify this span by.
|
|
:type name: str
|
|
|
|
:param attributes: Key-value attributes to set on the span from the start.
|
|
These will also be accessible in the traces sampler.
|
|
:type attributes: "Optional[Attributes]"
|
|
|
|
:param parent_span: A span instance that the new span should consider its
|
|
parent. If not provided, the parent will be set to the currently active
|
|
span, if any. If set to `None`, this span will become a new root-level
|
|
span.
|
|
:type parent_span: "Optional[StreamedSpan]"
|
|
|
|
:param active: Controls whether spans started while this span is running
|
|
will automatically become its children. That's the default behavior. If
|
|
you want to create a span that shouldn't have any children (unless
|
|
provided explicitly via the `parent_span` argument), set this to `False`.
|
|
:type active: bool
|
|
|
|
:return: The span that has been started.
|
|
:rtype: StreamedSpan
|
|
"""
|
|
from sentry_sdk.tracing_utils import has_span_streaming_enabled
|
|
|
|
client = sentry_sdk.get_client()
|
|
if client.is_active() and not has_span_streaming_enabled(client.options):
|
|
warnings.warn(
|
|
"Using span streaming API in non-span-streaming mode. Use "
|
|
"sentry_sdk.start_transaction() and sentry_sdk.start_span() "
|
|
"instead.",
|
|
stacklevel=2,
|
|
)
|
|
return NoOpStreamedSpan()
|
|
|
|
return sentry_sdk.get_current_scope().start_streamed_span(
|
|
name, attributes, parent_span, active
|
|
)
|
|
|
|
|
|
def continue_trace(incoming: "dict[str, Any]") -> None:
|
|
"""
|
|
Continue a trace from headers or environment variables.
|
|
|
|
EXPERIMENTAL. Use sentry_sdk.continue_trace() instead.
|
|
|
|
This function sets the propagation context on the scope. Any span started
|
|
in the updated scope will belong under the trace extracted from the
|
|
provided propagation headers or environment variables.
|
|
|
|
continue_trace() doesn't start any spans on its own. Use the start_span()
|
|
API for that.
|
|
"""
|
|
# This is set both on the isolation and the current scope for compatibility
|
|
# reasons. Conceptually, it belongs on the isolation scope, and it also
|
|
# used to be set there in non-span-first mode. But in span first mode, we
|
|
# start spans on the current scope, regardless of type, like JS does, so we
|
|
# need to set the propagation context there.
|
|
sentry_sdk.get_isolation_scope().generate_propagation_context(
|
|
incoming,
|
|
)
|
|
sentry_sdk.get_current_scope().generate_propagation_context(
|
|
incoming,
|
|
)
|
|
|
|
|
|
def new_trace() -> None:
|
|
"""
|
|
Resets the propagation context, forcing a new trace.
|
|
|
|
EXPERIMENTAL.
|
|
|
|
This function sets the propagation context on the scope. Any span started
|
|
in the updated scope will start its own trace.
|
|
|
|
new_trace() doesn't start any spans on its own. Use the start_span() API
|
|
for that.
|
|
"""
|
|
sentry_sdk.get_isolation_scope().set_new_propagation_context()
|
|
sentry_sdk.get_current_scope().set_new_propagation_context()
|
|
|
|
|
|
class StreamedSpan:
|
|
"""
|
|
A span holds timing information of a block of code.
|
|
|
|
Spans can have multiple child spans, thus forming a span tree.
|
|
|
|
This is the Span First span implementation that streams spans. The original
|
|
transaction-based span implementation lives in tracing.Span.
|
|
"""
|
|
|
|
__slots__ = (
|
|
"_name",
|
|
"_attributes",
|
|
"_active",
|
|
"_span_id",
|
|
"_trace_id",
|
|
"_parent_span_id",
|
|
"_segment",
|
|
"_parent_sampled",
|
|
"_start_timestamp",
|
|
"_start_timestamp_monotonic_ns",
|
|
"_end_timestamp",
|
|
"_status",
|
|
"_scope",
|
|
"_previous_span_on_scope",
|
|
"_baggage",
|
|
"_sample_rand",
|
|
"_sample_rate",
|
|
"_continuous_profile",
|
|
)
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
name: str,
|
|
attributes: "Optional[Attributes]" = None,
|
|
active: bool = True,
|
|
scope: "sentry_sdk.Scope",
|
|
segment: "Optional[StreamedSpan]" = None,
|
|
trace_id: "Optional[str]" = None,
|
|
parent_span_id: "Optional[str]" = None,
|
|
parent_sampled: "Optional[bool]" = None,
|
|
baggage: "Optional[Baggage]" = None,
|
|
sample_rate: "Optional[float]" = None,
|
|
sample_rand: "Optional[float]" = None,
|
|
):
|
|
self._name: str = name
|
|
self._active: bool = active
|
|
self._attributes: "Attributes" = {
|
|
"sentry.origin": "manual",
|
|
"sentry.trace_lifecycle": "stream",
|
|
}
|
|
|
|
if attributes:
|
|
for attribute, value in attributes.items():
|
|
self.set_attribute(attribute, value)
|
|
|
|
self._scope = scope
|
|
|
|
self._segment = segment or self
|
|
|
|
self._trace_id: "Optional[str]" = trace_id
|
|
self._parent_span_id = parent_span_id
|
|
self._parent_sampled = parent_sampled
|
|
self._baggage = baggage
|
|
self._sample_rand = sample_rand
|
|
self._sample_rate = sample_rate
|
|
|
|
self._start_timestamp = datetime.now(timezone.utc)
|
|
self._end_timestamp: "Optional[datetime]" = None
|
|
|
|
# profiling depends on this value and requires that
|
|
# it is measured in nanoseconds
|
|
self._start_timestamp_monotonic_ns = nanosecond_time()
|
|
|
|
self._span_id: "Optional[str]" = None
|
|
|
|
self._status = SpanStatus.OK.value
|
|
|
|
self._update_active_thread()
|
|
|
|
self._continuous_profile: "Optional[ContinuousProfile]" = None
|
|
self._start_profile()
|
|
self._set_profile_id(get_profiler_id())
|
|
|
|
self._set_segment_attributes()
|
|
|
|
self._start()
|
|
|
|
def __repr__(self) -> str:
|
|
return (
|
|
f"<{self.__class__.__name__}("
|
|
f"name={self._name}, "
|
|
f"trace_id={self.trace_id}, "
|
|
f"span_id={self.span_id}, "
|
|
f"parent_span_id={self._parent_span_id}, "
|
|
f"active={self._active})>"
|
|
)
|
|
|
|
def __enter__(self) -> "StreamedSpan":
|
|
return self
|
|
|
|
def __exit__(
|
|
self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]"
|
|
) -> None:
|
|
if self._end_timestamp is not None:
|
|
# This span is already finished, ignore
|
|
return
|
|
|
|
if value is not None and should_be_treated_as_error(ty, value):
|
|
self.status = SpanStatus.ERROR.value
|
|
|
|
self._end()
|
|
|
|
def end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None:
|
|
"""
|
|
Finish this span and queue it for sending.
|
|
|
|
:param end_timestamp: End timestamp to use instead of current time.
|
|
:type end_timestamp: "Optional[Union[float, datetime]]"
|
|
"""
|
|
self._end(end_timestamp)
|
|
|
|
def finish(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None:
|
|
warnings.warn(
|
|
"span.finish() is deprecated. Use span.end() instead.",
|
|
stacklevel=2,
|
|
category=DeprecationWarning,
|
|
)
|
|
|
|
self.end(end_timestamp)
|
|
|
|
def _start(self) -> None:
|
|
if self._active:
|
|
old_span = self._scope.streamed_span
|
|
self._scope.streamed_span = self
|
|
self._previous_span_on_scope = old_span
|
|
|
|
def _end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None:
|
|
if self._end_timestamp is not None:
|
|
# This span is already finished, ignore.
|
|
return
|
|
|
|
# Stop the profiler
|
|
if self._is_segment() and self._continuous_profile is not None:
|
|
with capture_internal_exceptions():
|
|
self._continuous_profile.stop()
|
|
|
|
# Detach from scope
|
|
if self._active:
|
|
with capture_internal_exceptions():
|
|
old_span = self._previous_span_on_scope
|
|
del self._previous_span_on_scope
|
|
self._scope.streamed_span = old_span
|
|
|
|
# Set attributes from the segment. These are set on span end on purpose
|
|
# so that we have the best chance to capture the segment's final name
|
|
# (since it might change during its lifetime)
|
|
self.set_attribute("sentry.segment.id", self._segment.span_id)
|
|
self.set_attribute("sentry.segment.name", self._segment.name)
|
|
|
|
# Set the end timestamp
|
|
if end_timestamp is not None:
|
|
if isinstance(end_timestamp, (float, int)):
|
|
try:
|
|
end_timestamp = datetime.fromtimestamp(end_timestamp, timezone.utc)
|
|
except Exception:
|
|
pass
|
|
|
|
if isinstance(end_timestamp, datetime):
|
|
self._end_timestamp = end_timestamp
|
|
else:
|
|
logger.debug(
|
|
"[Tracing] Failed to set end_timestamp. Using current time instead."
|
|
)
|
|
|
|
if self._end_timestamp is None:
|
|
elapsed = nanosecond_time() - self._start_timestamp_monotonic_ns
|
|
self._end_timestamp = self._start_timestamp + timedelta(
|
|
microseconds=elapsed / 1000
|
|
)
|
|
|
|
client = sentry_sdk.get_client()
|
|
if not client.is_active():
|
|
return
|
|
|
|
# Finally, queue the span for sending to Sentry
|
|
self._scope._capture_span(self)
|
|
|
|
def get_attributes(self) -> "Attributes":
|
|
return self._attributes
|
|
|
|
def set_attribute(self, key: str, value: "AttributeValue") -> None:
|
|
self._attributes[key] = format_attribute(value)
|
|
|
|
def set_attributes(self, attributes: "Attributes") -> None:
|
|
for key, value in attributes.items():
|
|
self.set_attribute(key, value)
|
|
|
|
def remove_attribute(self, key: str) -> None:
|
|
try:
|
|
del self._attributes[key]
|
|
except KeyError:
|
|
pass
|
|
|
|
@property
|
|
def status(self) -> "str":
|
|
return self._status
|
|
|
|
@status.setter
|
|
def status(self, status: "Union[SpanStatus, str]") -> None:
|
|
if isinstance(status, Enum):
|
|
status = status.value
|
|
|
|
if status not in _VALID_SPAN_STATUSES:
|
|
logger.debug(
|
|
f'[Tracing] Unsupported span status {status}. Expected one of: "ok", "error"'
|
|
)
|
|
return
|
|
|
|
self._status = status
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
return self._name
|
|
|
|
@name.setter
|
|
def name(self, name: str) -> None:
|
|
self._name = name
|
|
|
|
@property
|
|
def active(self) -> bool:
|
|
return self._active
|
|
|
|
@property
|
|
def span_id(self) -> str:
|
|
if not self._span_id:
|
|
self._span_id = uuid.uuid4().hex[16:]
|
|
|
|
return self._span_id
|
|
|
|
@property
|
|
def trace_id(self) -> str:
|
|
if not self._trace_id:
|
|
self._trace_id = uuid.uuid4().hex
|
|
|
|
return self._trace_id
|
|
|
|
@property
|
|
def sampled(self) -> "Optional[bool]":
|
|
return True
|
|
|
|
@property
|
|
def start_timestamp(self) -> "Optional[datetime]":
|
|
return self._start_timestamp
|
|
|
|
@property
|
|
def end_timestamp(self) -> "Optional[datetime]":
|
|
return self._end_timestamp
|
|
|
|
def _is_segment(self) -> bool:
|
|
return self._segment is self
|
|
|
|
def _update_active_thread(self) -> None:
|
|
thread_id, thread_name = get_current_thread_meta()
|
|
|
|
if thread_id is not None:
|
|
self.set_attribute(SPANDATA.THREAD_ID, str(thread_id))
|
|
|
|
if thread_name is not None:
|
|
self.set_attribute(SPANDATA.THREAD_NAME, thread_name)
|
|
|
|
def _dynamic_sampling_context(self) -> "dict[str, str]":
|
|
return self._segment._get_baggage().dynamic_sampling_context()
|
|
|
|
def _to_traceparent(self) -> str:
|
|
if self._segment.sampled is True:
|
|
sampled = "1"
|
|
elif self._segment.sampled is False:
|
|
sampled = "0"
|
|
else:
|
|
sampled = None
|
|
|
|
traceparent = f"{self.trace_id}-{self.span_id}"
|
|
if sampled is not None:
|
|
traceparent += f"-{sampled}"
|
|
|
|
return traceparent
|
|
|
|
def _to_baggage(self) -> "Optional[Baggage]":
|
|
if self._segment:
|
|
return self._segment._get_baggage()
|
|
return None
|
|
|
|
def _get_baggage(self) -> "Baggage":
|
|
"""
|
|
Return the :py:class:`~sentry_sdk.tracing_utils.Baggage` associated with
|
|
the segment.
|
|
|
|
The first time a new baggage with Sentry items is made, it will be frozen.
|
|
"""
|
|
if not self._baggage or self._baggage.mutable:
|
|
self._baggage = Baggage.populate_from_segment(self)
|
|
|
|
return self._baggage
|
|
|
|
def _iter_headers(self) -> "Iterator[tuple[str, str]]":
|
|
if not self._segment:
|
|
return
|
|
|
|
yield SENTRY_TRACE_HEADER_NAME, self._to_traceparent()
|
|
|
|
baggage = self._segment._get_baggage().serialize()
|
|
if baggage:
|
|
yield BAGGAGE_HEADER_NAME, baggage
|
|
|
|
def _get_trace_context(self) -> "dict[str, Any]":
|
|
# Even if spans themselves are not event-based anymore, we need this
|
|
# to populate trace context on events
|
|
context: "dict[str, Any]" = {
|
|
"trace_id": self.trace_id,
|
|
"span_id": self.span_id,
|
|
"parent_span_id": self._parent_span_id,
|
|
"dynamic_sampling_context": self._dynamic_sampling_context(),
|
|
}
|
|
|
|
if "sentry.op" in self._attributes:
|
|
context["op"] = self._attributes["sentry.op"]
|
|
if "sentry.origin" in self._attributes:
|
|
context["origin"] = self._attributes["sentry.origin"]
|
|
|
|
return context
|
|
|
|
def _set_profile_id(self, profiler_id: "Optional[str]") -> None:
|
|
if profiler_id is not None:
|
|
self.set_attribute("sentry.profiler_id", profiler_id)
|
|
|
|
def _start_profile(self) -> None:
|
|
if not self._is_segment():
|
|
return
|
|
|
|
try_autostart_continuous_profiler()
|
|
|
|
self._continuous_profile = try_profile_lifecycle_trace_start()
|
|
|
|
def _set_segment_attributes(self) -> None:
|
|
if not self._is_segment():
|
|
return
|
|
|
|
client = sentry_sdk.get_client()
|
|
|
|
self.set_attribute(SPANDATA.SENTRY_PLATFORM, "python")
|
|
self.set_attribute(SPANDATA.PROCESS_COMMAND_ARGS, sys.argv)
|
|
self.set_attribute(
|
|
SPANDATA.SENTRY_SDK_INTEGRATIONS, sorted(client.integrations.keys())
|
|
)
|
|
|
|
if client.options.get("dist") and SPANDATA.SENTRY_DIST not in self._attributes:
|
|
self.set_attribute(
|
|
SPANDATA.SENTRY_DIST, str(client.options["dist"]).strip()
|
|
)
|
|
|
|
def _to_json(self) -> "SpanJSON":
|
|
res: "SpanJSON" = {
|
|
"trace_id": self.trace_id,
|
|
"span_id": self.span_id,
|
|
"name": self._name if self._name is not None else "<unlabeled span>",
|
|
"status": self._status,
|
|
"is_segment": self._is_segment(),
|
|
"start_timestamp": self._start_timestamp.timestamp(),
|
|
}
|
|
|
|
if self._end_timestamp:
|
|
res["end_timestamp"] = self._end_timestamp.timestamp()
|
|
|
|
if self._parent_span_id:
|
|
res["parent_span_id"] = self._parent_span_id
|
|
|
|
res["attributes"] = {k: v for k, v in self._attributes.items()}
|
|
|
|
return res
|
|
|
|
|
|
class NoOpStreamedSpan(StreamedSpan):
|
|
__slots__ = (
|
|
"_sampled",
|
|
"_finished",
|
|
"_unsampled_reason",
|
|
)
|
|
|
|
def __init__(
|
|
self,
|
|
segment: "Optional[StreamedSpan]" = None,
|
|
trace_id: "Optional[str]" = None,
|
|
parent_span_id: "Optional[str]" = None,
|
|
parent_sampled: "Optional[bool]" = None,
|
|
baggage: "Optional[Baggage]" = None,
|
|
sampled: "Optional[bool]" = False,
|
|
unsampled_reason: "Optional[str]" = None,
|
|
scope: "Optional[sentry_sdk.Scope]" = None,
|
|
sample_rand: "Optional[float]" = None,
|
|
sample_rate: "Optional[float]" = None,
|
|
) -> None:
|
|
self._span_id: "Optional[str]" = None
|
|
|
|
self._sampled = sampled
|
|
self._segment = segment or self
|
|
|
|
self._trace_id: "Optional[str]" = trace_id
|
|
self._parent_span_id = parent_span_id
|
|
self._parent_sampled = parent_sampled
|
|
self._baggage = baggage
|
|
self._sample_rand = sample_rand
|
|
self._sample_rate = sample_rate
|
|
|
|
self._scope = scope # type: ignore[assignment]
|
|
self._unsampled_reason = unsampled_reason
|
|
|
|
self._finished = False
|
|
|
|
self._start()
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<{self.__class__.__name__}(sampled={self.sampled})>"
|
|
|
|
def __enter__(self) -> "NoOpStreamedSpan":
|
|
return self
|
|
|
|
def __exit__(
|
|
self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]"
|
|
) -> None:
|
|
self._end()
|
|
|
|
def _start(self) -> None:
|
|
if self._scope is None:
|
|
return
|
|
|
|
old_span = self._scope.streamed_span
|
|
self._scope.streamed_span = self
|
|
self._previous_span_on_scope = old_span
|
|
|
|
def _end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None:
|
|
if self._finished:
|
|
return
|
|
|
|
if self._unsampled_reason is not None:
|
|
client = sentry_sdk.get_client()
|
|
if client.is_active() and client.transport:
|
|
logger.debug(
|
|
f"[Tracing] Discarding span because sampled=False (reason: {self._unsampled_reason})"
|
|
)
|
|
client.transport.record_lost_event(
|
|
reason=self._unsampled_reason,
|
|
data_category="span",
|
|
quantity=1,
|
|
)
|
|
|
|
if self._scope and hasattr(self, "_previous_span_on_scope"):
|
|
with capture_internal_exceptions():
|
|
old_span = self._previous_span_on_scope
|
|
del self._previous_span_on_scope
|
|
self._scope.streamed_span = old_span
|
|
|
|
self._finished = True
|
|
|
|
def end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None:
|
|
self._end()
|
|
|
|
def finish(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None:
|
|
warnings.warn(
|
|
"span.finish() is deprecated. Use span.end() instead.",
|
|
stacklevel=2,
|
|
category=DeprecationWarning,
|
|
)
|
|
|
|
self._end()
|
|
|
|
def get_attributes(self) -> "Attributes":
|
|
return {}
|
|
|
|
def set_attribute(self, key: str, value: "AttributeValue") -> None:
|
|
pass
|
|
|
|
def set_attributes(self, attributes: "Attributes") -> None:
|
|
pass
|
|
|
|
def remove_attribute(self, key: str) -> None:
|
|
pass
|
|
|
|
@property
|
|
def status(self) -> "str":
|
|
return SpanStatus.OK.value
|
|
|
|
@status.setter
|
|
def status(self, status: "Union[SpanStatus, str]") -> None:
|
|
pass
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
return ""
|
|
|
|
@name.setter
|
|
def name(self, value: str) -> None:
|
|
pass
|
|
|
|
@property
|
|
def active(self) -> bool:
|
|
return True
|
|
|
|
@property
|
|
def sampled(self) -> "Optional[bool]":
|
|
return self._sampled
|
|
|
|
@property
|
|
def start_timestamp(self) -> "Optional[datetime]":
|
|
return None
|
|
|
|
@property
|
|
def end_timestamp(self) -> "Optional[datetime]":
|
|
return None
|
|
|
|
def _get_trace_context(self) -> "dict[str, Any]":
|
|
return {
|
|
"trace_id": self.trace_id,
|
|
"span_id": self.span_id,
|
|
"parent_span_id": self._parent_span_id,
|
|
"dynamic_sampling_context": self._dynamic_sampling_context(),
|
|
}
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
|
|
@overload
|
|
def trace(
|
|
func: "Callable[P, R]",
|
|
) -> "Callable[P, R]": ...
|
|
|
|
@overload
|
|
def trace(
|
|
func: None = None,
|
|
*,
|
|
name: "Optional[str]" = None,
|
|
attributes: "Optional[dict[str, Any]]" = None,
|
|
active: bool = True,
|
|
) -> "Callable[[Callable[P, R]], Callable[P, R]]": ...
|
|
|
|
|
|
def trace(
|
|
func: "Optional[Callable[P, R]]" = None,
|
|
*,
|
|
name: "Optional[str]" = None,
|
|
attributes: "Optional[dict[str, Any]]" = None,
|
|
active: bool = True,
|
|
) -> "Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]]":
|
|
"""
|
|
Decorator to start a span around a function call.
|
|
|
|
EXPERIMENTAL. Use @sentry_sdk.trace instead.
|
|
|
|
This decorator automatically creates a new span when the decorated function
|
|
is called, and finishes the span when the function returns or raises an exception.
|
|
|
|
:param func: The function to trace. When used as a decorator without parentheses,
|
|
this is the function being decorated. When used with parameters (e.g.,
|
|
``@trace(op="custom")``, this should be None.
|
|
:type func: Callable or None
|
|
|
|
:param name: The human-readable name/description for the span. If not provided,
|
|
defaults to the function name. This provides more specific details about
|
|
what the span represents (e.g., "GET /api/users", "process_user_data").
|
|
:type name: str or None
|
|
|
|
:param attributes: A dictionary of key-value pairs to add as attributes to the span.
|
|
Attribute values must be strings, integers, floats, or booleans. These
|
|
attributes provide additional context about the span's execution.
|
|
:type attributes: dict[str, Any] or None
|
|
|
|
:param active: Controls whether spans started while this span is running
|
|
will automatically become its children. That's the default behavior. If
|
|
you want to create a span that shouldn't have any children (unless
|
|
provided explicitly via the `parent_span` argument), set this to False.
|
|
:type active: bool
|
|
|
|
:returns: When used as ``@trace``, returns the decorated function. When used as
|
|
``@trace(...)`` with parameters, returns a decorator function.
|
|
:rtype: Callable or decorator function
|
|
|
|
Example::
|
|
|
|
import sentry_sdk
|
|
|
|
# Simple usage with default values
|
|
@sentry_sdk.trace
|
|
def process_data():
|
|
# Function implementation
|
|
pass
|
|
|
|
# With custom parameters
|
|
@sentry_sdk.trace(
|
|
name="Get user data",
|
|
attributes={"postgres": True}
|
|
)
|
|
def make_db_query(sql):
|
|
# Function implementation
|
|
pass
|
|
"""
|
|
from sentry_sdk.tracing_utils import (
|
|
create_streaming_span_decorator,
|
|
)
|
|
|
|
decorator = create_streaming_span_decorator(
|
|
name=name,
|
|
attributes=attributes,
|
|
active=active,
|
|
)
|
|
|
|
if func:
|
|
return decorator(func)
|
|
else:
|
|
return decorator
|
|
|
|
|
|
def get_current_span(
|
|
scope: "Optional[sentry_sdk.Scope]" = None,
|
|
) -> "Optional[StreamedSpan]":
|
|
"""
|
|
Returns the currently active span on the scope if the span is a `StreamedSpan`, otherwise `None`.
|
|
|
|
This function will only return a non-`None` value when the streaming trace lifecycle is enabled.
|
|
To enable the lifecycle, pass `trace_lifecycle="stream"` to `sentry.init()`.
|
|
"""
|
|
scope = scope or sentry_sdk.get_current_scope()
|
|
current_span = scope.streamed_span
|
|
return current_span
|