From 969a764378e2a932510761afa8d8c5f2300317e6 Mon Sep 17 00:00:00 2001 From: Huynh Duc Tran Date: Fri, 19 Jun 2026 06:59:59 +0000 Subject: [PATCH 1/2] feat(proxy): add LITELLM_DISABLE_ACCESS_LOG_PATHS to drop noisy access logs Adds a `HealthCheckAccessLogFilter` to `litellm._logging` that drops `uvicorn.access` records whose request path matches a comma-separated list in the new `LITELLM_DISABLE_ACCESS_LOG_PATHS` env var. The filter is wired into both: - the JSON `log_config` produced by `_get_uvicorn_json_log_config()` (used when `JSON_LOGS=true`), via `dictConfig`'s native filter binding on the access handler, and - the plain `uvicorn.access` logger at module import (covers the non-JSON code path). This is useful when the proxy runs behind k8s liveness/readiness probes, ALB health pings, or Prometheus `/metrics/` scrapes that otherwise drown real request logs at a multi-line-per-second rate. Example: LITELLM_DISABLE_ACCESS_LOG_PATHS="/,/health/liveliness,/health/readiness,/metrics/" Path matching is exact (after stripping any query string) and only applies to the `uvicorn.access` logger -- application logs and `uvicorn.error` are untouched. Default behaviour is unchanged: when the env var is empty/unset the filter short-circuits and all access lines are emitted. Tests cover env-unset pass-through, configured-path drops, query string stripping, and robustness to malformed log records. --- litellm/_logging.py | 57 +++++++++++++++++++++- tests/test_litellm/test_logging.py | 76 ++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+), 1 deletion(-) diff --git a/litellm/_logging.py b/litellm/_logging.py index bb743c32878..32e04deca8a 100644 --- a/litellm/_logging.py +++ b/litellm/_logging.py @@ -81,6 +81,50 @@ def filter(self, record: logging.LogRecord) -> bool: _secret_filter = SecretRedactionFilter() +def _parse_disabled_access_log_paths() -> frozenset: + """ + Parse LITELLM_DISABLE_ACCESS_LOG_PATHS env var into a frozenset of paths. + + Comma-separated list of exact request paths whose access-log lines should + be dropped (e.g. health checks, root probes, metrics scrapes that flood + logs). Empty/unset disables filtering. + """ + raw = os.getenv("LITELLM_DISABLE_ACCESS_LOG_PATHS", "") + return frozenset(p.strip() for p in raw.split(",") if p.strip()) + + +_DISABLED_ACCESS_LOG_PATHS = _parse_disabled_access_log_paths() + + +class HealthCheckAccessLogFilter(logging.Filter): + """ + Drops uvicorn.access records for request paths in + LITELLM_DISABLE_ACCESS_LOG_PATHS. Uvicorn passes args as + (client_addr, method, full_path, http_version, status). Path is matched + against the portion before any query string. + """ + + def filter(self, record: logging.LogRecord) -> bool: + if not _DISABLED_ACCESS_LOG_PATHS: + return True + try: + args = record.args + if not isinstance(args, tuple) or len(args) < 3: + return True + full_path = args[2] + if not isinstance(full_path, str): + return True + path = full_path.split("?", 1)[0] + if path in _DISABLED_ACCESS_LOG_PATHS: + return False + except Exception: + return True + return True + + +_healthcheck_filter = HealthCheckAccessLogFilter() + + json_logs = bool(os.getenv("JSON_LOGS", False)) # Create a handler for the logger (you may need to adapt this based on your needs) log_level = os.getenv("LITELLM_LOG", "DEBUG") @@ -278,6 +322,11 @@ def _suppress_loggers(): apscheduler_scheduler_logger = logging.getLogger("apscheduler.scheduler") apscheduler_scheduler_logger.setLevel(logging.WARNING) + # Drop access-log lines for paths in LITELLM_DISABLE_ACCESS_LOG_PATHS + # (covers the non-JSON path; JSON path wires the filter via log_config). + if _DISABLED_ACCESS_LOG_PATHS: + logging.getLogger("uvicorn.access").addFilter(_healthcheck_filter) + # Call the suppression function _suppress_loggers() @@ -336,7 +385,7 @@ def _get_uvicorn_json_log_config(): # Use the module-level log_level variable for consistency uvicorn_log_level = log_level.upper() - log_config = { + log_config: Dict[str, Any] = { "version": 1, "disable_existing_loggers": False, "formatters": { @@ -350,6 +399,11 @@ def _get_uvicorn_json_log_config(): "()": json_formatter_class, }, }, + "filters": { + "healthcheck": { + "()": "litellm._logging.HealthCheckAccessLogFilter", + }, + }, "handlers": { "default": { "formatter": "json", @@ -360,6 +414,7 @@ def _get_uvicorn_json_log_config(): "formatter": "access", "class": "logging.StreamHandler", "stream": "ext://sys.stdout", + "filters": ["healthcheck"], }, }, "loggers": { diff --git a/tests/test_litellm/test_logging.py b/tests/test_litellm/test_logging.py index fbed044445b..4c9fd11dfd3 100644 --- a/tests/test_litellm/test_logging.py +++ b/tests/test_litellm/test_logging.py @@ -13,8 +13,10 @@ import sys import litellm +from litellm import _logging as litellm_logging from litellm._logging import ( ALL_LOGGERS, + HealthCheckAccessLogFilter, JsonFormatter, _initialize_loggers_with_handler, _turn_on_json, @@ -328,3 +330,77 @@ async def test_cache_hit_includes_custom_llm_provider(): # Clean up litellm.callbacks = original_callbacks litellm.cache = None + + +def _make_uvicorn_access_record( + full_path: str, status: int = 200 +) -> logging.LogRecord: + # Mirror uvicorn's AccessFormatter args: + # (client_addr, method, full_path, http_version, status_code) + record = logging.LogRecord( + name="uvicorn.access", + level=logging.INFO, + pathname="", + lineno=0, + msg='%s - "%s %s HTTP/%s" %d', + args=("10.0.0.1:12345", "GET", full_path, "1.1", status), + exc_info=None, + ) + return record + + +def test_healthcheck_filter_passes_when_env_unset(monkeypatch): + monkeypatch.delenv("LITELLM_DISABLE_ACCESS_LOG_PATHS", raising=False) + monkeypatch.setattr( + litellm_logging, "_DISABLED_ACCESS_LOG_PATHS", frozenset() + ) + f = HealthCheckAccessLogFilter() + assert f.filter(_make_uvicorn_access_record("/health/liveliness")) is True + assert f.filter(_make_uvicorn_access_record("/")) is True + + +def test_healthcheck_filter_drops_configured_paths(monkeypatch): + paths = frozenset({"/", "/health/liveliness", "/health/readiness", "/metrics/"}) + monkeypatch.setattr(litellm_logging, "_DISABLED_ACCESS_LOG_PATHS", paths) + f = HealthCheckAccessLogFilter() + + # All four configured paths must be dropped + assert f.filter(_make_uvicorn_access_record("/")) is False + assert f.filter(_make_uvicorn_access_record("/health/liveliness")) is False + assert f.filter(_make_uvicorn_access_record("/health/readiness")) is False + assert f.filter(_make_uvicorn_access_record("/metrics/")) is False + + # Real traffic must still pass + assert f.filter(_make_uvicorn_access_record("/v1/chat/completions")) is True + assert f.filter(_make_uvicorn_access_record("/health")) is True # not in list + + +def test_healthcheck_filter_strips_query_string(monkeypatch): + monkeypatch.setattr( + litellm_logging, + "_DISABLED_ACCESS_LOG_PATHS", + frozenset({"/health/readiness"}), + ) + f = HealthCheckAccessLogFilter() + assert f.filter(_make_uvicorn_access_record("/health/readiness?x=1")) is False + + +def test_healthcheck_filter_robust_to_malformed_args(monkeypatch): + monkeypatch.setattr( + litellm_logging, "_DISABLED_ACCESS_LOG_PATHS", frozenset({"/"}) + ) + f = HealthCheckAccessLogFilter() + # No args + record = logging.LogRecord( + name="uvicorn.access", + level=logging.INFO, + pathname="", + lineno=0, + msg="raw msg", + args=None, + exc_info=None, + ) + assert f.filter(record) is True + # Args too short + record.args = ("10.0.0.1:12345", "GET") + assert f.filter(record) is True From 7df0ca6017799bccee8c7fcffbe10ae85a0dc01a Mon Sep 17 00:00:00 2001 From: Duck Date: Mon, 22 Jun 2026 22:47:01 +0700 Subject: [PATCH 2/2] fix(proxy): only wire healthcheck filter in JSON log config when paths configured Mirror the _suppress_loggers() guard so JSON_LOGS=true users who have not set LITELLM_DISABLE_ACCESS_LOG_PATHS no longer get unused dictConfig filter machinery --- litellm/_logging.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/litellm/_logging.py b/litellm/_logging.py index 32e04deca8a..109d6653e84 100644 --- a/litellm/_logging.py +++ b/litellm/_logging.py @@ -399,11 +399,17 @@ def _get_uvicorn_json_log_config(): "()": json_formatter_class, }, }, - "filters": { - "healthcheck": { - "()": "litellm._logging.HealthCheckAccessLogFilter", - }, - }, + **( + { + "filters": { + "healthcheck": { + "()": "litellm._logging.HealthCheckAccessLogFilter", + }, + }, + } + if _DISABLED_ACCESS_LOG_PATHS + else {} + ), "handlers": { "default": { "formatter": "json", @@ -414,7 +420,7 @@ def _get_uvicorn_json_log_config(): "formatter": "access", "class": "logging.StreamHandler", "stream": "ext://sys.stdout", - "filters": ["healthcheck"], + **({"filters": ["healthcheck"]} if _DISABLED_ACCESS_LOG_PATHS else {}), }, }, "loggers": {