From 36018b8013eba13e7127018251139622ab0f9d14 Mon Sep 17 00:00:00 2001 From: Aryan Pardeshi Date: Tue, 11 Aug 2026 01:37:18 +0530 Subject: [PATCH] fix(proxy): let uvicorn inherit LITELLM_LOG when it is quieter than INFO Setting the helm chart's logLevel to WARN sets LITELLM_LOG, which quiets LiteLLM's own loggers but leaves uvicorn on its default config, where uvicorn.access sits at INFO. Liveness and readiness probes then keep emitting one access log line per poll with no way to turn them off. uvicorn now inherits the level, but only when it is WARNING or above, so an operator asking for less noise gets it. Levels at or below INFO are left alone: LITELLM_LOG defaults to DEBUG and propagating that would switch on asgi-internals logging nobody asked for. An explicit --log_config, and the JSON log config, both still win. --- helm/litellm-helm/values.yaml | 5 +++ litellm/_logging.py | 23 +++++++++++++ litellm/proxy/proxy_cli.py | 6 +++- tests/test_litellm/proxy/test_proxy_cli.py | 38 ++++++++++++++++++++++ 4 files changed, 71 insertions(+), 1 deletion(-) diff --git a/helm/litellm-helm/values.yaml b/helm/litellm-helm/values.yaml index df2b55723fe..3dc565edae8 100644 --- a/helm/litellm-helm/values.yaml +++ b/helm/litellm-helm/values.yaml @@ -457,6 +457,11 @@ migrationJob: # # Setting LITELLM_LOG inside `envVars:` below also wins: the template skips # this injection entirely when envVars already defines LITELLM_LOG. +# +# At WARNING or above, uvicorn inherits this level too, which silences the +# per-request access log line that liveness and readiness probes would +# otherwise emit on every poll. At INFO or below uvicorn keeps its own +# default, so access logs stay on. logLevel: INFO # Additional environment variables to be added to the deployment as a map of key-value pairs diff --git a/litellm/_logging.py b/litellm/_logging.py index 6add9d79a5b..ba83771a577 100644 --- a/litellm/_logging.py +++ b/litellm/_logging.py @@ -5,6 +5,7 @@ import os import sys from datetime import datetime from logging import Formatter +from types import MappingProxyType from typing import Any, Final import litellm @@ -513,6 +514,28 @@ def _get_uvicorn_json_log_config(): return log_config +_UVICORN_LOG_LEVELS_QUIETER_THAN_INFO: Final = MappingProxyType( + { + "WARN": "warning", + "WARNING": "warning", + "ERROR": "error", + "FATAL": "critical", + "CRITICAL": "critical", + } +) + + +def get_uvicorn_log_level() -> str | None: + """Uvicorn's log level to inherit from LITELLM_LOG, or None to leave uvicorn's default alone. + + Only levels quieter than INFO are inherited. uvicorn.access logs one line per request, so at + WARNING or above an operator asking for less noise also means the health probe access lines. + Levels at or below INFO are not propagated: LITELLM_LOG defaults to DEBUG, and mapping that + onto uvicorn would turn on asgi-internals logging nobody asked for. + """ + return _UVICORN_LOG_LEVELS_QUIETER_THAN_INFO.get(log_level.upper()) + + def _turn_on_json(): """ Turn on JSON logging diff --git a/litellm/proxy/proxy_cli.py b/litellm/proxy/proxy_cli.py index ab159e84b6a..afa5a5cd87c 100644 --- a/litellm/proxy/proxy_cli.py +++ b/litellm/proxy/proxy_cli.py @@ -256,7 +256,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, get_uvicorn_log_level uvicorn_args: Final = { "app": "litellm.proxy.proxy_server:app", @@ -269,6 +269,10 @@ 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() + else: + uvicorn_log_level = get_uvicorn_log_level() + if uvicorn_log_level is not None: + uvicorn_args["log_level"] = uvicorn_log_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 2ccaa0df440..12507ad63b1 100644 --- a/tests/test_litellm/proxy/test_proxy_cli.py +++ b/tests/test_litellm/proxy/test_proxy_cli.py @@ -139,6 +139,44 @@ class TestProxyInitializationHelpers: ) assert args["timeout_worker_healthcheck"] == 15 + @pytest.mark.parametrize( + "litellm_log, expected_uvicorn_level", + [ + ("WARNING", "warning"), + ("WARN", "warning"), + ("warning", "warning"), + ("ERROR", "error"), + ("CRITICAL", "critical"), + ("FATAL", "critical"), + ], + ) + def test_uvicorn_log_level_follows_litellm_log_when_quieter_than_info( + self, litellm_log, expected_uvicorn_level + ): + with patch("litellm._logging.log_level", litellm_log): + args = ProxyInitializationHelpers._get_default_unvicorn_init_args( + "localhost", 8000 + ) + assert args["log_level"] == expected_uvicorn_level + + @pytest.mark.parametrize("litellm_log", ["DEBUG", "INFO", "NOTSET", "nonsense"]) + def test_uvicorn_log_level_is_left_alone_when_litellm_log_is_not_quieter( + self, litellm_log + ): + with patch("litellm._logging.log_level", litellm_log): + args = ProxyInitializationHelpers._get_default_unvicorn_init_args( + "localhost", 8000 + ) + assert "log_level" not in args + + def test_explicit_log_config_wins_over_litellm_log(self): + with patch("litellm._logging.log_level", "WARNING"): + args = ProxyInitializationHelpers._get_default_unvicorn_init_args( + "localhost", 8000, "log_config.json" + ) + assert args["log_config"] == "log_config.json" + assert "log_level" not in args + def test_installed_uvicorn_supports_worker_flags(self): params = inspect.signature(uvicorn.Config.__init__).parameters assert "timeout_worker_healthcheck" in params