Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 62 additions & 1 deletion litellm/_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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": {
Expand All @@ -350,6 +399,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",
Expand All @@ -360,6 +420,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": {
Expand Down
76 changes: 76 additions & 0 deletions tests/test_litellm/test_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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