fix(traefik): the rows page reads every router's history in one index-only query (#5355)
#5355 "Traefik page doesn't load": /plugins/traefik/rows took 12-28 s. It ran a query per router ever seen, and each raw row in the range was a heap fetch. traefik_004 rebuilds the router index to cover the charted columns and indexes scraped_at for the request chart; router_history reads the current routers' buckets in one query. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+41
-38
@@ -180,44 +180,21 @@ async def rows():
|
||||
now = datetime.now(timezone.utc)
|
||||
thresholds = current_app.config.get("THRESHOLDS", {})
|
||||
|
||||
scrape = current_app.config["PLUGINS"]["traefik"]["scrape_interval_seconds"]
|
||||
|
||||
async with current_app.db_sessionmaker() as db:
|
||||
# Every known router, with its most recent raw row
|
||||
newest = {m.router_name: m for m in await latest_per_router(db)}
|
||||
routers = sorted(newest)
|
||||
# Every known router, with its most recent raw row; a router Traefik
|
||||
# stopped reporting gets no sparklines, so its history is not read.
|
||||
newest = await latest_per_router(db)
|
||||
current, gone = gone_routers([{"name": m.router_name, "latest": m} for m in newest], scrape)
|
||||
|
||||
b_secs = bucket_seconds(since)
|
||||
bucket_col = _time_bucket(TraefikMetric.scraped_at, b_secs)
|
||||
|
||||
router_data = []
|
||||
for router in routers:
|
||||
# Bucketed history for sparklines — always ~80 points regardless of range
|
||||
result = await db.execute(
|
||||
select(
|
||||
func.avg(TraefikMetric.request_rate).label("request_rate"),
|
||||
func.avg(TraefikMetric.latency_p95_ms).label("latency_p95_ms"),
|
||||
func.avg(TraefikMetric.error_rate_5xx_pct).label("error_rate_5xx_pct"),
|
||||
func.min(TraefikMetric.scraped_at).label("scraped_at"),
|
||||
bucket_col,
|
||||
)
|
||||
.where(TraefikMetric.router_name == router)
|
||||
.where(TraefikMetric.scraped_at >= since)
|
||||
.group_by(bucket_col)
|
||||
.order_by(bucket_col)
|
||||
)
|
||||
history = result.all()
|
||||
|
||||
latest = newest[router]
|
||||
|
||||
state, why = router_state(latest, thresholds)
|
||||
router_data.append({
|
||||
"name": router,
|
||||
"latest": latest,
|
||||
"state": state,
|
||||
"why": why,
|
||||
"sparkline_req": _sparkline([r.request_rate for r in history]),
|
||||
"sparkline_p95": _sparkline([r.latency_p95_ms for r in history]),
|
||||
"sparkline_5xx": _sparkline([r.error_rate_5xx_pct for r in history]),
|
||||
})
|
||||
history = await router_history(db, [r["name"] for r in current], since, bucket_seconds(since))
|
||||
for r in current:
|
||||
points = history[r["name"]]
|
||||
r["state"], r["why"] = router_state(r["latest"], thresholds)
|
||||
r["sparkline_req"] = _sparkline([p.request_rate for p in points])
|
||||
r["sparkline_p95"] = _sparkline([p.latency_p95_ms for p in points])
|
||||
r["sparkline_5xx"] = _sparkline([p.error_rate_5xx_pct for p in points])
|
||||
|
||||
# Latest global stat (always most recent, not range-filtered)
|
||||
result = await db.execute(
|
||||
@@ -233,8 +210,6 @@ async def rows():
|
||||
)
|
||||
raw_certs = result.scalars().all()
|
||||
|
||||
scrape = current_app.config["PLUGINS"]["traefik"]["scrape_interval_seconds"]
|
||||
current, gone = gone_routers(router_data, scrape)
|
||||
served, old = current_certs(raw_certs, scrape)
|
||||
domains = router_domains()
|
||||
|
||||
@@ -382,6 +357,34 @@ def _time_bucket(column, seconds: int):
|
||||
func.to_timestamp(0)).label("bucket")
|
||||
|
||||
|
||||
async def router_history(db, names: list[str], since, bucket_secs: int) -> dict[str, list]:
|
||||
"""{router: [rows of request_rate, latency_p95_ms, error_rate_5xx_pct]}
|
||||
per `bucket_secs` window since `since`, oldest first: one query for every
|
||||
router, answered from the covering (router_name, scraped_at) index
|
||||
(traefik_004). It was a query per router, each row a heap read, which
|
||||
took the rows page to 12-28 s."""
|
||||
history: dict[str, list] = {n: [] for n in names}
|
||||
if not history:
|
||||
return history
|
||||
bucket_col = _time_bucket(TraefikMetric.scraped_at, bucket_secs)
|
||||
result = await db.execute(
|
||||
select(
|
||||
TraefikMetric.router_name,
|
||||
func.avg(TraefikMetric.request_rate).label("request_rate"),
|
||||
func.avg(TraefikMetric.latency_p95_ms).label("latency_p95_ms"),
|
||||
func.avg(TraefikMetric.error_rate_5xx_pct).label("error_rate_5xx_pct"),
|
||||
bucket_col,
|
||||
)
|
||||
.where(TraefikMetric.router_name.in_(names))
|
||||
.where(TraefikMetric.scraped_at >= since)
|
||||
.group_by(TraefikMetric.router_name, bucket_col)
|
||||
.order_by(TraefikMetric.router_name, bucket_col)
|
||||
)
|
||||
for row in result.all():
|
||||
history[row.router_name].append(row)
|
||||
return history
|
||||
|
||||
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user