diff --git a/litellm/integrations/otel/model/config.py b/litellm/integrations/otel/model/config.py index 4ba1357a3d8..528ac76256f 100644 --- a/litellm/integrations/otel/model/config.py +++ b/litellm/integrations/otel/model/config.py @@ -377,7 +377,8 @@ def validate_otel_v2_excluded_services_env(settings: object) -> None: init errors, so a bogus value would otherwise degrade to the legacy callback silently. Splitting and normalizing here raises the same ``ValueError`` the field raises. An explicit ``callback_settings.otel.excluded_services`` wins - over the env var, so a bad env value is inert then and must not block boot. + over the env var, so the caller skips this check only when no V2 preset + callback that would still parse the env is configured. """ if not is_otel_v2_enabled(): return diff --git a/litellm/proxy/common_utils/callback_utils.py b/litellm/proxy/common_utils/callback_utils.py index 88b5f556977..50b0b0faa64 100644 --- a/litellm/proxy/common_utils/callback_utils.py +++ b/litellm/proxy/common_utils/callback_utils.py @@ -180,8 +180,14 @@ def initialize_callbacks_on_proxy( validate_otel_v2_callback_settings(callback_specific_params.get("otel")) from litellm.integrations.otel.model.config import validate_otel_v2_excluded_services_env + from litellm.integrations.otel.presets import PRESET_BY_CALLBACK - validate_otel_v2_excluded_services_env(callback_specific_params.get("otel") if "otel" in value else None) + preset_present: Final = any( + isinstance(entry, str) and entry != "otel" and entry in PRESET_BY_CALLBACK for entry in value + ) + validate_otel_v2_excluded_services_env( + callback_specific_params.get("otel") if "otel" in value and not preset_present else None + ) # check if callback is a custom logger compatible callback if isinstance(callback, str): diff --git a/tests/integration/observability/test_otel_excluded_services.py b/tests/integration/observability/test_otel_excluded_services.py index a6646cb4242..0473a99a7ef 100644 --- a/tests/integration/observability/test_otel_excluded_services.py +++ b/tests/integration/observability/test_otel_excluded_services.py @@ -296,12 +296,7 @@ def test_valid_config_excluded_services_tolerates_bogus_env( langfuse_vars: dict[str, JsonValue], tmp_path: Path, ) -> None: - def with_langfuse_otel(config: dict) -> None: - config["litellm_settings"]["callbacks"] = ["otel", "langfuse_otel"] - - config: Final = _config_with( - tmp_path, otel_audit_config, otel={"excluded_services": ["postgres"]}, extra=with_langfuse_otel - ) + config: Final = _config_with(tmp_path, otel_audit_config, otel={"excluded_services": ["postgres"]}) with owned_proxy( gateway, tmp_path, {"LITELLM_OTEL_V2": "1", "LITELLM_OTEL_EXCLUDED_SERVICES": "auth"}, config=config, workers=2 ) as candidate: @@ -318,6 +313,28 @@ def test_valid_config_excluded_services_tolerates_bogus_env( ) +def test_bogus_excluded_services_env_fails_proxy_start_with_preset_alongside_otel( + gateway: Gateway, otel_audit_config: AuditConfigWriter, tmp_path: Path +) -> None: + def with_langfuse_otel(config: dict) -> None: + config["litellm_settings"]["callbacks"] = ["otel", "langfuse_otel"] + + config: Final = _config_with( + tmp_path, otel_audit_config, otel={"excluded_services": ["postgres"]}, extra=with_langfuse_otel + ) + log_dir: Final = Path(os.environ.get("INTEGRATION_RESULTS_DIR", str(tmp_path))) + before: Final = frozenset(log_dir.glob("owned-proxy-*.log")) + with pytest.raises(AssertionError, match="readiness"): + with owned_proxy_process( + gateway, tmp_path, {"LITELLM_OTEL_V2": "1", "LITELLM_OTEL_EXCLUDED_SERVICES": "auth"}, config=config, workers=2 + ): + pass + logs: Final = [path.read_text() for path in frozenset(log_dir.glob("owned-proxy-*.log")) - before] + assert logs, "no owned proxy log written" + text: Final = "\n".join(logs) + assert "'auth' is not a datastore service" in text, text[-3000:] + + def test_bogus_excluded_services_env_fails_proxy_start_without_otel_callback( gateway: Gateway, otel_audit_config: AuditConfigWriter, tmp_path: Path ) -> None: