sentry_sdk.tracing
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 INSTRUMENTER, SPANDATA, SPANSTATUS, SPANTEMPLATE from sentry_sdk.profiler.continuous_profiler import get_profiler_id from sentry_sdk.utils import ( capture_internal_exceptions, get_current_thread_meta, is_valid_sample_rate, logger, nanosecond_time, should_be_treated_as_error, ) if TYPE_CHECKING: from collections.abc import Callable, Mapping, MutableMapping from typing import ( Any, Dict, Iterator, List, Optional, ParamSpec, Tuple, TypeVar, Union, overload, ) from typing_extensions import TypedDict, Unpack P = ParamSpec("P") R = TypeVar("R") from sentry_sdk._types import ( Event, MeasurementUnit, MeasurementValue, SamplingContext, ) from sentry_sdk.profiler.continuous_profiler import ContinuousProfile from sentry_sdk.profiler.transaction_profiler import Profile class SpanKwargs(TypedDict, total=False): trace_id: str """ The trace ID of the root span. If this new span is to be the root span, omit this parameter, and a new trace ID will be generated. """ span_id: str """The span ID of this span. If omitted, a new span ID will be generated.""" parent_span_id: str """The span ID of the parent span, if applicable.""" same_process_as_parent: bool """Whether this span is in the same process as the parent span.""" sampled: bool """ Whether the span should be sampled. Overrides the default sampling decision for this span when provided. """ op: str """ The span's operation. A list of recommended values is available here: https://develop.sentry.dev/sdk/performance/span-operations/ """ description: str """A description of what operation is being performed within the span. This argument is DEPRECATED. Please use the `name` parameter, instead.""" hub: "Optional[sentry_sdk.Hub]" """The hub to use for this span. This argument is DEPRECATED. Please use the `scope` parameter, instead.""" status: str """The span's status. Possible values are listed at https://develop.sentry.dev/sdk/event-payloads/span/""" containing_transaction: "Optional[Transaction]" """The transaction that this span belongs to.""" start_timestamp: "Optional[Union[datetime, float]]" """ The timestamp when the span started. If omitted, the current time will be used. """ scope: "sentry_sdk.Scope" """The scope to use for this span. If not provided, we use the current scope.""" origin: str """ The origin of the span. See https://develop.sentry.dev/sdk/performance/trace-origin/ Default "manual". """ name: str """A string describing what operation is being performed within the span/transaction.""" class TransactionKwargs(SpanKwargs, total=False): source: str """ A string describing the source of the transaction name. This will be used to determine the transaction's type. See https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations for more information. Default "custom". """ parent_sampled: bool """Whether the parent transaction was sampled. If True this transaction will be kept, if False it will be discarded.""" baggage: "Baggage" """The W3C baggage header value. (see https://www.w3.org/TR/baggage/)""" ProfileContext = TypedDict( "ProfileContext", { "profiler_id": str, }, ) BAGGAGE_HEADER_NAME = "baggage" SENTRY_TRACE_HEADER_NAME = "sentry-trace" # Transaction source # see https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations class TransactionSource(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_TRANSACTION_SOURCES = [ TransactionSource.URL, ] SOURCE_FOR_STYLE = { "endpoint": TransactionSource.COMPONENT, "function_name": TransactionSource.COMPONENT, "handler_name": TransactionSource.COMPONENT, "method_and_path_pattern": TransactionSource.ROUTE, "path": TransactionSource.URL, "route_name": TransactionSource.COMPONENT, "route_pattern": TransactionSource.ROUTE, "uri_template": TransactionSource.ROUTE, "url": TransactionSource.ROUTE, } def get_span_status_from_http_code(http_status_code: int) -> str: """ Returns the Sentry status corresponding to the given HTTP status code. See: https://develop.sentry.dev/sdk/event-payloads/contexts/#trace-context """ if http_status_code < 400: return SPANSTATUS.OK elif 400 <= http_status_code < 500: if http_status_code == 403: return SPANSTATUS.PERMISSION_DENIED elif http_status_code == 404: return SPANSTATUS.NOT_FOUND elif http_status_code == 429: return SPANSTATUS.RESOURCE_EXHAUSTED elif http_status_code == 413: return SPANSTATUS.FAILED_PRECONDITION elif http_status_code == 401: return SPANSTATUS.UNAUTHENTICATED elif http_status_code == 409: return SPANSTATUS.ALREADY_EXISTS else: return SPANSTATUS.INVALID_ARGUMENT elif 500 <= http_status_code < 600: if http_status_code == 504: return SPANSTATUS.DEADLINE_EXCEEDED elif http_status_code == 501: return SPANSTATUS.UNIMPLEMENTED elif http_status_code == 503: return SPANSTATUS.UNAVAILABLE else: return SPANSTATUS.INTERNAL_ERROR return SPANSTATUS.UNKNOWN_ERROR class _SpanRecorder: """Limits the number of spans recorded in a transaction.""" __slots__ = ("maxlen", "spans", "dropped_spans") def __init__(self, maxlen: int) -> None: # FIXME: this is `maxlen - 1` only to preserve historical behavior # enforced by tests. # Either this should be changed to `maxlen` or the JS SDK implementation # should be changed to match a consistent interpretation of what maxlen # limits: either transaction+spans or only child spans. self.maxlen = maxlen - 1 self.spans: "List[Span]" = [] self.dropped_spans: int = 0 def add(self, span: "Span") -> None: if len(self.spans) > self.maxlen: span._span_recorder = None self.dropped_spans += 1 else: self.spans.append(span) class Span: """A span holds timing information of a block of code. Spans can have multiple child spans thus forming a span tree. :param trace_id: The trace ID of the root span. If this new span is to be the root span, omit this parameter, and a new trace ID will be generated. :param span_id: The span ID of this span. If omitted, a new span ID will be generated. :param parent_span_id: The span ID of the parent span, if applicable. :param same_process_as_parent: Whether this span is in the same process as the parent span. :param sampled: Whether the span should be sampled. Overrides the default sampling decision for this span when provided. :param op: The span's operation. A list of recommended values is available here: https://develop.sentry.dev/sdk/performance/span-operations/ :param description: A description of what operation is being performed within the span. .. deprecated:: 2.15.0 Please use the `name` parameter, instead. :param name: A string describing what operation is being performed within the span. :param hub: The hub to use for this span. .. deprecated:: 2.0.0 Please use the `scope` parameter, instead. :param status: The span's status. Possible values are listed at https://develop.sentry.dev/sdk/event-payloads/span/ :param containing_transaction: The transaction that this span belongs to. :param start_timestamp: The timestamp when the span started. If omitted, the current time will be used. :param scope: The scope to use for this span. If not provided, we use the current scope. """ __slots__ = ( "_trace_id", "_span_id", "parent_span_id", "same_process_as_parent", "sampled", "op", "description", "_measurements", "start_timestamp", "_start_timestamp_monotonic_ns", "status", "timestamp", "_tags", "_data", "_span_recorder", "hub", "_context_manager_state", "_containing_transaction", "scope", "origin", "name", "_flags", "_flags_capacity", ) def __init__( self, trace_id: "Optional[str]" = None, span_id: "Optional[str]" = None, parent_span_id: "Optional[str]" = None, same_process_as_parent: bool = True, sampled: "Optional[bool]" = None, op: "Optional[str]" = None, description: "Optional[str]" = None, hub: "Optional[sentry_sdk.Hub]" = None, # deprecated status: "Optional[str]" = None, containing_transaction: "Optional[Transaction]" = None, start_timestamp: "Optional[Union[datetime, float]]" = None, scope: "Optional[sentry_sdk.Scope]" = None, origin: str = "manual", name: "Optional[str]" = None, ) -> None: self._trace_id = trace_id self._span_id = span_id self.parent_span_id = parent_span_id self.same_process_as_parent = same_process_as_parent self.sampled = sampled self.op = op self.description = name or description self.status = status self.hub = hub # backwards compatibility self.scope = scope self.origin = origin self._measurements: "Dict[str, MeasurementValue]" = {} self._tags: "MutableMapping[str, str]" = {} self._data: "Dict[str, Any]" = {} self._containing_transaction = containing_transaction self._flags: "Dict[str, bool]" = {} self._flags_capacity = 10 if hub is not None: warnings.warn( "The `hub` parameter is deprecated. Please use `scope` instead.", DeprecationWarning, stacklevel=2, ) self.scope = self.scope or hub.scope if start_timestamp is None: start_timestamp = datetime.now(timezone.utc) elif isinstance(start_timestamp, float): start_timestamp = datetime.fromtimestamp(start_timestamp, timezone.utc) self.start_timestamp = start_timestamp try: # profiling depends on this value and requires that # it is measured in nanoseconds self._start_timestamp_monotonic_ns = nanosecond_time() except AttributeError: pass #: End timestamp of span self.timestamp: "Optional[datetime]" = None self._span_recorder: "Optional[_SpanRecorder]" = None self.update_active_thread() self.set_profiler_id(get_profiler_id()) # TODO this should really live on the Transaction class rather than the Span # class def init_span_recorder(self, maxlen: int) -> None: if self._span_recorder is None: self._span_recorder = _SpanRecorder(maxlen) @property def trace_id(self) -> str: if not self._trace_id: self._trace_id = uuid.uuid4().hex return self._trace_id @trace_id.setter def trace_id(self, value: str) -> None: self._trace_id = value @property def span_id(self) -> str: if not self._span_id: self._span_id = uuid.uuid4().hex[16:] return self._span_id @span_id.setter def span_id(self, value: str) -> None: self._span_id = value def __repr__(self) -> str: return ( "<%s(op=%r, description:%r, trace_id=%r, span_id=%r, parent_span_id=%r, sampled=%r, origin=%r)>" % ( self.__class__.__name__, self.op, self.description, self.trace_id, self.span_id, self.parent_span_id, self.sampled, self.origin, ) ) def __enter__(self) -> "Span": scope = self.scope or sentry_sdk.get_current_scope() old_span = scope.span scope.span = self self._context_manager_state = (scope, old_span) return self def __exit__( self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" ) -> None: if value is not None and should_be_treated_as_error(ty, value): self.set_status(SPANSTATUS.INTERNAL_ERROR) with capture_internal_exceptions(): scope, old_span = self._context_manager_state del self._context_manager_state self.finish(scope) scope.span = old_span @property def containing_transaction(self) -> "Optional[Transaction]": """The ``Transaction`` that this span belongs to. The ``Transaction`` is the root of the span tree, so one could also think of this ``Transaction`` as the "root span".""" # this is a getter rather than a regular attribute so that transactions # can return `self` here instead (as a way to prevent them circularly # referencing themselves) return self._containing_transaction def start_child( self, instrumenter: str = INSTRUMENTER.SENTRY, **kwargs: "Any" ) -> "Span": """ Start a sub-span from the current span or transaction. Takes the same arguments as the initializer of :py:class:`Span`. The trace id, sampling decision, transaction pointer, and span recorder are inherited from the current span/transaction. The instrumenter parameter is deprecated for user code, and it will be removed in the next major version. Going forward, it should only be used by the SDK itself. """ if kwargs.get("description") is not None: warnings.warn( "The `description` parameter is deprecated. Please use `name` instead.", DeprecationWarning, stacklevel=2, ) configuration_instrumenter = sentry_sdk.get_client().options["instrumenter"] if instrumenter != configuration_instrumenter: return NoOpSpan() kwargs.setdefault("sampled", self.sampled) child = Span( trace_id=self.trace_id, parent_span_id=self.span_id, containing_transaction=self.containing_transaction, **kwargs, ) span_recorder = ( self.containing_transaction and self.containing_transaction._span_recorder ) if span_recorder: span_recorder.add(child) return child