fix(traefik): bucket history with date_bin; integer-epoch division never grouped scrapes (#5256)
CI 8551 integration: "integer out of range" for a 2099 timestamp. The SQL in the log showed a second problem: `CAST(epoch AS INTEGER) / 60` compiled to `/ CAST(60 AS NUMERIC)`, i.e. decimal division. Every scrape got its own "bucket", so the request chart and the Traefik page's sparklines never averaged anything. The int4 cast would also overflow in 2038. Both now use date_bin from the epoch, as host_agent/metrics_query.py does. The integration test adds a second scrape 30 s later in the same minute and checks that the bucket averages the totals and weights the 5xx share over both scrapes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from quart import Blueprint, current_app, render_template, request
|
||||
from sqlalchemy import Integer, cast, func, select, text
|
||||
from sqlalchemy import func, select, text
|
||||
import json
|
||||
|
||||
from steward.auth.middleware import require_role
|
||||
@@ -186,9 +186,7 @@ async def rows():
|
||||
routers = sorted(newest)
|
||||
|
||||
b_secs = bucket_seconds(since)
|
||||
bucket_col = (
|
||||
cast(func.extract('epoch', TraefikMetric.scraped_at), Integer) / b_secs
|
||||
).label("bucket")
|
||||
bucket_col = _time_bucket(TraefikMetric.scraped_at, b_secs)
|
||||
|
||||
router_data = []
|
||||
for router in routers:
|
||||
@@ -375,6 +373,15 @@ async def widget_access_log():
|
||||
summary=summary, widget_id=widget_id)
|
||||
|
||||
|
||||
def _time_bucket(column, seconds: int):
|
||||
"""The start of the `seconds`-wide window a timestamp falls in, as
|
||||
host_agent/metrics_query.py buckets. Integer epoch / int rendered as
|
||||
numeric division (no two scrapes shared a bucket) and overflowed int4
|
||||
after 2038 (#5256)."""
|
||||
return func.date_bin(func.make_interval(0, 0, 0, 0, 0, 0, int(seconds)), column,
|
||||
func.to_timestamp(0)).label("bucket")
|
||||
|
||||
|
||||
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;
|
||||
@@ -390,7 +397,7 @@ async def request_history(db, since, bucket_seconds: int) -> list[dict]:
|
||||
.group_by(TraefikMetric.scraped_at)
|
||||
.subquery()
|
||||
)
|
||||
bucket_col = (cast(func.extract("epoch", per_scrape.c.at), Integer) / bucket_seconds).label("bucket")
|
||||
bucket_col = _time_bucket(per_scrape.c.at, bucket_seconds)
|
||||
rows = (await db.execute(
|
||||
select(
|
||||
func.avg(per_scrape.c.req).label("req_rate"),
|
||||
|
||||
Reference in New Issue
Block a user