Skip to content

Commit 688eb83

Browse files
authored
feat(tracing): allow custom OpenTelemetry ID generators (#1724)
1 parent 8143977 commit 688eb83

4 files changed

Lines changed: 90 additions & 3 deletions

File tree

langfuse/_client/client.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
from opentelemetry import trace as otel_trace_api
3434
from opentelemetry.sdk.trace import ReadableSpan, TracerProvider
3535
from opentelemetry.sdk.trace.export import SpanExporter
36-
from opentelemetry.sdk.trace.id_generator import RandomIdGenerator
36+
from opentelemetry.sdk.trace.id_generator import IdGenerator, RandomIdGenerator
3737
from opentelemetry.util._decorator import (
3838
_AgnosticContextManager,
3939
_agnosticcontextmanager,
@@ -230,6 +230,7 @@ def mask_otel_spans(
230230
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).
231231
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.
232232
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.
233+
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.
233234
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.
234235
235236
Example:
@@ -295,6 +296,7 @@ def __init__(
295296
should_export_span: Optional[Callable[[ReadableSpan], bool]] = None,
296297
additional_headers: Optional[Dict[str, str]] = None,
297298
tracer_provider: Optional[TracerProvider] = None,
299+
id_generator: Optional[IdGenerator] = None,
298300
span_exporter: Optional[SpanExporter] = None,
299301
):
300302
self._base_url = (
@@ -393,6 +395,7 @@ def __init__(
393395
should_export_span=should_export_span,
394396
additional_headers=additional_headers,
395397
tracer_provider=tracer_provider,
398+
id_generator=id_generator,
396399
span_exporter=span_exporter,
397400
)
398401
self._mask = self._resources.mask

langfuse/_client/get_client.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ def _create_client_from_instance(
5555
should_export_span=instance.should_export_span,
5656
additional_headers=instance.additional_headers,
5757
tracer_provider=instance.tracer_provider,
58+
id_generator=instance.id_generator,
5859
span_exporter=instance.span_exporter,
5960
httpx_client=instance.httpx_client,
6061
)

langfuse/_client/resource_manager.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from opentelemetry.sdk.resources import Resource
2626
from opentelemetry.sdk.trace import ReadableSpan, TracerProvider
2727
from opentelemetry.sdk.trace.export import SpanExporter
28+
from opentelemetry.sdk.trace.id_generator import IdGenerator
2829
from opentelemetry.sdk.trace.sampling import Decision, TraceIdRatioBased
2930
from opentelemetry.trace import Tracer
3031

@@ -123,6 +124,7 @@ def __new__(
123124
should_export_span: Optional[Callable[[ReadableSpan], bool]] = None,
124125
additional_headers: Optional[Dict[str, str]] = None,
125126
tracer_provider: Optional[TracerProvider] = None,
127+
id_generator: Optional[IdGenerator] = None,
126128
span_exporter: Optional[SpanExporter] = None,
127129
) -> "LangfuseResourceManager":
128130
if public_key in cls._instances:
@@ -160,6 +162,7 @@ def __new__(
160162
should_export_span=should_export_span,
161163
additional_headers=additional_headers,
162164
tracer_provider=tracer_provider,
165+
id_generator=id_generator,
163166
span_exporter=span_exporter,
164167
)
165168

@@ -188,6 +191,7 @@ def _initialize_instance(
188191
should_export_span: Optional[Callable[[ReadableSpan], bool]] = None,
189192
additional_headers: Optional[Dict[str, str]] = None,
190193
tracer_provider: Optional[TracerProvider] = None,
194+
id_generator: Optional[IdGenerator] = None,
191195
span_exporter: Optional[SpanExporter] = None,
192196
) -> None:
193197
self.public_key = public_key
@@ -208,6 +212,7 @@ def _initialize_instance(
208212
self.blocked_instrumentation_scopes = blocked_instrumentation_scopes
209213
self.should_export_span = should_export_span
210214
self.additional_headers = additional_headers
215+
self.id_generator = id_generator
211216
self.span_exporter = span_exporter
212217
self.tracer_provider: Optional[TracerProvider] = None
213218

@@ -269,7 +274,10 @@ def _initialize_instance(
269274
# OTEL Tracer
270275
if tracing_enabled:
271276
tracer_provider = tracer_provider or _init_tracer_provider(
272-
environment=environment, release=release, sample_rate=sample_rate
277+
environment=environment,
278+
release=release,
279+
sample_rate=sample_rate,
280+
id_generator=id_generator,
273281
)
274282
self.tracer_provider = tracer_provider
275283

@@ -490,6 +498,7 @@ def _init_tracer_provider(
490498
environment: Optional[str] = None,
491499
release: Optional[str] = None,
492500
sample_rate: Optional[float] = None,
501+
id_generator: Optional[IdGenerator] = None,
493502
) -> TracerProvider:
494503
environment = environment or os.environ.get(LANGFUSE_TRACING_ENVIRONMENT)
495504
release = release or os.environ.get(LANGFUSE_RELEASE) or get_common_release_envs()
@@ -512,10 +521,17 @@ def _init_tracer_provider(
512521
sampler=TraceIdRatioBased(sample_rate)
513522
if sample_rate is not None and sample_rate < 1
514523
else None,
524+
id_generator=id_generator,
515525
)
516526
otel_trace_api.set_tracer_provider(provider)
517527

518528
else:
529+
if id_generator is not None:
530+
langfuse_logger.warning(
531+
"Configuration: id_generator was ignored because an OpenTelemetry TracerProvider is already registered. "
532+
"Pass a TracerProvider configured with the desired id_generator to Langfuse(tracer_provider=...) instead."
533+
)
534+
519535
provider = default_provider
520536

521537
return provider

tests/unit/test_otel.py

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
SpanExporter,
1313
SpanExportResult,
1414
)
15-
from opentelemetry.sdk.trace.id_generator import RandomIdGenerator
15+
from opentelemetry.sdk.trace.id_generator import IdGenerator, RandomIdGenerator
1616

1717
from langfuse import propagate_attributes
1818
from langfuse._client.attributes import LangfuseOtelSpanAttributes
@@ -46,6 +46,31 @@ def clear(self):
4646
self._finished_spans.clear()
4747

4848

49+
class PredictableIdGenerator(IdGenerator):
50+
"""Deterministic generator for tests that need exact OTel IDs."""
51+
52+
def __init__(
53+
self,
54+
*,
55+
trace_ids: Sequence[int] = (),
56+
span_ids: Sequence[int] = (),
57+
) -> None:
58+
self._trace_ids = list(trace_ids)
59+
self._span_ids = list(span_ids)
60+
61+
def generate_trace_id(self) -> int:
62+
if not self._trace_ids:
63+
raise AssertionError("No trace IDs left in PredictableIdGenerator")
64+
65+
return self._trace_ids.pop(0)
66+
67+
def generate_span_id(self) -> int:
68+
if not self._span_ids:
69+
raise AssertionError("No span IDs left in PredictableIdGenerator")
70+
71+
return self._span_ids.pop(0)
72+
73+
4974
class TestOTelBase:
5075
"""Base class for OTEL tests with common fixtures and helper methods."""
5176

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

3454+
def test_langfuse_owned_provider_uses_otel_default_id_generator(
3455+
self, mock_processor_init
3456+
):
3457+
"""Langfuse-owned providers keep the OpenTelemetry default generator."""
3458+
3459+
client = Langfuse(
3460+
public_key="test-public-key",
3461+
secret_key="test-secret-key",
3462+
base_url="http://test-host",
3463+
tracing_enabled=True,
3464+
)
3465+
3466+
assert client._resources is not None
3467+
assert client._resources.tracer_provider is not None
3468+
assert isinstance(
3469+
client._resources.tracer_provider.id_generator,
3470+
RandomIdGenerator,
3471+
)
3472+
3473+
def test_langfuse_owned_provider_accepts_custom_id_generator(
3474+
self, mock_processor_init
3475+
):
3476+
"""Custom ID generators are passed to the provider Langfuse creates."""
3477+
3478+
id_generator = PredictableIdGenerator(
3479+
trace_ids=[0x1234567890ABCDEF1234567890ABCDEF],
3480+
span_ids=[0x1234567890ABCDEF],
3481+
)
3482+
client = Langfuse(
3483+
public_key="test-public-key",
3484+
secret_key="test-secret-key",
3485+
base_url="http://test-host",
3486+
tracing_enabled=True,
3487+
id_generator=id_generator,
3488+
)
3489+
3490+
span = client.start_observation(name="custom-id-span")
3491+
span.end()
3492+
3493+
assert span.trace_id == "1234567890abcdef1234567890abcdef"
3494+
assert span.id == "1234567890abcdef"
3495+
34293496
def test_observation_id_with_seed(self, langfuse_client):
34303497
"""Test observation_id generation with seed (should be deterministic)."""
34313498
seed = "test-identifier"

0 commit comments

Comments
 (0)