feat(traefik): each router problem says what it means, whether it's real, and what to do (#5712)
#5712 "Each router problem says what it means, whether it's real, and what to do". A problem card now names its kind and reads its codes: - Server errors: 500 means the app is failing; 502/503/504 means Traefik couldn't reach it or it timed out. - Client errors: 404 scanners, a 401/403 login wall, 429 rate limiting, or 499 clients giving up on a slow app. - Slow: p95 against p50. Each card says whether the problem is likely real, and ends with one next step. A fault links to the swarm service behind the router, matched by name, and traffic links to Settings → Thresholds. The wording lives in plugins/traefik/meaning.py. The plugin manager now hands out a loaded plugin's submodule (loaded_plugin_submodule), so Traefik and host_agent reach Docker's models the same way (#5775). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@ from steward.core.formatting import cert_left, status_class
|
||||
from steward.core.norms import norms_at
|
||||
from steward.core.settings import floor_of
|
||||
from steward.models.metrics import PluginMetricLatest
|
||||
from . import meaning
|
||||
from .models import TraefikAccessSummary, TraefikCert, TraefikGlobalStat, TraefikMetric, TraefikStatusCode
|
||||
from .scheduler import router_domains
|
||||
from .scraper import cert_names, covers
|
||||
@@ -183,6 +184,30 @@ def grade_routers(routers: list[dict], window: dict, thresholds: dict, norms: di
|
||||
reading, own = window.get(r["name"]), norms.get(r["name"])
|
||||
r["state"], r["why"] = (router_state(reading, thresholds, STATE_WINDOW_WORDS, own) if reading
|
||||
else router_state(r["latest"], thresholds, norms=own))
|
||||
r["kind"] = meaning.problem_kind(r["state"], r["why"])
|
||||
r["normed"] = bool(own) and {"slow": "latency_p95_ms", "4xx": "error_rate_4xx_pct"}.get(
|
||||
r["kind"], "") in own
|
||||
|
||||
|
||||
async def swarm_service_names(db) -> list[str]:
|
||||
"""Every swarm service the Docker plugin has stored; [] without it."""
|
||||
from steward.core.plugin_manager import loaded_plugin_submodule
|
||||
models = loaded_plugin_submodule("docker", "models")
|
||||
if models is None:
|
||||
return []
|
||||
return list((await db.execute(select(models.DockerSwarmService.service_name).distinct())).scalars())
|
||||
|
||||
|
||||
def explain_problems(problems: list[dict], services: list[str]) -> None:
|
||||
"""Give each problem card what it means and its one next step (#5712):
|
||||
`meaning` (meaning.explain's) and `service`, the swarm service behind
|
||||
the router when Steward can tell which."""
|
||||
for r in problems:
|
||||
r["meaning"] = meaning.explain(r["kind"], r.get("codes", []),
|
||||
getattr(r["latest"], "latency_p50_ms", None),
|
||||
getattr(r["latest"], "latency_p95_ms", None),
|
||||
r.get("normed", False))
|
||||
r["service"] = meaning.swarm_service(r["name"], services)
|
||||
|
||||
|
||||
# A router missing from this many scrapes in a row is no longer reported:
|
||||
@@ -362,6 +387,7 @@ async def rows():
|
||||
# old, a router with none listed may simply predate the recording.
|
||||
first_code = await db.scalar(select(func.min(TraefikStatusCode.scraped_at)))
|
||||
codes_known = first_code is not None and first_code <= now - STATE_WINDOW
|
||||
services = await swarm_service_names(db) if any(r["state"] != "ok" for r in current) else []
|
||||
phases.mark("codes")
|
||||
|
||||
# Latest global stat (always most recent, not range-filtered)
|
||||
@@ -400,6 +426,7 @@ async def rows():
|
||||
certs = [cert_row(c) for c in served]
|
||||
old_certs = [cert_row(c) for c in old]
|
||||
problems, healthy = split_routers(current)
|
||||
explain_problems(problems, services)
|
||||
phases.mark("cert_rows")
|
||||
error_5xx, error_4xx = error_cutoffs(thresholds)
|
||||
page = await render_template(
|
||||
|
||||
Reference in New Issue
Block a user