diff --git a/sentry_sdk/integrations/opentelemetry/contextvars_context.py b/sentry_sdk/integrations/opentelemetry/contextvars_context.py new file mode 100644 index 0000000000..7a382064c9 --- /dev/null +++ b/sentry_sdk/integrations/opentelemetry/contextvars_context.py @@ -0,0 +1,14 @@ +from opentelemetry.context.context import Context # type: ignore +from opentelemetry.context.contextvars_context import ContextVarsRuntimeContext # type: ignore + + +class SentryContextVarsRuntimeContext(ContextVarsRuntimeContext): # type: ignore + def attach(self, context): + # type: (Context) -> object + # TODO-neel-potel do scope management + return super().attach(context) + + def detach(self, token): + # type: (object) -> None + # TODO-neel-potel not sure if we need anything here, see later + super().detach(token) diff --git a/sentry_sdk/integrations/opentelemetry/integration.py b/sentry_sdk/integrations/opentelemetry/integration.py index 9e62d1feca..3a33c7f2d0 100644 --- a/sentry_sdk/integrations/opentelemetry/integration.py +++ b/sentry_sdk/integrations/opentelemetry/integration.py @@ -8,7 +8,12 @@ from importlib import import_module from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations.opentelemetry.span_processor import SentrySpanProcessor +from sentry_sdk.integrations.opentelemetry.potel_span_processor import ( + PotelSentrySpanProcessor, +) +from sentry_sdk.integrations.opentelemetry.contextvars_context import ( + SentryContextVarsRuntimeContext, +) from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator from sentry_sdk.utils import logger, _get_installed_modules from sentry_sdk._types import TYPE_CHECKING @@ -21,6 +26,7 @@ ) from opentelemetry.propagate import set_global_textmap # type: ignore from opentelemetry.sdk.trace import TracerProvider # type: ignore + from opentelemetry import context except ImportError: raise DidNotEnable("opentelemetry not installed") @@ -165,9 +171,14 @@ def _import_by_path(path): def _setup_sentry_tracing(): # type: () -> None + + # TODO-neel-potel make sure lifecycle is correct + # TODO-neel-potel contribute upstream so this is not necessary + context._RUNTIME_CONTEXT = SentryContextVarsRuntimeContext() + provider = TracerProvider() - provider.add_span_processor(SentrySpanProcessor()) + provider.add_span_processor(PotelSentrySpanProcessor()) trace.set_tracer_provider(provider) diff --git a/sentry_sdk/integrations/opentelemetry/potel_span_processor.py b/sentry_sdk/integrations/opentelemetry/potel_span_processor.py new file mode 100644 index 0000000000..795068033e --- /dev/null +++ b/sentry_sdk/integrations/opentelemetry/potel_span_processor.py @@ -0,0 +1,44 @@ +from opentelemetry.sdk.trace import SpanProcessor # type: ignore +from opentelemetry.context import Context # type: ignore +from opentelemetry.trace import Span # type: ignore + +from sentry_sdk._types import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Optional + + +class PotelSentrySpanProcessor(SpanProcessor): # type: ignore + """ + Converts OTel spans into Sentry spans so they can be sent to the Sentry backend. + """ + + def __new__(cls): + # type: () -> PotelSentrySpanProcessor + if not hasattr(cls, "instance"): + cls.instance = super().__new__(cls) + + return cls.instance + + def __init__(self): + # type: () -> None + pass + + def on_start(self, span, parent_context=None): + # type: (Span, Optional[Context]) -> None + pass + + def on_end(self, span): + # type: (Span) -> None + pass + + # TODO-neel-potel not sure we need a clear like JS + def shutdown(self): + # type: () -> None + pass + + # TODO-neel-potel change default? this is 30 sec + # TODO-neel-potel call this in client.flush + def force_flush(self, timeout_millis=30000): + # type: (int) -> bool + return True