mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
fix(otel v2): identify a destination account by its credentials, not its header names
Under additive the fan-out skips a destination the operator's own exporter already writes to, so the same account is not written twice. It compared header names as well as values, and one account answers to more than one spelling: the operator's Arize exporter sends space_id where a team destination sends arize-space-id, so every span landed in the operator's own space twice. The credentials are the identity. Compare those and leave the spelling to each backend.
This commit is contained in:
parent
3c36082c62
commit
c9929554bb
2 changed files with 30 additions and 7 deletions
|
|
@ -218,8 +218,8 @@ _DRAIN_WORKERS: Final = 2
|
|||
#: proxy open.
|
||||
_SHUTDOWN_DRAIN_SECONDS: Final = 5.0
|
||||
|
||||
#: An exporter's account: its normalized endpoint and its credentials.
|
||||
_SinkKey = tuple[str, tuple[tuple[str, str], ...]]
|
||||
#: An exporter's account: its normalized endpoint and the credentials it presents.
|
||||
_SinkKey = tuple[str, tuple[str, ...]]
|
||||
|
||||
|
||||
class _DrainPool:
|
||||
|
|
@ -851,14 +851,16 @@ def operator_sink_keys(config: OpenTelemetryV2Config | None) -> frozenset[_SinkK
|
|||
def _sink_key(endpoint: str | None, headers: Mapping[str, str]) -> "_SinkKey | None":
|
||||
"""The account an exporter writes to, or ``None`` when it has no fixed one.
|
||||
|
||||
Normalized on both counts that make the same account look like two: the operator's
|
||||
spec carries the signal path a tenant destination leaves for the exporter to
|
||||
append, and header names survive one round trip lowercased and the other not.
|
||||
The credentials are the identity; the header names are only how each backend
|
||||
spells them, and one account answers to more than one spelling (Arize takes the
|
||||
operator's ``space_id`` and a tenant's ``arize-space-id``). The endpoint needs
|
||||
normalizing too: the operator's spec carries the signal path that a tenant
|
||||
destination leaves for the exporter to append.
|
||||
"""
|
||||
normalized: Final = _otlp_traces_endpoint(endpoint)
|
||||
if normalized is None:
|
||||
return None
|
||||
return (normalized, tuple(sorted((name.lower(), value) for name, value in headers.items())))
|
||||
return (normalized, tuple(sorted(headers.values())))
|
||||
|
||||
|
||||
def _attached_processors(provider: TracerProvider) -> "tuple[SpanProcessor, ...]":
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ from litellm.integrations.otel.presets.destinations import (
|
|||
destination_capable_backends,
|
||||
destination_for,
|
||||
)
|
||||
from litellm.integrations.otel.presets.arize import arize_preset
|
||||
from litellm.integrations.otel.presets.langfuse import langfuse_preset
|
||||
from litellm.proxy._types import UserAPIKeyAuth
|
||||
from litellm.types.utils import StandardCallbackDynamicParams
|
||||
|
|
@ -128,7 +129,7 @@ class TestRoutingMode:
|
|||
moment a team configures its own is what ``additive`` exists to prevent.
|
||||
"""
|
||||
|
||||
OPERATOR_SINK = ("https://cloud.langfuse.com/api/public/otel/v1/traces", (("authorization", "Basic op"),))
|
||||
OPERATOR_SINK = ("https://cloud.langfuse.com/api/public/otel/v1/traces", ("Basic op",))
|
||||
#: What a tenant destination for that same project looks like before normalizing:
|
||||
#: no signal path yet, and the header name cased the way the backend writes it.
|
||||
SAME_ACCOUNT_ENDPOINT = "https://cloud.langfuse.com/api/public/otel"
|
||||
|
|
@ -345,6 +346,26 @@ class TestRoutingMode:
|
|||
assert sink("pk-op", "sk-op") in operator, "a team naming the operator's own project"
|
||||
assert sink("pk-team", "sk-team") not in operator, "a different project on the same server"
|
||||
|
||||
def test_the_operators_own_arize_space_and_a_team_naming_it_are_one_account(self, monkeypatch):
|
||||
"""One account answers to two header names here: the operator's exporter sends
|
||||
``space_id`` and a team destination sends ``arize-space-id``. Keyed on the names,
|
||||
additive would write the operator's own space twice for every request."""
|
||||
monkeypatch.setenv("ARIZE_SPACE_ID", "space-op")
|
||||
monkeypatch.setenv("ARIZE_API_KEY", "key-op")
|
||||
monkeypatch.delenv("ARIZE_SPACE_KEY", raising=False)
|
||||
operator = operator_sink_keys(arize_preset())
|
||||
|
||||
def sink(space, api_key):
|
||||
destination = destination_for(
|
||||
"arize",
|
||||
StandardCallbackDynamicParams(arize_space_key=space, arize_api_key=api_key),
|
||||
)
|
||||
assert destination is not None
|
||||
return _sink_key(destination.endpoint, destination.headers)
|
||||
|
||||
assert sink("space-op", "key-op") in operator, "a team naming the operator's own space"
|
||||
assert sink("space-team", "key-team") not in operator, "a different Arize space"
|
||||
|
||||
|
||||
class TestFanOut:
|
||||
def test_every_span_of_the_request_reaches_the_destination_in_one_trace(self):
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue