diff --git a/.gitignore b/.gitignore index 3b1e93c41a..a37ad4a60f 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ pip-wheel-metadata .mypy_cache .vscode/ .claude/ +.tool-versions # for running AWS Lambda tests using AWS SAM sam.template.yaml diff --git a/sentry_sdk/integrations/otlp.py b/sentry_sdk/integrations/otlp.py new file mode 100644 index 0000000000..8ba51c629b --- /dev/null +++ b/sentry_sdk/integrations/otlp.py @@ -0,0 +1,90 @@ +from sentry_sdk.integrations import Integration, DidNotEnable +from sentry_sdk.scope import add_global_event_processor +from sentry_sdk.utils import logger + +try: + from opentelemetry import trace + from opentelemetry.propagate import set_global_textmap + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import BatchSpanProcessor + from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter + + from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator +except ImportError: + raise DidNotEnable("opentelemetry-distro[otlp] is not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Optional + from sentry_sdk._types import Event, Hint + + +def link_event_to_trace(event, _hint): + # type: (Event, Optional[Hint]) -> Optional[Event] + """ + Get the trace info from the running otel span and + update the trace context on sentry events of other types + (errors, logs, checkins, etc). + """ + 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 + + +def setup_otlp_exporter(): + # type: () -> None + tracer_provider = trace.get_tracer_provider() + + if not isinstance(tracer_provider, TracerProvider): + logger.debug("[OTLP] No TracerProvider configured by user, creating a new one") + tracer_provider = TracerProvider() + trace.set_tracer_provider(tracer_provider) + + otlp_exporter = OTLPSpanExporter() + span_processor = BatchSpanProcessor(otlp_exporter) + tracer_provider.add_span_processor(span_processor) + + +class OtlpIntegration(Integration): + identifier = "otlp" + setup_otlp_exporter = True + setup_propagator = True + + def __init__(self, setup_otlp_exporter=True, setup_propagator=True): + # type: (bool, bool) -> None + OtlpIntegration.setup_otlp_exporter = setup_otlp_exporter + OtlpIntegration.setup_propagator = setup_propagator + + @staticmethod + def setup_once(): + # type: () -> None + logger.debug("[OTLP] Setting up trace linking for all events") + add_global_event_processor(link_event_to_trace) + + if OtlpIntegration.setup_otlp_exporter: + logger.debug("[OTLP] Setting up OTLP exporter") + setup_otlp_exporter() + + if OtlpIntegration.setup_propagator: + logger.debug("[OTLP] Setting up propagator for distributed tracing") + set_global_textmap(SentryPropagator()) diff --git a/setup.py b/setup.py index b3b7ebb737..a872d1ddf6 100644 --- a/setup.py +++ b/setup.py @@ -72,7 +72,7 @@ def get_file_text(file_name): "openai": ["openai>=1.0.0", "tiktoken>=0.3.0"], "openfeature": ["openfeature-sdk>=0.7.1"], "opentelemetry": ["opentelemetry-distro>=0.35b0"], - "opentelemetry-experimental": ["opentelemetry-distro"], + "opentelemetry-otlp": ["opentelemetry-distro[otlp]>=0.35b0"], "pure-eval": ["pure_eval", "executing", "asttokens"], "pydantic_ai": ["pydantic-ai>=1.0.0"], "pymongo": ["pymongo>=3.1"], diff --git a/tox.ini b/tox.ini index 9a663a12c7..e49aa4b4ec 100644 --- a/tox.ini +++ b/tox.ini @@ -40,6 +40,9 @@ envlist = # OpenTelemetry (OTel) {py3.7,py3.9,py3.12,py3.13,py3.14,py3.14t}-opentelemetry + # OpenTelemetry with OTLP + {py3.7,py3.9,py3.12,py3.13,py3.14,py3.14t}-otlp + # OpenTelemetry Experimental (POTel) {py3.8,py3.9,py3.10,py3.11,py3.12,py3.13}-potel @@ -342,6 +345,9 @@ deps = # OpenTelemetry (OTel) opentelemetry: opentelemetry-distro + # OpenTelemetry with OTLP + otlp: opentelemetry-distro[otlp] + # OpenTelemetry Experimental (POTel) potel: -e .[opentelemetry-experimental] @@ -765,6 +771,7 @@ setenv = cloud_resource_context: TESTPATH=tests/integrations/cloud_resource_context gcp: TESTPATH=tests/integrations/gcp opentelemetry: TESTPATH=tests/integrations/opentelemetry + otlp: TESTPATH=tests/integrations/otlp potel: TESTPATH=tests/integrations/opentelemetry socket: TESTPATH=tests/integrations/socket