The machinery landed last commit; these are the events. What a routed request
now leaves behind, at info:
route id=r9116d9 cat=coding_refactor tier=2 ctx=500 src=classifier
tools=0 latency=interactive cand=8 pick=deepseek-v4-flash
est_usd=0.00016296 prof=1 ms=1868
dispatch id=r9116d9 model=deepseek-v4-flash rid=chatcmpl-... sess=c090d751
p_tok=59511 c_tok=415 kwh=4.8e-05 usd=0.000783 verdict=unverifiable
upstream_ms=3980 total_ms=5692
`rid` and `sess` are columns in energy_observations, so a journal line pivots
to its database row and back with a --grep. That is why no schema change was
needed to get traceability.
At debug it also says why, which is the half that was never recorded anywhere:
classify id=r9116d9 cat=coding_refactor tier=2 ctx=500 confidence=0.95 ms=1863
filter id=r9116d9 model=gemma-4-31b reason=tier(1<2)
filter id=r9116d9 model=glm-5.2-short reason=access_level(preview)
filter id=r9116d9 model=kimi-k3-flex reason=latency_class(flex)
rank id=r9116d9 pos=0 model=deepseek-v4-flash prof=1 est_usd=0.00016296
Those reasons come from routing.rejection_reason, which is is_eligible turned
inside out: it returns the failing filter and its numbers instead of False, and
is_eligible is now a one-line wrapper over it. One copy of the rules, so the
log cannot drift from the decision it describes.
log_decision is called by the ENDPOINTS, not by route(). chat_completions
routes twice whenever the measured conversation exceeds the classifier's
estimate -- with opencode sending ~32K of system prompt, essentially always --
and logging inside route() would double every line for no new information.
Two things the tests caught that reasoning had not:
The streaming path could not use the ContextVar. starlette resumes the
generator through a threadpool and every next() gets a fresh COPY of the
caller's context, so a trace id set inside is gone by the finally block that
logs the dispatch line. It logged id=- until the id was carried explicitly by
logs.bind() -- on the path all agent traffic takes.
$JOURNAL_STREAM is inherited. A foreground uvicorn started from a
systemd-managed session, output redirected to a file, put a literal <7> on
every line: the variable was set, but described a stream that was no longer
ours. It is now compared against the actual fd's device:inode, which is what
systemd documents.
A test asserts no conversation text reaches the log at any level, prompt or
answer. Prompts here run 60k-150k tokens and the journal is on disk.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WSkcSD2Jzkxo1Kw27ecfXJ
239 lines
6.6 KiB
Python
239 lines
6.6 KiB
Python
"""Tests for the router's structured logging.
|
|
|
|
Written against the real handler and formatter rather than pytest's caplog,
|
|
because the thing worth pinning is what actually lands in the journal --
|
|
including the priority prefix, which caplog would never see.
|
|
"""
|
|
|
|
import io
|
|
import logging
|
|
import os
|
|
|
|
import pytest
|
|
|
|
import logs
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def isolated_logger(monkeypatch):
|
|
"""Every test gets a fresh logger and a clean environment."""
|
|
monkeypatch.delenv(logs.LEVEL_ENV, raising=False)
|
|
monkeypatch.delenv("JOURNAL_STREAM", raising=False)
|
|
logs.set_trace(logs.NO_TRACE)
|
|
yield
|
|
for handler in list(logs.log.handlers):
|
|
logs.log.removeHandler(handler)
|
|
|
|
|
|
def _capture(level="debug", **kwargs):
|
|
buf = io.StringIO()
|
|
logs.configure(level, stream=buf, **kwargs)
|
|
return buf
|
|
|
|
|
|
# --- rendering ------------------------------------------------------------
|
|
|
|
def test_a_line_is_logfmt_with_the_trace_id_first():
|
|
buf = _capture()
|
|
logs.set_trace("r123456")
|
|
|
|
logs.info("route", cat="coding_general", tier=2)
|
|
|
|
assert buf.getvalue() == "route id=r123456 cat=coding_general tier=2\n"
|
|
|
|
|
|
def test_values_with_spaces_are_quoted():
|
|
buf = _capture()
|
|
|
|
logs.info("filter", detail="held during peak")
|
|
|
|
assert 'detail="held during peak"' in buf.getvalue()
|
|
|
|
|
|
def test_a_value_never_breaks_the_line():
|
|
"""One event, one line -- a newline in a detail string would forge a record."""
|
|
buf = _capture()
|
|
|
|
logs.info("verify", detail="line one\nline two")
|
|
|
|
assert buf.getvalue().count("\n") == 1
|
|
|
|
|
|
def test_none_is_a_dash_and_bools_are_binary():
|
|
buf = _capture()
|
|
|
|
logs.info("route", pick=None, tools=True, stream=False)
|
|
|
|
assert "pick=- tools=1 stream=0" in buf.getvalue()
|
|
|
|
|
|
def test_floats_are_readable_not_repr():
|
|
"""Microdollars and kWh, without 0.0007830000000000001."""
|
|
buf = _capture()
|
|
|
|
logs.info("dispatch", usd=0.000783123456, kwh=4.75e-05)
|
|
|
|
assert "usd=0.000783123" in buf.getvalue()
|
|
assert "kwh=4.75e-05" in buf.getvalue()
|
|
|
|
|
|
def test_an_empty_string_is_visible():
|
|
buf = _capture()
|
|
|
|
logs.info("classify", cat="")
|
|
|
|
assert 'cat=""' in buf.getvalue()
|
|
|
|
|
|
# --- levels ---------------------------------------------------------------
|
|
|
|
def test_debug_is_silent_at_info_level():
|
|
buf = _capture("info")
|
|
|
|
logs.debug("filter", model="m")
|
|
logs.info("route", pick="m")
|
|
|
|
assert "filter" not in buf.getvalue()
|
|
assert "route" in buf.getvalue()
|
|
|
|
|
|
def test_enabled_for_debug_reports_the_level():
|
|
_capture("info")
|
|
assert logs.enabled_for_debug() is False
|
|
_capture("debug")
|
|
assert logs.enabled_for_debug() is True
|
|
|
|
|
|
def test_the_env_var_overrides_the_configured_level(monkeypatch):
|
|
"""So a live service can be turned up without editing a tracked file."""
|
|
monkeypatch.setenv(logs.LEVEL_ENV, "debug")
|
|
buf = _capture("warning")
|
|
|
|
logs.debug("filter", model="m")
|
|
|
|
assert "filter" in buf.getvalue()
|
|
|
|
|
|
def test_a_nonsense_env_level_falls_back_rather_than_crashing(monkeypatch):
|
|
monkeypatch.setenv(logs.LEVEL_ENV, "loud")
|
|
|
|
assert logs.resolve_level("warning") == logging.WARNING
|
|
|
|
|
|
# --- journald -------------------------------------------------------------
|
|
|
|
def test_no_priority_prefix_in_a_terminal():
|
|
"""A foreground uvicorn should not print a literal <6> on every line."""
|
|
buf = _capture(journald=False)
|
|
|
|
logs.info("route", pick="m")
|
|
|
|
assert buf.getvalue().startswith("route ")
|
|
|
|
|
|
def test_priority_prefix_under_journald():
|
|
"""This is what makes `journalctl -p warning` mean anything."""
|
|
buf = _capture(journald=True)
|
|
|
|
logs.warning("retry", verdict="malformed")
|
|
logs.error("upstream", status=500)
|
|
logs.info("route", pick="m")
|
|
|
|
lines = buf.getvalue().splitlines()
|
|
assert lines[0].startswith("<4>") # warning
|
|
assert lines[1].startswith("<3>") # error
|
|
assert lines[2].startswith("<6>") # info
|
|
|
|
|
|
def test_journald_is_detected_by_matching_the_actual_stream(tmp_path, monkeypatch):
|
|
"""The env var alone is not enough -- it is inherited by every child.
|
|
|
|
A foreground uvicorn started from a systemd-managed session, with output
|
|
redirected to a file, inherited JOURNAL_STREAM and put a literal <7> on
|
|
every line. systemd documents comparing the value against the fd.
|
|
"""
|
|
path = tmp_path / "out.log"
|
|
with open(path, "w") as handle:
|
|
stat = os.fstat(handle.fileno())
|
|
|
|
monkeypatch.setenv("JOURNAL_STREAM", f"{stat.st_dev}:{stat.st_ino}")
|
|
assert logs.under_journald(handle) is True
|
|
|
|
# Same variable, a different stream: this is the inherited case.
|
|
monkeypatch.setenv("JOURNAL_STREAM", f"{stat.st_dev}:{stat.st_ino + 1}")
|
|
assert logs.under_journald(handle) is False
|
|
|
|
|
|
def test_no_journald_variable_means_no_prefix():
|
|
assert logs.under_journald() is False
|
|
|
|
|
|
def test_a_stream_with_no_fileno_is_not_journald():
|
|
"""StringIO under test, and anything else without an fd."""
|
|
assert logs.under_journald(io.StringIO()) is False
|
|
|
|
|
|
def test_a_malformed_journald_variable_is_ignored(monkeypatch):
|
|
monkeypatch.setenv("JOURNAL_STREAM", "not-a-device")
|
|
assert logs.under_journald() is False
|
|
|
|
|
|
# --- trace ids ------------------------------------------------------------
|
|
|
|
def test_a_trace_id_is_stable_until_a_new_one_is_started():
|
|
first = logs.new_trace()
|
|
|
|
assert logs.current_trace() == first
|
|
assert logs.new_trace() != first
|
|
|
|
|
|
def test_trace_ids_do_not_collide():
|
|
assert len({logs.new_trace() for _ in range(200)}) == 200
|
|
|
|
|
|
def test_configure_is_idempotent():
|
|
"""Re-importing or re-configuring must not double every line."""
|
|
buf = io.StringIO()
|
|
logs.configure("info", stream=buf, journald=False)
|
|
logs.configure("info", stream=buf, journald=False)
|
|
|
|
logs.info("route", pick="m")
|
|
|
|
assert buf.getvalue().count("route") == 1
|
|
|
|
|
|
def test_the_logger_does_not_propagate():
|
|
"""uvicorn configures its own logging; a root handler would double-print."""
|
|
_capture()
|
|
assert logs.log.propagate is False
|
|
|
|
|
|
# --- bound emitters -------------------------------------------------------
|
|
|
|
def test_a_bound_emitter_ignores_the_ambient_trace():
|
|
"""What the streaming path needs: an id that survives a context copy."""
|
|
buf = _capture()
|
|
bound = logs.bind("rfixed1")
|
|
logs.set_trace("rother2")
|
|
|
|
bound.info("dispatch", model="m")
|
|
|
|
assert "id=rfixed1" in buf.getvalue()
|
|
|
|
|
|
def test_bind_defaults_to_the_current_trace():
|
|
buf = _capture()
|
|
logs.set_trace("rcurrent")
|
|
|
|
logs.bind().warning("retry", verdict="malformed")
|
|
|
|
assert "id=rcurrent" in buf.getvalue()
|
|
|
|
|
|
def test_a_bound_emitter_respects_the_level():
|
|
buf = _capture("info")
|
|
|
|
logs.bind("r1").debug("filter", model="m")
|
|
|
|
assert buf.getvalue() == ""
|