| import uuid |
| import warnings |
| from datetime import datetime, timedelta, timezone |
| from enum import Enum |
|
|
| import sentry_sdk |
| from sentry_sdk.consts import INSTRUMENTER, SPANSTATUS, SPANDATA |
| from sentry_sdk.profiler.continuous_profiler import get_profiler_id |
| from sentry_sdk.utils import ( |
| get_current_thread_meta, |
| is_valid_sample_rate, |
| logger, |
| nanosecond_time, |
| should_be_treated_as_error, |
| ) |
|
|
| from typing import TYPE_CHECKING |
|
|
|
|
| if TYPE_CHECKING: |
| from collections.abc import Callable, Mapping, MutableMapping |
| from typing import Any |
| from typing import Dict |
| from typing import Iterator |
| from typing import List |
| from typing import Optional |
| from typing import overload |
| from typing import ParamSpec |
| from typing import Tuple |
| from typing import Union |
| from typing import TypeVar |
|
|
| from typing_extensions import TypedDict, Unpack |
|
|
| P = ParamSpec("P") |
| R = TypeVar("R") |
|
|
| from sentry_sdk.profiler.continuous_profiler import ContinuousProfile |
| from sentry_sdk.profiler.transaction_profiler import Profile |
| from sentry_sdk._types import ( |
| Event, |
| MeasurementUnit, |
| SamplingContext, |
| MeasurementValue, |
| ) |
|
|
| 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" |
|
|
|
|
| |
| |
| class TransactionSource(str, Enum): |
| COMPONENT = "component" |
| CUSTOM = "custom" |
| ROUTE = "route" |
| TASK = "task" |
| URL = "url" |
| VIEW = "view" |
|
|
| def __str__(self): |
| |
| return self.value |
|
|
|
|
| |
| 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): |
| |
| """ |
| 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): |
| |
| |
| |
| |
| |
| |
| self.maxlen = maxlen - 1 |
| self.spans = [] |
| self.dropped_spans = 0 |
|
|
| def add(self, span): |
| |
| 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", |
| "_local_aggregator", |
| "scope", |
| "origin", |
| "name", |
| ) |
|
|
| def __init__( |
| self, |
| trace_id=None, |
| span_id=None, |
| parent_span_id=None, |
| same_process_as_parent=True, |
| sampled=None, |
| op=None, |
| description=None, |
| hub=None, |
| status=None, |
| containing_transaction=None, |
| start_timestamp=None, |
| scope=None, |
| origin="manual", |
| name=None, |
| ): |
| |
| self.trace_id = trace_id or uuid.uuid4().hex |
| self.span_id = span_id or uuid.uuid4().hex[16:] |
| 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 |
| self.scope = scope |
| self.origin = origin |
| self._measurements = {} |
| self._tags = {} |
| self._data = {} |
| self._containing_transaction = containing_transaction |
|
|
| 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: |
| |
| |
| self._start_timestamp_monotonic_ns = nanosecond_time() |
| except AttributeError: |
| pass |
|
|
| |
| self.timestamp = None |
|
|
| self._span_recorder = None |
| self._local_aggregator = None |
|
|
| self.update_active_thread() |
| self.set_profiler_id(get_profiler_id()) |
|
|
| |
| |
| def init_span_recorder(self, maxlen): |
| |
| if self._span_recorder is None: |
| self._span_recorder = _SpanRecorder(maxlen) |
|
|
| def _get_local_aggregator(self): |
| |
| rv = self._local_aggregator |
| if rv is None: |
| rv = self._local_aggregator = LocalAggregator() |
| return rv |
|
|
| def __repr__(self): |
| |
| 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): |
| |
| 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, value, tb): |
| |
| if value is not None and should_be_treated_as_error(ty, value): |
| self.set_status(SPANSTATUS.INTERNAL_ERROR) |
|
|
| scope, old_span = self._context_manager_state |
| del self._context_manager_state |
| self.finish(scope) |
| scope.span = old_span |
|
|
| @property |
| def containing_transaction(self): |
| |
| """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".""" |
|
|
| |
| |
| |
| return self._containing_transaction |
|
|
| def start_child(self, instrumenter=INSTRUMENTER.SENTRY, **kwargs): |
| |
| """ |
| 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 |
|
|
| @classmethod |
| def continue_from_environ( |
| cls, |
| environ, |
| **kwargs, |
| ): |
| |
| """ |
| Create a Transaction with the given params, then add in data pulled from |
| the ``sentry-trace`` and ``baggage`` headers from the environ (if any) |
| before returning the Transaction. |
| |
| This is different from :py:meth:`~sentry_sdk.tracing.Span.continue_from_headers` |
| in that it assumes header names in the form ``HTTP_HEADER_NAME`` - |
| such as you would get from a WSGI/ASGI environ - |
| rather than the form ``header-name``. |
| |
| :param environ: The ASGI/WSGI environ to pull information from. |
| """ |
| if cls is Span: |
| logger.warning( |
| "Deprecated: use Transaction.continue_from_environ " |
| "instead of Span.continue_from_environ." |
| ) |
| return Transaction.continue_from_headers(EnvironHeaders(environ), **kwargs) |
|
|
| @classmethod |
| def continue_from_headers( |
| cls, |
| headers, |
| *, |
| _sample_rand=None, |
| **kwargs, |
| ): |
| |
| """ |
| Create a transaction with the given params (including any data pulled from |
| the ``sentry-trace`` and ``baggage`` headers). |
| |
| :param headers: The dictionary with the HTTP headers to pull information from. |
| :param _sample_rand: If provided, we override the sample_rand value from the |
| incoming headers with this value. (internal use only) |
| """ |
| |
| if cls is Span: |
| logger.warning( |
| "Deprecated: use Transaction.continue_from_headers " |
| "instead of Span.continue_from_headers." |
| ) |
|
|
| |
| |
| baggage = Baggage.from_incoming_header( |
| headers.get(BAGGAGE_HEADER_NAME), _sample_rand=_sample_rand |
| ) |
| kwargs.update({BAGGAGE_HEADER_NAME: baggage}) |
|
|
| sentrytrace_kwargs = extract_sentrytrace_data( |
| headers.get(SENTRY_TRACE_HEADER_NAME) |
| ) |
|
|
| if sentrytrace_kwargs is not None: |
| kwargs.update(sentrytrace_kwargs) |
|
|
| |
| |
| |
| baggage.freeze() |
|
|
| transaction = Transaction(**kwargs) |
| transaction.same_process_as_parent = False |
|
|
| return transaction |
|
|
| def iter_headers(self): |
| |
| """ |
| Creates a generator which returns the span's ``sentry-trace`` and ``baggage`` headers. |
| If the span's containing transaction doesn't yet have a ``baggage`` value, |
| this will cause one to be generated and stored. |
| """ |
| if not self.containing_transaction: |
| |
| |
| |
| |
| return |
|
|
| yield SENTRY_TRACE_HEADER_NAME, self.to_traceparent() |
|
|
| baggage = self.containing_transaction.get_baggage().serialize() |
| if baggage: |
| yield BAGGAGE_HEADER_NAME, baggage |
|
|
| @classmethod |
| def from_traceparent( |
| cls, |
| traceparent, |
| **kwargs, |
| ): |
| |
| """ |
| DEPRECATED: Use :py:meth:`sentry_sdk.tracing.Span.continue_from_headers`. |
| |
| Create a ``Transaction`` with the given params, then add in data pulled from |
| the given ``sentry-trace`` header value before returning the ``Transaction``. |
| """ |
| logger.warning( |
| "Deprecated: Use Transaction.continue_from_headers(headers, **kwargs) " |
| "instead of from_traceparent(traceparent, **kwargs)" |
| ) |
|
|
| if not traceparent: |
| return None |
|
|
| return cls.continue_from_headers( |
| {SENTRY_TRACE_HEADER_NAME: traceparent}, **kwargs |
| ) |
|
|
| def to_traceparent(self): |
| |
| if self.sampled is True: |
| sampled = "1" |
| elif self.sampled is False: |
| sampled = "0" |
| else: |
| sampled = None |
|
|
| traceparent = "%s-%s" % (self.trace_id, self.span_id) |
| if sampled is not None: |
| traceparent += "-%s" % (sampled,) |
|
|
| return traceparent |
|
|
| def to_baggage(self): |
| |
| """Returns the :py:class:`~sentry_sdk.tracing_utils.Baggage` |
| associated with this ``Span``, if any. (Taken from the root of the span tree.) |
| """ |
| if self.containing_transaction: |
| return self.containing_transaction.get_baggage() |
| return None |
|
|
| def set_tag(self, key, value): |
| |
| self._tags[key] = value |
|
|
| def set_data(self, key, value): |
| |
| self._data[key] = value |
|
|
| def set_status(self, value): |
| |
| self.status = value |
|
|
| def set_measurement(self, name, value, unit=""): |
| |
| self._measurements[name] = {"value": value, "unit": unit} |
|
|
| def set_thread(self, thread_id, thread_name): |
| |
|
|
| if thread_id is not None: |
| self.set_data(SPANDATA.THREAD_ID, str(thread_id)) |
|
|
| if thread_name is not None: |
| self.set_data(SPANDATA.THREAD_NAME, thread_name) |
|
|
| def set_profiler_id(self, profiler_id): |
| |
| if profiler_id is not None: |
| self.set_data(SPANDATA.PROFILER_ID, profiler_id) |
|
|
| def set_http_status(self, http_status): |
| |
| self.set_tag( |
| "http.status_code", str(http_status) |
| ) |
| self.set_data(SPANDATA.HTTP_STATUS_CODE, http_status) |
| self.set_status(get_span_status_from_http_code(http_status)) |
|
|
| def is_success(self): |
| |
| return self.status == "ok" |
|
|
| def finish(self, scope=None, end_timestamp=None): |
| |
| """ |
| Sets the end timestamp of the span. |
| |
| Additionally it also creates a breadcrumb from the span, |
| if the span represents a database or HTTP request. |
| |
| :param scope: The scope to use for this transaction. |
| If not provided, the current scope will be used. |
| :param end_timestamp: Optional timestamp that should |
| be used as timestamp instead of the current time. |
| |
| :return: Always ``None``. The type is ``Optional[str]`` to match |
| the return value of :py:meth:`sentry_sdk.tracing.Transaction.finish`. |
| """ |
| if self.timestamp is not None: |
| |
| return None |
|
|
| try: |
| if end_timestamp: |
| if isinstance(end_timestamp, float): |
| end_timestamp = datetime.fromtimestamp(end_timestamp, timezone.utc) |
| self.timestamp = end_timestamp |
| else: |
| elapsed = nanosecond_time() - self._start_timestamp_monotonic_ns |
| self.timestamp = self.start_timestamp + timedelta( |
| microseconds=elapsed / 1000 |
| ) |
| except AttributeError: |
| self.timestamp = datetime.now(timezone.utc) |
|
|
| scope = scope or sentry_sdk.get_current_scope() |
| maybe_create_breadcrumbs_from_span(scope, self) |
|
|
| return None |
|
|
| def to_json(self): |
| |
| """Returns a JSON-compatible representation of the span.""" |
|
|
| rv = { |
| "trace_id": self.trace_id, |
| "span_id": self.span_id, |
| "parent_span_id": self.parent_span_id, |
| "same_process_as_parent": self.same_process_as_parent, |
| "op": self.op, |
| "description": self.description, |
| "start_timestamp": self.start_timestamp, |
| "timestamp": self.timestamp, |
| "origin": self.origin, |
| } |
|
|
| if self.status: |
| self._tags["status"] = self.status |
|
|
| if self._local_aggregator is not None: |
| metrics_summary = self._local_aggregator.to_json() |
| if metrics_summary: |
| rv["_metrics_summary"] = metrics_summary |
|
|
| if len(self._measurements) > 0: |
| rv["measurements"] = self._measurements |
|
|
| tags = self._tags |
| if tags: |
| rv["tags"] = tags |
|
|
| data = self._data |
| if data: |
| rv["data"] = data |
|
|
| return rv |
|
|
| def get_trace_context(self): |
| |
| rv = { |
| "trace_id": self.trace_id, |
| "span_id": self.span_id, |
| "parent_span_id": self.parent_span_id, |
| "op": self.op, |
| "description": self.description, |
| "origin": self.origin, |
| } |
| if self.status: |
| rv["status"] = self.status |
|
|
| if self.containing_transaction: |
| rv["dynamic_sampling_context"] = ( |
| self.containing_transaction.get_baggage().dynamic_sampling_context() |
| ) |
|
|
| data = {} |
|
|
| thread_id = self._data.get(SPANDATA.THREAD_ID) |
| if thread_id is not None: |
| data["thread.id"] = thread_id |
|
|
| thread_name = self._data.get(SPANDATA.THREAD_NAME) |
| if thread_name is not None: |
| data["thread.name"] = thread_name |
|
|
| if data: |
| rv["data"] = data |
|
|
| return rv |
|
|
| def get_profile_context(self): |
| |
| profiler_id = self._data.get(SPANDATA.PROFILER_ID) |
| if profiler_id is None: |
| return None |
|
|
| return { |
| "profiler_id": profiler_id, |
| } |
|
|
| def update_active_thread(self): |
| |
| thread_id, thread_name = get_current_thread_meta() |
| self.set_thread(thread_id, thread_name) |
|
|
|
|
| class Transaction(Span): |
| """The Transaction is the root element that holds all the spans |
| for Sentry performance instrumentation. |
| |
| :param name: Identifier of the transaction. |
| Will show up in the Sentry UI. |
| :param parent_sampled: Whether the parent transaction was sampled. |
| If True this transaction will be kept, if False it will be discarded. |
| :param baggage: The W3C baggage header value. |
| (see https://www.w3.org/TR/baggage/) |
| :param source: 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". |
| :param kwargs: Additional arguments to be passed to the Span constructor. |
| See :py:class:`sentry_sdk.tracing.Span` for available arguments. |
| """ |
|
|
| __slots__ = ( |
| "name", |
| "source", |
| "parent_sampled", |
| |
| "sample_rate", |
| "_measurements", |
| "_contexts", |
| "_profile", |
| "_continuous_profile", |
| "_baggage", |
| "_sample_rand", |
| ) |
|
|
| def __init__( |
| self, |
| name="", |
| parent_sampled=None, |
| baggage=None, |
| source=TransactionSource.CUSTOM, |
| **kwargs, |
| ): |
| |
|
|
| super().__init__(**kwargs) |
|
|
| self.name = name |
| self.source = source |
| self.sample_rate = None |
| self.parent_sampled = parent_sampled |
| self._measurements = {} |
| self._contexts = {} |
| self._profile = None |
| self._continuous_profile = None |
| self._baggage = baggage |
|
|
| baggage_sample_rand = ( |
| None if self._baggage is None else self._baggage._sample_rand() |
| ) |
| if baggage_sample_rand is not None: |
| self._sample_rand = baggage_sample_rand |
| else: |
| self._sample_rand = _generate_sample_rand(self.trace_id) |
|
|
| def __repr__(self): |
| |
| return ( |
| "<%s(name=%r, op=%r, trace_id=%r, span_id=%r, parent_span_id=%r, sampled=%r, source=%r, origin=%r)>" |
| % ( |
| self.__class__.__name__, |
| self.name, |
| self.op, |
| self.trace_id, |
| self.span_id, |
| self.parent_span_id, |
| self.sampled, |
| self.source, |
| self.origin, |
| ) |
| ) |
|
|
| def _possibly_started(self): |
| |
| """Returns whether the transaction might have been started. |
| |
| If this returns False, we know that the transaction was not started |
| with sentry_sdk.start_transaction, and therefore the transaction will |
| be discarded. |
| """ |
|
|
| |
| return self._span_recorder is not None or self.sampled is False |
|
|
| def __enter__(self): |
| |
| if not self._possibly_started(): |
| logger.debug( |
| "Transaction was entered without being started with sentry_sdk.start_transaction." |
| "The transaction will not be sent to Sentry. To fix, start the transaction by" |
| "passing it to sentry_sdk.start_transaction." |
| ) |
|
|
| super().__enter__() |
|
|
| if self._profile is not None: |
| self._profile.__enter__() |
|
|
| return self |
|
|
| def __exit__(self, ty, value, tb): |
| |
| if self._profile is not None: |
| self._profile.__exit__(ty, value, tb) |
|
|
| if self._continuous_profile is not None: |
| self._continuous_profile.stop() |
|
|
| super().__exit__(ty, value, tb) |
|
|
| @property |
| def containing_transaction(self): |
| |
| """The root element of the span tree. |
| In the case of a transaction it is the transaction itself. |
| """ |
|
|
| |
| |
| |
| return self |
|
|
| def _get_scope_from_finish_args( |
| self, |
| scope_arg, |
| hub_arg, |
| ): |
| |
| """ |
| Logic to get the scope from the arguments passed to finish. This |
| function exists for backwards compatibility with the old finish. |
| |
| TODO: Remove this function in the next major version. |
| """ |
| scope_or_hub = scope_arg |
| if hub_arg is not None: |
| warnings.warn( |
| "The `hub` parameter is deprecated. Please use the `scope` parameter, instead.", |
| DeprecationWarning, |
| stacklevel=3, |
| ) |
|
|
| scope_or_hub = hub_arg |
|
|
| if isinstance(scope_or_hub, sentry_sdk.Hub): |
| warnings.warn( |
| "Passing a Hub to finish is deprecated. Please pass a Scope, instead.", |
| DeprecationWarning, |
| stacklevel=3, |
| ) |
|
|
| return scope_or_hub.scope |
|
|
| return scope_or_hub |
|
|
| def finish( |
| self, |
| scope=None, |
| end_timestamp=None, |
| *, |
| hub=None, |
| ): |
| |
| """Finishes the transaction and sends it to Sentry. |
| All finished spans in the transaction will also be sent to Sentry. |
| |
| :param scope: The Scope to use for this transaction. |
| If not provided, the current Scope will be used. |
| :param end_timestamp: Optional timestamp that should |
| be used as timestamp instead of the current time. |
| :param hub: The hub to use for this transaction. |
| This argument is DEPRECATED. Please use the `scope` |
| parameter, instead. |
| |
| :return: The event ID if the transaction was sent to Sentry, |
| otherwise None. |
| """ |
| if self.timestamp is not None: |
| |
| return None |
|
|
| |
| |
| scope = self._get_scope_from_finish_args( |
| scope, hub |
| ) |
|
|
| scope = scope or self.scope or sentry_sdk.get_current_scope() |
| client = sentry_sdk.get_client() |
|
|
| if not client.is_active(): |
| |
| return None |
|
|
| if self._span_recorder is None: |
| |
| if self.sampled is False: |
| logger.debug("Discarding transaction because sampled = False") |
| else: |
| logger.debug( |
| "Discarding transaction because it was not started with sentry_sdk.start_transaction" |
| ) |
|
|
| |
| |
| |
| if client.transport and has_tracing_enabled(client.options): |
| if client.monitor and client.monitor.downsample_factor > 0: |
| reason = "backpressure" |
| else: |
| reason = "sample_rate" |
|
|
| client.transport.record_lost_event(reason, data_category="transaction") |
|
|
| |
| client.transport.record_lost_event(reason, data_category="span") |
| return None |
|
|
| if not self.name: |
| logger.warning( |
| "Transaction has no name, falling back to `<unlabeled transaction>`." |
| ) |
| self.name = "<unlabeled transaction>" |
|
|
| super().finish(scope, end_timestamp) |
|
|
| if not self.sampled: |
| |
| |
| if self.sampled is None: |
| logger.warning("Discarding transaction without sampling decision.") |
|
|
| return None |
|
|
| finished_spans = [ |
| span.to_json() |
| for span in self._span_recorder.spans |
| if span.timestamp is not None |
| ] |
|
|
| len_diff = len(self._span_recorder.spans) - len(finished_spans) |
| dropped_spans = len_diff + self._span_recorder.dropped_spans |
|
|
| |
| |
| |
| |
| self._span_recorder = None |
|
|
| contexts = {} |
| contexts.update(self._contexts) |
| contexts.update({"trace": self.get_trace_context()}) |
| profile_context = self.get_profile_context() |
| if profile_context is not None: |
| contexts.update({"profile": profile_context}) |
|
|
| event = { |
| "type": "transaction", |
| "transaction": self.name, |
| "transaction_info": {"source": self.source}, |
| "contexts": contexts, |
| "tags": self._tags, |
| "timestamp": self.timestamp, |
| "start_timestamp": self.start_timestamp, |
| "spans": finished_spans, |
| } |
|
|
| if dropped_spans > 0: |
| event["_dropped_spans"] = dropped_spans |
|
|
| if self._profile is not None and self._profile.valid(): |
| event["profile"] = self._profile |
| self._profile = None |
|
|
| event["measurements"] = self._measurements |
|
|
| |
| |
| if self._local_aggregator is not None: |
| metrics_summary = self._local_aggregator.to_json() |
| if metrics_summary: |
| event["_metrics_summary"] = metrics_summary |
|
|
| return scope.capture_event(event) |
|
|
| def set_measurement(self, name, value, unit=""): |
| |
| self._measurements[name] = {"value": value, "unit": unit} |
|
|
| def set_context(self, key, value): |
| |
| """Sets a context. Transactions can have multiple contexts |
| and they should follow the format described in the "Contexts Interface" |
| documentation. |
| |
| :param key: The name of the context. |
| :param value: The information about the context. |
| """ |
| self._contexts[key] = value |
|
|
| def set_http_status(self, http_status): |
| |
| """Sets the status of the Transaction according to the given HTTP status. |
| |
| :param http_status: The HTTP status code.""" |
| super().set_http_status(http_status) |
| self.set_context("response", {"status_code": http_status}) |
|
|
| def to_json(self): |
| |
| """Returns a JSON-compatible representation of the transaction.""" |
| rv = super().to_json() |
|
|
| rv["name"] = self.name |
| rv["source"] = self.source |
| rv["sampled"] = self.sampled |
|
|
| return rv |
|
|
| def get_trace_context(self): |
| |
| trace_context = super().get_trace_context() |
|
|
| if self._data: |
| trace_context["data"] = self._data |
|
|
| return trace_context |
|
|
| def get_baggage(self): |
| |
| """Returns the :py:class:`~sentry_sdk.tracing_utils.Baggage` |
| associated with the Transaction. |
| |
| 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_transaction(self) |
|
|
| return self._baggage |
|
|
| def _set_initial_sampling_decision(self, sampling_context): |
| |
| """ |
| Sets the transaction's sampling decision, according to the following |
| precedence rules: |
| |
| 1. If a sampling decision is passed to `start_transaction` |
| (`start_transaction(name: "my transaction", sampled: True)`), that |
| decision will be used, regardless of anything else |
| |
| 2. If `traces_sampler` is defined, its decision will be used. It can |
| choose to keep or ignore any parent sampling decision, or use the |
| sampling context data to make its own decision or to choose a sample |
| rate for the transaction. |
| |
| 3. If `traces_sampler` is not defined, but there's a parent sampling |
| decision, the parent sampling decision will be used. |
| |
| 4. If `traces_sampler` is not defined and there's no parent sampling |
| decision, `traces_sample_rate` will be used. |
| """ |
| client = sentry_sdk.get_client() |
|
|
| transaction_description = "{op}transaction <{name}>".format( |
| op=("<" + self.op + "> " if self.op else ""), name=self.name |
| ) |
|
|
| |
| if not has_tracing_enabled(client.options): |
| self.sampled = False |
| return |
|
|
| |
| |
| if self.sampled is not None: |
| self.sample_rate = float(self.sampled) |
| return |
|
|
| |
| |
| |
| sample_rate = ( |
| client.options["traces_sampler"](sampling_context) |
| if callable(client.options.get("traces_sampler")) |
| else ( |
| |
| sampling_context["parent_sampled"] |
| if sampling_context["parent_sampled"] is not None |
| else client.options["traces_sample_rate"] |
| ) |
| ) |
|
|
| |
| |
| |
| if not is_valid_sample_rate(sample_rate, source="Tracing"): |
| logger.warning( |
| "[Tracing] Discarding {transaction_description} because of invalid sample rate.".format( |
| transaction_description=transaction_description, |
| ) |
| ) |
| self.sampled = False |
| return |
|
|
| self.sample_rate = float(sample_rate) |
|
|
| if client.monitor: |
| self.sample_rate /= 2**client.monitor.downsample_factor |
|
|
| |
| |
| if not self.sample_rate: |
| logger.debug( |
| "[Tracing] Discarding {transaction_description} because {reason}".format( |
| transaction_description=transaction_description, |
| reason=( |
| "traces_sampler returned 0 or False" |
| if callable(client.options.get("traces_sampler")) |
| else "traces_sample_rate is set to 0" |
| ), |
| ) |
| ) |
| self.sampled = False |
| return |
|
|
| |
| |
| |
| self.sampled = self._sample_rand < self.sample_rate |
|
|
| if self.sampled: |
| logger.debug( |
| "[Tracing] Starting {transaction_description}".format( |
| transaction_description=transaction_description, |
| ) |
| ) |
| else: |
| logger.debug( |
| "[Tracing] Discarding {transaction_description} because it's not included in the random sample (sampling rate = {sample_rate})".format( |
| transaction_description=transaction_description, |
| sample_rate=self.sample_rate, |
| ) |
| ) |
|
|
|
|
| class NoOpSpan(Span): |
| def __repr__(self): |
| |
| return "<%s>" % self.__class__.__name__ |
|
|
| @property |
| def containing_transaction(self): |
| |
| return None |
|
|
| def start_child(self, instrumenter=INSTRUMENTER.SENTRY, **kwargs): |
| |
| return NoOpSpan() |
|
|
| def to_traceparent(self): |
| |
| return "" |
|
|
| def to_baggage(self): |
| |
| return None |
|
|
| def get_baggage(self): |
| |
| return None |
|
|
| def iter_headers(self): |
| |
| return iter(()) |
|
|
| def set_tag(self, key, value): |
| |
| pass |
|
|
| def set_data(self, key, value): |
| |
| pass |
|
|
| def set_status(self, value): |
| |
| pass |
|
|
| def set_http_status(self, http_status): |
| |
| pass |
|
|
| def is_success(self): |
| |
| return True |
|
|
| def to_json(self): |
| |
| return {} |
|
|
| def get_trace_context(self): |
| |
| return {} |
|
|
| def get_profile_context(self): |
| |
| return {} |
|
|
| def finish( |
| self, |
| scope=None, |
| end_timestamp=None, |
| *, |
| hub=None, |
| ): |
| |
| """ |
| The `hub` parameter is deprecated. Please use the `scope` parameter, instead. |
| """ |
| pass |
|
|
| def set_measurement(self, name, value, unit=""): |
| |
| pass |
|
|
| def set_context(self, key, value): |
| |
| pass |
|
|
| def init_span_recorder(self, maxlen): |
| |
| pass |
|
|
| def _set_initial_sampling_decision(self, sampling_context): |
| |
| pass |
|
|
|
|
| if TYPE_CHECKING: |
|
|
| @overload |
| def trace(func=None): |
| |
| pass |
|
|
| @overload |
| def trace(func): |
| |
| pass |
|
|
|
|
| def trace(func=None): |
| |
| """ |
| Decorator to start a child span under the existing current transaction. |
| If there is no current transaction, then nothing will be traced. |
| |
| .. code-block:: |
| :caption: Usage |
| |
| import sentry_sdk |
| |
| @sentry_sdk.trace |
| def my_function(): |
| ... |
| |
| @sentry_sdk.trace |
| async def my_async_function(): |
| ... |
| """ |
| from sentry_sdk.tracing_utils import start_child_span_decorator |
|
|
| |
| |
| if func: |
| return start_child_span_decorator(func) |
| else: |
| return start_child_span_decorator |
|
|
|
|
| |
|
|
| from sentry_sdk.tracing_utils import ( |
| Baggage, |
| EnvironHeaders, |
| extract_sentrytrace_data, |
| _generate_sample_rand, |
| has_tracing_enabled, |
| maybe_create_breadcrumbs_from_span, |
| ) |
|
|
| with warnings.catch_warnings(): |
| |
| warnings.simplefilter("ignore", DeprecationWarning) |
| from sentry_sdk.metrics import LocalAggregator |
|
|