mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-25 01:02:15 +00:00
fix(otel v2): record the wider scope when two operator exporters write one account
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
755890b59a
commit
d264cdf231
4 changed files with 31 additions and 12 deletions
|
|
@ -1142,8 +1142,7 @@ def deliverable_destinations(
|
|||
|
||||
|
||||
def operator_sink_scopes(*configs: OpenTelemetryV2Config) -> 'Mapping[_SinkKey, "OtelSpanScope"]':
|
||||
"""The accounts the operator's own exporters write to, in destination terms, and
|
||||
how much of the tree each one receives.
|
||||
"""The accounts the operator's own exporters write to, in destination terms.
|
||||
|
||||
Every v2 logger's config counts, since each logger exports through its own
|
||||
provider. An exporter with no endpoint of its own resolves one from the
|
||||
|
|
@ -1151,20 +1150,23 @@ def operator_sink_scopes(*configs: OpenTelemetryV2Config) -> 'Mapping[_SinkKey,
|
|||
and so is one that never reaches the wire: a console kind ignores the endpoint,
|
||||
and a header-gated spec with no credentials is skipped when the provider is built.
|
||||
"""
|
||||
return MappingProxyType(
|
||||
{
|
||||
key: _operator_scope(config, spec)
|
||||
for config in configs
|
||||
for spec in config.exporters
|
||||
if _exports_to_the_wire(spec) and (key := _sink_key(spec.endpoint, parse_headers(spec.headers))) is not None
|
||||
}
|
||||
scoped: Final = tuple(
|
||||
(key, _operator_scope(config, spec))
|
||||
for config in configs
|
||||
for spec in config.exporters
|
||||
if _exports_to_the_wire(spec) and (key := _sink_key(spec.endpoint, parse_headers(spec.headers))) is not None
|
||||
)
|
||||
return MappingProxyType({key: _widest(scope for other, scope in scoped if other == key) for key, _ in scoped})
|
||||
|
||||
|
||||
def _operator_scope(config: OpenTelemetryV2Config, spec: ExporterSpec) -> "OtelSpanScope":
|
||||
return config.langfuse_span_scope if spec.owner is ExporterOwner.LANGFUSE_OTEL else "full"
|
||||
|
||||
|
||||
def _widest(scopes: "Iterable[OtelSpanScope]") -> "OtelSpanScope":
|
||||
return "full" if any(scope == "full" for scope in scopes) else "llm_only"
|
||||
|
||||
|
||||
def _exports_to_the_wire(spec: ExporterSpec) -> bool:
|
||||
"""Whether ``build_tracer_provider`` gives ``spec`` an exporter that sends OTLP."""
|
||||
return exporter_transport(spec.kind) != "headerless" and not (spec.requires_headers and not spec.headers)
|
||||
|
|
|
|||
|
|
@ -112,7 +112,6 @@ _NO_ATTRS: Final[Mapping[str, str]] = MappingProxyType({})
|
|||
|
||||
|
||||
def _span_scope(callback_name: str, params: StandardCallbackDynamicParams) -> OtelSpanScope:
|
||||
"""The export scope the tenant configured; only Langfuse offers one, every other backend gets the full tree."""
|
||||
if callback_name != "langfuse_otel":
|
||||
return "full"
|
||||
return params.get("langfuse_span_scope") or "full"
|
||||
|
|
|
|||
|
|
@ -48,8 +48,6 @@ def _langfuse_environment_error(callback_vars: Mapping[str, str]) -> str | None:
|
|||
|
||||
|
||||
def _langfuse_span_scope_error(callback_name: str | None, callback_vars: Mapping[str, str]) -> str | None:
|
||||
"""Only the OTel Langfuse callback reads the scope; on any other callback the
|
||||
value would be stored and then ignored, with the full tree still exported."""
|
||||
value: Final = callback_vars.get("langfuse_span_scope")
|
||||
if value is None:
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -382,6 +382,26 @@ class TestRoutingMode:
|
|||
_sink_key("https://otlp.arize.com/v1/traces", {"space_id": "s", "api_key": "k"}): "full",
|
||||
}
|
||||
|
||||
@pytest.mark.parametrize("langfuse_first", [False, True])
|
||||
def test_two_operator_exporters_on_one_account_record_the_wider_scope(self, langfuse_first):
|
||||
"""A plain collector pointed at the Langfuse ingest with the same credentials as
|
||||
the narrowed Langfuse exporter still sends the whole tree there. Recording
|
||||
``llm_only`` for that account would make additive hand a same-account team the
|
||||
non-model spans a second time."""
|
||||
langfuse = ExporterSpec(
|
||||
kind="otlp_http",
|
||||
endpoint=self.OPERATOR_SINK[0],
|
||||
headers="authorization=Basic op",
|
||||
owner=ExporterOwner.LANGFUSE_OTEL,
|
||||
)
|
||||
collector = ExporterSpec(kind="otlp_http", endpoint=self.OPERATOR_SINK[0], headers="authorization=Basic op")
|
||||
config = OpenTelemetryV2Config(
|
||||
langfuse_span_scope="llm_only",
|
||||
exporters=(langfuse, collector) if langfuse_first else (collector, langfuse),
|
||||
)
|
||||
|
||||
assert dict(operator_sink_scopes(config)) == {self.OPERATOR_SINK: "full"}
|
||||
|
||||
def test_a_team_pointing_at_a_credential_less_operator_exporter_still_gets_its_spans(self, monkeypatch):
|
||||
"""Under additive the fan-out skips a destination the operator already writes
|
||||
to. An exporter the provider never built writes nothing, so skipping it would
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue