From 9ce9cd9c8d6a319e3a7000cbad462b68498cbce4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 27 Jan 2026 10:15:02 +0000 Subject: [PATCH] Fix compatibility with ddtrace v4.x and other non-SDK TracerProviders When ddtrace (v4.x) or similar OpenTelemetry integrations set up an API-only TracerProvider before Langfuse initializes, Langfuse would fail with: AttributeError: 'TracerProvider' object has no attribute 'add_span_processor' This occurred because Langfuse assumed that any non-ProxyTracerProvider would be an SDK TracerProvider with the add_span_processor method. The fix updates _init_tracer_provider() to handle three scenarios: 1. No provider set (ProxyTracerProvider): Creates SDK provider and sets as global 2. SDK TracerProvider already set: Reuses the existing provider 3. Non-SDK TracerProvider (e.g., ddtrace): Creates a new SDK provider for Langfuse without overriding the global provider This allows Langfuse to work alongside ddtrace and other OpenTelemetry integrations that don't use the SDK TracerProvider. Addresses issue reported in GitHub discussions about ddtrace v4.x compatibility. Co-authored-by: jannik --- langfuse/_client/resource_manager.py | 58 ++++++++++--- tests/test_otel.py | 124 +++++++++++++++++++++++++++ 2 files changed, 171 insertions(+), 11 deletions(-) diff --git a/langfuse/_client/resource_manager.py b/langfuse/_client/resource_manager.py index 08c008234..4cca747ad 100644 --- a/langfuse/_client/resource_manager.py +++ b/langfuse/_client/resource_manager.py @@ -425,6 +425,18 @@ def _init_tracer_provider( release: Optional[str] = None, sample_rate: Optional[float] = None, ) -> TracerProvider: + """Initialize or retrieve a TracerProvider compatible with Langfuse. + + This function handles three scenarios: + 1. No provider set (ProxyTracerProvider): Creates a new SDK TracerProvider and sets it as global + 2. SDK TracerProvider already set: Reuses the existing provider + 3. Non-SDK TracerProvider (e.g., ddtrace's API-only provider): Creates a new SDK TracerProvider + for Langfuse without overriding the global provider to avoid conflicts + + The third case addresses compatibility with libraries like ddtrace that use + opentelemetry-api but not opentelemetry-sdk. These providers don't have the + add_span_processor method that Langfuse requires. + """ environment = environment or os.environ.get(LANGFUSE_TRACING_ENVIRONMENT) release = release or os.environ.get(LANGFUSE_RELEASE) or get_common_release_envs() @@ -437,19 +449,43 @@ def _init_tracer_provider( {k: v for k, v in resource_attributes.items() if v is not None} ) - provider = None - default_provider = cast(TracerProvider, otel_trace_api.get_tracer_provider()) + default_provider = otel_trace_api.get_tracer_provider() - if isinstance(default_provider, otel_trace_api.ProxyTracerProvider): - provider = TracerProvider( - resource=resource, - sampler=TraceIdRatioBased(sample_rate) - if sample_rate is not None and sample_rate < 1 - else None, - ) - otel_trace_api.set_tracer_provider(provider) + # Check if the existing provider is an SDK TracerProvider (has add_span_processor method). + # Some OpenTelemetry integrations (like ddtrace) use API-only TracerProviders that don't + # have the add_span_processor method required by Langfuse. + is_sdk_tracer_provider = isinstance(default_provider, TracerProvider) + is_proxy_tracer_provider = isinstance( + default_provider, otel_trace_api.ProxyTracerProvider + ) + if is_sdk_tracer_provider: + # Reuse the existing SDK TracerProvider + return default_provider + + # Create a new SDK TracerProvider for Langfuse + provider = TracerProvider( + resource=resource, + sampler=TraceIdRatioBased(sample_rate) + if sample_rate is not None and sample_rate < 1 + else None, + ) + + if is_proxy_tracer_provider: + # No provider has been set yet, so we can set ours as the global provider + otel_trace_api.set_tracer_provider(provider) else: - provider = default_provider + # Another non-SDK provider exists (e.g., ddtrace's API-only provider). + # Don't override the global provider to avoid "Overriding of current + # TracerProvider is not allowed" errors. Langfuse will use its own + # provider internally while other integrations keep their own. + langfuse_logger.info( + "Detected an existing OpenTelemetry TracerProvider that is not an SDK TracerProvider " + "(e.g., from ddtrace or another OpenTelemetry integration). Langfuse will create its " + "own internal TracerProvider for tracing. To share a TracerProvider with other " + "libraries, set a global SDK TracerProvider before initializing any OpenTelemetry " + "integrations, or pass a dedicated TracerProvider to Langfuse via the tracer_provider " + "parameter." + ) return provider diff --git a/tests/test_otel.py b/tests/test_otel.py index 89c028c68..4d39e56ab 100644 --- a/tests/test_otel.py +++ b/tests/test_otel.py @@ -3328,3 +3328,127 @@ def test_langfuse_event_update_immutability(self, langfuse_client, caplog): assert result is event parent_span.end() + + +class TestTracerProviderCompatibility(TestOTelBase): + """Tests for TracerProvider compatibility with non-SDK providers like ddtrace.""" + + def test_init_tracer_provider_with_non_sdk_provider(self, monkeypatch, caplog): + """Test that _init_tracer_provider creates a new SDK provider when a non-SDK provider exists. + + This tests the core logic that handles ddtrace-like scenarios where an + API-only TracerProvider (without add_span_processor) is set globally. + """ + import logging + + from opentelemetry import trace as trace_api_module + from opentelemetry.sdk.trace import TracerProvider as SDKTracerProvider + from opentelemetry.trace import TracerProvider as APITracerProvider + + from langfuse._client.resource_manager import _init_tracer_provider + + # Create a mock non-SDK provider (like ddtrace's API-only provider) + class MockNonSDKTracerProvider(APITracerProvider): + """A mock TracerProvider without add_span_processor.""" + + def get_tracer( + self, + instrumenting_module_name, + instrumenting_library_version=None, + schema_url=None, + attributes=None, + ): + return trace_api_module.NoOpTracer() + + mock_provider = MockNonSDKTracerProvider() + + # Mock get_tracer_provider to return our non-SDK provider + monkeypatch.setattr( + "langfuse._client.resource_manager.otel_trace_api.get_tracer_provider", + lambda: mock_provider, + ) + + # Track if set_tracer_provider was called + set_provider_calls = [] + + def mock_set_provider(provider): + set_provider_calls.append(provider) + # Don't actually set it to avoid affecting other tests + + monkeypatch.setattr( + "langfuse._client.resource_manager.otel_trace_api.set_tracer_provider", + mock_set_provider, + ) + + # Call _init_tracer_provider + with caplog.at_level(logging.INFO, logger="langfuse"): + provider = _init_tracer_provider() + + # Verify a new SDK TracerProvider was created + assert isinstance(provider, SDKTracerProvider) + assert provider is not mock_provider + + # Verify set_tracer_provider was NOT called (because non-SDK provider exists) + assert len(set_provider_calls) == 0 + + # Verify the info message was logged + assert "not an SDK TracerProvider" in caplog.text + + def test_init_tracer_provider_with_sdk_provider(self, monkeypatch): + """Test that _init_tracer_provider reuses an existing SDK provider.""" + from opentelemetry.sdk.trace import TracerProvider as SDKTracerProvider + + from langfuse._client.resource_manager import _init_tracer_provider + + # Create an existing SDK provider + existing_sdk_provider = SDKTracerProvider() + + # Mock get_tracer_provider to return our SDK provider + monkeypatch.setattr( + "langfuse._client.resource_manager.otel_trace_api.get_tracer_provider", + lambda: existing_sdk_provider, + ) + + # Call _init_tracer_provider + provider = _init_tracer_provider() + + # Verify the existing SDK provider is reused + assert provider is existing_sdk_provider + + def test_init_tracer_provider_with_proxy_provider(self, monkeypatch): + """Test that _init_tracer_provider creates and sets a new SDK provider when no provider is set.""" + from opentelemetry import trace as trace_api_module + from opentelemetry.sdk.trace import TracerProvider as SDKTracerProvider + + from langfuse._client.resource_manager import _init_tracer_provider + + # Create a ProxyTracerProvider (the default when nothing is set) + proxy_provider = trace_api_module.ProxyTracerProvider() + + # Mock get_tracer_provider to return the proxy provider + monkeypatch.setattr( + "langfuse._client.resource_manager.otel_trace_api.get_tracer_provider", + lambda: proxy_provider, + ) + + # Track if set_tracer_provider was called + set_provider_calls = [] + + def mock_set_provider(provider): + set_provider_calls.append(provider) + + monkeypatch.setattr( + "langfuse._client.resource_manager.otel_trace_api.set_tracer_provider", + mock_set_provider, + ) + + # Call _init_tracer_provider + provider = _init_tracer_provider() + + # Verify a new SDK TracerProvider was created + assert isinstance(provider, SDKTracerProvider) + + # Verify set_tracer_provider WAS called (because only a proxy provider existed) + assert len(set_provider_calls) == 1 + assert set_provider_calls[0] is provider +