Skip to content

Commit 46697dd

Browse files
authored
Add instrumenter config to switch between Otel and Sentry instrumentation. (getsentry#1766)
* Add instrumenter config to switch between Sentry and OTel instrumentation. * Add API to set arbitrary context in Transaction. (getsentry#1769) * Add API to set custom Span timestamps (getsentry#1770)
1 parent 01dc7ee commit 46697dd

File tree

5 files changed

+106
-14
lines changed

5 files changed

+106
-14
lines changed

sentry_sdk/api.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from sentry_sdk.scope import Scope
55

66
from sentry_sdk._types import MYPY
7+
from sentry_sdk.tracing import NoOpSpan
78

89
if MYPY:
910
from typing import Any
@@ -210,5 +211,5 @@ def start_transaction(
210211
transaction=None, # type: Optional[Transaction]
211212
**kwargs # type: Any
212213
):
213-
# type: (...) -> Transaction
214+
# type: (...) -> Union[Transaction, NoOpSpan]
214215
return Hub.current.start_transaction(transaction, **kwargs)

sentry_sdk/client.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from sentry_sdk.transport import make_transport
2121
from sentry_sdk.consts import (
2222
DEFAULT_OPTIONS,
23+
INSTRUMENTER,
2324
VERSION,
2425
ClientConstructor,
2526
)
@@ -86,6 +87,9 @@ def _get_options(*args, **kwargs):
8687
if rv["server_name"] is None and hasattr(socket, "gethostname"):
8788
rv["server_name"] = socket.gethostname()
8889

90+
if rv["instrumenter"] is None:
91+
rv["instrumenter"] = INSTRUMENTER.SENTRY
92+
8993
return rv
9094

9195

sentry_sdk/consts.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@
4444
DEFAULT_MAX_BREADCRUMBS = 100
4545

4646

47+
class INSTRUMENTER:
48+
SENTRY = "sentry"
49+
OTEL = "otel"
50+
51+
4752
class OP:
4853
DB = "db"
4954
DB_REDIS = "db.redis"
@@ -107,6 +112,7 @@ def __init__(
107112
send_client_reports=True, # type: bool
108113
_experiments={}, # type: Experiments # noqa: B006
109114
proxy_headers=None, # type: Optional[Dict[str, str]]
115+
instrumenter=INSTRUMENTER.SENTRY, # type: Optional[str]
110116
):
111117
# type: (...) -> None
112118
pass

sentry_sdk/hub.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
from contextlib import contextmanager
66

77
from sentry_sdk._compat import with_metaclass
8+
from sentry_sdk.consts import INSTRUMENTER
89
from sentry_sdk.scope import Scope
910
from sentry_sdk.client import Client
10-
from sentry_sdk.tracing import Span, Transaction
11+
from sentry_sdk.tracing import NoOpSpan, Span, Transaction
1112
from sentry_sdk.session import Session
1213
from sentry_sdk.utils import (
1314
exc_info_from_error,
@@ -450,6 +451,7 @@ def add_breadcrumb(
450451
def start_span(
451452
self,
452453
span=None, # type: Optional[Span]
454+
instrumenter=INSTRUMENTER.SENTRY, # type: str
453455
**kwargs # type: Any
454456
):
455457
# type: (...) -> Span
@@ -464,6 +466,11 @@ def start_span(
464466
for every incoming HTTP request. Use `start_transaction` to start a new
465467
transaction when one is not already in progress.
466468
"""
469+
configuration_instrumenter = self.client and self.client.options["instrumenter"]
470+
471+
if instrumenter != configuration_instrumenter:
472+
return NoOpSpan()
473+
467474
# TODO: consider removing this in a future release.
468475
# This is for backwards compatibility with releases before
469476
# start_transaction existed, to allow for a smoother transition.
@@ -494,9 +501,10 @@ def start_span(
494501
def start_transaction(
495502
self,
496503
transaction=None, # type: Optional[Transaction]
504+
instrumenter=INSTRUMENTER.SENTRY, # type: str
497505
**kwargs # type: Any
498506
):
499-
# type: (...) -> Transaction
507+
# type: (...) -> Union[Transaction, NoOpSpan]
500508
"""
501509
Start and return a transaction.
502510
@@ -519,6 +527,11 @@ def start_transaction(
519527
When the transaction is finished, it will be sent to Sentry with all its
520528
finished child spans.
521529
"""
530+
configuration_instrumenter = self.client and self.client.options["instrumenter"]
531+
532+
if instrumenter != configuration_instrumenter:
533+
return NoOpSpan()
534+
522535
custom_sampling_context = kwargs.pop("custom_sampling_context", {})
523536

524537
# if we haven't been given a transaction, make one

sentry_sdk/tracing.py

Lines changed: 79 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from datetime import datetime, timedelta
77

88
import sentry_sdk
9+
from sentry_sdk.consts import INSTRUMENTER
910
from sentry_sdk.utils import logger
1011
from sentry_sdk._types import MYPY
1112

@@ -125,6 +126,7 @@ def __init__(
125126
status=None, # type: Optional[str]
126127
transaction=None, # type: Optional[str] # deprecated
127128
containing_transaction=None, # type: Optional[Transaction]
129+
start_timestamp=None, # type: Optional[datetime]
128130
):
129131
# type: (...) -> None
130132
self.trace_id = trace_id or uuid.uuid4().hex
@@ -139,7 +141,7 @@ def __init__(
139141
self._tags = {} # type: Dict[str, str]
140142
self._data = {} # type: Dict[str, Any]
141143
self._containing_transaction = containing_transaction
142-
self.start_timestamp = datetime.utcnow()
144+
self.start_timestamp = start_timestamp or datetime.utcnow()
143145
try:
144146
# TODO: For Python 3.7+, we could use a clock with ns resolution:
145147
# self._start_timestamp_monotonic = time.perf_counter_ns()
@@ -206,15 +208,22 @@ def containing_transaction(self):
206208
# referencing themselves)
207209
return self._containing_transaction
208210

209-
def start_child(self, **kwargs):
210-
# type: (**Any) -> Span
211+
def start_child(self, instrumenter=INSTRUMENTER.SENTRY, **kwargs):
212+
# type: (str, **Any) -> Span
211213
"""
212214
Start a sub-span from the current span or transaction.
213215
214216
Takes the same arguments as the initializer of :py:class:`Span`. The
215217
trace id, sampling decision, transaction pointer, and span recorder are
216218
inherited from the current span/transaction.
217219
"""
220+
hub = self.hub or sentry_sdk.Hub.current
221+
client = hub.client
222+
configuration_instrumenter = client and client.options["instrumenter"]
223+
224+
if instrumenter != configuration_instrumenter:
225+
return NoOpSpan()
226+
218227
kwargs.setdefault("sampled", self.sampled)
219228

220229
child = Span(
@@ -461,8 +470,8 @@ def is_success(self):
461470
# type: () -> bool
462471
return self.status == "ok"
463472

464-
def finish(self, hub=None):
465-
# type: (Optional[sentry_sdk.Hub]) -> Optional[str]
473+
def finish(self, hub=None, end_timestamp=None):
474+
# type: (Optional[sentry_sdk.Hub], Optional[datetime]) -> Optional[str]
466475
# XXX: would be type: (Optional[sentry_sdk.Hub]) -> None, but that leads
467476
# to incompatible return types for Span.finish and Transaction.finish.
468477
if self.timestamp is not None:
@@ -472,8 +481,13 @@ def finish(self, hub=None):
472481
hub = hub or self.hub or sentry_sdk.Hub.current
473482

474483
try:
475-
duration_seconds = time.perf_counter() - self._start_timestamp_monotonic
476-
self.timestamp = self.start_timestamp + timedelta(seconds=duration_seconds)
484+
if end_timestamp:
485+
self.timestamp = end_timestamp
486+
else:
487+
duration_seconds = time.perf_counter() - self._start_timestamp_monotonic
488+
self.timestamp = self.start_timestamp + timedelta(
489+
seconds=duration_seconds
490+
)
477491
except AttributeError:
478492
self.timestamp = datetime.utcnow()
479493

@@ -550,6 +564,7 @@ class Transaction(Span):
550564
# tracestate data from other vendors, of the form `dogs=yes,cats=maybe`
551565
"_third_party_tracestate",
552566
"_measurements",
567+
"_contexts",
553568
"_profile",
554569
"_baggage",
555570
"_active_thread_id",
@@ -575,7 +590,9 @@ def __init__(
575590
"instead of Span(transaction=...)."
576591
)
577592
name = kwargs.pop("transaction")
593+
578594
Span.__init__(self, **kwargs)
595+
579596
self.name = name
580597
self.source = source
581598
self.sample_rate = None # type: Optional[float]
@@ -586,6 +603,7 @@ def __init__(
586603
self._sentry_tracestate = sentry_tracestate
587604
self._third_party_tracestate = third_party_tracestate
588605
self._measurements = {} # type: Dict[str, Any]
606+
self._contexts = {} # type: Dict[str, Any]
589607
self._profile = None # type: Optional[sentry_sdk.profiler.Profile]
590608
self._baggage = baggage
591609
# for profiling, we want to know on which thread a transaction is started
@@ -619,8 +637,8 @@ def containing_transaction(self):
619637
# reference.
620638
return self
621639

622-
def finish(self, hub=None):
623-
# type: (Optional[sentry_sdk.Hub]) -> Optional[str]
640+
def finish(self, hub=None, end_timestamp=None):
641+
# type: (Optional[sentry_sdk.Hub], Optional[datetime]) -> Optional[str]
624642
if self.timestamp is not None:
625643
# This transaction is already finished, ignore.
626644
return None
@@ -652,7 +670,7 @@ def finish(self, hub=None):
652670
)
653671
self.name = "<unlabeled transaction>"
654672

655-
Span.finish(self, hub)
673+
Span.finish(self, hub, end_timestamp)
656674

657675
if not self.sampled:
658676
# At this point a `sampled = None` should have already been resolved
@@ -674,11 +692,15 @@ def finish(self, hub=None):
674692
# to be garbage collected
675693
self._span_recorder = None
676694

695+
contexts = {}
696+
contexts.update(self._contexts)
697+
contexts.update({"trace": self.get_trace_context()})
698+
677699
event = {
678700
"type": "transaction",
679701
"transaction": self.name,
680702
"transaction_info": {"source": self.source},
681-
"contexts": {"trace": self.get_trace_context()},
703+
"contexts": contexts,
682704
"tags": self._tags,
683705
"timestamp": self.timestamp,
684706
"start_timestamp": self.start_timestamp,
@@ -703,6 +725,10 @@ def set_measurement(self, name, value, unit=""):
703725

704726
self._measurements[name] = {"value": value, "unit": unit}
705727

728+
def set_context(self, key, value):
729+
# type: (str, Any) -> None
730+
self._contexts[key] = value
731+
706732
def to_json(self):
707733
# type: () -> Dict[str, Any]
708734
rv = super(Transaction, self).to_json()
@@ -828,6 +854,48 @@ def _set_initial_sampling_decision(self, sampling_context):
828854
)
829855

830856

857+
class NoOpSpan(Span):
858+
def __repr__(self):
859+
# type: () -> Any
860+
return self.__class__.__name__
861+
862+
def __enter__(self):
863+
# type: () -> Any
864+
return self
865+
866+
def __exit__(self, ty, value, tb):
867+
# type: (Any, Any, Any) -> Any
868+
pass
869+
870+
def start_child(self, instrumenter=INSTRUMENTER.SENTRY, **kwargs):
871+
# type: (str, **Any) -> Any
872+
pass
873+
874+
def new_span(self, **kwargs):
875+
# type: (**Any) -> Any
876+
pass
877+
878+
def set_tag(self, key, value):
879+
# type: (Any, Any) -> Any
880+
pass
881+
882+
def set_data(self, key, value):
883+
# type: (Any, Any) -> Any
884+
pass
885+
886+
def set_status(self, value):
887+
# type: (Any) -> Any
888+
pass
889+
890+
def set_http_status(self, http_status):
891+
# type: (Any) -> Any
892+
pass
893+
894+
def finish(self, hub=None, end_timestamp=None):
895+
# type: (Any, Any) -> Any
896+
pass
897+
898+
831899
# Circular imports
832900

833901
from sentry_sdk.tracing_utils import (

0 commit comments

Comments
 (0)