diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py index cf748bb8ea..45afb56cc9 100644 --- a/sentry_sdk/hub.py +++ b/sentry_sdk/hub.py @@ -7,17 +7,10 @@ from sentry_sdk.consts import INSTRUMENTER from sentry_sdk.scope import Scope from sentry_sdk.client import Client -from sentry_sdk.profiler import Profile from sentry_sdk.tracing import ( NoOpSpan, Span, Transaction, - BAGGAGE_HEADER_NAME, - SENTRY_TRACE_HEADER_NAME, -) -from sentry_sdk.tracing_utils import ( - has_tracing_enabled, - normalize_incoming_data, ) from sentry_sdk.utils import ( @@ -28,18 +21,18 @@ from sentry_sdk._types import TYPE_CHECKING if TYPE_CHECKING: - from typing import Union from typing import Any - from typing import Optional - from typing import Tuple - from typing import Dict - from typing import List from typing import Callable + from typing import ContextManager + from typing import Dict from typing import Generator + from typing import List + from typing import Optional + from typing import overload + from typing import Tuple from typing import Type from typing import TypeVar - from typing import overload - from typing import ContextManager + from typing import Union from sentry_sdk.integrations import Integration from sentry_sdk._types import ( @@ -447,54 +440,12 @@ def start_span(self, span=None, instrumenter=INSTRUMENTER.SENTRY, **kwargs): For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Span`. """ - configuration_instrumenter = self.client and self.client.options["instrumenter"] - - if instrumenter != configuration_instrumenter: - return NoOpSpan() - - # THIS BLOCK IS DEPRECATED - # TODO: consider removing this in a future release. - # This is for backwards compatibility with releases before - # start_transaction existed, to allow for a smoother transition. - if isinstance(span, Transaction) or "transaction" in kwargs: - deprecation_msg = ( - "Deprecated: use start_transaction to start transactions and " - "Transaction.start_child to start spans." - ) - - if isinstance(span, Transaction): - logger.warning(deprecation_msg) - return self.start_transaction(span) - - if "transaction" in kwargs: - logger.warning(deprecation_msg) - name = kwargs.pop("transaction") - return self.start_transaction(name=name, **kwargs) - - # THIS BLOCK IS DEPRECATED - # We do not pass a span into start_span in our code base, so I deprecate this. - if span is not None: - deprecation_msg = "Deprecated: passing a span into `start_span` is deprecated and will be removed in the future." - logger.warning(deprecation_msg) - return span - - kwargs.setdefault("hub", self) - - active_span = self.scope.span - if active_span is not None: - new_child_span = active_span.start_child(**kwargs) - return new_child_span + client, scope = self._stack[-1] - # If there is already a trace_id in the propagation context, use it. - # This does not need to be done for `start_child` above because it takes - # the trace_id from the parent span. - if "trace_id" not in kwargs: - traceparent = self.get_traceparent() - trace_id = traceparent.split("-")[0] if traceparent else None - if trace_id is not None: - kwargs["trace_id"] = trace_id + kwargs["hub"] = self + kwargs["client"] = client - return Span(**kwargs) + return scope.start_span(span=span, instrumenter=instrumenter, **kwargs) def start_transaction( self, transaction=None, instrumenter=INSTRUMENTER.SENTRY, **kwargs @@ -524,55 +475,25 @@ def start_transaction( For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Transaction`. """ - configuration_instrumenter = self.client and self.client.options["instrumenter"] - - if instrumenter != configuration_instrumenter: - return NoOpSpan() - - custom_sampling_context = kwargs.pop("custom_sampling_context", {}) - - # if we haven't been given a transaction, make one - if transaction is None: - kwargs.setdefault("hub", self) - transaction = Transaction(**kwargs) - - # use traces_sample_rate, traces_sampler, and/or inheritance to make a - # sampling decision - sampling_context = { - "transaction_context": transaction.to_json(), - "parent_sampled": transaction.parent_sampled, - } - sampling_context.update(custom_sampling_context) - transaction._set_initial_sampling_decision(sampling_context=sampling_context) - - profile = Profile(transaction, hub=self) - profile._set_initial_sampling_decision(sampling_context=sampling_context) + client, scope = self._stack[-1] - # we don't bother to keep spans if we already know we're not going to - # send the transaction - if transaction.sampled: - max_spans = ( - self.client and self.client.options["_experiments"].get("max_spans") - ) or 1000 - transaction.init_span_recorder(maxlen=max_spans) + kwargs["hub"] = self + kwargs["client"] = client - return transaction + return scope.start_transaction( + transaction=transaction, instrumenter=instrumenter, **kwargs + ) def continue_trace(self, environ_or_headers, op=None, name=None, source=None): # type: (Dict[str, Any], Optional[str], Optional[str], Optional[str]) -> Transaction """ Sets the propagation context from environment or headers and returns a transaction. """ - with self.configure_scope() as scope: - scope.generate_propagation_context(environ_or_headers) + scope = self._stack[-1][1] - transaction = Transaction.continue_from_headers( - normalize_incoming_data(environ_or_headers), - op=op, - name=name, - source=source, + return scope.continue_trace( + environ_or_headers=environ_or_headers, op=op, name=name, source=source ) - return transaction @overload def push_scope( @@ -735,25 +656,16 @@ def get_traceparent(self): """ Returns the traceparent either from the active span or from the scope. """ - if self.client is not None: - if has_tracing_enabled(self.client.options) and self.scope.span is not None: - return self.scope.span.to_traceparent() - - return self.scope.get_traceparent() + client, scope = self._stack[-1] + return scope.get_traceparent(client=client) def get_baggage(self): # type: () -> Optional[str] """ Returns Baggage either from the active span or from the scope. """ - if ( - self.client is not None - and has_tracing_enabled(self.client.options) - and self.scope.span is not None - ): - baggage = self.scope.span.to_baggage() - else: - baggage = self.scope.get_baggage() + client, scope = self._stack[-1] + baggage = scope.get_baggage(client=client) if baggage is not None: return baggage.serialize() @@ -767,19 +679,9 @@ def iter_trace_propagation_headers(self, span=None): from the span representing the request, if available, or the current span on the scope if not. """ - client = self._stack[-1][0] - propagate_traces = client and client.options["propagate_traces"] - if not propagate_traces: - return - - span = span or self.scope.span + client, scope = self._stack[-1] - if client and has_tracing_enabled(client.options) and span is not None: - for header in span.iter_headers(): - yield header - else: - for header in self.scope.iter_headers(): - yield header + return scope.iter_trace_propagation_headers(span=span, client=client) def trace_propagation_meta(self, span=None): # type: (Optional[Span]) -> str @@ -792,23 +694,8 @@ def trace_propagation_meta(self, span=None): "The parameter `span` in trace_propagation_meta() is deprecated and will be removed in the future." ) - meta = "" - - sentry_trace = self.get_traceparent() - if sentry_trace is not None: - meta += '' % ( - SENTRY_TRACE_HEADER_NAME, - sentry_trace, - ) - - baggage = self.get_baggage() - if baggage is not None: - meta += '' % ( - BAGGAGE_HEADER_NAME, - baggage, - ) - - return meta + client, scope = self._stack[-1] + return scope.trace_propagation_meta(span=span, client=client) GLOBAL_HUB = Hub() diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index c715847d38..9507306812 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -7,8 +7,9 @@ from sentry_sdk.attachments import Attachment from sentry_sdk._compat import datetime_utcnow -from sentry_sdk.consts import FALSE_VALUES +from sentry_sdk.consts import FALSE_VALUES, INSTRUMENTER from sentry_sdk._functools import wraps +from sentry_sdk.profiler import Profile from sentry_sdk.session import Session from sentry_sdk.tracing_utils import ( Baggage, @@ -19,6 +20,8 @@ from sentry_sdk.tracing import ( BAGGAGE_HEADER_NAME, SENTRY_TRACE_HEADER_NAME, + NoOpSpan, + Span, Transaction, ) from sentry_sdk._types import TYPE_CHECKING @@ -31,14 +34,16 @@ if TYPE_CHECKING: from typing import Any + from typing import Callable + from typing import Deque from typing import Dict + from typing import Generator from typing import Iterator - from typing import Optional - from typing import Deque from typing import List - from typing import Callable + from typing import Optional from typing import Tuple from typing import TypeVar + from typing import Union from sentry_sdk._types import ( Breadcrumb, @@ -53,8 +58,6 @@ ) import sentry_sdk - from sentry_sdk.profiler import Profile - from sentry_sdk.tracing import Span F = TypeVar("F", bound=Callable[..., Any]) T = TypeVar("T") @@ -274,11 +277,22 @@ def get_dynamic_sampling_context(self): return self._propagation_context["dynamic_sampling_context"] - def get_traceparent(self): - # type: () -> Optional[str] + def get_traceparent(self, *args, **kwargs): + # type: (Any, Any) -> Optional[str] """ - Returns the Sentry "sentry-trace" header (aka the traceparent) from the Propagation Context. + Returns the Sentry "sentry-trace" header (aka the traceparent) from the + currently active span or the scopes Propagation Context. """ + client = kwargs.pop("client", None) + + # If we have an active span, return traceparent from there + if ( + client is not None + and has_tracing_enabled(client.options) + and self.span is not None + ): + return self.span.to_traceparent() + if self._propagation_context is None: return None @@ -288,8 +302,18 @@ def get_traceparent(self): ) return traceparent - def get_baggage(self): - # type: () -> Optional[Baggage] + def get_baggage(self, *args, **kwargs): + # type: (Any, Any) -> Optional[Baggage] + client = kwargs.pop("client", None) + + # If we have an active span, return baggage from there + if ( + client is not None + and has_tracing_enabled(client.options) + and self.span is not None + ): + return self.span.to_baggage() + if self._propagation_context is None: return None @@ -318,6 +342,38 @@ def get_trace_context(self): return trace_context + def trace_propagation_meta(self, *args, **kwargs): + # type: (*Any, **Any) -> str + """ + Return meta tags which should be injected into HTML templates + to allow propagation of trace information. + """ + span = kwargs.pop("span", None) + if span is not None: + logger.warning( + "The parameter `span` in trace_propagation_meta() is deprecated and will be removed in the future." + ) + + client = kwargs.pop("client", None) + + meta = "" + + sentry_trace = self.get_traceparent(client=client) + if sentry_trace is not None: + meta += '' % ( + SENTRY_TRACE_HEADER_NAME, + sentry_trace, + ) + + baggage = self.get_baggage(client=client) + if baggage is not None: + meta += '' % ( + BAGGAGE_HEADER_NAME, + baggage.serialize(), + ) + + return meta + def iter_headers(self): # type: () -> Iterator[Tuple[str, str]] """ @@ -333,6 +389,29 @@ def iter_headers(self): baggage = Baggage(dsc).serialize() yield BAGGAGE_HEADER_NAME, baggage + def iter_trace_propagation_headers(self, *args, **kwargs): + # type: (Any, Any) -> Generator[Tuple[str, str], None, None] + """ + Return HTTP headers which allow propagation of trace data. Data taken + from the span representing the request, if available, or the current + span on the scope if not. + """ + span = kwargs.pop("span", None) + client = kwargs.pop("client", None) + + propagate_traces = client and client.options["propagate_traces"] + if not propagate_traces: + return + + span = span or self.span + + if client and has_tracing_enabled(client.options) and span is not None: + for header in span.iter_headers(): + yield header + else: + for header in self.iter_headers(): + yield header + def clear(self): # type: () -> None """Clears the entire scope.""" @@ -589,6 +668,155 @@ def add_breadcrumb(self, crumb=None, hint=None, **kwargs): while len(self._breadcrumbs) > max_breadcrumbs: self._breadcrumbs.popleft() + def start_transaction( + self, transaction=None, instrumenter=INSTRUMENTER.SENTRY, **kwargs + ): + # type: (Optional[Transaction], str, Any) -> Union[Transaction, NoOpSpan] + """ + Start and return a transaction. + + Start an existing transaction if given, otherwise create and start a new + transaction with kwargs. + + This is the entry point to manual tracing instrumentation. + + A tree structure can be built by adding child spans to the transaction, + and child spans to other spans. To start a new child span within the + transaction or any span, call the respective `.start_child()` method. + + Every child span must be finished before the transaction is finished, + otherwise the unfinished spans are discarded. + + When used as context managers, spans and transactions are automatically + finished at the end of the `with` block. If not using context managers, + call the `.finish()` method. + + When the transaction is finished, it will be sent to Sentry with all its + finished child spans. + + For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Transaction`. + """ + hub = kwargs.pop("hub", None) + client = kwargs.pop("client", None) + + configuration_instrumenter = client and client.options["instrumenter"] + + if instrumenter != configuration_instrumenter: + return NoOpSpan() + + custom_sampling_context = kwargs.pop("custom_sampling_context", {}) + + # if we haven't been given a transaction, make one + if transaction is None: + kwargs.setdefault("hub", hub) + transaction = Transaction(**kwargs) + + # use traces_sample_rate, traces_sampler, and/or inheritance to make a + # sampling decision + sampling_context = { + "transaction_context": transaction.to_json(), + "parent_sampled": transaction.parent_sampled, + } + sampling_context.update(custom_sampling_context) + transaction._set_initial_sampling_decision(sampling_context=sampling_context) + + profile = Profile(transaction, hub=hub) + profile._set_initial_sampling_decision(sampling_context=sampling_context) + + # we don't bother to keep spans if we already know we're not going to + # send the transaction + if transaction.sampled: + max_spans = ( + client and client.options["_experiments"].get("max_spans") + ) or 1000 + transaction.init_span_recorder(maxlen=max_spans) + + return transaction + + def start_span(self, span=None, instrumenter=INSTRUMENTER.SENTRY, **kwargs): + # type: (Optional[Span], str, Any) -> Span + """ + Start a span whose parent is the currently active span or transaction, if any. + + The return value is a :py:class:`sentry_sdk.tracing.Span` instance, + typically used as a context manager to start and stop timing in a `with` + block. + + Only spans contained in a transaction are sent to Sentry. Most + integrations start a transaction at the appropriate time, for example + for every incoming HTTP request. Use + :py:meth:`sentry_sdk.start_transaction` to start a new transaction when + one is not already in progress. + + For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Span`. + """ + client = kwargs.get("client", None) + + configuration_instrumenter = client and client.options["instrumenter"] + + if instrumenter != configuration_instrumenter: + return NoOpSpan() + + # THIS BLOCK IS DEPRECATED + # TODO: consider removing this in a future release. + # This is for backwards compatibility with releases before + # start_transaction existed, to allow for a smoother transition. + if isinstance(span, Transaction) or "transaction" in kwargs: + deprecation_msg = ( + "Deprecated: use start_transaction to start transactions and " + "Transaction.start_child to start spans." + ) + + if isinstance(span, Transaction): + logger.warning(deprecation_msg) + return self.start_transaction(span, **kwargs) + + if "transaction" in kwargs: + logger.warning(deprecation_msg) + name = kwargs.pop("transaction") + return self.start_transaction(name=name, **kwargs) + + # THIS BLOCK IS DEPRECATED + # We do not pass a span into start_span in our code base, so I deprecate this. + if span is not None: + deprecation_msg = "Deprecated: passing a span into `start_span` is deprecated and will be removed in the future." + logger.warning(deprecation_msg) + return span + + kwargs.pop("client") + + active_span = self.span + if active_span is not None: + new_child_span = active_span.start_child(**kwargs) + return new_child_span + + # If there is already a trace_id in the propagation context, use it. + # This does not need to be done for `start_child` above because it takes + # the trace_id from the parent span. + if "trace_id" not in kwargs: + traceparent = self.get_traceparent() + trace_id = traceparent.split("-")[0] if traceparent else None + if trace_id is not None: + kwargs["trace_id"] = trace_id + + return Span(**kwargs) + + def continue_trace(self, environ_or_headers, op=None, name=None, source=None): + # type: (Dict[str, Any], Optional[str], Optional[str], Optional[str]) -> Transaction + """ + Sets the propagation context from environment or headers and returns a transaction. + """ + self.generate_propagation_context(environ_or_headers) + + transaction = Transaction.continue_from_headers( + normalize_incoming_data(environ_or_headers), + op=op, + name=name, + source=source, + ) + + return transaction + def capture_event(self, event, hint=None, client=None, scope=None, **scope_kwargs): # type: (Event, Optional[Hint], Optional[sentry_sdk.Client], Optional[Scope], Any) -> Optional[str] """