From 2268bbaf5ef0ac5a70bc75f77e2dc0203f4b0eef Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 16:39:54 +0000 Subject: [PATCH 1/3] feat(proxy): honor LITELLM_DISABLE_ACCESS_LOG_PATHS to drop noisy uvicorn access log lines Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/_logging.py | 32 +++++++++++++++ tests/test_litellm/test_logging.py | 63 ++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+) diff --git a/litellm/_logging.py b/litellm/_logging.py index c73b5175a31..33bd7b7bbf3 100644 --- a/litellm/_logging.py +++ b/litellm/_logging.py @@ -225,6 +225,37 @@ class AccessLogRedactionFilter(logging.Filter): _access_log_filter: Final = AccessLogRedactionFilter() +def _parse_disabled_access_log_paths(raw: str) -> frozenset[str]: + return frozenset(path for path in (segment.strip() for segment in raw.split(",")) if path) + + +_DISABLED_ACCESS_LOG_PATHS: Final = _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 __init__(self, disabled_paths: frozenset[str]) -> None: + super().__init__() + self._disabled_paths: Final = disabled_paths + + def filter(self, record: logging.LogRecord) -> bool: + if not self._disabled_paths: + return True + 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 self._disabled_paths + + +_access_log_path_filter: Final = AccessLogPathFilter(_DISABLED_ACCESS_LOG_PATHS) + + 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.""" @@ -664,6 +695,7 @@ def _redact_third_party_loggers() -> None: logging.getLogger(name).addFilter(_secret_filter) for name in _REDACTED_ACCESS_LOGGERS: logging.getLogger(name).addFilter(_access_log_filter) + logging.getLogger(name).addFilter(_access_log_path_filter) # Call the suppression function diff --git a/tests/test_litellm/test_logging.py b/tests/test_litellm/test_logging.py index bf3757e6886..9972be01f00 100644 --- a/tests/test_litellm/test_logging.py +++ b/tests/test_litellm/test_logging.py @@ -2,6 +2,7 @@ import ast import asyncio import json import logging +import os import re import sys import time @@ -17,6 +18,7 @@ from litellm._logging import ( _MAX_SCRUBBED_ACCESS_ARG, _PLAIN_LOG_FORMAT, ALL_LOGGERS, + AccessLogPathFilter, AccessLogRedactionFilter, CorrelationContextFilter, CorrelationPlainFormatter, @@ -26,6 +28,7 @@ from litellm._logging import ( StdoutLogTruncationFilter, _get_uvicorn_json_log_config, _initialize_loggers_with_handler, + _parse_disabled_access_log_paths, _parse_json_logs_env, _plain_log_format, _stdout_truncation_marker, @@ -1178,3 +1181,63 @@ def test_access_redaction_survives_the_uvicorn_json_log_config(): lg.handlers[:] = handlers lg.setLevel(level) lg.propagate = True + + +@pytest.mark.parametrize( + "raw, expected", + [ + ("", frozenset()), + ("/health,/metrics", frozenset({"/health", "/metrics"})), + (" /health , /metrics/ ,", frozenset({"/health", "/metrics/"})), + ], +) +def test_parse_disabled_access_log_paths(raw, expected): + assert _parse_disabled_access_log_paths(raw) == expected + + +def test_access_log_path_filter_drops_a_listed_path(): + access_log_filter = AccessLogPathFilter(frozenset({"/health/liveliness", "/metrics"})) + + assert access_log_filter.filter(_access_record("/health/liveliness")) is False + assert access_log_filter.filter(_access_record("/metrics?format=prometheus")) is False + + +def test_access_log_path_filter_keeps_an_unlisted_path(): + access_log_filter = AccessLogPathFilter(frozenset({"/health/liveliness", "/metrics"})) + + assert access_log_filter.filter(_access_record("/v1/chat/completions")) is True + assert access_log_filter.filter(_access_record("/health")) is True + assert access_log_filter.filter(_access_record("/metrics/")) is True + + +def test_access_log_path_filter_is_a_no_op_when_unset(): + assert AccessLogPathFilter(frozenset()).filter(_access_record("/health/liveliness")) is True + + +def test_access_log_path_filter_keeps_a_record_without_positional_args(): + record = logging.LogRecord( + name="uvicorn.access", + level=logging.INFO, + pathname="", + lineno=0, + msg="access line", + args=None, + exc_info=None, + ) + + assert AccessLogPathFilter(frozenset({"/health/liveliness"})).filter(record) is True + + +def test_uvicorn_access_logger_drops_a_listed_path_end_to_end(monkeypatch): + monkeypatch.setenv("LITELLM_DISABLE_ACCESS_LOG_PATHS", "/health/liveliness") + access_log_filter = AccessLogPathFilter( + _parse_disabled_access_log_paths(os.environ["LITELLM_DISABLE_ACCESS_LOG_PATHS"]) + ) + logger = logging.getLogger("uvicorn.access") + assert any(isinstance(f, AccessLogPathFilter) for f in logger.filters) + logger.addFilter(access_log_filter) + try: + assert _emit_access_line("/health/liveliness") == "" + assert _emit_access_line("/v1/chat/completions") != "" + finally: + logger.removeFilter(access_log_filter) From 66ce1eea97e32da969f5268dc4d0b75fc7929d6c Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 16:54:38 +0000 Subject: [PATCH 2/3] test(proxy): cover non-string access log paths Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- tests/test_litellm/test_logging.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_litellm/test_logging.py b/tests/test_litellm/test_logging.py index 9972be01f00..7106d25b7c2 100644 --- a/tests/test_litellm/test_logging.py +++ b/tests/test_litellm/test_logging.py @@ -1228,6 +1228,20 @@ def test_access_log_path_filter_keeps_a_record_without_positional_args(): assert AccessLogPathFilter(frozenset({"/health/liveliness"})).filter(record) is True +def test_access_log_path_filter_keeps_a_record_with_a_non_string_path(): + record = logging.LogRecord( + name="uvicorn.access", + level=logging.INFO, + pathname="", + lineno=0, + msg="%s - %s %s", + args=("127.0.0.1:1", "GET", 42), + exc_info=None, + ) + + assert AccessLogPathFilter(frozenset({"/health/liveliness"})).filter(record) is True + + def test_uvicorn_access_logger_drops_a_listed_path_end_to_end(monkeypatch): monkeypatch.setenv("LITELLM_DISABLE_ACCESS_LOG_PATHS", "/health/liveliness") access_log_filter = AccessLogPathFilter( From 83d16a4690ad1d22f26b9df36b82f000bbd1270b Mon Sep 17 00:00:00 2001 From: yassin Date: Mon, 14 Sep 2026 17:27:43 +0000 Subject: [PATCH 3/3] fix(proxy): read LITELLM_DISABLE_ACCESS_LOG_PATHS per record and match before redaction Values loaded after import via proxy config environment_variables or dotenv were ignored, and a long query string was truncated by the redaction filter before the path filter could match it. Tests now go through the production registration on the uvicorn.access logger instead of a hand-built filter. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/_logging.py | 21 +++--- tests/test_litellm/test_logging.py | 115 ++++++++++++++--------------- 2 files changed, 64 insertions(+), 72 deletions(-) diff --git a/litellm/_logging.py b/litellm/_logging.py index 33bd7b7bbf3..03a9bcf21cf 100644 --- a/litellm/_logging.py +++ b/litellm/_logging.py @@ -1,5 +1,6 @@ import ast import contextvars +import functools import logging import os import sys @@ -225,11 +226,15 @@ 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(path for path in (segment.strip() for segment in raw.split(",")) if path) + return frozenset(stripped for path in raw.split(",") if (stripped := path.strip())) -_DISABLED_ACCESS_LOG_PATHS: Final = _parse_disabled_access_log_paths(os.getenv("LITELLM_DISABLE_ACCESS_LOG_PATHS", "")) +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): @@ -238,22 +243,16 @@ class AccessLogPathFilter(logging.Filter): uvicorn passes record.args as (client_addr, method, full_path, http_version, status_code). """ - def __init__(self, disabled_paths: frozenset[str]) -> None: - super().__init__() - self._disabled_paths: Final = disabled_paths - def filter(self, record: logging.LogRecord) -> bool: - if not self._disabled_paths: - return True 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 self._disabled_paths + return full_path.partition("?")[0] not in _disabled_access_log_paths() -_access_log_path_filter: Final = AccessLogPathFilter(_DISABLED_ACCESS_LOG_PATHS) +_access_log_path_filter: Final = AccessLogPathFilter() def _get_max_string_length_stdout_log() -> int: @@ -694,8 +693,8 @@ 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_filter) logging.getLogger(name).addFilter(_access_log_path_filter) + logging.getLogger(name).addFilter(_access_log_filter) # Call the suppression function diff --git a/tests/test_litellm/test_logging.py b/tests/test_litellm/test_logging.py index 7106d25b7c2..57c39280a9f 100644 --- a/tests/test_litellm/test_logging.py +++ b/tests/test_litellm/test_logging.py @@ -2,7 +2,6 @@ import ast import asyncio import json import logging -import os import re import sys import time @@ -28,7 +27,6 @@ from litellm._logging import ( StdoutLogTruncationFilter, _get_uvicorn_json_log_config, _initialize_loggers_with_handler, - _parse_disabled_access_log_paths, _parse_json_logs_env, _plain_log_format, _stdout_truncation_marker, @@ -1183,75 +1181,70 @@ def test_access_redaction_survives_the_uvicorn_json_log_config(): lg.propagate = True +_DISABLED_ACCESS_LOG_PATHS_RAW = " /health/liveliness , ,/metrics/" + + @pytest.mark.parametrize( - "raw, expected", + "full_path", [ - ("", frozenset()), - ("/health,/metrics", frozenset({"/health", "/metrics"})), - (" /health , /metrics/ ,", frozenset({"/health", "/metrics/"})), + "/health/liveliness", + "/health/liveliness?x=1", + "/health/liveliness?probe=" + "x" * _MAX_SCRUBBED_ACCESS_ARG, + "/metrics/", + "/metrics/?format=prometheus&job=a", ], ) -def test_parse_disabled_access_log_paths(raw, expected): - assert _parse_disabled_access_log_paths(raw) == expected +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) == "" -def test_access_log_path_filter_drops_a_listed_path(): - access_log_filter = AccessLogPathFilter(frozenset({"/health/liveliness", "/metrics"})) - - assert access_log_filter.filter(_access_record("/health/liveliness")) is False - assert access_log_filter.filter(_access_record("/metrics?format=prometheus")) is False +@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) -def test_access_log_path_filter_keeps_an_unlisted_path(): - access_log_filter = AccessLogPathFilter(frozenset({"/health/liveliness", "/metrics"})) - - assert access_log_filter.filter(_access_record("/v1/chat/completions")) is True - assert access_log_filter.filter(_access_record("/health")) is True - assert access_log_filter.filter(_access_record("/metrics/")) is True +@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_is_a_no_op_when_unset(): - assert AccessLogPathFilter(frozenset()).filter(_access_record("/health/liveliness")) is True +def test_access_log_path_filter_survives_the_uvicorn_json_log_config(monkeypatch): + import logging.config - -def test_access_log_path_filter_keeps_a_record_without_positional_args(): - record = logging.LogRecord( - name="uvicorn.access", - level=logging.INFO, - pathname="", - lineno=0, - msg="access line", - args=None, - exc_info=None, - ) - - assert AccessLogPathFilter(frozenset({"/health/liveliness"})).filter(record) is True - - -def test_access_log_path_filter_keeps_a_record_with_a_non_string_path(): - record = logging.LogRecord( - name="uvicorn.access", - level=logging.INFO, - pathname="", - lineno=0, - msg="%s - %s %s", - args=("127.0.0.1:1", "GET", 42), - exc_info=None, - ) - - assert AccessLogPathFilter(frozenset({"/health/liveliness"})).filter(record) is True - - -def test_uvicorn_access_logger_drops_a_listed_path_end_to_end(monkeypatch): - monkeypatch.setenv("LITELLM_DISABLE_ACCESS_LOG_PATHS", "/health/liveliness") - access_log_filter = AccessLogPathFilter( - _parse_disabled_access_log_paths(os.environ["LITELLM_DISABLE_ACCESS_LOG_PATHS"]) - ) - logger = logging.getLogger("uvicorn.access") - assert any(isinstance(f, AccessLogPathFilter) for f in logger.filters) - logger.addFilter(access_log_filter) + 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: - assert _emit_access_line("/health/liveliness") == "" - assert _emit_access_line("/v1/chat/completions") != "" + 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: - logger.removeFilter(access_log_filter) + 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