refactor(otel): share the console placeholder check with the presets

This commit is contained in:
Yucheng He 2026-09-05 08:18:46 -07:00
parent 88526c39c6
commit 4372884404
3 changed files with 17 additions and 14 deletions

View file

@ -32,12 +32,12 @@ def credential_gated_exporters(
override filter still recognises which backend this provider speaks for.
"""
return (
*(spec for spec in exporters if not _is_unconfigured_placeholder(spec)),
*(spec for spec in exporters if not is_unconfigured_placeholder(spec)),
ExporterSpec(owner=owner, requires_headers=True),
)
def _is_unconfigured_placeholder(spec: "ExporterSpec") -> bool:
def is_unconfigured_placeholder(spec: "ExporterSpec") -> bool:
"""Whether ``spec`` is the one ``_normalize`` folds in when nothing was configured.
Every field at its default is what says the operator asked for nothing: an exporter

View file

@ -4806,8 +4806,9 @@ def _maybe_construct_otel_v2(callback_name: str, _in_memory_loggers: list[Custom
only when this request has a key/team destination for that backend and another
V2 logger is already registered to carry the fan-out. The resulting logger keeps
only its credential-gated exporter, while the registered logger owns operator
delivery. Without that carrier, the preset raises as it always did and the
caller falls through to the legacy path.
delivery. Without that carrier, a preset that raises or that ends up with nothing
but its gated exporter and the default console placeholder returns ``None``, so the
caller falls through to the legacy path exactly as before V2 landed.
"""
from litellm.integrations.otel.model.config import is_otel_v2_enabled
@ -4822,6 +4823,7 @@ def _maybe_construct_otel_v2(callback_name: str, _in_memory_loggers: list[Custom
return None
serves_a_destination: Final = callback_name in destination_backends()
has_v2_logger: Final = any(isinstance(callback, OpenTelemetryV2) for callback in _in_memory_loggers)
carried: Final = serves_a_destination and has_v2_logger
for callback in _in_memory_loggers:
if (
isinstance(callback, OpenTelemetryV2)
@ -4830,20 +4832,15 @@ def _maybe_construct_otel_v2(callback_name: str, _in_memory_loggers: list[Custom
):
return callback
try:
built: Final = preset_fn(allow_missing_credentials=serves_a_destination and has_v2_logger)
built: Final = preset_fn(allow_missing_credentials=carried)
except Exception:
# If env vars are missing or the preset raises, defer to the legacy path
# so customers get the same error story they had before V2 landed.
return None
gated: Final = _is_credential_gated(built)
has_operator_exporter: Final = any(
not _is_gated(spec) and bool(spec.model_dump(exclude_defaults=True)) for spec in built.exporters
)
if _exports_nowhere(built) and not (serves_a_destination and has_v2_logger):
if gated and not carried and not _has_operator_exporter(built):
return None
if gated and not has_operator_exporter and not (serves_a_destination and has_v2_logger):
return None
config: Final = _only_the_gated_exporter(built) if gated and serves_a_destination and has_v2_logger else built
config: Final = _only_the_gated_exporter(built) if gated and carried else built
if _exports_nowhere(config):
verbose_logger.warning(
"OTel V2: no operator credentials for '%s'; only key/team destinations will receive its traces",
@ -4864,6 +4861,13 @@ def _is_credential_gated(config: "OpenTelemetryV2Config") -> bool:
return any(_is_gated(spec) for spec in config.exporters)
def _has_operator_exporter(config: "OpenTelemetryV2Config") -> bool:
"""Whether the operator configured somewhere real to export, beyond the default console placeholder."""
from litellm.integrations.otel.presets.utils import is_unconfigured_placeholder
return any(not _is_gated(spec) and not is_unconfigured_placeholder(spec) for spec in config.exporters)
def _only_the_gated_exporter(config: "OpenTelemetryV2Config") -> "OpenTelemetryV2Config":
return config.model_copy(
update={"exporters": [spec for spec in config.exporters if _is_gated(spec)]} # mutable-ok: model_copy update

View file

@ -1174,7 +1174,7 @@ class TestPresetDegradation:
"https://otlp.nr-data.net",
]
def test_a_credentialless_newrelic_without_a_base_exporter_falls_back(self, monkeypatch, capsys):
def test_a_credentialless_newrelic_without_a_base_exporter_falls_back(self, monkeypatch):
from litellm.litellm_core_utils.litellm_logging import _maybe_construct_otel_v2
monkeypatch.delenv("NEW_RELIC_LICENSE_KEY", raising=False)
@ -1187,7 +1187,6 @@ class TestPresetDegradation:
is_otel_v2_enabled.cache_clear()
assert logger is None
assert capsys.readouterr().out == ""
def test_a_destination_for_one_backend_does_not_degrade_another(self, monkeypatch):
from litellm.litellm_core_utils.litellm_logging import _maybe_construct_otel_v2