fix(logging): keep transformed server failures and config JSON mode on the error threshold

When a failure callback rewrites a malformed key rejection into a non-401 response,
the proxy now also logs that response at ERROR on stderr, after the WARNING that
describes the malformed key. The JSON handler installed by litellm_settings.json_logs
carries the LITELLM_LOG level like the JSON_LOGS handler, so LITELLM_LOG=ERROR hides
warnings in both JSON modes.
This commit is contained in:
Yucheng Zhu 2026-08-29 14:19:47 -07:00
parent 6646bb4319
commit 4c8e9d78dc
4 changed files with 34 additions and 7 deletions

View file

@ -690,6 +690,7 @@ def _turn_on_json():
- Adds a JSON formatter to all loggers
"""
handler: Final = LevelRoutingStreamHandler()
handler.setLevel(numeric_level)
handler.setFormatter(JsonFormatter())
_initialize_loggers_with_handler(handler)
# Set up exception handlers

View file

@ -150,6 +150,7 @@ class UserAPIKeyAuthExceptionHandler:
request=request,
use_x_forwarded_for=general_settings.get("use_x_forwarded_for") is True,
)
log_extra: Final = {"requester_ip": requester_ip}
is_invalid_virtual_key: Final = is_invalid_virtual_key_error(e)
is_quiet_log: Final = is_invalid_virtual_key and not litellm.log_client_error_tracebacks
logger: Final = verbose_proxy_stdout_logger if is_quiet_log else verbose_proxy_logger
@ -159,7 +160,7 @@ class UserAPIKeyAuthExceptionHandler:
e,
requester_ip,
exc_info=True if litellm.log_client_error_tracebacks or not is_expected_client_error(e) else None,
extra={"requester_ip": requester_ip},
extra=log_extra,
)
# Log this exception to OTEL, Datadog etc. Reuse the identity resolved
@ -207,4 +208,12 @@ class UserAPIKeyAuthExceptionHandler:
if transformed_exception is not None:
e = transformed_exception
raise mark_invalid_virtual_key_error(_as_proxy_exception(e), is_invalid_virtual_key)
final_exception: Final = mark_invalid_virtual_key_error(_as_proxy_exception(e), is_invalid_virtual_key)
if is_quiet_log and str(final_exception.code) != str(status.HTTP_401_UNAUTHORIZED):
verbose_proxy_logger.error(
"litellm.proxy.proxy_server.user_api_key_auth(): Exception occured - %s\nRequester IP Address:%s",
final_exception,
requester_ip,
extra=log_extra,
)
raise final_exception

View file

@ -882,7 +882,7 @@ async def test_handle_authentication_error_keeps_unexpected_source_traceback_aft
@pytest.mark.asyncio
async def test_handle_authentication_error_does_not_preserve_invalid_virtual_key_marker_for_callback_503(caplog):
async def test_handle_authentication_error_logs_callback_503_at_error_level_without_the_marker(caplog):
handler = UserAPIKeyAuthExceptionHandler()
transformed_exception = HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
@ -895,11 +895,12 @@ async def test_handle_authentication_error_does_not_preserve_invalid_virtual_key
)
records = _auth_failure_records(caplog)
assert len(records) == 1
assert records[0].name == verbose_proxy_stdout_logger.name
assert records[0].levelno == logging.WARNING
assert records[0].exc_info is None
assert [(r.name, r.levelno, r.exc_info) for r in records] == [
(verbose_proxy_stdout_logger.name, logging.WARNING, None),
(verbose_proxy_logger.name, logging.ERROR, None),
]
assert "LiteLLM Virtual Key expected" in records[0].getMessage()
assert "Authentication service temporarily unavailable" in records[1].getMessage()
assert exc_info.value.message == "Authentication service temporarily unavailable"
assert exc_info.value.code == str(status.HTTP_503_SERVICE_UNAVAILABLE)
assert not hasattr(exc_info.value, "_litellm_invalid_virtual_key_error")

View file

@ -11,6 +11,7 @@ from typing import List
import pytest
import litellm
import litellm._logging as litellm_logging
from litellm._logging import (
_COLOR_LOG_FORMAT,
_PLAIN_LOG_FORMAT,
@ -986,6 +987,21 @@ def test_error_threshold_suppresses_invalid_key_warning_like_ordinary_warnings(c
assert "ordinary proxy error" in err
def test_config_json_mode_handler_honors_the_litellm_log_threshold(monkeypatch, capsys):
monkeypatch.setattr(litellm_logging, "numeric_level", logging.ERROR)
_turn_on_json()
verbose_proxy_stdout_logger.warning("invalid virtual key")
verbose_proxy_logger.warning("ordinary proxy warning")
verbose_proxy_logger.error("ordinary proxy error")
out, err = capsys.readouterr()
assert out == ""
assert "invalid virtual key" not in err
assert "ordinary proxy warning" not in err
assert "ordinary proxy error" in err
def test_ordinary_proxy_records_still_propagate_to_root_handler():
root_logger = logging.getLogger()
root_stream = StringIO()