feat(mcp): Traefik routers, certificates and traffic (#5838)
list_routers grades every router as the Traefik page does (the last 15 minutes, 5xx on its cutoff, 4xx and p95 against the router's normal) and gives a troubled one its status codes, what they mean and the swarm service behind it. get_router adds its history, codes, normal and anomalies; list_certificates the served certificates and the routers they cover; get_traffic the access log's clients, countries and routers. get_overview names a router answering 5xx and a certificate near expiry. The access-log aggregation moves into routes.summarize_access, shared by the traffic page and get_traffic. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+38
-45
@@ -709,6 +709,42 @@ async def widget_request_chart():
|
||||
)
|
||||
|
||||
|
||||
def summarize_access(rows, top: int = 10) -> dict:
|
||||
"""The access-log summaries in a range added up: totals, the internal and
|
||||
external split, and the top client IPs, countries and routers."""
|
||||
total = sum(r.total_requests for r in rows)
|
||||
external = sum(r.external_requests for r in rows)
|
||||
ip_counts: dict = defaultdict(lambda: {"count": 0, "internal": True, "country": None})
|
||||
country_counts: dict[str, int] = defaultdict(int)
|
||||
router_counts: dict[str, int] = defaultdict(int)
|
||||
for row in rows:
|
||||
for entry in json.loads(row.top_ips_json):
|
||||
k = entry["ip"]
|
||||
ip_counts[k]["count"] += entry["count"]
|
||||
ip_counts[k]["internal"] = entry["internal"]
|
||||
ip_counts[k]["country"] = entry.get("country")
|
||||
for entry in json.loads(row.top_countries_json):
|
||||
country_counts[entry["country"]] += entry["count"]
|
||||
for entry in json.loads(row.top_routers_json):
|
||||
router_counts[entry["router"]] += entry["count"]
|
||||
|
||||
def ranked(counts: dict, label: str) -> list[dict]:
|
||||
return sorted(({label: k, "count": v} for k, v in counts.items()),
|
||||
key=lambda x: x["count"], reverse=True)[:top]
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"internal": sum(r.internal_requests for r in rows),
|
||||
"external": external,
|
||||
"total_bytes": sum(r.total_bytes for r in rows),
|
||||
"external_pct": (external / total * 100.0) if total else 0.0,
|
||||
"top_ips": sorted(({"ip": k, **v} for k, v in ip_counts.items()),
|
||||
key=lambda x: x["count"], reverse=True)[:top],
|
||||
"top_countries": ranked(country_counts, "country"),
|
||||
"top_routers": ranked(router_counts, "router"),
|
||||
}
|
||||
|
||||
|
||||
@traefik_bp.get("/traffic")
|
||||
@require_role(UserRole.viewer)
|
||||
async def traffic():
|
||||
@@ -732,38 +768,7 @@ async def traffic():
|
||||
access_log_enabled=bool(al.get("enabled", False)), access_log_path=al.get("path", ""),
|
||||
)
|
||||
|
||||
total = sum(r.total_requests for r in rows)
|
||||
internal = sum(r.internal_requests for r in rows)
|
||||
external = sum(r.external_requests for r in rows)
|
||||
total_bytes = sum(r.total_bytes for r in rows)
|
||||
|
||||
ip_counts: dict = defaultdict(lambda: {"count": 0, "internal": True, "country": None})
|
||||
country_counts: dict[str, int] = defaultdict(int)
|
||||
router_counts: dict[str, int] = defaultdict(int)
|
||||
|
||||
for row in rows:
|
||||
for entry in json.loads(row.top_ips_json):
|
||||
k = entry["ip"]
|
||||
ip_counts[k]["count"] += entry["count"]
|
||||
ip_counts[k]["internal"] = entry["internal"]
|
||||
ip_counts[k]["country"] = entry.get("country")
|
||||
for entry in json.loads(row.top_countries_json):
|
||||
country_counts[entry["country"]] += entry["count"]
|
||||
for entry in json.loads(row.top_routers_json):
|
||||
router_counts[entry["router"]] += entry["count"]
|
||||
|
||||
top_ips = sorted(
|
||||
[{"ip": k, **v} for k, v in ip_counts.items()],
|
||||
key=lambda x: x["count"], reverse=True
|
||||
)[:10]
|
||||
top_countries = sorted(
|
||||
[{"country": k, "count": v} for k, v in country_counts.items()],
|
||||
key=lambda x: x["count"], reverse=True
|
||||
)[:10]
|
||||
top_routers = sorted(
|
||||
[{"router": k, "count": v} for k, v in router_counts.items()],
|
||||
key=lambda x: x["count"], reverse=True
|
||||
)[:10]
|
||||
summary = summarize_access(rows)
|
||||
|
||||
history = [
|
||||
{
|
||||
@@ -774,19 +779,7 @@ async def traffic():
|
||||
for r in rows
|
||||
]
|
||||
|
||||
summary = {
|
||||
"total": total,
|
||||
"internal": internal,
|
||||
"external": external,
|
||||
"total_bytes": total_bytes,
|
||||
"external_pct": (external / total * 100.0) if total else 0.0,
|
||||
"top_ips": top_ips,
|
||||
"top_countries": top_countries,
|
||||
"top_routers": top_routers,
|
||||
"sparkline_external": _sparkline(
|
||||
[h["external_pct"] for h in subsample(history, 80)]
|
||||
),
|
||||
}
|
||||
summary["sparkline_external"] = _sparkline([h["external_pct"] for h in subsample(history, 80)])
|
||||
|
||||
return await render_template(
|
||||
"traefik/traffic.html", summary=summary, history=history, range_key=range_key
|
||||
|
||||
Reference in New Issue
Block a user