feat(traefik): Routers widget shows current routers only, one header row, certs on the routers they serve (#5254)
#5254 "Traefik — Routers: current routers only, header row, certs on the router they serve". - Router hosts come from Traefik's API (/api/http|tcp/routers), read best-effort on each scrape. An optional api_url overrides it; the default is /api on the metrics host. A certificate's expiry then shows in a TLS column on the router whose host it covers. An expiring one makes that router a problem, graded by Settings → Thresholds. - Only current: routers and certificates must have appeared in the last three scrapes. Certificates are upserted by serial and never deleted, so renewed or retired ones lingered on the widget. - Widget order: verdict, problem routers with the reason, then the busiest healthy routers under one header (Router / Req/s / Bandwidth / p95 / TLS). A missing value shows "—". - If the API is unreachable, expiring certificates are listed after the routers, not above them. - The page splits certificates into served (with the routers using each, plus SANs) and "No longer served". - Latest row per router: an index walk (recursive CTE + LATERAL) replaces DISTINCT plus a query per router. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,16 @@ logger = logging.getLogger(__name__)
|
||||
_prev_metrics: dict = {}
|
||||
_prev_time: float = 0.0
|
||||
|
||||
# Router name -> host names from Traefik's API, refreshed each scrape (#5254).
|
||||
# None until the API has answered once, and again whenever it stops answering,
|
||||
# so readers can tell "no domains" from "don't know".
|
||||
_router_domains: dict[str, list[str]] | None = None
|
||||
|
||||
|
||||
def router_domains() -> dict[str, list[str]] | None:
|
||||
return _router_domains
|
||||
|
||||
|
||||
# Access log state
|
||||
_geoip_db = None
|
||||
_access_log_last_tick: "datetime | None" = None
|
||||
@@ -38,15 +48,17 @@ def make_scrape_task(app: "Quart") -> ScheduledTask:
|
||||
|
||||
async def _do_scrape(app: "Quart") -> None:
|
||||
"""Fetch Traefik metrics, write to DB, and emit plugin_metrics."""
|
||||
global _prev_metrics, _prev_time
|
||||
global _prev_metrics, _prev_time, _router_domains
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from .models import TraefikCert, TraefikGlobalStat, TraefikMetric
|
||||
from .scraper import (
|
||||
api_base,
|
||||
compute_global_stats,
|
||||
compute_router_metrics,
|
||||
extract_certs,
|
||||
fetch_metrics,
|
||||
fetch_router_domains,
|
||||
)
|
||||
from steward.core.alerts import MetricBatch
|
||||
|
||||
@@ -57,6 +69,13 @@ async def _do_scrape(app: "Quart") -> None:
|
||||
except Exception:
|
||||
logger.exception("Traefik scrape failed (url=%s)", metrics_url)
|
||||
return
|
||||
cfg = app.config["PLUGINS"]["traefik"]
|
||||
try:
|
||||
_router_domains = await fetch_router_domains(api_base(metrics_url, cfg.get("api_url") or ""))
|
||||
except Exception as exc:
|
||||
if _router_domains is not None:
|
||||
logger.warning("Traefik API stopped answering (%s); certificates shown without routers", exc)
|
||||
_router_domains = None
|
||||
|
||||
now = time.monotonic()
|
||||
elapsed = now - _prev_time if _prev_time else 60.0
|
||||
|
||||
Reference in New Issue
Block a user