mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-16 23:41:43 +00:00
fix(otel): promote nested metadata keys under the caller's dotted path
Strip only the proxy's requester_metadata. wrapper from an allowlisted key so requester_metadata.trace_id lands as litellm.metadata.trace_id while other dotted keys keep their full path and cannot collide on a shared leaf name Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
8cab3a7846
commit
8a059cd4b4
6 changed files with 36 additions and 21 deletions
|
|
@ -18,7 +18,7 @@ from collections.abc import Callable, Mapping
|
|||
from types import MappingProxyType
|
||||
from typing import Final
|
||||
|
||||
from litellm.integrations.otel.model.metadata import RequestIdentity
|
||||
from litellm.integrations.otel.model.metadata import REQUESTER_METADATA_PATH, RequestIdentity
|
||||
from litellm.integrations.otel.model.semconv import GenAI, LiteLLM
|
||||
|
||||
# Attribute key -> value extractor over (identity, request_model,
|
||||
|
|
@ -91,15 +91,10 @@ def promoted_baggage(
|
|||
|
||||
|
||||
def promoted_metadata(metadata: Mapping[str, str], metadata_keys: tuple[str, ...]) -> Mapping[str, str]:
|
||||
"""Allowlisted entries of a flattened metadata mapping under ``litellm.metadata.*``.
|
||||
|
||||
A dotted key such as ``requester_metadata.trace_id`` reads the nested value and
|
||||
is promoted under its last segment (``litellm.metadata.trace_id``), so the
|
||||
caller-facing attribute name is independent of where the proxy stored it.
|
||||
"""
|
||||
"""Allowlisted entries of a flattened metadata mapping under ``litellm.metadata.*``."""
|
||||
return MappingProxyType(
|
||||
{
|
||||
f"{LiteLLM.METADATA_PREFIX}{meta_key.rsplit('.', 1)[-1]}": value
|
||||
f"{LiteLLM.METADATA_PREFIX}{meta_key.removeprefix(REQUESTER_METADATA_PATH)}": value
|
||||
for meta_key in metadata_keys
|
||||
if (value := metadata.get(meta_key))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -211,8 +211,8 @@ class OpenTelemetryV2Config(BaseSettings):
|
|||
description=(
|
||||
"Metadata sub-keys promoted under the ``litellm.metadata.*`` "
|
||||
"namespace. A dotted path such as ``requester_metadata.trace_id`` "
|
||||
"reads the caller's nested ``metadata.trace_id`` and is promoted under "
|
||||
"its last segment (``litellm.metadata.trace_id``). "
|
||||
"reads the caller's nested ``metadata.trace_id`` and is promoted as "
|
||||
"``litellm.metadata.trace_id``; other dotted keys keep their full path. "
|
||||
"Configure via the ``LITELLM_OTEL_BAGGAGE_METADATA_KEYS`` "
|
||||
"env var (comma-separated) or "
|
||||
"``callback_settings.otel.baggage_metadata_keys`` in config.yaml."
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ if TYPE_CHECKING:
|
|||
from litellm.types.utils import StandardLoggingPayload
|
||||
|
||||
LANGFUSE_TRACE_NAME_HEADER: Final = "langfuse_trace_name"
|
||||
REQUESTER_METADATA_PATH: Final = "requester_metadata."
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
|
|
|||
|
|
@ -168,31 +168,43 @@ def test_allowlisted_metadata_subkey_promoted_blob_excluded():
|
|||
assert all("private_note" not in k for k in span.attributes)
|
||||
|
||||
|
||||
def test_nested_metadata_key_promoted_under_leaf_name():
|
||||
def test_nested_metadata_key_promoted_under_caller_path():
|
||||
"""A dotted allowlist entry reads the nested caller metadata the proxy stores
|
||||
under ``requester_metadata`` and lands on the LLM-call span as
|
||||
``litellm.metadata.<leaf>``; unlisted siblings and the blob stay out."""
|
||||
under ``requester_metadata`` and lands on the LLM-call span under the caller's
|
||||
own path (``litellm.metadata.trace_id``, ``litellm.metadata.nested.deep``);
|
||||
a pre-existing flat dotted key keeps its full name, and unlisted siblings and
|
||||
the blob stay out."""
|
||||
engine, exporter = _engine_and_exporter()
|
||||
payload = _payload()
|
||||
payload["metadata"]["a.b"] = "flat"
|
||||
payload["metadata"]["requester_metadata"] = {
|
||||
"trace_id": "abc",
|
||||
"attempt": 0,
|
||||
"empty": "",
|
||||
"nested": {"deep": "x"},
|
||||
"nested": {"deep": "x", "skipped": "y"},
|
||||
}
|
||||
data = LLMCallSpanData.from_standard_logging_payload(payload)
|
||||
bag = promoted_baggage(
|
||||
data.identity,
|
||||
data.request_model,
|
||||
BAGGAGE_PROMOTED_KEYS,
|
||||
metadata_keys=("requester_metadata.trace_id", "requester_metadata.attempt", "requester_metadata.empty"),
|
||||
metadata_keys=(
|
||||
"requester_metadata.trace_id",
|
||||
"requester_metadata.attempt",
|
||||
"requester_metadata.empty",
|
||||
"requester_metadata.nested.deep",
|
||||
"a.b",
|
||||
),
|
||||
)
|
||||
engine.emit(SpanRole.LLM_CALL, data, ctx_mod.set_request_baggage(bag))
|
||||
(span,) = exporter.get_finished_spans()
|
||||
assert span.attributes[f"{LiteLLM.METADATA_PREFIX}trace_id"] == "abc"
|
||||
assert span.attributes[f"{LiteLLM.METADATA_PREFIX}attempt"] == "0"
|
||||
assert span.attributes[f"{LiteLLM.METADATA_PREFIX}nested.deep"] == "x"
|
||||
assert span.attributes[f"{LiteLLM.METADATA_PREFIX}a.b"] == "flat"
|
||||
assert f"{LiteLLM.METADATA_PREFIX}empty" not in span.attributes
|
||||
assert f"{LiteLLM.METADATA_PREFIX}deep" not in span.attributes
|
||||
assert f"{LiteLLM.METADATA_PREFIX}nested.skipped" not in span.attributes
|
||||
assert not any(k.startswith(f"{LiteLLM.METADATA_PREFIX}requester_metadata") for k in span.attributes)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1684,7 +1684,7 @@ def test_pre_call_hook_promotes_nested_request_metadata_key():
|
|||
assert spans["redis set"].attributes[key] == "abc"
|
||||
assert data == {"model": "gpt-4o", "metadata": {"requester_metadata": {"trace_id": "abc", "nested": {"deep": "x"}}}}
|
||||
assert not any(
|
||||
k.startswith(f"{LiteLLM.METADATA_PREFIX}requester_metadata") or k == f"{LiteLLM.METADATA_PREFIX}deep"
|
||||
k.startswith(f"{LiteLLM.METADATA_PREFIX}requester_metadata") or k.endswith("deep")
|
||||
for s in spans.values()
|
||||
for k in s.attributes
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5581,21 +5581,28 @@ class TestOpenTelemetryInferenceIdentityAttributes(unittest.TestCase):
|
|||
otel.set_attributes(span, kwargs, {"model": "azure/gpt-4o"})
|
||||
assert "http.route" not in self._attr(span, exp)
|
||||
|
||||
def test_nested_metadata_key_promoted_under_leaf_name(self):
|
||||
def test_nested_metadata_key_promoted_under_caller_path(self):
|
||||
"""``baggage_metadata_keys: [requester_metadata.trace_id]`` stamps the
|
||||
caller's nested metadata value as ``litellm.metadata.trace_id``; unlisted
|
||||
siblings stay inside the ``metadata.requester_metadata`` blob."""
|
||||
otel = OpenTelemetry(config=OpenTelemetryConfig(baggage_metadata_keys=["requester_metadata.trace_id"]))
|
||||
caller's nested metadata value as ``litellm.metadata.trace_id`` and a deeper
|
||||
path keeps its dotted name; unlisted siblings stay inside the
|
||||
``metadata.requester_metadata`` blob."""
|
||||
otel = OpenTelemetry(
|
||||
config=OpenTelemetryConfig(
|
||||
baggage_metadata_keys=["requester_metadata.trace_id", "requester_metadata.nested.deep"]
|
||||
)
|
||||
)
|
||||
kwargs = self._kwargs()
|
||||
kwargs["standard_logging_object"]["metadata"]["requester_metadata"] = {
|
||||
"trace_id": "abc",
|
||||
"nested": {"deep": "x"},
|
||||
"nested": {"deep": "x", "skipped": "y"},
|
||||
}
|
||||
span, exp = self._span()
|
||||
otel.set_attributes(span, kwargs, {"model": "azure/gpt-4o"})
|
||||
attrs = self._attr(span, exp)
|
||||
assert attrs["litellm.metadata.trace_id"] == "abc"
|
||||
assert attrs["litellm.metadata.nested.deep"] == "x"
|
||||
assert "litellm.metadata.deep" not in attrs
|
||||
assert "litellm.metadata.nested.skipped" not in attrs
|
||||
assert not any(k.startswith("litellm.metadata.requester_metadata") for k in attrs)
|
||||
|
||||
def test_metadata_keys_default_to_none_promoted(self):
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue