diff --git a/litellm/integrations/otel/logger.py b/litellm/integrations/otel/logger.py index fa5770f9b81..4ddddbd92f1 100644 --- a/litellm/integrations/otel/logger.py +++ b/litellm/integrations/otel/logger.py @@ -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, ...]: diff --git a/litellm/integrations/otel/model/config.py b/litellm/integrations/otel/model/config.py index cde366b099e..c0a61c46190 100644 --- a/litellm/integrations/otel/model/config.py +++ b/litellm/integrations/otel/model/config.py @@ -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. diff --git a/tests/unit/integrations/otel/test_otel_v2_destinations.py b/tests/unit/integrations/otel/test_otel_v2_destinations.py index 87e89626ad9..5a7057203e4 100644 --- a/tests/unit/integrations/otel/test_otel_v2_destinations.py +++ b/tests/unit/integrations/otel/test_otel_v2_destinations.py @@ -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(