overview: one endpoint, live projections and a response cache (m36)

This commit is contained in:
2026-08-27 17:48:20 +02:00
parent 7cdb74ec17
commit c8470724ce
40 changed files with 3298 additions and 2242 deletions
+70 -201
View File
@@ -422,139 +422,29 @@ paths:
"503":
$ref: "#/components/responses/Unavailable"
/api/stats:
/api/overview:
get:
summary: Totals for a period
parameters:
- $ref: "#/components/parameters/Period"
responses:
"200":
description: Totals over the period's UTC-aligned window.
content:
application/json:
schema:
$ref: "#/components/schemas/StatsTotals"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"429":
$ref: "#/components/responses/RateLimited"
"500":
$ref: "#/components/responses/Internal"
"503":
$ref: "#/components/responses/Unavailable"
/api/stats/timeseries:
get:
summary: Bucketed counts for a period
summary: Everything the Overview page draws, for one period
description: |
Fixed-width UTC buckets covering the same window `/api/stats`
reports for the period: 1h into 60 one-minute buckets, 24h into 48
half-hour buckets, 7d into 168 one-hour buckets, 30d into 120
six-hour buckets. Empty buckets are zero-filled.
parameters:
- $ref: "#/components/parameters/Period"
responses:
"200":
description: The bucket series.
content:
application/json:
schema:
$ref: "#/components/schemas/StatsTimeseries"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"429":
$ref: "#/components/responses/RateLimited"
"500":
$ref: "#/components/responses/Internal"
"503":
$ref: "#/components/responses/Unavailable"
One response over one read transaction: the period's totals, its
fixed-width UTC buckets, the per-client series, the query-type
breakdown and the answering-route breakdown, plus the coverage
watermark judged against the same window. The panels therefore describe
one database state rather than five, so the breakdowns sum to the
totals on a quiet box.
/api/stats/types:
get:
summary: Query-type breakdown for a period
description: |
How many queries of each DNS type the period's window holds, over the
same UTC-aligned window `/api/stats` reports for. Rows carry the numeric
type only: the type-name table lives in the admin, and a second copy
here would drift out of agreement with it. `qtype` is nullable in the
query log, so the rows that carry no type group into a row of their own
rather than vanishing from a breakdown that claims to add up. Ordered by
count descending, then type ascending with the null row last. Types
absent from the window are absent from the list.
Buckets are UTC-aligned and zero-filled: 1h into 60 one-minute buckets,
24h into 48 half-hour buckets, 7d into 168 one-hour buckets, 30d into
120 six-hour buckets. The last bucket is the one in progress.
parameters:
- $ref: "#/components/parameters/Period"
responses:
"200":
description: The type breakdown.
description: The period's overview.
content:
application/json:
schema:
$ref: "#/components/schemas/StatsTypes"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"429":
$ref: "#/components/responses/RateLimited"
"500":
$ref: "#/components/responses/Internal"
"503":
$ref: "#/components/responses/Unavailable"
/api/stats/routes:
get:
summary: How the period's queries were answered
description: |
A breakdown by answering route over the same window `/api/stats`
reports for. `source` is the answering resolver's identity — the
upstream url on `upstream` rows, the zone on `forward_zone` rows, null
on every other kind and on rows whose identity the log did not record.
It is not the blocklist a block came from. Ordered by count descending,
then route ascending, then source ascending with nulls last.
parameters:
- $ref: "#/components/parameters/Period"
responses:
"200":
description: The route breakdown.
content:
application/json:
schema:
$ref: "#/components/schemas/StatsRoutes"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"429":
$ref: "#/components/responses/RateLimited"
"500":
$ref: "#/components/responses/Internal"
"503":
$ref: "#/components/responses/Unavailable"
/api/stats/clients:
get:
summary: Per-client bucketed counts for a period
description: |
One zero-filled series per client, bucketed exactly like
`/api/stats/timeseries` so the two charts share an x-axis. The eight
clients with the most queries in the window are named, ranked by count
descending then address ascending; every other client sums into
`other`, which is always present and always holds one entry per bucket
in the window — including when `clients` is empty, when no client fell
outside the named eight, and when the window holds no queries at all.
parameters:
- $ref: "#/components/parameters/Period"
responses:
"200":
description: The per-client series.
content:
application/json:
schema:
$ref: "#/components/schemas/StatsClients"
$ref: "#/components/schemas/Overview"
"400":
$ref: "#/components/responses/BadRequest"
"401":
@@ -2316,31 +2206,6 @@ components:
type: integer
description: How many resolved events the purge removed; zero when there were none.
StatsTotals:
type: object
required: [period, since, until, queries, blocked, clients, avg_response_time_us, coverage]
properties:
period:
type: string
enum: [1h, 24h, 7d, 30d]
since:
type: integer
description: Window start, unix seconds, inclusive.
until:
type: integer
description: Window end, unix seconds, exclusive.
queries: { type: integer }
blocked: { type: integer }
clients:
type: integer
description: Distinct client addresses in the window.
avg_response_time_us:
type: integer
nullable: true
description: Null when no query in the window recorded a time.
coverage:
$ref: "#/components/schemas/Coverage"
Bucket:
type: object
required: [ts, queries, blocked, cached]
@@ -2352,23 +2217,6 @@ components:
blocked: { type: integer }
cached: { type: integer }
StatsTimeseries:
type: object
required: [period, since, until, bucket_seconds, buckets, coverage]
properties:
period:
type: string
enum: [1h, 24h, 7d, 30d]
since: { type: integer }
until: { type: integer }
bucket_seconds: { type: integer }
buckets:
type: array
items:
$ref: "#/components/schemas/Bucket"
coverage:
$ref: "#/components/schemas/Coverage"
TypeCount:
type: object
required: [qtype, count]
@@ -2381,22 +2229,6 @@ components:
recorded no type, not an absent row.
count: { type: integer }
StatsTypes:
type: object
required: [period, since, until, types, coverage]
properties:
period:
type: string
enum: [1h, 24h, 7d, 30d]
since: { type: integer }
until: { type: integer }
types:
type: array
items:
$ref: "#/components/schemas/TypeCount"
coverage:
$ref: "#/components/schemas/Coverage"
RouteCount:
type: object
required: [route, source, count]
@@ -2412,22 +2244,6 @@ components:
row recorded no identity.
count: { type: integer }
StatsRoutes:
type: object
required: [period, since, until, routes, coverage]
properties:
period:
type: string
enum: [1h, 24h, 7d, 30d]
since: { type: integer }
until: { type: integer }
routes:
type: array
items:
$ref: "#/components/schemas/RouteCount"
coverage:
$ref: "#/components/schemas/Coverage"
ClientSeries:
type: object
required: [client, buckets]
@@ -2443,18 +2259,47 @@ components:
items:
type: integer
StatsClients:
OverviewTotals:
type: object
required: [period, since, until, bucket_seconds, clients, other, coverage]
required: [queries, blocked, clients, avg_response_time_us]
properties:
queries: { type: integer }
blocked: { type: integer }
clients:
type: integer
description: Distinct client addresses in the window.
avg_response_time_us:
type: integer
nullable: true
description: Null when no query in the window recorded a time.
Overview:
type: object
required: [period, since, until, bucket_seconds, totals, buckets, clients, other, types, routes, coverage]
properties:
period:
type: string
enum: [1h, 24h, 7d, 30d]
since: { type: integer }
until: { type: integer }
since:
type: integer
description: Window start, unix seconds, inclusive.
until:
type: integer
description: Window end, unix seconds, exclusive.
bucket_seconds: { type: integer }
totals:
$ref: "#/components/schemas/OverviewTotals"
buckets:
type: array
description: One entry per bucket in the window, zero-filled.
items:
$ref: "#/components/schemas/Bucket"
clients:
type: array
description: |
The eight clients with the most queries in the window, ranked by
count descending then address ascending. Every other client sums
into `other`.
items:
$ref: "#/components/schemas/ClientSeries"
other:
@@ -2466,6 +2311,30 @@ components:
eight, and when the window holds no queries at all.
items:
type: integer
types:
type: array
description: |
How many queries of each DNS type the window holds. Rows carry the
numeric type only: the type-name table lives in the admin, and a
second copy here would drift out of agreement with it. `qtype` is
nullable in the query log, so the rows that carry no type group
into a row of their own rather than vanishing from a breakdown that
claims to add up. Ordered by count descending, then type ascending
with the null row last. Types absent from the window are absent
from the list.
items:
$ref: "#/components/schemas/TypeCount"
routes:
type: array
description: |
A breakdown by answering route. `source` is the answering
resolver's identity — the upstream url on `upstream` rows, the zone
on `forward_zone` rows, null on every other kind and on rows whose
identity the log did not record. It is not the blocklist a block
came from. Ordered by count descending, then route ascending, then
source ascending with nulls last.
items:
$ref: "#/components/schemas/RouteCount"
coverage:
$ref: "#/components/schemas/Coverage"