mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-12 08:42:18 +00:00
fix(otel): shed a destination's Langfuse request metadata before its other Langfuse fields
At its attribute limit a span sheds the Langfuse fields only a destination asked for first, in the mapper's order, so the generation marker went before the request metadata blob. Put the request metadata first, as a Langfuse-configured logger already does.
This commit is contained in:
parent
f372899f12
commit
cea1923222
2 changed files with 42 additions and 18 deletions
|
|
@ -169,7 +169,6 @@ class SpanEmitter:
|
|||
self._mappers: list[AttributeMapper] = (
|
||||
list(mappers) if mappers is not None else resolve_mappers(config.mapper_names)
|
||||
)
|
||||
# A key's or team's Langfuse destination reads the Langfuse vocabulary, which this logger may not write.
|
||||
self._langfuse: Final = (
|
||||
None if any(isinstance(mapper, LangfuseMapper) for mapper in self._mappers) else LangfuseMapper()
|
||||
)
|
||||
|
|
@ -302,7 +301,12 @@ class SpanEmitter:
|
|||
)
|
||||
configured: Final = {key: value for mapper in self._mappers for key, value in mapper.map(data).items()}
|
||||
for_langfuse: Final = (
|
||||
self._langfuse.map(data)
|
||||
dict(
|
||||
sorted(
|
||||
self._langfuse.map(data).items(),
|
||||
key=lambda item: item[0] not in LANGFUSE_REQUEST_METADATA_ATTRIBUTES,
|
||||
)
|
||||
)
|
||||
if self._langfuse is not None and ExporterOwner.LANGFUSE_OTEL in destination_backends()
|
||||
else _NO_ATTRIBUTES
|
||||
)
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ from litellm.integrations.otel.logger import (
|
|||
publish_global_otel_v2_provider,
|
||||
)
|
||||
from litellm.integrations.otel.mappers import resolve_mappers
|
||||
from litellm.integrations.otel.mappers.langfuse import LANGFUSE_REQUEST_METADATA_ATTRIBUTES
|
||||
from litellm.integrations.otel.model.config import (
|
||||
CaptureMessageContent,
|
||||
ExporterOwner,
|
||||
|
|
@ -3450,7 +3451,7 @@ class TestLangfuseDestinationFields:
|
|||
credential_less_proxy(monkeypatch)
|
||||
capture: Final = _ExporterCapture()
|
||||
for kind in ("otlp_grpc", "otlp_http", "console"):
|
||||
monkeypatch.setitem(otel_providers._EXPORTER_FACTORIES, kind, capture.build)
|
||||
monkeypatch.setitem(otel_providers._EXPORTER_FACTORIES, kind, capture.build) # pyright: ignore[reportPrivateUsage] # the factory table is the seam for capturing each exporter
|
||||
config: Final = OpenTelemetryV2Config()
|
||||
provider: Final = build_tracer_provider(config, tenant_overrides=True)
|
||||
attach_tenant_fan_out(provider, config)
|
||||
|
|
@ -3480,24 +3481,43 @@ class TestLangfuseDestinationFields:
|
|||
assert "langfuse.observation.metadata" in to_langfuse
|
||||
assert "langfuse.observation.metadata" not in received[None]
|
||||
|
||||
@staticmethod
|
||||
def _attributes(
|
||||
destinations: tuple[OtelDestination, ...], limit: int | None, call: LLMCallSpanData
|
||||
) -> dict[str, object]:
|
||||
exporter: Final = InMemorySpanExporter()
|
||||
provider: Final = TracerProvider(span_limits=SpanLimits(max_span_attributes=limit))
|
||||
provider.add_span_processor(SimpleSpanProcessor(exporter))
|
||||
emitter: Final = SpanEmitter(get_tracer(provider, "litellm"), OpenTelemetryV2Config())
|
||||
|
||||
def run() -> None:
|
||||
set_request_destinations(destinations)
|
||||
emitter.emit(SpanRole.LLM_CALL, call)
|
||||
|
||||
in_fresh_context(run)
|
||||
return dict(exporter.get_finished_spans()[0].attributes or {})
|
||||
|
||||
def test_a_span_at_its_attribute_limit_sheds_the_destinations_langfuse_fields_first(self) -> None:
|
||||
def attributes(destinations: tuple[OtelDestination, ...], limit: int | None) -> dict[str, object]:
|
||||
exporter: Final = InMemorySpanExporter()
|
||||
provider: Final = TracerProvider(span_limits=SpanLimits(max_span_attributes=limit))
|
||||
provider.add_span_processor(SimpleSpanProcessor(exporter))
|
||||
emitter: Final = SpanEmitter(get_tracer(provider, "litellm"), OpenTelemetryV2Config())
|
||||
|
||||
def run() -> None:
|
||||
set_request_destinations(destinations)
|
||||
emitter.emit(SpanRole.LLM_CALL, _MODEL_CALL_WITH_CONTENT)
|
||||
|
||||
in_fresh_context(run)
|
||||
return dict(exporter.get_finished_spans()[0].attributes or {})
|
||||
|
||||
generic: Final = attributes((), None)
|
||||
generic: Final = self._attributes((), None, _MODEL_CALL_WITH_CONTENT)
|
||||
|
||||
assert "user.id" not in generic
|
||||
assert attributes((LANGFUSE_DEST,), len(generic)) == generic, "the generic attributes all survive"
|
||||
assert self._attributes((LANGFUSE_DEST,), len(generic), _MODEL_CALL_WITH_CONTENT) == generic, (
|
||||
"the generic attributes all survive"
|
||||
)
|
||||
|
||||
def test_a_span_at_its_attribute_limit_sheds_the_destinations_request_metadata_before_its_other_fields(
|
||||
self,
|
||||
) -> None:
|
||||
call: Final = replace(
|
||||
_MODEL_CALL_WITH_CONTENT, request_metadata=MappingProxyType({"user_api_key_alias": "prod-key"})
|
||||
)
|
||||
whole: Final = self._attributes((LANGFUSE_DEST,), None, call)
|
||||
|
||||
squeezed: Final = self._attributes((LANGFUSE_DEST,), len(whole) - 1, call)
|
||||
|
||||
assert len(squeezed) == len(whole) - 1
|
||||
assert whole.keys() - squeezed.keys() <= LANGFUSE_REQUEST_METADATA_ATTRIBUTES
|
||||
assert squeezed["langfuse.observation.type"] == "generation"
|
||||
|
||||
|
||||
class TestContextIsolation:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue