fix(mcp): a failing router in get_overview says since when, as the page does (#5860)
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:
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
Reference in New Issue
Block a user