Skip to content

Commit 9ce9cd9

Browse files
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 <jannik@langfuse.com>
1 parent cf653e9 commit 9ce9cd9

2 files changed

Lines changed: 171 additions & 11 deletions

File tree

langfuse/_client/resource_manager.py

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,18 @@ def _init_tracer_provider(
425425
release: Optional[str] = None,
426426
sample_rate: Optional[float] = None,
427427
) -> TracerProvider:
428+
"""Initialize or retrieve a TracerProvider compatible with Langfuse.
429+
430+
This function handles three scenarios:
431+
1. No provider set (ProxyTracerProvider): Creates a new SDK TracerProvider and sets it as global
432+
2. SDK TracerProvider already set: Reuses the existing provider
433+
3. Non-SDK TracerProvider (e.g., ddtrace's API-only provider): Creates a new SDK TracerProvider
434+
for Langfuse without overriding the global provider to avoid conflicts
435+
436+
The third case addresses compatibility with libraries like ddtrace that use
437+
opentelemetry-api but not opentelemetry-sdk. These providers don't have the
438+
add_span_processor method that Langfuse requires.
439+
"""
428440
environment = environment or os.environ.get(LANGFUSE_TRACING_ENVIRONMENT)
429441
release = release or os.environ.get(LANGFUSE_RELEASE) or get_common_release_envs()
430442

@@ -437,19 +449,43 @@ def _init_tracer_provider(
437449
{k: v for k, v in resource_attributes.items() if v is not None}
438450
)
439451

440-
provider = None
441-
default_provider = cast(TracerProvider, otel_trace_api.get_tracer_provider())
452+
default_provider = otel_trace_api.get_tracer_provider()
442453

443-
if isinstance(default_provider, otel_trace_api.ProxyTracerProvider):
444-
provider = TracerProvider(
445-
resource=resource,
446-
sampler=TraceIdRatioBased(sample_rate)
447-
if sample_rate is not None and sample_rate < 1
448-
else None,
449-
)
450-
otel_trace_api.set_tracer_provider(provider)
454+
# Check if the existing provider is an SDK TracerProvider (has add_span_processor method).
455+
# Some OpenTelemetry integrations (like ddtrace) use API-only TracerProviders that don't
456+
# have the add_span_processor method required by Langfuse.
457+
is_sdk_tracer_provider = isinstance(default_provider, TracerProvider)
458+
is_proxy_tracer_provider = isinstance(
459+
default_provider, otel_trace_api.ProxyTracerProvider
460+
)
451461

462+
if is_sdk_tracer_provider:
463+
# Reuse the existing SDK TracerProvider
464+
return default_provider
465+
466+
# Create a new SDK TracerProvider for Langfuse
467+
provider = TracerProvider(
468+
resource=resource,
469+
sampler=TraceIdRatioBased(sample_rate)
470+
if sample_rate is not None and sample_rate < 1
471+
else None,
472+
)
473+
474+
if is_proxy_tracer_provider:
475+
# No provider has been set yet, so we can set ours as the global provider
476+
otel_trace_api.set_tracer_provider(provider)
452477
else:
453-
provider = default_provider
478+
# Another non-SDK provider exists (e.g., ddtrace's API-only provider).
479+
# Don't override the global provider to avoid "Overriding of current
480+
# TracerProvider is not allowed" errors. Langfuse will use its own
481+
# provider internally while other integrations keep their own.
482+
langfuse_logger.info(
483+
"Detected an existing OpenTelemetry TracerProvider that is not an SDK TracerProvider "
484+
"(e.g., from ddtrace or another OpenTelemetry integration). Langfuse will create its "
485+
"own internal TracerProvider for tracing. To share a TracerProvider with other "
486+
"libraries, set a global SDK TracerProvider before initializing any OpenTelemetry "
487+
"integrations, or pass a dedicated TracerProvider to Langfuse via the tracer_provider "
488+
"parameter."
489+
)
454490

455491
return provider

tests/test_otel.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3328,3 +3328,127 @@ def test_langfuse_event_update_immutability(self, langfuse_client, caplog):
33283328
assert result is event
33293329

33303330
parent_span.end()
3331+
3332+
3333+
class TestTracerProviderCompatibility(TestOTelBase):
3334+
"""Tests for TracerProvider compatibility with non-SDK providers like ddtrace."""
3335+
3336+
def test_init_tracer_provider_with_non_sdk_provider(self, monkeypatch, caplog):
3337+
"""Test that _init_tracer_provider creates a new SDK provider when a non-SDK provider exists.
3338+
3339+
This tests the core logic that handles ddtrace-like scenarios where an
3340+
API-only TracerProvider (without add_span_processor) is set globally.
3341+
"""
3342+
import logging
3343+
3344+
from opentelemetry import trace as trace_api_module
3345+
from opentelemetry.sdk.trace import TracerProvider as SDKTracerProvider
3346+
from opentelemetry.trace import TracerProvider as APITracerProvider
3347+
3348+
from langfuse._client.resource_manager import _init_tracer_provider
3349+
3350+
# Create a mock non-SDK provider (like ddtrace's API-only provider)
3351+
class MockNonSDKTracerProvider(APITracerProvider):
3352+
"""A mock TracerProvider without add_span_processor."""
3353+
3354+
def get_tracer(
3355+
self,
3356+
instrumenting_module_name,
3357+
instrumenting_library_version=None,
3358+
schema_url=None,
3359+
attributes=None,
3360+
):
3361+
return trace_api_module.NoOpTracer()
3362+
3363+
mock_provider = MockNonSDKTracerProvider()
3364+
3365+
# Mock get_tracer_provider to return our non-SDK provider
3366+
monkeypatch.setattr(
3367+
"langfuse._client.resource_manager.otel_trace_api.get_tracer_provider",
3368+
lambda: mock_provider,
3369+
)
3370+
3371+
# Track if set_tracer_provider was called
3372+
set_provider_calls = []
3373+
3374+
def mock_set_provider(provider):
3375+
set_provider_calls.append(provider)
3376+
# Don't actually set it to avoid affecting other tests
3377+
3378+
monkeypatch.setattr(
3379+
"langfuse._client.resource_manager.otel_trace_api.set_tracer_provider",
3380+
mock_set_provider,
3381+
)
3382+
3383+
# Call _init_tracer_provider
3384+
with caplog.at_level(logging.INFO, logger="langfuse"):
3385+
provider = _init_tracer_provider()
3386+
3387+
# Verify a new SDK TracerProvider was created
3388+
assert isinstance(provider, SDKTracerProvider)
3389+
assert provider is not mock_provider
3390+
3391+
# Verify set_tracer_provider was NOT called (because non-SDK provider exists)
3392+
assert len(set_provider_calls) == 0
3393+
3394+
# Verify the info message was logged
3395+
assert "not an SDK TracerProvider" in caplog.text
3396+
3397+
def test_init_tracer_provider_with_sdk_provider(self, monkeypatch):
3398+
"""Test that _init_tracer_provider reuses an existing SDK provider."""
3399+
from opentelemetry.sdk.trace import TracerProvider as SDKTracerProvider
3400+
3401+
from langfuse._client.resource_manager import _init_tracer_provider
3402+
3403+
# Create an existing SDK provider
3404+
existing_sdk_provider = SDKTracerProvider()
3405+
3406+
# Mock get_tracer_provider to return our SDK provider
3407+
monkeypatch.setattr(
3408+
"langfuse._client.resource_manager.otel_trace_api.get_tracer_provider",
3409+
lambda: existing_sdk_provider,
3410+
)
3411+
3412+
# Call _init_tracer_provider
3413+
provider = _init_tracer_provider()
3414+
3415+
# Verify the existing SDK provider is reused
3416+
assert provider is existing_sdk_provider
3417+
3418+
def test_init_tracer_provider_with_proxy_provider(self, monkeypatch):
3419+
"""Test that _init_tracer_provider creates and sets a new SDK provider when no provider is set."""
3420+
from opentelemetry import trace as trace_api_module
3421+
from opentelemetry.sdk.trace import TracerProvider as SDKTracerProvider
3422+
3423+
from langfuse._client.resource_manager import _init_tracer_provider
3424+
3425+
# Create a ProxyTracerProvider (the default when nothing is set)
3426+
proxy_provider = trace_api_module.ProxyTracerProvider()
3427+
3428+
# Mock get_tracer_provider to return the proxy provider
3429+
monkeypatch.setattr(
3430+
"langfuse._client.resource_manager.otel_trace_api.get_tracer_provider",
3431+
lambda: proxy_provider,
3432+
)
3433+
3434+
# Track if set_tracer_provider was called
3435+
set_provider_calls = []
3436+
3437+
def mock_set_provider(provider):
3438+
set_provider_calls.append(provider)
3439+
3440+
monkeypatch.setattr(
3441+
"langfuse._client.resource_manager.otel_trace_api.set_tracer_provider",
3442+
mock_set_provider,
3443+
)
3444+
3445+
# Call _init_tracer_provider
3446+
provider = _init_tracer_provider()
3447+
3448+
# Verify a new SDK TracerProvider was created
3449+
assert isinstance(provider, SDKTracerProvider)
3450+
3451+
# Verify set_tracer_provider WAS called (because only a proxy provider existed)
3452+
assert len(set_provider_calls) == 1
3453+
assert set_provider_calls[0] is provider
3454+

0 commit comments

Comments
 (0)