fix(langfuse): start generation span under metadata trace-id context

Langfuse derives a trace's identity from the OTEL span-context trace id, but
LangfuseOtelLogger only ever wrote metadata.trace_id as a span attribute, so
the exported trace kept a random id and metadata.trace_id from extra_body was
ignored (#31378).

Override _get_span_context to anchor the otherwise-random root span to the
normalized trace id derived from existing_trace_id/trace_id, building a
non-recording parent context the same way the Langfuse SDK does for
trace_context={'trace_id': ...}. Any upstream parent (explicit parent span,
traceparent header, active span) still wins, so distributed tracing and
existing_trace_id continuation are preserved.

Fixes #31378
This commit is contained in:
Dhanush Reddy Chilakala 2026-06-26 00:52:33 -05:00
parent 246dfd1872
commit 9d2063a169
No known key found for this signature in database
GPG key ID: 51C52D74B4BAB2DC

View file

@ -18,11 +18,14 @@ from litellm.types.integrations.langfuse_otel import (
from litellm.types.utils import StandardCallbackDynamicParams
if TYPE_CHECKING:
from opentelemetry.context import Context as _Context
from opentelemetry.trace import Span as _Span
Span = Union[_Span, Any]
Context = Union[_Context, Any]
else:
Span = Any
Context = Any
LANGFUSE_CLOUD_EU_ENDPOINT = "https://cloud.langfuse.com/api/public/otel"
@ -101,6 +104,61 @@ class LangfuseOtelLogger(OpenTelemetry):
return metadata
@staticmethod
def _effective_langfuse_trace_id(kwargs) -> Optional[str]:
"""Return the user-provided Langfuse trace id seed, if any.
``existing_trace_id`` continues an existing trace and wins over
``trace_id``, matching the vanilla Langfuse precedence.
"""
metadata = LangfuseOtelLogger._extract_langfuse_metadata(kwargs)
raw = metadata.get("existing_trace_id") or metadata.get("trace_id")
return raw if isinstance(raw, str) and raw else None
@staticmethod
def _trace_id_parent_context(otel_trace_id_hex: str) -> "Context":
"""Build an OTEL context whose non-recording parent carries
``otel_trace_id_hex`` so a span started under it inherits that trace id.
Mirrors how the Langfuse SDK turns ``trace_context={"trace_id": ...}``
into a context: a non-recording parent with a synthetic span id and the
sampled flag set.
"""
from opentelemetry.sdk.trace.id_generator import RandomIdGenerator
from opentelemetry.trace import (
NonRecordingSpan,
SpanContext,
TraceFlags,
set_span_in_context,
)
span_context = SpanContext(
trace_id=int(otel_trace_id_hex, 16),
span_id=RandomIdGenerator().generate_span_id(),
is_remote=False,
trace_flags=TraceFlags(TraceFlags.SAMPLED),
)
return set_span_in_context(NonRecordingSpan(span_context))
def _get_span_context(self, kwargs, default_span: Optional[Span] = None):
"""Honor an explicit Langfuse trace id from request metadata.
Any upstream parent (explicit parent span, traceparent header, or active
span) still wins, preserving distributed-tracing behavior. Only the
otherwise-random root span is anchored to the normalized trace id so the
Langfuse trace identity matches the caller's ``metadata.trace_id``.
"""
ctx, parent_span = super()._get_span_context(kwargs, default_span=default_span)
if ctx is not None or parent_span is not None:
return ctx, parent_span
effective_trace_id = self._effective_langfuse_trace_id(kwargs)
if effective_trace_id is None:
return ctx, parent_span
otel_trace_id = normalize_trace_id_for_otel(effective_trace_id)
return self._trace_id_parent_context(otel_trace_id), None
@staticmethod
def _set_metadata_attributes(span: Span, metadata: dict):
"""Helper to set metadata attributes from mapping."""