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 import anomalies as anom
from steward.core.norms import norms_at 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._context import mcp_app
from steward.mcp.tools._shape import ( from steward.mcp.tools._shape import (
age_s, bounded, clamp_limit, db_session, iso, lookback, now_utc, rnd, summarize, 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]: def router_findings(routers: list[dict]) -> list[dict]:
"""A router answering 5xx past its cutoff: failing. 4xx and latency are """A router answering 5xx past its cutoff: failing. 4xx and latency are
left to the metric anomalies, which judge them against the router's 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 = [] out = []
for r in routers: for r in routers:
if r["state"] != "crit": if r["state"] != "crit":
@@ -287,14 +289,29 @@ def router_findings(routers: list[dict]) -> list[dict]:
said = (r.get("meaning") or {}).get("meaning") said = (r.get("meaning") or {}).get("meaning")
out.append(finding("critical", "traefik_router_failing", r["name"], out.append(finding("critical", "traefik_router_failing", r["name"],
f"Traefik router {r['name']}: {r['why']}" + (f". {said}" if said else ""), 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 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]: async def mcp_overview(db) -> list[dict]:
thresholds, scrape = _cfg() thresholds, scrape = _cfg()
now = now_utc() now = now_utc()
current, _gone = await _graded(db, now, thresholds, scrape) 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) served, _old = page.current_certs((await db.execute(select(TraefikCert))).scalars().all(), scrape)
return router_findings(current) + cert_findings(served, now, thresholds) 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) [f] = t.router_findings(routers)
assert f["severity"] == "critical" and f["subject"] == "app@swarm" assert f["severity"] == "critical" and f["subject"] == "app@swarm"
assert "Mostly 502" in f["summary"] and f["next"] == 'get_router(router="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(): def test_a_router_is_found_with_or_without_its_provider():