Merge pull request #41096 from BerriAI/litellm_disable_access_log_paths

feat(proxy): honor LITELLM_DISABLE_ACCESS_LOG_PATHS to drop noisy uvicorn access log lines
This commit is contained in:
Yassin Kortam 2026-09-14 12:41:34 -07:00 committed by GitHub
commit b94b8bca21
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 101 additions and 0 deletions

View file

@ -1,5 +1,6 @@
import ast
import contextvars
import functools
import logging
import os
import sys
@ -225,6 +226,35 @@ class AccessLogRedactionFilter(logging.Filter):
_access_log_filter: Final = AccessLogRedactionFilter()
@functools.lru_cache(maxsize=1)
def _parse_disabled_access_log_paths(raw: str) -> frozenset[str]:
return frozenset(stripped for path in raw.split(",") if (stripped := path.strip()))
def _disabled_access_log_paths() -> frozenset[str]:
"""Read the variable per record so a value loaded later via proxy config
environment_variables or dotenv is honored."""
return _parse_disabled_access_log_paths(os.getenv("LITELLM_DISABLE_ACCESS_LOG_PATHS", ""))
class AccessLogPathFilter(logging.Filter):
"""Drops uvicorn.access records for request paths listed in LITELLM_DISABLE_ACCESS_LOG_PATHS.
uvicorn passes record.args as (client_addr, method, full_path, http_version, status_code).
"""
def filter(self, record: logging.LogRecord) -> bool:
if not isinstance(record.args, tuple) or len(record.args) < 3:
return True
full_path: Final = record.args[2]
if not isinstance(full_path, str):
return True
return full_path.partition("?")[0] not in _disabled_access_log_paths()
_access_log_path_filter: Final = AccessLogPathFilter()
def _get_max_string_length_stdout_log() -> int:
"""Read the limit per record so a value loaded later via proxy config
environment_variables is honored."""
@ -663,6 +693,7 @@ def _redact_third_party_loggers() -> None:
for name in _REDACTED_THIRD_PARTY_LOGGERS:
logging.getLogger(name).addFilter(_secret_filter)
for name in _REDACTED_ACCESS_LOGGERS:
logging.getLogger(name).addFilter(_access_log_path_filter)
logging.getLogger(name).addFilter(_access_log_filter)

View file

@ -17,6 +17,7 @@ from litellm._logging import (
_MAX_SCRUBBED_ACCESS_ARG,
_PLAIN_LOG_FORMAT,
ALL_LOGGERS,
AccessLogPathFilter,
AccessLogRedactionFilter,
CorrelationContextFilter,
CorrelationPlainFormatter,
@ -1178,3 +1179,72 @@ def test_access_redaction_survives_the_uvicorn_json_log_config():
lg.handlers[:] = handlers
lg.setLevel(level)
lg.propagate = True
_DISABLED_ACCESS_LOG_PATHS_RAW = " /health/liveliness , ,/metrics/"
@pytest.mark.parametrize(
"full_path",
[
"/health/liveliness",
"/health/liveliness?x=1",
"/health/liveliness?probe=" + "x" * _MAX_SCRUBBED_ACCESS_ARG,
"/metrics/",
"/metrics/?format=prometheus&job=a",
],
)
def test_uvicorn_access_logger_drops_a_configured_path(monkeypatch, full_path):
monkeypatch.setenv("LITELLM_DISABLE_ACCESS_LOG_PATHS", _DISABLED_ACCESS_LOG_PATHS_RAW)
assert _emit_access_line(full_path) == ""
@pytest.mark.parametrize(
"full_path",
["/v1/chat/completions", "/health", "/health/liveliness/", "/metrics", "/v1/models?health=/health/liveliness"],
)
def test_uvicorn_access_logger_keeps_an_unconfigured_path(monkeypatch, full_path):
monkeypatch.setenv("LITELLM_DISABLE_ACCESS_LOG_PATHS", _DISABLED_ACCESS_LOG_PATHS_RAW)
assert f'"GET {full_path} HTTP/1.1" 200' in _emit_access_line(full_path)
@pytest.mark.parametrize("raw", [None, "", " , ,"])
def test_uvicorn_access_logger_keeps_every_line_when_no_path_is_configured(monkeypatch, raw):
if raw is None:
monkeypatch.delenv("LITELLM_DISABLE_ACCESS_LOG_PATHS", raising=False)
else:
monkeypatch.setenv("LITELLM_DISABLE_ACCESS_LOG_PATHS", raw)
assert '"GET /health/liveliness HTTP/1.1" 200' in _emit_access_line("/health/liveliness")
def test_access_log_path_filter_survives_the_uvicorn_json_log_config(monkeypatch):
import logging.config
monkeypatch.setenv("LITELLM_DISABLE_ACCESS_LOG_PATHS", _DISABLED_ACCESS_LOG_PATHS_RAW)
names = ("uvicorn", "uvicorn.error", "uvicorn.access")
saved = tuple((logging.getLogger(n), logging.getLogger(n).handlers[:], logging.getLogger(n).level) for n in names)
try:
logging.config.dictConfig(_get_uvicorn_json_log_config())
assert _emit_access_line("/health/liveliness?x=1") == ""
assert '"GET /v1/models HTTP/1.1" 200' in _emit_access_line("/v1/models")
finally:
for lg, handlers, level in saved:
lg.handlers[:] = handlers
lg.setLevel(level)
lg.propagate = True
@pytest.mark.parametrize("args", [None, ("127.0.0.1:1", "GET", 42)])
def test_access_log_path_filter_keeps_a_record_without_a_string_path_arg(monkeypatch, args):
monkeypatch.setenv("LITELLM_DISABLE_ACCESS_LOG_PATHS", "/health/liveliness")
record = logging.LogRecord(
name="uvicorn.access",
level=logging.INFO,
pathname="",
lineno=0,
msg='127.0.0.1:1 - "GET /health/liveliness HTTP/1.1" 200',
args=args,
exc_info=None,
)
assert AccessLogPathFilter().filter(record) is True