fix(logging): emit startup warnings as WARNING records in JSON mode

This commit is contained in:
mateo-berri 2026-10-20 23:29:05 -07:00
parent ecfb597376
commit 593f07a1ee
4 changed files with 18 additions and 15 deletions

View file

@ -943,17 +943,18 @@ def print_startup_message(message: str) -> None:
if litellm.suppress_startup_output is True:
return
frame: Final = inspect.currentframe()
_write_startup_message(message, frame.f_back if frame is not None else None)
_write_startup_message(message, frame.f_back if frame is not None else None, logging.INFO)
def print_startup_warning(message: str) -> None:
"""Write a boot-time warning exactly as print_startup_message writes a message, even with
suppress_startup_output on, since it names something the operator has to fix."""
"""Write a boot-time warning as print_startup_message writes a message, even with suppress_startup_output
on, since it names something the operator has to fix. With JSON logs on it is a WARNING record, on
stdout where the print put it, so a collector that filters on the level field keeps it."""
frame: Final = inspect.currentframe()
_write_startup_message(message, frame.f_back if frame is not None else None)
_write_startup_message(message, frame.f_back if frame is not None else None, logging.WARNING)
def _write_startup_message(message: str, caller: FrameType | None) -> None:
def _write_startup_message(message: str, caller: FrameType | None, level: int) -> None:
if not litellm.json_logs:
print(terminal_text(message), flush=True) # noqa: T201 # plain mode: this is the operator-terminal print it replaces
return
@ -962,7 +963,7 @@ def _write_startup_message(message: str, caller: FrameType | None) -> None:
return
record: Final = verbose_proxy_logger.makeRecord(
verbose_proxy_logger.name,
logging.INFO,
level,
caller.f_code.co_filename if caller is not None else __file__,
caller.f_lineno if caller is not None else 0,
text,
@ -992,8 +993,8 @@ def _write_startup_message(message: str, caller: FrameType | None) -> None:
def _startup_json_handler() -> logging.Handler:
"""The proxy's JSON handler on stdout, filters included, for one startup message."""
handler: Final = LevelRoutingStreamHandler()
"""The proxy's JSON handler on stdout, filters included, for one startup message or warning."""
handler: Final = logging.StreamHandler(sys.stdout)
handler.setFormatter(JsonFormatter())
handler.addFilter(_secret_filter)
handler.addFilter(_correlation_filter)

View file

@ -111,8 +111,8 @@ def test_policy_table_is_one_json_record_in_json_mode(monkeypatch, capfd):
def test_missing_attachments_warning_is_a_second_json_record(monkeypatch, capfd):
"""Without attachments the table and the yellow warning are two INFO records on stdout, where the two
prints put them."""
"""Without attachments the table and the yellow warning are an INFO and a WARNING record on stdout,
where the two prints put them."""
monkeypatch.setattr(litellm, "json_logs", True)
monkeypatch.setattr(sys, "excepthook", sys.excepthook)
turn_on_json()
@ -126,4 +126,4 @@ def test_missing_attachments_warning_is_a_second_json_record(monkeypatch, capfd)
"\n".join(("LiteLLM Policy Engine: Loaded 1 policies", "", *POLICY_ROWS)),
NO_ATTACHMENTS_WARNING,
]
assert {record["level"] for record in records} == {"INFO"}
assert [record["level"] for record in records] == ["INFO", "WARNING"]

View file

@ -4252,8 +4252,8 @@ class TestEnforcePrismaMigrationCheckNoticeAroundTheConfigRead:
def test_granian_flag_notices_are_json_records_in_json_mode(monkeypatch, capfd):
"""The yellow notices for the flags Granian ignores (`--max_requests_before_restart`, `--ciphers`) are
printed while the launcher starts; with json_logs on they are INFO records on stdout like every other
line, not plaintext between the records."""
printed while the launcher starts; with json_logs on they are WARNING records on stdout after the INFO
start line, not plaintext between the records."""
pytest.importorskip("granian")
monkeypatch.setattr(litellm, "json_logs", True)
monkeypatch.setattr(sys, "excepthook", sys.excepthook) # turn_on_json swaps it; put it back at teardown
@ -4284,4 +4284,4 @@ def test_granian_flag_notices_are_json_records_in_json_mode(monkeypatch, capfd):
"(Granian uses workers_lifetime in seconds, not a per-request limit).",
"LiteLLM: --ciphers is not applied when using --run_granian.",
]
assert {record["level"] for record in records} == {"INFO"}
assert [record["level"] for record in records] == ["INFO", "WARNING", "WARNING"]

View file

@ -1914,7 +1914,8 @@ def test_print_startup_warning_still_prints_when_suppressed(
monkeypatch: pytest.MonkeyPatch, capfd: pytest.CaptureFixture[str], json_logs: bool
):
"""A warning names something the operator has to fix, like an ignored flag or a failed migration diff,
so suppress_startup_output leaves it in place, at the caller's location in JSON mode."""
so suppress_startup_output leaves it in place. In JSON mode it is a WARNING record at the caller's
location, on stdout where the print put it, so a collector's WARNING filter keeps it."""
monkeypatch.setattr(litellm, "json_logs", json_logs)
monkeypatch.setattr(litellm, "suppress_startup_output", True)
monkeypatch.setattr(sys, "excepthook", sys.excepthook)
@ -1930,6 +1931,7 @@ def test_print_startup_warning_still_prints_when_suppressed(
if json_logs:
record: Final = json.loads(lines[0])
assert record["message"] == "LiteLLM: --ciphers is not applied when using --run_granian."
assert record["level"] == "WARNING"
assert str(record["logger"]).startswith("test_logging.py:"), record["logger"]
else:
assert lines[0] == "LiteLLM: --ciphers is not applied when using --run_granian."