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