From 3cb64978d47ea6d7b5cfe28bb296cf20a7a5eebb Mon Sep 17 00:00:00 2001 From: yassin Date: Tue, 15 Sep 2026 21:55:37 +0000 Subject: [PATCH 1/3] fix(proxy): honor LITELLM_LOG for uvicorn and proxy extras loggers LITELLM_LOG=ERROR still printed INFO lines from uvicorn (startup and access log) and from the litellm_proxy_extras migration logger, because neither read the variable. Forward the resolved level to uvicorn when LITELLM_LOG is set and no explicit log_config or JSON logging is in use, and let the extras logger take its level from LITELLM_LOG Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../litellm_proxy_extras/_logging.py | 2 +- litellm-proxy-extras/tests/test_logging.py | 34 +++++++++++++++++++ litellm/_logging.py | 2 +- litellm/proxy/proxy_cli.py | 4 ++- tests/test_litellm/proxy/test_proxy_cli.py | 33 ++++++++++++++++++ 5 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 litellm-proxy-extras/tests/test_logging.py diff --git a/litellm-proxy-extras/litellm_proxy_extras/_logging.py b/litellm-proxy-extras/litellm_proxy_extras/_logging.py index ecf467fbf45..64e07a180d3 100644 --- a/litellm-proxy-extras/litellm_proxy_extras/_logging.py +++ b/litellm-proxy-extras/litellm_proxy_extras/_logging.py @@ -40,4 +40,4 @@ if not logger.handlers: logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") ) logger.addHandler(handler) - logger.setLevel(logging.INFO) + logger.setLevel(os.getenv("LITELLM_LOG", "INFO").upper()) diff --git a/litellm-proxy-extras/tests/test_logging.py b/litellm-proxy-extras/tests/test_logging.py new file mode 100644 index 00000000000..b54d4fb72c9 --- /dev/null +++ b/litellm-proxy-extras/tests/test_logging.py @@ -0,0 +1,34 @@ +import importlib +import logging + +import pytest + +import litellm_proxy_extras._logging as extras_logging + + +def test_litellm_log_error_silences_extras_info_lines(monkeypatch): + saved_handlers = logging.getLogger("litellm_proxy_extras").handlers[:] + monkeypatch.setenv("LITELLM_LOG", "ERROR") + logging.getLogger("litellm_proxy_extras").handlers[:] = [] + try: + reloaded = importlib.reload(extras_logging).logger + assert reloaded.isEnabledFor(logging.INFO) is False + assert reloaded.isEnabledFor(logging.ERROR) is True + finally: + logging.getLogger("litellm_proxy_extras").handlers[:] = saved_handlers + logging.getLogger("litellm_proxy_extras").setLevel(logging.INFO) + + +@pytest.mark.parametrize("litellm_log", [None, "info", "DEBUG"]) +def test_unset_or_verbose_litellm_log_keeps_extras_info_lines(monkeypatch, litellm_log): + saved_handlers = logging.getLogger("litellm_proxy_extras").handlers[:] + if litellm_log is None: + monkeypatch.delenv("LITELLM_LOG", raising=False) + else: + monkeypatch.setenv("LITELLM_LOG", litellm_log) + logging.getLogger("litellm_proxy_extras").handlers[:] = [] + try: + assert importlib.reload(extras_logging).logger.isEnabledFor(logging.INFO) is True + finally: + logging.getLogger("litellm_proxy_extras").handlers[:] = saved_handlers + logging.getLogger("litellm_proxy_extras").setLevel(logging.INFO) diff --git a/litellm/_logging.py b/litellm/_logging.py index 03a9bcf21cf..893133d62fe 100644 --- a/litellm/_logging.py +++ b/litellm/_logging.py @@ -404,7 +404,7 @@ def _parse_json_logs_env(value: str | None) -> bool: json_logs: Final = _parse_json_logs_env(os.getenv("JSON_LOGS")) # Create a handler for the logger (you may need to adapt this based on your needs) log_level: Final = os.getenv("LITELLM_LOG", "DEBUG") -numeric_level: Final[str] = getattr(logging, log_level.upper()) +numeric_level: Final[int] = getattr(logging, log_level.upper()) handler: Final = LevelRoutingStreamHandler() handler.setLevel(numeric_level) handler.addFilter(_secret_filter) diff --git a/litellm/proxy/proxy_cli.py b/litellm/proxy/proxy_cli.py index 01a3da08998..6a23b422717 100644 --- a/litellm/proxy/proxy_cli.py +++ b/litellm/proxy/proxy_cli.py @@ -261,7 +261,7 @@ class ProxyInitializationHelpers: import uvicorn import litellm - from litellm._logging import _get_uvicorn_json_log_config + from litellm._logging import _get_uvicorn_json_log_config, numeric_level uvicorn_args: Final = { "app": "litellm.proxy.proxy_server:app", @@ -275,6 +275,8 @@ class ProxyInitializationHelpers: elif litellm.json_logs: # Use JSON log config for uvicorn to ensure all logs (including exceptions) are JSON uvicorn_args["log_config"] = _get_uvicorn_json_log_config() + elif os.environ.get("LITELLM_LOG"): + uvicorn_args["log_level"] = numeric_level if keepalive_timeout is not None: uvicorn_args["timeout_keep_alive"] = keepalive_timeout if timeout_worker_healthcheck is not None: diff --git a/tests/test_litellm/proxy/test_proxy_cli.py b/tests/test_litellm/proxy/test_proxy_cli.py index e25e6a59884..8f3c21e10c2 100644 --- a/tests/test_litellm/proxy/test_proxy_cli.py +++ b/tests/test_litellm/proxy/test_proxy_cli.py @@ -139,6 +139,39 @@ class TestProxyInitializationHelpers: ) assert args["timeout_worker_healthcheck"] == 15 + @staticmethod + def _uvicorn_access_info_enabled(args: dict) -> bool: + import logging + + names = ("uvicorn", "uvicorn.error", "uvicorn.access", "uvicorn.asgi") + saved = tuple((logging.getLogger(n), logging.getLogger(n).handlers[:], logging.getLogger(n).level) for n in names) + try: + uvicorn.Config(**args).configure_logging() + return logging.getLogger("uvicorn.access").isEnabledFor(logging.INFO) + finally: + for lg, handlers, level in saved: + lg.handlers[:] = handlers + lg.setLevel(level) + + def test_litellm_log_error_silences_uvicorn_info_lines(self, monkeypatch): + import logging + + monkeypatch.setenv("LITELLM_LOG", "ERROR") + with patch( # test-quality-ok: numeric_level is resolved from LITELLM_LOG once at import; no other way to set it + "litellm._logging.numeric_level", logging.ERROR + ): + args = ProxyInitializationHelpers._get_default_unvicorn_init_args("localhost", 8000) + + assert "log_config" not in args + assert self._uvicorn_access_info_enabled(args) is False + + def test_unset_litellm_log_keeps_uvicorn_default_info_lines(self, monkeypatch): + monkeypatch.delenv("LITELLM_LOG", raising=False) + args = ProxyInitializationHelpers._get_default_unvicorn_init_args("localhost", 8000) + + assert "log_level" not in args + assert self._uvicorn_access_info_enabled(args) is True + def test_installed_uvicorn_supports_worker_flags(self): params = inspect.signature(uvicorn.Config.__init__).parameters assert "timeout_worker_healthcheck" in params From 1b6e76af16efc781e10899c7dd433e2447d59dc0 Mon Sep 17 00:00:00 2001 From: yassin Date: Tue, 15 Sep 2026 22:27:24 +0000 Subject: [PATCH 2/3] fix(proxy): resolve LITELLM_LOG for uvicorn at startup and restore logger state in tests Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm-proxy-extras/tests/test_logging.py | 37 ++++++++++++---------- litellm/_logging.py | 6 +++- litellm/proxy/proxy_cli.py | 6 ++-- tests/test_litellm/proxy/test_proxy_cli.py | 14 +++----- 4 files changed, 33 insertions(+), 30 deletions(-) diff --git a/litellm-proxy-extras/tests/test_logging.py b/litellm-proxy-extras/tests/test_logging.py index b54d4fb72c9..fb209bc3925 100644 --- a/litellm-proxy-extras/tests/test_logging.py +++ b/litellm-proxy-extras/tests/test_logging.py @@ -1,34 +1,37 @@ import importlib import logging +from collections.abc import Iterator import pytest import litellm_proxy_extras._logging as extras_logging -def test_litellm_log_error_silences_extras_info_lines(monkeypatch): - saved_handlers = logging.getLogger("litellm_proxy_extras").handlers[:] - monkeypatch.setenv("LITELLM_LOG", "ERROR") - logging.getLogger("litellm_proxy_extras").handlers[:] = [] +@pytest.fixture +def fresh_extras_logger() -> Iterator[logging.Logger]: + logger = logging.getLogger("litellm_proxy_extras") + saved_handlers = logger.handlers[:] + saved_level = logger.level + logger.handlers[:] = [] try: - reloaded = importlib.reload(extras_logging).logger - assert reloaded.isEnabledFor(logging.INFO) is False - assert reloaded.isEnabledFor(logging.ERROR) is True + yield logger finally: - logging.getLogger("litellm_proxy_extras").handlers[:] = saved_handlers - logging.getLogger("litellm_proxy_extras").setLevel(logging.INFO) + logger.handlers[:] = saved_handlers + logger.setLevel(saved_level) + + +def test_litellm_log_error_silences_extras_info_lines(monkeypatch, fresh_extras_logger): + monkeypatch.setenv("LITELLM_LOG", "ERROR") + reloaded = importlib.reload(extras_logging).logger + assert reloaded is fresh_extras_logger + assert reloaded.isEnabledFor(logging.INFO) is False + assert reloaded.isEnabledFor(logging.ERROR) is True @pytest.mark.parametrize("litellm_log", [None, "info", "DEBUG"]) -def test_unset_or_verbose_litellm_log_keeps_extras_info_lines(monkeypatch, litellm_log): - saved_handlers = logging.getLogger("litellm_proxy_extras").handlers[:] +def test_unset_or_verbose_litellm_log_keeps_extras_info_lines(monkeypatch, fresh_extras_logger, litellm_log): if litellm_log is None: monkeypatch.delenv("LITELLM_LOG", raising=False) else: monkeypatch.setenv("LITELLM_LOG", litellm_log) - logging.getLogger("litellm_proxy_extras").handlers[:] = [] - try: - assert importlib.reload(extras_logging).logger.isEnabledFor(logging.INFO) is True - finally: - logging.getLogger("litellm_proxy_extras").handlers[:] = saved_handlers - logging.getLogger("litellm_proxy_extras").setLevel(logging.INFO) + assert importlib.reload(extras_logging).logger.isEnabledFor(logging.INFO) is True diff --git a/litellm/_logging.py b/litellm/_logging.py index 893133d62fe..873a6619a81 100644 --- a/litellm/_logging.py +++ b/litellm/_logging.py @@ -401,10 +401,14 @@ def _parse_json_logs_env(value: str | None) -> bool: return (value or "").lower() == "true" +def resolve_log_level(log_level: str) -> int: + return getattr(logging, log_level.upper()) + + json_logs: Final = _parse_json_logs_env(os.getenv("JSON_LOGS")) # Create a handler for the logger (you may need to adapt this based on your needs) log_level: Final = os.getenv("LITELLM_LOG", "DEBUG") -numeric_level: Final[int] = getattr(logging, log_level.upper()) +numeric_level: Final[int] = resolve_log_level(log_level) handler: Final = LevelRoutingStreamHandler() handler.setLevel(numeric_level) handler.addFilter(_secret_filter) diff --git a/litellm/proxy/proxy_cli.py b/litellm/proxy/proxy_cli.py index 6a23b422717..b25c77f6828 100644 --- a/litellm/proxy/proxy_cli.py +++ b/litellm/proxy/proxy_cli.py @@ -261,7 +261,7 @@ class ProxyInitializationHelpers: import uvicorn import litellm - from litellm._logging import _get_uvicorn_json_log_config, numeric_level + from litellm._logging import _get_uvicorn_json_log_config, resolve_log_level uvicorn_args: Final = { "app": "litellm.proxy.proxy_server:app", @@ -275,8 +275,8 @@ class ProxyInitializationHelpers: elif litellm.json_logs: # Use JSON log config for uvicorn to ensure all logs (including exceptions) are JSON uvicorn_args["log_config"] = _get_uvicorn_json_log_config() - elif os.environ.get("LITELLM_LOG"): - uvicorn_args["log_level"] = numeric_level + elif litellm_log := os.environ.get("LITELLM_LOG"): + uvicorn_args["log_level"] = resolve_log_level(litellm_log) if keepalive_timeout is not None: uvicorn_args["timeout_keep_alive"] = keepalive_timeout if timeout_worker_healthcheck is not None: diff --git a/tests/test_litellm/proxy/test_proxy_cli.py b/tests/test_litellm/proxy/test_proxy_cli.py index 8f3c21e10c2..712c526b244 100644 --- a/tests/test_litellm/proxy/test_proxy_cli.py +++ b/tests/test_litellm/proxy/test_proxy_cli.py @@ -143,24 +143,20 @@ class TestProxyInitializationHelpers: def _uvicorn_access_info_enabled(args: dict) -> bool: import logging - names = ("uvicorn", "uvicorn.error", "uvicorn.access", "uvicorn.asgi") - saved = tuple((logging.getLogger(n), logging.getLogger(n).handlers[:], logging.getLogger(n).level) for n in names) + loggers = tuple(logging.getLogger(n) for n in ("uvicorn", "uvicorn.error", "uvicorn.access", "uvicorn.asgi")) + saved = tuple((lg, lg.handlers[:], lg.level, lg.propagate) for lg in loggers) try: uvicorn.Config(**args).configure_logging() return logging.getLogger("uvicorn.access").isEnabledFor(logging.INFO) finally: - for lg, handlers, level in saved: + for lg, handlers, level, propagate in saved: lg.handlers[:] = handlers lg.setLevel(level) + lg.propagate = propagate def test_litellm_log_error_silences_uvicorn_info_lines(self, monkeypatch): - import logging - monkeypatch.setenv("LITELLM_LOG", "ERROR") - with patch( # test-quality-ok: numeric_level is resolved from LITELLM_LOG once at import; no other way to set it - "litellm._logging.numeric_level", logging.ERROR - ): - args = ProxyInitializationHelpers._get_default_unvicorn_init_args("localhost", 8000) + args = ProxyInitializationHelpers._get_default_unvicorn_init_args("localhost", 8000) assert "log_config" not in args assert self._uvicorn_access_info_enabled(args) is False From 557c237b81c3e26dbbdb102d32660956e6107fa8 Mon Sep 17 00:00:00 2001 From: yassin Date: Tue, 15 Sep 2026 22:57:23 +0000 Subject: [PATCH 3/3] test(proxy-extras): move the LITELLM_LOG logger test into the CI-run proxy-extras shard Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../litellm-proxy-extras/test_litellm_proxy_extras_logging.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename litellm-proxy-extras/tests/test_logging.py => tests/litellm-proxy-extras/test_litellm_proxy_extras_logging.py (100%) diff --git a/litellm-proxy-extras/tests/test_logging.py b/tests/litellm-proxy-extras/test_litellm_proxy_extras_logging.py similarity index 100% rename from litellm-proxy-extras/tests/test_logging.py rename to tests/litellm-proxy-extras/test_litellm_proxy_extras_logging.py