fix(otel v2): normalize callback_settings excluded_services without rereading OTel env vars
Some checks are pending
LiteLLM Rust / rust-lint (push) Waiting to run
LiteLLM Rust / rust-test (push) Waiting to run
LiteLLM Rust / rust-wheel (push) Waiting to run

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
yucheng 2026-09-28 21:53:08 +00:00
parent cf472733a9
commit 48e30fb45f
3 changed files with 27 additions and 3 deletions

View file

@ -29,7 +29,7 @@ from litellm.integrations.custom_logger import CustomLogger
from litellm.integrations.otel.emitter import SpanEmitter, stamp_error
from litellm.integrations.otel.mappers import resolve_mappers
from litellm.integrations.otel.model.baggage import promoted_baggage
from litellm.integrations.otel.model.config import OpenTelemetryV2Config
from litellm.integrations.otel.model.config import OpenTelemetryV2Config, excluded_db_systems_from
from litellm.integrations.otel.model.metadata import (
LLMCallEvent,
RequestIdentity,
@ -922,7 +922,7 @@ def _excluded_db_systems(logger: "OpenTelemetryV2") -> frozenset[str]:
configured: Final = litellm.callback_settings.get("otel", {}).get("excluded_services")
if configured is None:
return logger.config.excluded_services
return OpenTelemetryV2Config(excluded_services=configured).excluded_services
return excluded_db_systems_from(configured)
def _v2_configs(in_memory_loggers: Sequence[object], logger: "OpenTelemetryV2") -> tuple[OpenTelemetryV2Config, ...]:

View file

@ -4,7 +4,7 @@ from enum import Enum
from functools import lru_cache
from typing import Annotated, Any, Final
from pydantic import AliasChoices, BaseModel, Field, field_validator, model_validator
from pydantic import AliasChoices, BaseModel, Field, TypeAdapter, field_validator, model_validator
from pydantic_settings import BaseSettings, NoDecode, SettingsConfigDict
from litellm._logging import verbose_logger
@ -352,6 +352,16 @@ class OpenTelemetryV2Config(BaseSettings):
return cls()
_EXCLUDED_SERVICES_INPUT: Final = TypeAdapter(str | list[str])
def excluded_db_systems_from(value: object) -> frozenset[str]:
"""Normalize a raw ``excluded_services`` value without building a settings model that rereads the env"""
parsed: Final = _EXCLUDED_SERVICES_INPUT.validate_python(value)
names: Final = [item.strip() for item in parsed.split(",") if item.strip()] if isinstance(parsed, str) else parsed
return _normalize_excluded_services(frozenset(names))
def _normalize_excluded_services(services: frozenset[str]) -> frozenset[str]:
"""Fold each accepted spelling to its ``db.system.name`` value.

View file

@ -1091,6 +1091,20 @@ class TestProviderWiring:
assert self._fan_out_of(preset)._excluded_db_systems == frozenset({"postgresql"})
def test_callback_settings_excluded_services_apply_even_when_other_otel_env_vars_are_malformed(self, monkeypatch):
"""Reading the setting must not rebuild the whole settings model, or an unrelated bad env
value the operator overrode in config would stop publication before the fan-out is attached"""
preset = OpenTelemetryV2(
config=OpenTelemetryV2Config(exporters=[ExporterSpec(kind="in_memory")]),
callback_name="langfuse_otel",
)
monkeypatch.setenv("LITELLM_OTEL_LEGACY_COMPAT", "not-a-bool")
monkeypatch.setattr(litellm, "callback_settings", {"otel": {"excluded_services": ["postgres"]}}, raising=False)
publish_global_otel_v2_provider([], lambda _p: None, registered=preset)
assert self._fan_out_of(preset)._excluded_db_systems == frozenset({"postgresql"})
def test_excluded_services_fall_back_to_the_published_logger_config_without_callback_settings(self, monkeypatch):
monkeypatch.setattr(litellm, "callback_settings", {"otel": {"exporter": "in_memory"}}, raising=False)
preset = OpenTelemetryV2(