mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
feat(proxy): add LITELLM_DISABLE_ACCESS_LOG_PATHS to drop noisy access logs (#30818)
* 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.
* 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
---------
Co-authored-by: Huynh Duc Tran <ducth6@tcbs.com.vn>
This commit is contained in:
parent
b443037783
commit
97525e188c
2 changed files with 138 additions and 1 deletions
|
|
@ -77,6 +77,50 @@ class SecretRedactionFilter(logging.Filter):
|
|||
_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")
|
||||
|
|
@ -272,6 +316,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()
|
||||
|
|
@ -330,7 +379,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": {
|
||||
|
|
@ -344,6 +393,17 @@ def _get_uvicorn_json_log_config():
|
|||
"()": json_formatter_class,
|
||||
},
|
||||
},
|
||||
**(
|
||||
{
|
||||
"filters": {
|
||||
"healthcheck": {
|
||||
"()": "litellm._logging.HealthCheckAccessLogFilter",
|
||||
},
|
||||
},
|
||||
}
|
||||
if _DISABLED_ACCESS_LOG_PATHS
|
||||
else {}
|
||||
),
|
||||
"handlers": {
|
||||
"default": {
|
||||
"formatter": "json",
|
||||
|
|
@ -354,6 +414,7 @@ def _get_uvicorn_json_log_config():
|
|||
"formatter": "access",
|
||||
"class": "logging.StreamHandler",
|
||||
"stream": "ext://sys.stdout",
|
||||
**({"filters": ["healthcheck"]} if _DISABLED_ACCESS_LOG_PATHS else {}),
|
||||
},
|
||||
},
|
||||
"loggers": {
|
||||
|
|
|
|||
|
|
@ -13,8 +13,10 @@ import logging
|
|||
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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue