diff --git a/sentry_sdk/ai/monitoring.py b/sentry_sdk/ai/monitoring.py index e826f3bf90..e149ebe7df 100644 --- a/sentry_sdk/ai/monitoring.py +++ b/sentry_sdk/ai/monitoring.py @@ -3,7 +3,7 @@ import sentry_sdk.utils from sentry_sdk import start_span -from sentry_sdk.tracing import POTelSpan as Span +from sentry_sdk.tracing import Span from sentry_sdk.utils import ContextVar from typing import TYPE_CHECKING diff --git a/sentry_sdk/ai/utils.py b/sentry_sdk/ai/utils.py index 4a972071a9..ed3494f679 100644 --- a/sentry_sdk/ai/utils.py +++ b/sentry_sdk/ai/utils.py @@ -3,7 +3,7 @@ if TYPE_CHECKING: from typing import Any -from sentry_sdk.tracing import POTelSpan as Span +from sentry_sdk.tracing import Span from sentry_sdk.utils import logger diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index a44d3f440e..86577cc500 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -3,7 +3,7 @@ from sentry_sdk import tracing_utils, Client from sentry_sdk._init_implementation import init -from sentry_sdk.tracing import POTelSpan, Transaction, trace +from sentry_sdk.tracing import trace from sentry_sdk.crons import monitor # TODO-neel-potel make 2 scope strategies/impls and switch @@ -239,7 +239,7 @@ def flush( def start_span(**kwargs): - # type: (type.Any) -> POTelSpan + # type: (type.Any) -> Span """ Start and return a span. @@ -256,10 +256,10 @@ def start_span(**kwargs): def start_transaction( - transaction=None, # type: Optional[Transaction] + transaction=None, # type: Optional[Span] **kwargs, # type: Unpack[TransactionKwargs] ): - # type: (...) -> POTelSpan + # type: (...) -> Span """ .. deprecated:: 3.0.0 This function is deprecated and will be removed in a future release. diff --git a/sentry_sdk/integrations/langchain.py b/sentry_sdk/integrations/langchain.py index deb700bde2..c775f9d92b 100644 --- a/sentry_sdk/integrations/langchain.py +++ b/sentry_sdk/integrations/langchain.py @@ -6,7 +6,7 @@ from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS from sentry_sdk.ai.utils import set_data_normalized from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.tracing import POTelSpan as Span +from sentry_sdk.tracing import Span from sentry_sdk.integrations import DidNotEnable, Integration from sentry_sdk.utils import logger, capture_internal_exceptions diff --git a/sentry_sdk/integrations/opentelemetry/scope.py b/sentry_sdk/integrations/opentelemetry/scope.py index 89da1af68c..d16215ab20 100644 --- a/sentry_sdk/integrations/opentelemetry/scope.py +++ b/sentry_sdk/integrations/opentelemetry/scope.py @@ -28,7 +28,7 @@ ) from sentry_sdk.integrations.opentelemetry.utils import trace_state_from_baggage from sentry_sdk.scope import Scope, ScopeType -from sentry_sdk.tracing import POTelSpan +from sentry_sdk.tracing import Span from sentry_sdk._types import TYPE_CHECKING if TYPE_CHECKING: @@ -128,7 +128,7 @@ def _incoming_otel_span_context(self): return span_context def start_transaction(self, **kwargs): - # type: (Unpack[TransactionKwargs]) -> POTelSpan + # type: (Unpack[TransactionKwargs]) -> Span """ .. deprecated:: 3.0.0 This function is deprecated and will be removed in a future release. @@ -137,8 +137,8 @@ def start_transaction(self, **kwargs): return self.start_span(**kwargs) def start_span(self, **kwargs): - # type: (Any) -> POTelSpan - return POTelSpan(**kwargs, scope=self) + # type: (Any) -> Span + return Span(**kwargs, scope=self) _INITIAL_CURRENT_SCOPE = None diff --git a/sentry_sdk/integrations/opentelemetry/span_processor.py b/sentry_sdk/integrations/opentelemetry/span_processor.py index 8d513ec97d..a3cf545daf 100644 --- a/sentry_sdk/integrations/opentelemetry/span_processor.py +++ b/sentry_sdk/integrations/opentelemetry/span_processor.py @@ -98,7 +98,7 @@ def force_flush(self, timeout_millis=30000): def _add_root_span(self, span, parent_span): # type: (Span, AbstractSpan) -> None """ - This is required to make POTelSpan.root_span work + This is required to make Span.root_span work since we can't traverse back to the root purely with otel efficiently. """ if parent_span != INVALID_SPAN and not parent_span.get_span_context().is_remote: diff --git a/sentry_sdk/integrations/rust_tracing.py b/sentry_sdk/integrations/rust_tracing.py index ccf4e5aae0..9b5a83197e 100644 --- a/sentry_sdk/integrations/rust_tracing.py +++ b/sentry_sdk/integrations/rust_tracing.py @@ -37,7 +37,7 @@ import sentry_sdk from sentry_sdk.integrations import Integration from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.tracing import POTelSpan as SentrySpan +from sentry_sdk.tracing import Span from sentry_sdk.utils import SENSITIVE_DATA_SUBSTITUTE @@ -169,7 +169,7 @@ def _include_tracing_fields(self) -> bool: else self.include_tracing_fields ) - def on_event(self, event: str, _span_state: Optional[SentrySpan]) -> None: + def on_event(self, event: str, _span_state: Optional[Span]) -> None: deserialized_event = json.loads(event) metadata = deserialized_event.get("metadata", {}) @@ -183,7 +183,7 @@ def on_event(self, event: str, _span_state: Optional[SentrySpan]) -> None: elif event_type == EventTypeMapping.Event: process_event(deserialized_event) - def on_new_span(self, attrs: str, span_id: str) -> Optional[SentrySpan]: + def on_new_span(self, attrs: str, span_id: str) -> Optional[Span]: attrs = json.loads(attrs) metadata = attrs.get("metadata", {}) @@ -220,11 +220,11 @@ def on_new_span(self, attrs: str, span_id: str) -> Optional[SentrySpan]: return span - def on_close(self, span_id: str, span: Optional[SentrySpan]) -> None: + def on_close(self, span_id: str, span: Optional[Span]) -> None: if span is not None: span.__exit__(None, None, None) - def on_record(self, span_id: str, values: str, span: Optional[SentrySpan]) -> None: + def on_record(self, span_id: str, values: str, span: Optional[Span]) -> None: if span is not None: deserialized_values = json.loads(values) for key, value in deserialized_values.items(): diff --git a/sentry_sdk/integrations/wsgi.py b/sentry_sdk/integrations/wsgi.py index 7f7360a341..e9cc65d716 100644 --- a/sentry_sdk/integrations/wsgi.py +++ b/sentry_sdk/integrations/wsgi.py @@ -12,7 +12,7 @@ _request_headers_to_span_attributes, ) from sentry_sdk.sessions import track_session -from sentry_sdk.tracing import Transaction, TRANSACTION_SOURCE_ROUTE +from sentry_sdk.tracing import Span, TRANSACTION_SOURCE_ROUTE from sentry_sdk.utils import ( ContextVar, capture_internal_exceptions, @@ -157,7 +157,7 @@ def __call__(self, environ, start_response): def _sentry_start_response( # type: ignore old_start_response, # type: StartResponse - transaction, # type: Optional[Transaction] + transaction, # type: Optional[Span] status, # type: str response_headers, # type: WsgiResponseHeaders exc_info=None, # type: Optional[WsgiExcInfo] diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 09595f88d2..af69aca4ee 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -26,8 +26,6 @@ SENTRY_TRACE_HEADER_NAME, NoOpSpan, Span, - POTelSpan, - Transaction, ) from sentry_sdk.utils import ( capture_internal_exception, @@ -677,7 +675,7 @@ def clear(self): self.clear_breadcrumbs() self._should_capture = True # type: bool - self._span = None # type: Optional[POTelSpan] + self._span = None # type: Optional[Span] self._session = None # type: Optional[Session] self._force_auto_session_tracking = None # type: Optional[bool] @@ -707,7 +705,7 @@ def fingerprint(self, value): @property def transaction(self): # type: () -> Any - # would be type: () -> Optional[Transaction], see https://github.com/python/mypy/issues/3004 + # would be type: () -> Optional[Span], see https://github.com/python/mypy/issues/3004 """Return the transaction (root span) in the scope, if any.""" # there is no span/transaction on the scope @@ -734,7 +732,7 @@ def transaction(self, value): # anything set in the scope. # XXX: note that with the introduction of the Scope.transaction getter, # there is a semantic and type mismatch between getter and setter. The - # getter returns a Transaction, the setter sets a transaction name. + # getter returns a Span, the setter sets a transaction name. # Without breaking version compatibility, we could make the setter set a # transaction name or transaction (self._span) depending on the type of # the value argument. @@ -785,13 +783,13 @@ def set_user(self, value): @property def span(self): - # type: () -> Optional[POTelSpan] + # type: () -> Optional[Span] """Get current tracing span.""" return self._span @span.setter def span(self, span): - # type: (Optional[POTelSpan]) -> None + # type: (Optional[Span]) -> None """Set current tracing span.""" self._span = span @@ -952,7 +950,7 @@ def add_breadcrumb(self, crumb=None, hint=None, **kwargs): self._breadcrumbs.popleft() def start_transaction(self, transaction=None, **kwargs): - # type: (Optional[Transaction], Optional[SamplingContext], Unpack[TransactionKwargs]) -> Union[Transaction, NoOpSpan] + # type: (Optional[Span], Optional[SamplingContext], Unpack[TransactionKwargs]) -> Union[Span, NoOpSpan] """ Start and return a transaction. @@ -981,6 +979,7 @@ def start_transaction(self, transaction=None, **kwargs): constructor. See :py:class:`sentry_sdk.tracing.Transaction` for available arguments. """ + # TODO-neel-potel fix signature and no op kwargs.setdefault("scope", self) client = self.get_client() @@ -988,7 +987,7 @@ def start_transaction(self, transaction=None, **kwargs): try_autostart_continuous_profiler() # if we haven't been given a transaction, make one - transaction = Transaction(**kwargs) + transaction = Span(**kwargs) # use traces_sample_rate, traces_sampler, and/or inheritance to make a # sampling decision @@ -1024,6 +1023,7 @@ def start_span(self, **kwargs): For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Span`. """ + # TODO-neel-potel fix signature and no op if kwargs.get("description") is not None: warnings.warn( "The `description` parameter is deprecated. Please use `name` instead.", @@ -1054,13 +1054,14 @@ def start_span(self, **kwargs): def continue_trace( self, environ_or_headers, op=None, name=None, source=None, origin=None ): - # type: (Dict[str, Any], Optional[str], Optional[str], Optional[str], Optional[str]) -> Transaction + # TODO-neel-potel fix signature and no op + # type: (Dict[str, Any], Optional[str], Optional[str], Optional[str], Optional[str]) -> Span """ Sets the propagation context from environment or headers and returns a transaction. """ self.generate_propagation_context(environ_or_headers) - transaction = Transaction.continue_from_headers( + transaction = Span.continue_from_headers( normalize_incoming_data(environ_or_headers), op=op, origin=origin, diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 3ee155aedb..e67301e1a7 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -1,9 +1,5 @@ import json -import uuid -import random -import time -import warnings -from datetime import datetime, timedelta, timezone +from datetime import datetime from opentelemetry import trace as otel_trace, context from opentelemetry.trace import ( @@ -19,22 +15,19 @@ import sentry_sdk from sentry_sdk.consts import SPANSTATUS, SPANDATA -from sentry_sdk.profiler.continuous_profiler import get_profiler_id from sentry_sdk.utils import ( _serialize_span_attribute, get_current_thread_meta, - is_valid_sample_rate, logger, ) from typing import TYPE_CHECKING, cast if TYPE_CHECKING: - from collections.abc import Callable, Mapping, MutableMapping + from collections.abc import Callable 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 @@ -42,21 +35,19 @@ from typing import Union from typing import TypeVar - from typing_extensions import TypedDict, Unpack - - from opentelemetry.utils import types as OTelSpanAttributes + from typing_extensions import TypedDict P = ParamSpec("P") R = TypeVar("R") import sentry_sdk.profiler from sentry_sdk._types import ( - Event, MeasurementUnit, SamplingContext, - MeasurementValue, ) + from sentry_sdk.tracing_utils import Baggage + class SpanKwargs(TypedDict, total=False): trace_id: str """ @@ -91,7 +82,7 @@ class SpanKwargs(TypedDict, total=False): status: str """The span's status. Possible values are listed at https://develop.sentry.dev/sdk/event-payloads/span/""" - containing_transaction: Optional["Transaction"] + containing_transaction: Optional["Span"] """The transaction that this span belongs to.""" start_timestamp: Optional[Union[datetime, float]] @@ -208,914 +199,14 @@ def get_span_status_from_http_code(http_status_code): return SPANSTATUS.UNKNOWN_ERROR -class _SpanRecorder: - """Limits the number of spans recorded in a transaction.""" - - __slots__ = ("maxlen", "spans") - - def __init__(self, maxlen): - # type: (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 = [] # type: List[Span] - - def add(self, span): - # type: (Span) -> None - if len(self.spans) > self.maxlen: - span._span_recorder = None - 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", - "_context_manager_state", - "_containing_transaction", - "scope", - "origin", - "name", - ) - - def __init__( - self, - trace_id=None, # type: Optional[str] - span_id=None, # type: Optional[str] - parent_span_id=None, # type: Optional[str] - same_process_as_parent=True, # type: bool - sampled=None, # type: Optional[bool] - op=None, # type: Optional[str] - description=None, # type: Optional[str] - status=None, # type: Optional[str] - containing_transaction=None, # type: Optional[Transaction] - start_timestamp=None, # type: Optional[Union[datetime, float]] - scope=None, # type: Optional[sentry_sdk.Scope] - origin=None, # type: Optional[str] - name=None, # type: Optional[str] - ): - # type: (...) -> 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.scope = scope - self.origin = origin or DEFAULT_SPAN_ORIGIN - self._measurements = {} # type: Dict[str, MeasurementValue] - self._tags = {} # type: MutableMapping[str, str] - self._data = {} # type: Dict[str, Any] - self._containing_transaction = containing_transaction - - 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 = time.perf_counter_ns() - except AttributeError: - pass - - #: End timestamp of span - self.timestamp = None # type: Optional[datetime] - - self._span_recorder = None # type: Optional[_SpanRecorder] - - 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): - # type: (int) -> None - if self._span_recorder is None: - self._span_recorder = _SpanRecorder(maxlen) - - def __repr__(self): - # type: () -> 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): - # type: () -> 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, value, tb): - # type: (Optional[Any], Optional[Any], Optional[Any]) -> None - if value is not None: - 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): - # type: () -> 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, **kwargs): - # type: (**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, - ) - - 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, # type: Mapping[str, str] - **kwargs, # type: Any - ): - # type: (...) -> Transaction - """ - 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, # type: Mapping[str, str] - **kwargs, # type: Any - ): - # type: (...) -> Transaction - """ - 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. - """ - # TODO move this to the Transaction class - if cls is Span: - logger.warning( - "Deprecated: use Transaction.continue_from_headers " - "instead of Span.continue_from_headers." - ) - - # TODO-neel move away from this kwargs stuff, it's confusing and opaque - # make more explicit - baggage = Baggage.from_incoming_header(headers.get(BAGGAGE_HEADER_NAME)) - 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) - - # If there's an incoming sentry-trace but no incoming baggage header, - # for instance in traces coming from older SDKs, - # baggage will be empty and immutable and won't be populated as head SDK. - baggage.freeze() - - transaction = Transaction(**kwargs) - transaction.same_process_as_parent = False - - return transaction - - def iter_headers(self): - # type: () -> Iterator[Tuple[str, str]] - """ - 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: - # Do not propagate headers if there is no containing transaction. Otherwise, this - # span ends up being the root span of a new trace, and since it does not get sent - # to Sentry, the trace will be missing a root transaction. The dynamic sampling - # context will also be missing, breaking dynamic sampling & traces. - 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, # type: Optional[str] - **kwargs, # type: Any - ): - # type: (...) -> Optional[Transaction] - """ - 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): - # type: () -> str - 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): - # type: () -> Optional[Baggage] - """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): - # type: (str, Any) -> None - self._tags[key] = value - - def set_data(self, key, value): - # type: (str, Any) -> None - self._data[key] = value - - def set_status(self, value): - # type: (str) -> None - self.status = value - - def set_measurement(self, name, value, unit=""): - # type: (str, float, MeasurementUnit) -> None - self._measurements[name] = {"value": value, "unit": unit} - - def set_thread(self, thread_id, thread_name): - # type: (Optional[int], Optional[str]) -> None - - 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): - # type: (Optional[str]) -> None - if profiler_id is not None: - self.set_data(SPANDATA.PROFILER_ID, profiler_id) - - def set_http_status(self, http_status): - # type: (int) -> None - self.set_tag( - "http.status_code", str(http_status) - ) # we keep this for backwards compatibility - self.set_data(SPANDATA.HTTP_STATUS_CODE, http_status) - self.set_status(get_span_status_from_http_code(http_status)) - - def is_success(self): - # type: () -> bool - return self.status == "ok" - - def finish(self, scope=None, end_timestamp=None): - # type: (Optional[sentry_sdk.Scope], Optional[Union[float, datetime]]) -> Optional[str] - """ - 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: - # This span is already finished, ignore. - 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 = time.perf_counter_ns() - 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): - # type: () -> Dict[str, Any] - """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, - } # type: Dict[str, Any] - - if self.status: - self._tags["status"] = self.status - - 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): - # type: () -> Any - 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, - } # type: Dict[str, Any] - 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): - # type: () -> Optional[ProfileContext] - 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): - # type: () -> None - 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", - # used to create baggage value for head SDKs in dynamic sampling - "sample_rate", - "_measurements", - "_contexts", - "_profile", - "_baggage", - ) - - def __init__( # type: ignore[misc] - self, - name="", # type: str - parent_sampled=None, # type: Optional[bool] - baggage=None, # type: Optional[Baggage] - source=TRANSACTION_SOURCE_CUSTOM, # type: str - **kwargs, # type: Unpack[SpanKwargs] - ): - # type: (...) -> None - - super().__init__(**kwargs) - - self.name = name - self.source = source - self.sample_rate = None # type: Optional[float] - self.parent_sampled = parent_sampled - self._measurements = {} # type: Dict[str, MeasurementValue] - self._contexts = {} # type: Dict[str, Any] - self._profile = ( - None - ) # type: Optional[sentry_sdk.profiler.transaction_profiler.Profile] - self._baggage = baggage - - def __repr__(self): - # type: () -> str - 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): - # type: () -> bool - """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. - """ - - # We must explicitly check self.sampled is False since self.sampled can be None - return self._span_recorder is not None or self.sampled is False - - def __enter__(self): - # type: () -> Transaction - 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): - # type: (Optional[Any], Optional[Any], Optional[Any]) -> None - if self._profile is not None: - self._profile.__exit__(ty, value, tb) - - super().__exit__(ty, value, tb) - - @property - def containing_transaction(self): - # type: () -> Transaction - """The root element of the span tree. - In the case of a transaction it is the transaction itself. - """ - - # Transactions (as spans) belong to themselves (as transactions). This - # is a getter rather than a regular attribute to avoid having a circular - # reference. - return self - - def finish( - self, - scope=None, # type: Optional[sentry_sdk.Scope] - end_timestamp=None, # type: Optional[Union[float, datetime]] - ): - # type: (...) -> Optional[str] - """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. - - :return: The event ID if the transaction was sent to Sentry, - otherwise None. - """ - if self.timestamp is not None: - # This transaction is already finished, ignore. - return None - - scope = scope or self.scope or sentry_sdk.get_current_scope() - client = sentry_sdk.get_client() - - if not client.is_active(): - # We have no active client and therefore nowhere to send this transaction. - return None - - if self._span_recorder is None: - # Explicit check against False needed because self.sampled might be 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" - ) - - # This is not entirely accurate because discards here are not - # exclusively based on sample rate but also traces sampler, but - # we handle this the same here. - 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") - - # Only one span (the transaction itself) is discarded, since we did not record any spans here. - client.transport.record_lost_event(reason, data_category="span") - return None - - if not self.name: - logger.warning( - "Transaction has no name, falling back to ``." - ) - self.name = "" - - super().finish(scope, end_timestamp) - - if not self.sampled: - # At this point a `sampled = None` should have already been resolved - # to a concrete decision. - 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 - ] - - # we do this to break the circular reference of transaction -> span - # recorder -> span -> containing transaction (which is where we started) - # before either the spans or the transaction goes out of scope and has - # to be garbage collected - 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, - } # type: Event - - if self._profile is not None and self._profile.valid(): - event["profile"] = self._profile - self._profile = None - - event["measurements"] = self._measurements - - return scope.capture_event(event) - - def set_measurement(self, name, value, unit=""): - # type: (str, float, MeasurementUnit) -> None - self._measurements[name] = {"value": value, "unit": unit} - - def set_context(self, key, value): - # type: (str, Any) -> None - """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): - # type: (int) -> None - """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): - # type: () -> Dict[str, Any] - """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): - # type: () -> Any - trace_context = super().get_trace_context() - - if self._data: - trace_context["data"] = self._data - - return trace_context - - def get_baggage(self): - # type: () -> Baggage - """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): - # type: (SamplingContext) -> None - """ - 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 - ) - - # nothing to do if tracing is disabled - if not has_tracing_enabled(client.options): - self.sampled = False - return - - # if the user has forced a sampling decision by passing a `sampled` - # value when starting the transaction, go with that - if self.sampled is not None: - self.sample_rate = float(self.sampled) - return - - # we would have bailed already if neither `traces_sampler` nor - # `traces_sample_rate` were defined, so one of these should work; prefer - # the hook if so - sample_rate = ( - client.options["traces_sampler"](sampling_context) - if callable(client.options.get("traces_sampler")) - else ( - # default inheritance behavior - sampling_context["parent_sampled"] - if sampling_context["parent_sampled"] is not None - else client.options["traces_sample_rate"] - ) - ) - - # Since this is coming from the user (or from a function provided by the - # user), who knows what we might get. (The only valid values are - # booleans or numbers between 0 and 1.) - 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 the function returned 0 (or false), or if `traces_sample_rate` is - # 0, it's a sign the transaction should be dropped - 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 - - # Now we roll the dice. random.random is inclusive of 0, but not of 1, - # so strict < is safe here. In case sample_rate is a boolean, cast it - # to a float (True becomes 1.0 and False becomes 0.0) - self.sampled = random.random() < 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): +class NoOpSpan: def __repr__(self): # type: () -> str return "<%s>" % self.__class__.__name__ @property def containing_transaction(self): - # type: () -> Optional[Transaction] + # type: () -> Optional[Span] return None def start_child(self, **kwargs): @@ -1195,7 +286,7 @@ def _set_initial_sampling_decision(self, sampling_context): pass -class POTelSpan: +class Span: """ OTel span wrapper providing compatibility with the old span interface. """ @@ -1211,9 +302,9 @@ def __init__( origin=None, # type: Optional[str] name=None, # type: Optional[str] source=TRANSACTION_SOURCE_CUSTOM, # type: str - attributes=None, # type: OTelSpanAttributes + attributes=None, # type: Optional[dict[str, Any]] only_if_parent=False, # type: bool - parent_span=None, # type: Optional[POTelSpan] + parent_span=None, # type: Optional[Span] otel_span=None, # type: Optional[OtelSpan] **_, # type: dict[str, object] ): @@ -1284,7 +375,7 @@ def __init__( self.set_status(status) def __eq__(self, other): - # type: (POTelSpan) -> bool + # type: (Span) -> bool return self._otel_span == other._otel_span def __repr__(self): @@ -1304,7 +395,7 @@ def __repr__(self): ) def __enter__(self): - # type: () -> POTelSpan + # type: () -> Span # XXX use_span? https://github.com/open-telemetry/opentelemetry-python/blob/3836da8543ce9751051e38a110c0468724042e62/opentelemetry-api/src/opentelemetry/trace/__init__.py#L547 # # create a Context object with parent set as current span @@ -1364,7 +455,7 @@ def origin(self, value): @property def containing_transaction(self): - # type: () -> Optional[POTelSpan] + # type: () -> Optional[Span] """ Get the transaction this span is a child of. @@ -1378,7 +469,7 @@ def containing_transaction(self): @property def root_span(self): - # type: () -> Optional[POTelSpan] + # type: () -> Optional[Span] from sentry_sdk.integrations.opentelemetry.utils import ( get_sentry_meta, ) @@ -1386,7 +477,7 @@ def root_span(self): root_otel_span = cast( "Optional[OtelSpan]", get_sentry_meta(self._otel_span, "root_span") ) - return POTelSpan(otel_span=root_otel_span) if root_otel_span else None + return Span(otel_span=root_otel_span) if root_otel_span else None @property def is_root_span(self): @@ -1515,8 +606,8 @@ def timestamp(self): return convert_from_otel_timestamp(end_time) def start_child(self, **kwargs): - # type: (**Any) -> POTelSpan - return POTelSpan(sampled=self.sampled, parent_span=self, **kwargs) + # type: (**Any) -> Span + return Span(sampled=self.sampled, parent_span=self, **kwargs) def iter_headers(self): # type: () -> Iterator[Tuple[str, str]] @@ -1691,6 +782,10 @@ def set_context(self, key, value): self.set_attribute(f"{SentrySpanAttribute.CONTEXT}.{key}", value) +# TODO-neel-potel add deprecation +Transaction = Span + + if TYPE_CHECKING: @overload @@ -1731,14 +826,3 @@ async def my_async_function(): return start_child_span_decorator(func) else: return start_child_span_decorator - - -# Circular imports - -from sentry_sdk.tracing_utils import ( - Baggage, - EnvironHeaders, - extract_sentrytrace_data, - has_tracing_enabled, - maybe_create_breadcrumbs_from_span, -) diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index e217994839..6ebe7e0322 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -154,12 +154,6 @@ def record_sql_queries( yield span -def maybe_create_breadcrumbs_from_span(scope, span): - # type: (sentry_sdk.Scope, sentry_sdk.tracing.Span) -> None - # TODO: can be removed when POtelSpan replaces Span - pass - - def _get_frame_module_abs_path(frame): # type: (FrameType) -> Optional[str] try: diff --git a/tests/integrations/asyncpg/test_asyncpg.py b/tests/integrations/asyncpg/test_asyncpg.py index 4604557a4a..579052da27 100644 --- a/tests/integrations/asyncpg/test_asyncpg.py +++ b/tests/integrations/asyncpg/test_asyncpg.py @@ -676,7 +676,7 @@ def fake_record_sql_queries(*args, **kwargs): yield span with mock.patch( - "sentry_sdk.tracing.POTelSpan.start_timestamp", + "sentry_sdk.tracing.Span.start_timestamp", datetime.datetime(2024, 1, 1, microsecond=0, tzinfo=datetime.timezone.utc), ): with mock.patch( @@ -723,7 +723,7 @@ def fake_record_sql_queries(*args, **kwargs): yield span with mock.patch( - "sentry_sdk.tracing.POTelSpan.start_timestamp", + "sentry_sdk.tracing.Span.start_timestamp", datetime.datetime(2024, 1, 1, microsecond=0, tzinfo=datetime.timezone.utc), ): with mock.patch( diff --git a/tests/integrations/httpx/test_httpx.py b/tests/integrations/httpx/test_httpx.py index a8a509152f..12a0038f6b 100644 --- a/tests/integrations/httpx/test_httpx.py +++ b/tests/integrations/httpx/test_httpx.py @@ -3,7 +3,6 @@ import httpx import pytest -import responses import sentry_sdk from sentry_sdk import capture_message, start_span @@ -62,7 +61,9 @@ def before_breadcrumb(crumb, hint): "httpx_client", (httpx.Client(), httpx.AsyncClient()), ) -def test_outgoing_trace_headers(sentry_init, httpx_client, capture_envelopes, httpx_mock): +def test_outgoing_trace_headers( + sentry_init, httpx_client, capture_envelopes, httpx_mock +): httpx_mock.add_response() sentry_init( diff --git a/tests/integrations/opentelemetry/test_compat.py b/tests/integrations/opentelemetry/test_compat.py index f2292d9ff2..1ae73494cd 100644 --- a/tests/integrations/opentelemetry/test_compat.py +++ b/tests/integrations/opentelemetry/test_compat.py @@ -19,7 +19,7 @@ def test_transaction_name_span_description_compat( ) as spn: ... - assert trx.__class__.__name__ == "POTelSpan" + assert trx.__class__.__name__ == "Span" assert trx.op == "trx-op" assert trx.name == "trx-name" assert trx.description is None @@ -30,7 +30,7 @@ def test_transaction_name_span_description_compat( assert trx._otel_span.attributes["sentry.name"] == "trx-name" assert "sentry.description" not in trx._otel_span.attributes - assert spn.__class__.__name__ == "POTelSpan" + assert spn.__class__.__name__ == "Span" assert spn.op == "span-op" assert spn.description == "span-desc" assert spn.name == "span-desc" diff --git a/tests/tracing/test_http_headers.py b/tests/tracing/test_http_headers.py deleted file mode 100644 index 6a8467101e..0000000000 --- a/tests/tracing/test_http_headers.py +++ /dev/null @@ -1,56 +0,0 @@ -from unittest import mock - -import pytest - -from sentry_sdk.tracing import Transaction -from sentry_sdk.tracing_utils import extract_sentrytrace_data - - -@pytest.mark.parametrize("sampled", [True, False, None]) -def test_to_traceparent(sampled): - transaction = Transaction( - name="/interactions/other-dogs/new-dog", - op="greeting.sniff", - trace_id="12312012123120121231201212312012", - sampled=sampled, - ) - - traceparent = transaction.to_traceparent() - - parts = traceparent.split("-") - assert parts[0] == "12312012123120121231201212312012" # trace_id - assert parts[1] == transaction.span_id # parent_span_id - if sampled is None: - assert len(parts) == 2 - else: - assert parts[2] == "1" if sampled is True else "0" # sampled - - -@pytest.mark.parametrize("sampling_decision", [True, False]) -def test_sentrytrace_extraction(sampling_decision): - sentrytrace_header = "12312012123120121231201212312012-0415201309082013-{}".format( - 1 if sampling_decision is True else 0 - ) - assert extract_sentrytrace_data(sentrytrace_header) == { - "trace_id": "12312012123120121231201212312012", - "parent_span_id": "0415201309082013", - "parent_sampled": sampling_decision, - } - - -def test_iter_headers(monkeypatch): - monkeypatch.setattr( - Transaction, - "to_traceparent", - mock.Mock(return_value="12312012123120121231201212312012-0415201309082013-0"), - ) - - transaction = Transaction( - name="/interactions/other-dogs/new-dog", - op="greeting.sniff", - ) - - headers = dict(transaction.iter_headers()) - assert ( - headers["sentry-trace"] == "12312012123120121231201212312012-0415201309082013-0" - )