Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add instrumenter config to switch between Otel and Sentry instrumentation. #1766

Merged
Merged
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]
antonpirker marked this conversation as resolved.
Show resolved Hide resolved
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:
antonpirker marked this conversation as resolved.
Show resolved Hide resolved
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