From 028a4442dfc0ac0505c1642deef6cc042a1ac530 Mon Sep 17 00:00:00 2001 From: Neel Shah Date: Wed, 1 Oct 2025 14:15:02 +0200 Subject: [PATCH 1/2] POC OtlpIntegration --- sentry_sdk/integrations/otlp.py | 55 +++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 sentry_sdk/integrations/otlp.py diff --git a/sentry_sdk/integrations/otlp.py b/sentry_sdk/integrations/otlp.py new file mode 100644 index 0000000000..dbb62b1e49 --- /dev/null +++ b/sentry_sdk/integrations/otlp.py @@ -0,0 +1,55 @@ +import sentry_sdk +from sentry_sdk.integrations import Integration, DidNotEnable +from sentry_sdk.scope import add_global_event_processor + +try: + from opentelemetry import trace +except ImportError: + raise DidNotEnable("opentelemetry is not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Optional + + from sentry_sdk._types import Event, Hint + + +class OtlpIntegration(Integration): + identifier = "otlp" + + @staticmethod + def setup_once(): + # type: () -> None + @add_global_event_processor + def link_trace_context_to_error_event(event, hint): + # type: (Event, Optional[Hint]) -> Optional[Event] + integration = sentry_sdk.get_client().get_integration(OtlpIntegration) + if integration is None: + return event + + if hasattr(event, "type") and event["type"] == "transaction": + return event + + otel_span = trace.get_current_span() + if not otel_span: + return event + + ctx = otel_span.get_span_context() + + if ( + ctx.trace_id == trace.INVALID_TRACE_ID + or ctx.span_id == trace.INVALID_SPAN_ID + ): + return event + + contexts = event.setdefault("contexts", {}) + contexts.setdefault("trace", {}).update( + { + "trace_id": trace.format_trace_id(ctx.trace_id), + "span_id": trace.format_span_id(ctx.span_id), + "status": "ok", # TODO + } + ) + + return event From cd418f737128f43fad85fe330cd43c938d1cbe16 Mon Sep 17 00:00:00 2001 From: Neel Shah Date: Thu, 23 Oct 2025 16:13:14 +0200 Subject: [PATCH 2/2] Part of otel integration wip --- .../integrations/opentelemetry/integration.py | 43 +++++++++++++++++-- setup.py | 2 +- tox.ini | 1 + 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/integrations/opentelemetry/integration.py b/sentry_sdk/integrations/opentelemetry/integration.py index 43e0396c16..169f3437c9 100644 --- a/sentry_sdk/integrations/opentelemetry/integration.py +++ b/sentry_sdk/integrations/opentelemetry/integration.py @@ -3,7 +3,7 @@ are experimental and not suitable for production use. They may be changed or removed at any time without prior notice. """ - +import sentry_sdk from sentry_sdk.integrations import DidNotEnable, Integration from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator from sentry_sdk.integrations.opentelemetry.span_processor import SentrySpanProcessor @@ -13,9 +13,16 @@ from opentelemetry import trace from opentelemetry.propagate import set_global_textmap from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import BatchSpanProcessor except ImportError: raise DidNotEnable("opentelemetry not installed") +try: + from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter +except ImportError: + OTLPSpanExporter = None + + try: from opentelemetry.instrumentation.django import DjangoInstrumentor # type: ignore[import-not-found] except ImportError: @@ -30,6 +37,20 @@ class OpenTelemetryIntegration(Integration): identifier = "opentelemetry" + def __init__(self, enable_span_processor=True, enable_otlp_exporter=False, enable_propagator=True): + # type: (bool, bool, bool) -> None + self.enable_span_processor = enable_span_processor + self.enable_otlp_exporter = enable_otlp_exporter + self.enable_propagator = enable_propagator + + if self.enable_otlp_exporter and OTLPSpanExporter is None: + logger.warning("[Otel] OTLPSpanExporter not installed.") + self.enable_otlp_exporter = False + + if self.enable_span_processor and self.enable_otlp_exporter: + logger.warning("[Otel] Disabling span processor because otlp exporter is set.") + self.enable_span_processor = False + @staticmethod def setup_once(): # type: () -> None @@ -46,10 +67,26 @@ def setup_once(): def _setup_sentry_tracing(): # type: () -> None + integration = sentry_sdk.get_client().get_integration(OpenTelemetryIntegration) + if integration is None: + return + + # TODO provider oblivious provider = TracerProvider() - provider.add_span_processor(SentrySpanProcessor()) + + if integration.enable_otlp_exporter: + otlp_exporter = OTLPSpanExporter( + # endpoint="___OTLP_TRACES_URL___", + # headers={"x-sentry-auth": "sentry sentry_key=___PUBLIC_KEY___"}, + ) + otlp_span_processor = BatchSpanProcessor(otlp_exporter) + provider.add_span_processor(otlp_span_processor) + elif integration.enable_span_processor: + provider.add_span_processor(SentrySpanProcessor()) trace.set_tracer_provider(provider) - set_global_textmap(SentryPropagator()) + + if integration.enable_propagator: + set_global_textmap(SentryPropagator()) def _setup_instrumentors(): diff --git a/setup.py b/setup.py index b1662204fb..3be444c81a 100644 --- a/setup.py +++ b/setup.py @@ -70,7 +70,7 @@ def get_file_text(file_name): "loguru": ["loguru>=0.5"], "openai": ["openai>=1.0.0", "tiktoken>=0.3.0"], "openfeature": ["openfeature-sdk>=0.7.1"], - "opentelemetry": ["opentelemetry-distro>=0.35b0"], + "opentelemetry": ["opentelemetry-distro>=0.35b0", "opentelemetry-exporter-otlp-proto-http>=1.0.0"], "opentelemetry-experimental": ["opentelemetry-distro"], "pure-eval": ["pure_eval", "executing", "asttokens"], "pydantic_ai": ["pydantic-ai>=1.0.0"], diff --git a/tox.ini b/tox.ini index 0e9d3a7bb7..b218f118f5 100644 --- a/tox.ini +++ b/tox.ini @@ -335,6 +335,7 @@ deps = # OpenTelemetry (OTel) opentelemetry: opentelemetry-distro + opentelemetry: opentelemetry-exporter-otlp-proto-http # OpenTelemetry Experimental (POTel) potel: -e .[opentelemetry-experimental]