diff --git a/plugins/traefik/routes.py b/plugins/traefik/routes.py index ed26933..92520f4 100644 --- a/plugins/traefik/routes.py +++ b/plugins/traefik/routes.py @@ -375,38 +375,57 @@ async def widget_access_log(): summary=summary, widget_id=widget_id) +async def request_history(db, since, bucket_seconds: int) -> list[dict]: + """[{"at", "req_rate", "err_pct"}] per time bucket since `since`: each + scrape summed over routers (the total req/s), averaged over the bucket; + the 5xx share weighted by each router's traffic, so a quiet router's + errors don't count as much as a busy one's.""" + per_scrape = ( + select( + TraefikMetric.scraped_at.label("at"), + func.sum(TraefikMetric.request_rate).label("req"), + func.sum(TraefikMetric.request_rate * TraefikMetric.error_rate_5xx_pct).label("err_weighted"), + ) + .where(TraefikMetric.scraped_at >= since) + .group_by(TraefikMetric.scraped_at) + .subquery() + ) + bucket_col = (cast(func.extract("epoch", per_scrape.c.at), Integer) / bucket_seconds).label("bucket") + rows = (await db.execute( + select( + func.avg(per_scrape.c.req).label("req_rate"), + func.sum(per_scrape.c.err_weighted).label("err_weighted"), + func.sum(per_scrape.c.req).label("req_sum"), + func.min(per_scrape.c.at).label("at"), + bucket_col, + ) + .group_by(bucket_col) + .order_by(bucket_col) + )).all() + return [{"at": r.at, "req_rate": round(r.req_rate or 0, 3), + "err_pct": round((r.err_weighted or 0) / r.req_sum, 2) if r.req_sum else 0.0} + for r in rows] + + @traefik_bp.get("/widget/request_chart") @require_role(UserRole.viewer) async def widget_request_chart(): - """HTMX dashboard widget: Chart.js request rate + error % over time.""" - from datetime import timedelta + """HTMX dashboard widget: requests per second across all routers, and the + share of them answered 5xx, over the last few hours (#5256). Each scrape + is summed over routers first (the total), then averaged per bucket; the + 5xx share is weighted by each router's traffic, not averaged per router.""" hours = max(1, min(24, int(request.args.get("hours", 6) or 6))) widget_id = request.args.get("wid", "0") since = datetime.now(timezone.utc) - timedelta(hours=hours) b_secs = max(60, (hours * 3600) // 80) # ~80 buckets - bucket_col = ( - cast(func.extract('epoch', TraefikMetric.scraped_at), Integer) / b_secs - ).label("bucket") - async with current_app.db_sessionmaker() as db: - result = await db.execute( - select( - func.avg(TraefikMetric.request_rate).label("req_rate"), - func.avg(TraefikMetric.error_rate_5xx_pct).label("err_pct"), - func.min(TraefikMetric.scraped_at).label("scraped_at"), - bucket_col, - ) - .where(TraefikMetric.scraped_at >= since) - .group_by(bucket_col) - .order_by(bucket_col) - ) - history = result.all() + history = await request_history(db, since, b_secs) - labels = list(range(len(history))) - req_rates = [round(r.req_rate or 0, 3) for r in history] - err_pcts = [round(r.err_pct or 0, 2) for r in history] + labels = [int(r["at"].timestamp() * 1000) for r in history] # epoch ms; the browser shows local time + req_rates = [r["req_rate"] for r in history] + err_pcts = [r["err_pct"] for r in history] return await render_template( "traefik/widget_request_chart.html", @@ -415,6 +434,9 @@ async def widget_request_chart(): error_rate_json=json.dumps(err_pcts), widget_id=widget_id, hours=hours, + now_req=req_rates[-1] if req_rates else None, + peak_req=max(req_rates) if req_rates else None, + peak_err=max(err_pcts) if err_pcts else None, ) diff --git a/plugins/traefik/templates/traefik/widget_request_chart.html b/plugins/traefik/templates/traefik/widget_request_chart.html index 3dee183..d84112c 100644 --- a/plugins/traefik/templates/traefik/widget_request_chart.html +++ b/plugins/traefik/templates/traefik/widget_request_chart.html @@ -1,74 +1,92 @@ -{# traefik/widget_request_chart.html — Chart.js request rate + error % over time #} +{# traefik/widget_request_chart.html — requests per second across all routers + and the share answered 5xx, over the last few hours (#5256). The verdict + carries the figures, so the widget still says something if the chart + can't draw; a chart that fails says why in place of the canvas. #} {% if not labels_json or labels_json == '[]' %} -