Skip to content

Commit

Permalink
Add instrumenter config to switch between Otel and Sentry instrumenta…
Browse files Browse the repository at this point in the history
…tion. (#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)
  • Loading branch information
antonpirker committed Dec 1, 2022
1 parent 01dc7ee commit 46697dd
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 14 deletions.
3 changes: 2 additions & 1 deletion sentry_sdk/api.py
Expand Up @@ -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
Expand Down Expand Up @@ -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)
4 changes: 4 additions & 0 deletions sentry_sdk/client.py
Expand Up @@ -20,6 +20,7 @@
from sentry_sdk.transport import make_transport
from sentry_sdk.consts import (
DEFAULT_OPTIONS,
INSTRUMENTER,
VERSION,
ClientConstructor,
)
Expand Down Expand Up @@ -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


Expand Down
6 changes: 6 additions & 0 deletions sentry_sdk/consts.py
Expand Up @@ -44,6 +44,11 @@
DEFAULT_MAX_BREADCRUMBS = 100


class INSTRUMENTER:
SENTRY = "sentry"
OTEL = "otel"


class OP:
DB = "db"
DB_REDIS = "db.redis"
Expand Down Expand Up @@ -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
Expand Down
17 changes: 15 additions & 2 deletions sentry_sdk/hub.py
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down
90 changes: 79 additions & 11 deletions sentry_sdk/tracing.py
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -206,15 +208,22 @@ 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.
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.
"""
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(
Expand Down Expand Up @@ -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:
Expand All @@ -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()

Expand Down Expand Up @@ -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",
Expand All @@ -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]
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -652,7 +670,7 @@ def finish(self, hub=None):
)
self.name = "<unlabeled transaction>"

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
Expand All @@ -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,
Expand All @@ -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()
Expand Down Expand Up @@ -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 (
Expand Down

0 comments on commit 46697dd

Please sign in to comment.