Skip to content
Merged
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
5 changes: 4 additions & 1 deletion langfuse/_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
from opentelemetry import trace as otel_trace_api
from opentelemetry.sdk.trace import ReadableSpan, TracerProvider
from opentelemetry.sdk.trace.export import SpanExporter
from opentelemetry.sdk.trace.id_generator import RandomIdGenerator
from opentelemetry.sdk.trace.id_generator import IdGenerator, RandomIdGenerator
from opentelemetry.util._decorator import (
_AgnosticContextManager,
_agnosticcontextmanager,
Expand Down Expand Up @@ -230,6 +230,7 @@ def mask_otel_spans(
should_export_span (Optional[Callable[[ReadableSpan], bool]]): Callback to decide whether to export a span. If omitted, Langfuse uses the default filter (Langfuse SDK spans, spans with `gen_ai.*` attributes, and known LLM instrumentation scopes).
additional_headers (Optional[Dict[str, str]]): Additional headers to include in all API requests and in the default OTLPSpanExporter requests. These headers will be merged with default headers. Note: If httpx_client is provided, additional_headers must be set directly on your custom httpx_client as well. If `span_exporter` is provided, these headers are not wired into that exporter and must be configured on the exporter instance directly.
tracer_provider(Optional[TracerProvider]): OpenTelemetry TracerProvider to use for Langfuse. This can be useful to set to have disconnected tracing between Langfuse and other OpenTelemetry-span emitting libraries. Note: To track active spans, the context is still shared between TracerProviders. This may lead to broken trace trees.
id_generator (Optional[IdGenerator]): OpenTelemetry ID generator to use when Langfuse creates its own TracerProvider. If omitted, the OpenTelemetry SDK default is used. If `tracer_provider` is provided, or an OpenTelemetry TracerProvider is already registered globally, configure the ID generator on that provider instead.
span_exporter (Optional[SpanExporter]): Custom OpenTelemetry span exporter for the Langfuse span processor. If omitted, Langfuse creates an OTLPSpanExporter pointed at the Langfuse OTLP endpoint. If provided, Langfuse does not wire `base_url`, exporter headers, exporter auth, or exporter timeout into it. Configure endpoint, headers, and timeout on the exporter instance directly. If you are sending spans to Langfuse v4 or using Langfuse Cloud Fast Preview, include `x-langfuse-ingestion-version=4` on the exporter to enable real time processing of exported spans.

Example:
Expand Down Expand Up @@ -295,6 +296,7 @@ def __init__(
should_export_span: Optional[Callable[[ReadableSpan], bool]] = None,
additional_headers: Optional[Dict[str, str]] = None,
tracer_provider: Optional[TracerProvider] = None,
id_generator: Optional[IdGenerator] = None,
span_exporter: Optional[SpanExporter] = None,
):
self._base_url = (
Expand Down Expand Up @@ -393,6 +395,7 @@ def __init__(
should_export_span=should_export_span,
additional_headers=additional_headers,
tracer_provider=tracer_provider,
id_generator=id_generator,
span_exporter=span_exporter,
)
self._mask = self._resources.mask
Expand Down
1 change: 1 addition & 0 deletions langfuse/_client/get_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ def _create_client_from_instance(
should_export_span=instance.should_export_span,
additional_headers=instance.additional_headers,
tracer_provider=instance.tracer_provider,
id_generator=instance.id_generator,
span_exporter=instance.span_exporter,
httpx_client=instance.httpx_client,
)
Expand Down
18 changes: 17 additions & 1 deletion langfuse/_client/resource_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import ReadableSpan, TracerProvider
from opentelemetry.sdk.trace.export import SpanExporter
from opentelemetry.sdk.trace.id_generator import IdGenerator
from opentelemetry.sdk.trace.sampling import Decision, TraceIdRatioBased
from opentelemetry.trace import Tracer

Expand Down Expand Up @@ -123,6 +124,7 @@ def __new__(
should_export_span: Optional[Callable[[ReadableSpan], bool]] = None,
additional_headers: Optional[Dict[str, str]] = None,
tracer_provider: Optional[TracerProvider] = None,
id_generator: Optional[IdGenerator] = None,
span_exporter: Optional[SpanExporter] = None,
) -> "LangfuseResourceManager":
if public_key in cls._instances:
Expand Down Expand Up @@ -160,6 +162,7 @@ def __new__(
should_export_span=should_export_span,
additional_headers=additional_headers,
tracer_provider=tracer_provider,
id_generator=id_generator,
span_exporter=span_exporter,
)

Expand Down Expand Up @@ -188,6 +191,7 @@ def _initialize_instance(
should_export_span: Optional[Callable[[ReadableSpan], bool]] = None,
additional_headers: Optional[Dict[str, str]] = None,
tracer_provider: Optional[TracerProvider] = None,
id_generator: Optional[IdGenerator] = None,
span_exporter: Optional[SpanExporter] = None,
) -> None:
self.public_key = public_key
Expand All @@ -208,6 +212,7 @@ def _initialize_instance(
self.blocked_instrumentation_scopes = blocked_instrumentation_scopes
self.should_export_span = should_export_span
self.additional_headers = additional_headers
self.id_generator = id_generator
self.span_exporter = span_exporter
self.tracer_provider: Optional[TracerProvider] = None

Expand Down Expand Up @@ -269,7 +274,10 @@ def _initialize_instance(
# OTEL Tracer
if tracing_enabled:
tracer_provider = tracer_provider or _init_tracer_provider(
environment=environment, release=release, sample_rate=sample_rate
environment=environment,
release=release,
sample_rate=sample_rate,
id_generator=id_generator,
)
self.tracer_provider = tracer_provider

Expand Down Expand Up @@ -490,6 +498,7 @@ def _init_tracer_provider(
environment: Optional[str] = None,
release: Optional[str] = None,
sample_rate: Optional[float] = None,
id_generator: Optional[IdGenerator] = None,
) -> TracerProvider:
environment = environment or os.environ.get(LANGFUSE_TRACING_ENVIRONMENT)
release = release or os.environ.get(LANGFUSE_RELEASE) or get_common_release_envs()
Expand All @@ -512,10 +521,17 @@ def _init_tracer_provider(
sampler=TraceIdRatioBased(sample_rate)
if sample_rate is not None and sample_rate < 1
else None,
id_generator=id_generator,
)
otel_trace_api.set_tracer_provider(provider)

else:
if id_generator is not None:
langfuse_logger.warning(
"Configuration: id_generator was ignored because an OpenTelemetry TracerProvider is already registered. "
"Pass a TracerProvider configured with the desired id_generator to Langfuse(tracer_provider=...) instead."
)

provider = default_provider

return provider
69 changes: 68 additions & 1 deletion tests/unit/test_otel.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
SpanExporter,
SpanExportResult,
)
from opentelemetry.sdk.trace.id_generator import RandomIdGenerator
from opentelemetry.sdk.trace.id_generator import IdGenerator, RandomIdGenerator

from langfuse import propagate_attributes
from langfuse._client.attributes import LangfuseOtelSpanAttributes
Expand Down Expand Up @@ -46,6 +46,31 @@ def clear(self):
self._finished_spans.clear()


class PredictableIdGenerator(IdGenerator):
"""Deterministic generator for tests that need exact OTel IDs."""

def __init__(
self,
*,
trace_ids: Sequence[int] = (),
span_ids: Sequence[int] = (),
) -> None:
self._trace_ids = list(trace_ids)
self._span_ids = list(span_ids)

def generate_trace_id(self) -> int:
if not self._trace_ids:
raise AssertionError("No trace IDs left in PredictableIdGenerator")

return self._trace_ids.pop(0)

def generate_span_id(self) -> int:
if not self._span_ids:
raise AssertionError("No span IDs left in PredictableIdGenerator")

return self._span_ids.pop(0)


class TestOTelBase:
"""Base class for OTEL tests with common fixtures and helper methods."""

Expand Down Expand Up @@ -3426,6 +3451,48 @@ def mock_generate_span_id(self):
assert observation_id == "1234567890abcdef"
assert len(observation_id) == 16 # 8 bytes hex-encoded = 16 characters

def test_langfuse_owned_provider_uses_otel_default_id_generator(
self, mock_processor_init
):
"""Langfuse-owned providers keep the OpenTelemetry default generator."""

client = Langfuse(
public_key="test-public-key",
secret_key="test-secret-key",
base_url="http://test-host",
tracing_enabled=True,
)

assert client._resources is not None
assert client._resources.tracer_provider is not None
assert isinstance(
client._resources.tracer_provider.id_generator,
RandomIdGenerator,
)

def test_langfuse_owned_provider_accepts_custom_id_generator(
self, mock_processor_init
):
"""Custom ID generators are passed to the provider Langfuse creates."""

id_generator = PredictableIdGenerator(
trace_ids=[0x1234567890ABCDEF1234567890ABCDEF],
span_ids=[0x1234567890ABCDEF],
)
client = Langfuse(
public_key="test-public-key",
secret_key="test-secret-key",
base_url="http://test-host",
tracing_enabled=True,
id_generator=id_generator,
)

span = client.start_observation(name="custom-id-span")
span.end()

assert span.trace_id == "1234567890abcdef1234567890abcdef"
assert span.id == "1234567890abcdef"

def test_observation_id_with_seed(self, langfuse_client):
"""Test observation_id generation with seed (should be deterministic)."""
seed = "test-identifier"
Expand Down