diff --git a/sentry_sdk/integrations/opentelemetry/integration.py b/sentry_sdk/integrations/opentelemetry/integration.py index 3f71e86f02..595fdfcb6e 100644 --- a/sentry_sdk/integrations/opentelemetry/integration.py +++ b/sentry_sdk/integrations/opentelemetry/integration.py @@ -18,7 +18,8 @@ try: from opentelemetry import trace from opentelemetry.propagate import set_global_textmap - from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.trace import Span as AbstractSpan + from opentelemetry.sdk.trace import TracerProvider, Span, ReadableSpan except ImportError: raise DidNotEnable("opentelemetry not installed") @@ -27,6 +28,11 @@ except ImportError: DjangoInstrumentor = None +from sentry_sdk._types import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Union, Any + CONFIGURABLE_INSTRUMENTATIONS = { DjangoInstrumentor: {"is_sql_commentor_enabled": True}, @@ -45,11 +51,40 @@ def setup_once(): ) _setup_sentry_tracing() + _patch_readable_span() # _setup_instrumentors() logger.debug("[OTel] Finished setting up OpenTelemetry integration") +def _patch_readable_span(): + # type: () -> None + """ + We need to pass through sentry specific metadata/objects from Span to ReadableSpan + to work with them consistently in the SpanProcessor. + """ + + @property + def sentry_meta(self): + # type: (Union[AbstractSpan, Span, ReadableSpan]) -> dict[str, Any] + if not getattr(self, "_sentry_meta", None): + self._sentry_meta = {} + return self._sentry_meta + + AbstractSpan.sentry_meta = sentry_meta + ReadableSpan.sentry_meta = sentry_meta + + old_readable_span = Span._readable_span + + def sentry_patched_readable_span(self): + # type: (Span) -> ReadableSpan + readable_span = old_readable_span(self) + readable_span._sentry_meta = self._sentry_meta + return readable_span + + Span._readable_span = sentry_patched_readable_span + + def _setup_sentry_tracing(): # type: () -> None import opentelemetry.context diff --git a/sentry_sdk/integrations/opentelemetry/potel_span_processor.py b/sentry_sdk/integrations/opentelemetry/potel_span_processor.py index ed31608516..a254aada05 100644 --- a/sentry_sdk/integrations/opentelemetry/potel_span_processor.py +++ b/sentry_sdk/integrations/opentelemetry/potel_span_processor.py @@ -6,7 +6,7 @@ format_span_id, get_current_span, INVALID_SPAN, - Span as TraceApiSpan, + Span as AbstractSpan, ) from opentelemetry.context import Context from opentelemetry.sdk.trace import Span, ReadableSpan, SpanProcessor @@ -78,19 +78,19 @@ def force_flush(self, timeout_millis=30000): return True def _add_root_span(self, span, parent_span): - # type: (Span, TraceApiSpan) -> None + # type: (Span, AbstractSpan) -> None """ This is required to make POTelSpan.root_span work since we can't traverse back to the root purely with otel efficiently. """ if parent_span != INVALID_SPAN and not parent_span.get_span_context().is_remote: # child span points to parent's root or parent - span._sentry_root_otel_span = getattr( - parent_span, "_sentry_root_otel_span", parent_span + span.sentry_meta["root_span"] = parent_span.sentry_meta.get( + "root_span", parent_span ) else: # root span points to itself - span._sentry_root_otel_span = span + span.sentry_meta["root_span"] = span def _flush_root_span(self, span): # type: (ReadableSpan) -> None diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index d4033b52ef..bdfa846257 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -1327,8 +1327,7 @@ def containing_transaction(self): def root_span(self): # type: () -> Optional[POTelSpan] root_otel_span = cast( - "Optional[OtelSpan]", - getattr(self._otel_span, "_sentry_root_otel_span", None), + "Optional[OtelSpan]", self._otel_span.sentry_meta.get("root_span", None) ) return POTelSpan(otel_span=root_otel_span) if root_otel_span else None