Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 47 additions & 11 deletions langfuse/_client/resource_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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
124 changes: 124 additions & 0 deletions tests/test_otel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Loading