diff --git a/sentry_sdk/integrations/opentelemetry/propagator.py b/sentry_sdk/integrations/opentelemetry/propagator.py index a40f038ffa..5b5f13861d 100644 --- a/sentry_sdk/integrations/opentelemetry/propagator.py +++ b/sentry_sdk/integrations/opentelemetry/propagator.py @@ -109,7 +109,7 @@ def inject( span_id = trace.format_span_id(current_span_context.span_id) - span_map = SentrySpanProcessor().otel_span_map + span_map = SentrySpanProcessor.otel_span_map sentry_span = span_map.get(span_id, None) if not sentry_span: return diff --git a/sentry_sdk/integrations/opentelemetry/span_processor.py b/sentry_sdk/integrations/opentelemetry/span_processor.py index 8a589af308..de7d10a762 100644 --- a/sentry_sdk/integrations/opentelemetry/span_processor.py +++ b/sentry_sdk/integrations/opentelemetry/span_processor.py @@ -83,6 +83,8 @@ class SentrySpanProcessor(SpanProcessor): # The currently open spans. Elements will be discarded after SPAN_MAX_TIME_OPEN_MINUTES open_spans: "dict[int, set[str]]" = {} + initialized: "bool" = False + def __new__(cls) -> "SentrySpanProcessor": if not hasattr(cls, "instance"): cls.instance = super().__new__(cls) @@ -91,10 +93,15 @@ def __new__(cls) -> "SentrySpanProcessor": return cls.instance # type: ignore[misc] def __init__(self) -> None: + if self.initialized: + return + @add_global_event_processor def global_event_processor(event: "Event", hint: "Hint") -> "Event": return link_trace_context_to_error_event(event, self.otel_span_map) + self.initialized = True + def _prune_old_spans(self: "SentrySpanProcessor") -> None: """ Prune spans that have been open for too long. diff --git a/tests/integrations/opentelemetry/test_propagator.py b/tests/integrations/opentelemetry/test_propagator.py index d999b0bb2b..4374098b23 100644 --- a/tests/integrations/opentelemetry/test_propagator.py +++ b/tests/integrations/opentelemetry/test_propagator.py @@ -1,3 +1,10 @@ +from sentry_sdk.integrations.opentelemetry.integration import ( + OpenTelemetryIntegration, +) +from opentelemetry import trace +from sentry_sdk.scope import global_event_processors + + import pytest from unittest import mock @@ -298,3 +305,21 @@ def test_inject_sentry_span_baggage(): "baggage", baggage.serialize(), ) + + +def test_inject_no_memory_leak(): + + OpenTelemetryIntegration.setup_once() + + tracer = trace.get_tracer(__name__) + propagator = SentryPropagator() + + cnt_before = len(global_event_processors) + + with tracer.start_as_current_span("bar") as new_span: + context = set_span_in_context(new_span) + carrier = "any_carrier" + propagator.inject(carrier, context) + + cnt_after = len(global_event_processors) + assert cnt_after == cnt_before diff --git a/tests/integrations/opentelemetry/test_span_processor.py b/tests/integrations/opentelemetry/test_span_processor.py index e1cd849b94..0444475ffa 100644 --- a/tests/integrations/opentelemetry/test_span_processor.py +++ b/tests/integrations/opentelemetry/test_span_processor.py @@ -1,3 +1,4 @@ +from sentry_sdk.scope import global_event_processors import time from datetime import datetime, timezone from unittest import mock @@ -621,3 +622,14 @@ def test_pruning_old_spans_on_end(): span_processor.on_end(otel_span) assert sorted(list(span_processor.otel_span_map.keys())) == ["111111111abcdef"] assert sorted(list(span_processor.open_spans.values())) == [{"111111111abcdef"}] + + +def test_no_memory_leak(): + span_processor_1 = SentrySpanProcessor() + cnt_before = len(global_event_processors) + + span_processor_2 = SentrySpanProcessor() + + cnt_after = len(global_event_processors) + assert span_processor_1 is span_processor_2 + assert cnt_before == cnt_after