From 46697ddeb19f2d5989c8bae88dbad41f68797dca Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 1 Dec 2022 12:04:41 +0100 Subject: [PATCH] Add instrumenter config to switch between Otel and Sentry instrumentation. (#1766) * Add instrumenter config to switch between Sentry and OTel instrumentation. * Add API to set arbitrary context in Transaction. (#1769) * Add API to set custom Span timestamps (#1770) --- sentry_sdk/api.py | 3 +- sentry_sdk/client.py | 4 ++ sentry_sdk/consts.py | 6 +++ sentry_sdk/hub.py | 17 +++++++- sentry_sdk/tracing.py | 90 +++++++++++++++++++++++++++++++++++++------ 5 files changed, 106 insertions(+), 14 deletions(-) diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index cec914aca1..ffa017cfc1 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -4,6 +4,7 @@ from sentry_sdk.scope import Scope from sentry_sdk._types import MYPY +from sentry_sdk.tracing import NoOpSpan if MYPY: from typing import Any @@ -210,5 +211,5 @@ def start_transaction( transaction=None, # type: Optional[Transaction] **kwargs # type: Any ): - # type: (...) -> Transaction + # type: (...) -> Union[Transaction, NoOpSpan] return Hub.current.start_transaction(transaction, **kwargs) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index bf1e483634..8af7003156 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -20,6 +20,7 @@ from sentry_sdk.transport import make_transport from sentry_sdk.consts import ( DEFAULT_OPTIONS, + INSTRUMENTER, VERSION, ClientConstructor, ) @@ -86,6 +87,9 @@ def _get_options(*args, **kwargs): if rv["server_name"] is None and hasattr(socket, "gethostname"): rv["server_name"] = socket.gethostname() + if rv["instrumenter"] is None: + rv["instrumenter"] = INSTRUMENTER.SENTRY + return rv diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 6fd61d395b..47d630dee3 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -44,6 +44,11 @@ DEFAULT_MAX_BREADCRUMBS = 100 +class INSTRUMENTER: + SENTRY = "sentry" + OTEL = "otel" + + class OP: DB = "db" DB_REDIS = "db.redis" @@ -107,6 +112,7 @@ def __init__( send_client_reports=True, # type: bool _experiments={}, # type: Experiments # noqa: B006 proxy_headers=None, # type: Optional[Dict[str, str]] + instrumenter=INSTRUMENTER.SENTRY, # type: Optional[str] ): # type: (...) -> None pass diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py index 3d4a28d526..df9de10fe4 100644 --- a/sentry_sdk/hub.py +++ b/sentry_sdk/hub.py @@ -5,9 +5,10 @@ from contextlib import contextmanager from sentry_sdk._compat import with_metaclass +from sentry_sdk.consts import INSTRUMENTER from sentry_sdk.scope import Scope from sentry_sdk.client import Client -from sentry_sdk.tracing import Span, Transaction +from sentry_sdk.tracing import NoOpSpan, Span, Transaction from sentry_sdk.session import Session from sentry_sdk.utils import ( exc_info_from_error, @@ -450,6 +451,7 @@ def add_breadcrumb( def start_span( self, span=None, # type: Optional[Span] + instrumenter=INSTRUMENTER.SENTRY, # type: str **kwargs # type: Any ): # type: (...) -> Span @@ -464,6 +466,11 @@ def start_span( for every incoming HTTP request. Use `start_transaction` to start a new transaction when one is not already in progress. """ + configuration_instrumenter = self.client and self.client.options["instrumenter"] + + if instrumenter != configuration_instrumenter: + return NoOpSpan() + # 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. @@ -494,9 +501,10 @@ def start_span( def start_transaction( self, transaction=None, # type: Optional[Transaction] + instrumenter=INSTRUMENTER.SENTRY, # type: str **kwargs # type: Any ): - # type: (...) -> Transaction + # type: (...) -> Union[Transaction, NoOpSpan] """ Start and return a transaction. @@ -519,6 +527,11 @@ def start_transaction( When the transaction is finished, it will be sent to Sentry with all its finished child spans. """ + 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 diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 8be9028aa5..93d22dc758 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -6,6 +6,7 @@ from datetime import datetime, timedelta import sentry_sdk +from sentry_sdk.consts import INSTRUMENTER from sentry_sdk.utils import logger from sentry_sdk._types import MYPY @@ -125,6 +126,7 @@ def __init__( status=None, # type: Optional[str] transaction=None, # type: Optional[str] # deprecated containing_transaction=None, # type: Optional[Transaction] + start_timestamp=None, # type: Optional[datetime] ): # type: (...) -> None self.trace_id = trace_id or uuid.uuid4().hex @@ -139,7 +141,7 @@ def __init__( self._tags = {} # type: Dict[str, str] self._data = {} # type: Dict[str, Any] self._containing_transaction = containing_transaction - self.start_timestamp = datetime.utcnow() + self.start_timestamp = start_timestamp or datetime.utcnow() try: # TODO: For Python 3.7+, we could use a clock with ns resolution: # self._start_timestamp_monotonic = time.perf_counter_ns() @@ -206,8 +208,8 @@ def containing_transaction(self): # referencing themselves) return self._containing_transaction - def start_child(self, **kwargs): - # type: (**Any) -> Span + def start_child(self, instrumenter=INSTRUMENTER.SENTRY, **kwargs): + # type: (str, **Any) -> Span """ Start a sub-span from the current span or transaction. @@ -215,6 +217,13 @@ def start_child(self, **kwargs): trace id, sampling decision, transaction pointer, and span recorder are inherited from the current span/transaction. """ + hub = self.hub or sentry_sdk.Hub.current + client = hub.client + configuration_instrumenter = client and client.options["instrumenter"] + + if instrumenter != configuration_instrumenter: + return NoOpSpan() + kwargs.setdefault("sampled", self.sampled) child = Span( @@ -461,8 +470,8 @@ def is_success(self): # type: () -> bool return self.status == "ok" - def finish(self, hub=None): - # type: (Optional[sentry_sdk.Hub]) -> Optional[str] + def finish(self, hub=None, end_timestamp=None): + # type: (Optional[sentry_sdk.Hub], Optional[datetime]) -> Optional[str] # XXX: would be type: (Optional[sentry_sdk.Hub]) -> None, but that leads # to incompatible return types for Span.finish and Transaction.finish. if self.timestamp is not None: @@ -472,8 +481,13 @@ def finish(self, hub=None): hub = hub or self.hub or sentry_sdk.Hub.current try: - duration_seconds = time.perf_counter() - self._start_timestamp_monotonic - self.timestamp = self.start_timestamp + timedelta(seconds=duration_seconds) + if end_timestamp: + self.timestamp = end_timestamp + else: + duration_seconds = time.perf_counter() - self._start_timestamp_monotonic + self.timestamp = self.start_timestamp + timedelta( + seconds=duration_seconds + ) except AttributeError: self.timestamp = datetime.utcnow() @@ -550,6 +564,7 @@ class Transaction(Span): # tracestate data from other vendors, of the form `dogs=yes,cats=maybe` "_third_party_tracestate", "_measurements", + "_contexts", "_profile", "_baggage", "_active_thread_id", @@ -575,7 +590,9 @@ def __init__( "instead of Span(transaction=...)." ) name = kwargs.pop("transaction") + Span.__init__(self, **kwargs) + self.name = name self.source = source self.sample_rate = None # type: Optional[float] @@ -586,6 +603,7 @@ def __init__( self._sentry_tracestate = sentry_tracestate self._third_party_tracestate = third_party_tracestate self._measurements = {} # type: Dict[str, Any] + self._contexts = {} # type: Dict[str, Any] self._profile = None # type: Optional[sentry_sdk.profiler.Profile] self._baggage = baggage # for profiling, we want to know on which thread a transaction is started @@ -619,8 +637,8 @@ def containing_transaction(self): # reference. return self - def finish(self, hub=None): - # type: (Optional[sentry_sdk.Hub]) -> Optional[str] + def finish(self, hub=None, end_timestamp=None): + # type: (Optional[sentry_sdk.Hub], Optional[datetime]) -> Optional[str] if self.timestamp is not None: # This transaction is already finished, ignore. return None @@ -652,7 +670,7 @@ def finish(self, hub=None): ) self.name = "" - Span.finish(self, hub) + Span.finish(self, hub, end_timestamp) if not self.sampled: # At this point a `sampled = None` should have already been resolved @@ -674,11 +692,15 @@ def finish(self, hub=None): # to be garbage collected self._span_recorder = None + contexts = {} + contexts.update(self._contexts) + contexts.update({"trace": self.get_trace_context()}) + event = { "type": "transaction", "transaction": self.name, "transaction_info": {"source": self.source}, - "contexts": {"trace": self.get_trace_context()}, + "contexts": contexts, "tags": self._tags, "timestamp": self.timestamp, "start_timestamp": self.start_timestamp, @@ -703,6 +725,10 @@ def set_measurement(self, name, value, unit=""): self._measurements[name] = {"value": value, "unit": unit} + def set_context(self, key, value): + # type: (str, Any) -> None + self._contexts[key] = value + def to_json(self): # type: () -> Dict[str, Any] rv = super(Transaction, self).to_json() @@ -828,6 +854,48 @@ def _set_initial_sampling_decision(self, sampling_context): ) +class NoOpSpan(Span): + def __repr__(self): + # type: () -> Any + return self.__class__.__name__ + + def __enter__(self): + # type: () -> Any + return self + + def __exit__(self, ty, value, tb): + # type: (Any, Any, Any) -> Any + pass + + def start_child(self, instrumenter=INSTRUMENTER.SENTRY, **kwargs): + # type: (str, **Any) -> Any + pass + + def new_span(self, **kwargs): + # type: (**Any) -> Any + pass + + def set_tag(self, key, value): + # type: (Any, Any) -> Any + pass + + def set_data(self, key, value): + # type: (Any, Any) -> Any + pass + + def set_status(self, value): + # type: (Any) -> Any + pass + + def set_http_status(self, http_status): + # type: (Any) -> Any + pass + + def finish(self, hub=None, end_timestamp=None): + # type: (Any, Any) -> Any + pass + + # Circular imports from sentry_sdk.tracing_utils import (