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
6 changes: 4 additions & 2 deletions .vscode/settings.json
@@ -1,4 +1,6 @@
{
"python.pythonPath": ".venv/bin/python",
"python.formatting.provider": "black"
}
"python.formatting.provider": "black",
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
}
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
56 changes: 31 additions & 25 deletions sentry_sdk/consts.py
Expand Up @@ -44,6 +44,36 @@
DEFAULT_MAX_BREADCRUMBS = 100


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


class OP:
DB = "db"
DB_REDIS = "db.redis"
EVENT_DJANGO = "event.django"
FUNCTION = "function"
FUNCTION_AWS = "function.aws"
FUNCTION_GCP = "function.gcp"
HTTP_CLIENT = "http.client"
HTTP_CLIENT_STREAM = "http.client.stream"
HTTP_SERVER = "http.server"
MIDDLEWARE_DJANGO = "middleware.django"
MIDDLEWARE_STARLETTE = "middleware.starlette"
MIDDLEWARE_STARLETTE_RECEIVE = "middleware.starlette.receive"
MIDDLEWARE_STARLETTE_SEND = "middleware.starlette.send"
QUEUE_SUBMIT_CELERY = "queue.submit.celery"
QUEUE_TASK_CELERY = "queue.task.celery"
QUEUE_TASK_RQ = "queue.task.rq"
SUBPROCESS = "subprocess"
SUBPROCESS_WAIT = "subprocess.wait"
SUBPROCESS_COMMUNICATE = "subprocess.communicate"
TEMPLATE_RENDER = "template.render"
VIEW_RENDER = "view.render"
WEBSOCKET_SERVER = "websocket.server"


# This type exists to trick mypy and PyCharm into thinking `init` and `Client`
# take these arguments (even though they take opaque **kwargs)
class ClientConstructor(object):
Expand All @@ -57,6 +87,7 @@ def __init__(
server_name=None, # type: Optional[str]
shutdown_timeout=2, # type: float
integrations=[], # type: Sequence[Integration] # noqa: B006
instrumenter=INSTRUMENTER.SENTRY, # type: Optional[str]
antonpirker marked this conversation as resolved.
Show resolved Hide resolved
in_app_include=[], # type: List[str] # noqa: B006
in_app_exclude=[], # type: List[str] # noqa: B006
default_integrations=True, # type: bool
Expand Down Expand Up @@ -106,28 +137,3 @@ def _get_default_options():


VERSION = "1.11.1"


class OP:
DB = "db"
DB_REDIS = "db.redis"
EVENT_DJANGO = "event.django"
FUNCTION = "function"
FUNCTION_AWS = "function.aws"
FUNCTION_GCP = "function.gcp"
HTTP_CLIENT = "http.client"
HTTP_CLIENT_STREAM = "http.client.stream"
HTTP_SERVER = "http.server"
MIDDLEWARE_DJANGO = "middleware.django"
MIDDLEWARE_STARLETTE = "middleware.starlette"
MIDDLEWARE_STARLETTE_RECEIVE = "middleware.starlette.receive"
MIDDLEWARE_STARLETTE_SEND = "middleware.starlette.send"
QUEUE_SUBMIT_CELERY = "queue.submit.celery"
QUEUE_TASK_CELERY = "queue.task.celery"
QUEUE_TASK_RQ = "queue.task.rq"
SUBPROCESS = "subprocess"
SUBPROCESS_WAIT = "subprocess.wait"
SUBPROCESS_COMMUNICATE = "subprocess.communicate"
TEMPLATE_RENDER = "template.render"
VIEW_RENDER = "view.render"
WEBSOCKET_SERVER = "websocket.server"
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 @@ -464,6 +465,12 @@ def start_span(
for every incoming HTTP request. Use `start_transaction` to start a new
transaction when one is not already in progress.
"""
instrumenter = kwargs.get("instrumenter", INSTRUMENTER.SENTRY)
antonpirker marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -496,7 +503,7 @@ def start_transaction(
transaction=None, # type: Optional[Transaction]
**kwargs # type: Any
):
# type: (...) -> Transaction
# type: (...) -> Union[Transaction, NoOpSpan]
"""
Start and return a transaction.

Expand All @@ -519,6 +526,12 @@ def start_transaction(
When the transaction is finished, it will be sent to Sentry with all its
finished child spans.
"""
instrumenter = kwargs.get("instrumenter", INSTRUMENTER.SENTRY)
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
9 changes: 6 additions & 3 deletions sentry_sdk/integrations/flask.py
Expand Up @@ -6,7 +6,7 @@
from sentry_sdk.integrations._wsgi_common import RequestExtractor
from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
from sentry_sdk.scope import Scope
from sentry_sdk.tracing import SOURCE_FOR_STYLE
from sentry_sdk.tracing import SENTRY_TRACE_HEADER_NAME, SOURCE_FOR_STYLE
from sentry_sdk.utils import (
capture_internal_exceptions,
event_from_exception,
Expand Down Expand Up @@ -101,8 +101,11 @@ def _add_sentry_trace(sender, template, context, **extra):
sentry_span = Hub.current.scope.span
context["sentry_trace"] = (
Markup(
'<meta name="sentry-trace" content="%s" />'
% (sentry_span.to_traceparent(),)
'<meta name="%s" content="%s" />'
% (
SENTRY_TRACE_HEADER_NAME,
sentry_span.to_traceparent(),
)
)
if sentry_span
else ""
Expand Down
1 change: 0 additions & 1 deletion sentry_sdk/integrations/stdlib.py
Expand Up @@ -187,7 +187,6 @@ def sentry_patched_popen_init(self, *a, **kw):
env = None

with hub.start_span(op=OP.SUBPROCESS, description=description) as span:

for k, v in hub.iter_trace_propagation_headers(span):
if env is None:
env = _init_argument(
Expand Down
74 changes: 67 additions & 7 deletions sentry_sdk/tracing.py
Expand Up @@ -6,7 +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 All @@ -24,6 +24,9 @@
import sentry_sdk.profiler
from sentry_sdk._types import Event, SamplingContext, MeasurementUnit

BAGGAGE_HEADER_NAME = "baggage"
SENTRY_TRACE_HEADER_NAME = "sentry-trace"


# Transaction source
# see https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations
Expand Down Expand Up @@ -123,6 +126,7 @@ def __init__(
status=None, # type: Optional[str]
transaction=None, # type: Optional[str] # deprecated
containing_transaction=None, # type: Optional[Transaction]
instrumenter=None, # type: Optional[str]
):
# type: (...) -> None
self.trace_id = trace_id or uuid.uuid4().hex
Expand Down Expand Up @@ -213,6 +217,15 @@ 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

instrumenter = kwargs.get("instrumenter", INSTRUMENTER.SENTRY)
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 @@ -278,10 +291,12 @@ def continue_from_headers(

# TODO-neel move away from this kwargs stuff, it's confusing and opaque
# make more explicit
baggage = Baggage.from_incoming_header(headers.get("baggage"))
kwargs.update({"baggage": baggage})
baggage = Baggage.from_incoming_header(headers.get(BAGGAGE_HEADER_NAME))
kwargs.update({BAGGAGE_HEADER_NAME: baggage})

sentrytrace_kwargs = extract_sentrytrace_data(headers.get("sentry-trace"))
sentrytrace_kwargs = extract_sentrytrace_data(
headers.get(SENTRY_TRACE_HEADER_NAME)
)

if sentrytrace_kwargs is not None:
kwargs.update(sentrytrace_kwargs)
Expand All @@ -308,7 +323,7 @@ def iter_headers(self):
`sentry_tracestate` value, this will cause one to be generated and
stored.
"""
yield "sentry-trace", self.to_traceparent()
yield SENTRY_TRACE_HEADER_NAME, self.to_traceparent()

tracestate = self.to_tracestate() if has_tracestate_enabled(self) else None
# `tracestate` will only be `None` if there's no client or no DSN
Expand All @@ -320,7 +335,7 @@ def iter_headers(self):
if self.containing_transaction:
baggage = self.containing_transaction.get_baggage().serialize()
if baggage:
yield "baggage", baggage
yield BAGGAGE_HEADER_NAME, baggage

@classmethod
def from_traceparent(
Expand All @@ -344,7 +359,9 @@ def from_traceparent(
if not traceparent:
return None

return cls.continue_from_headers({"sentry-trace": traceparent}, **kwargs)
return cls.continue_from_headers(
{SENTRY_TRACE_HEADER_NAME: traceparent}, **kwargs
)

def to_traceparent(self):
# type: () -> str
Expand Down Expand Up @@ -653,6 +670,7 @@ def finish(self, hub=None):
# to a concrete decision.
if self.sampled is None:
logger.warning("Discarding transaction without sampling decision.")

return None

finished_spans = [
Expand Down Expand Up @@ -821,6 +839,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, **kwargs):
# type: (**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, **kwargs):
# type: (Any, **Any) -> Any
pass


# Circular imports

from sentry_sdk.tracing_utils import (
Expand Down