fix(mcp): a failing router in get_overview says since when, as the page does (#5860)
CI / lint (push) Successful in 2s
CI / unit (push) Successful in 1m27s
CI / integration (push) Successful in 1m33s
CI / publish (push) Successful in 19s

router_findings never passed `since`, so every failing router read as
brand new and sorted last among the criticals. The overview now reads
the failing routers' last day of history and dates each one with
problem_since, as the dashboard widget does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 15:40:35 -04:00
co-authored by Claude Opus 5.5
parent 80674603bc
commit c625528191
2 changed files with 27 additions and 2 deletions
+19 -2
View File
@@ -14,6 +14,7 @@ from sqlalchemy import select
from steward.core import anomalies as anom
from steward.core.norms import norms_at
from steward.core.time_range import bucket_seconds
from steward.mcp._context import mcp_app
from steward.mcp.tools._shape import (
age_s, bounded, clamp_limit, db_session, iso, lookback, now_utc, rnd, summarize,
@@ -279,7 +280,8 @@ def cert_findings(certs, now, thresholds: dict) -> list[dict]:
def router_findings(routers: list[dict]) -> list[dict]:
"""A router answering 5xx past its cutoff: failing. 4xx and latency are
left to the metric anomalies, which judge them against the router's
normal (System 182)."""
normal (System 182). `since` is when the failing began, as the page's
verdict says it (r["since"], None when too recent for a bucket to show)."""
out = []
for r in routers:
if r["state"] != "crit":
@@ -287,14 +289,29 @@ def router_findings(routers: list[dict]) -> list[dict]:
said = (r.get("meaning") or {}).get("meaning")
out.append(finding("critical", "traefik_router_failing", r["name"],
f"Traefik router {r['name']}: {r['why']}" + (f". {said}" if said else ""),
ids={"router": r["name"]}, next=f'get_router(router="{r["name"]}")'))
since=r.get("since"), ids={"router": r["name"]},
next=f'get_router(router="{r["name"]}")'))
return out
async def failing_since(db, routers: list[dict], now, thresholds: dict) -> None:
"""Sets r["since"] on each failing router from its last day of history,
as the dashboard widget reads it (#5860)."""
failing = [r["name"] for r in routers if r["state"] == "crit"]
if not failing:
return
since = now - page.WIDGET_SINCE
history = await page.router_history(db, failing, since, bucket_seconds(since))
for r in routers:
if r["name"] in history:
r["since"], _before = page.problem_since(history[r["name"]], thresholds)
async def mcp_overview(db) -> list[dict]:
thresholds, scrape = _cfg()
now = now_utc()
current, _gone = await _graded(db, now, thresholds, scrape)
await failing_since(db, current, now, thresholds)
served, _old = page.current_certs((await db.execute(select(TraefikCert))).scalars().all(), scrape)
return router_findings(current) + cert_findings(served, now, thresholds)
+8
View File
@@ -37,6 +37,14 @@ def test_only_a_failing_router_is_an_overview_finding():
[f] = t.router_findings(routers)
assert f["severity"] == "critical" and f["subject"] == "app@swarm"
assert "Mostly 502" in f["summary"] and f["next"] == 'get_router(router="app@swarm")'
assert f["since"] is None
def test_a_failing_router_says_since_when():
began = NOW - timedelta(hours=2)
[f] = t.router_findings([{"name": "app@swarm", "state": "crit", "why": "12.0% of responses are 5xx",
"since": began}])
assert f["since"] == "2026-10-15T10:00:00Z" and f["age_s"] > 0
def test_a_router_is_found_with_or_without_its_provider():