milestone 27: diagnostics — operational failures land in one curated log, resolved history purgeable
Gates / frontend (push) Successful in 1m33s
Gates / test (push) Successful in 1m48s
Gates / test-aarch64 (push) Successful in 7m10s
Gates / package (push) Successful in 5m31s
Gates / container (push) Successful in 15s
CI / gates (push) Successful in 14m51s

This commit is contained in:
2026-08-20 20:05:59 +02:00
parent 3dd8214ef2
commit 037f209179
50 changed files with 8608 additions and 102 deletions
+222 -1
View File
@@ -256,6 +256,131 @@ paths:
"503":
$ref: "#/components/responses/Unavailable"
/api/diagnostics:
get:
summary: Operational event log
description: |
Failure episodes, newest first. One event is one subject failing
continuously: it opens on the first failure, counts repeats in
`occurrences`, and gets a `resolved_at` when the subject recovers. A
subject that fails again opens a new event rather than reopening the
old one. Keyset pagination — follow `next_before` until it is null.
parameters:
- name: state
in: query
schema: { type: string, enum: [active, resolved, all], default: all }
- name: severity
in: query
schema: { type: string, enum: [warning, error] }
- name: component
in: query
description: Matches the part of `code` before the dot, exactly.
schema: { type: string, maxLength: 64 }
- name: since
in: query
description: |
Unix seconds. With `until`, selects episodes overlapping the
window; an episode resolved exactly at `since` does not overlap.
schema: { type: integer }
- name: until
in: query
description: Unix seconds, exclusive.
schema: { type: integer }
- name: limit
in: query
schema: { type: integer, minimum: 1, maximum: 1000, default: 100 }
- name: before
in: query
description: Return events with id strictly below this cursor.
schema: { type: integer, minimum: 1 }
responses:
"200":
description: One page.
content:
application/json:
schema:
$ref: "#/components/schemas/DiagnosticsPage"
"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"
delete:
summary: Purge every resolved event
description: |
Deletes the resolved history and answers with how many rows went.
Active events are never touched, so clearing the page cannot lose an
episode that is still failing. Resolution stays automatic; this only
decides when the history disappears. A runtime action, served in file
mode too — the event log is not configuration.
responses:
"200":
description: How many resolved events were removed.
content:
application/json:
schema:
$ref: "#/components/schemas/DiagnosticsPurge"
"401":
$ref: "#/components/responses/Unauthorized"
"429":
$ref: "#/components/responses/RateLimited"
"500":
$ref: "#/components/responses/Internal"
"503":
$ref: "#/components/responses/Unavailable"
/api/diagnostics/{id}:
parameters:
- $ref: "#/components/parameters/RowId"
get:
summary: One operational event
description: |
404 for an id that never existed and for one retention has removed —
the API does not distinguish them.
responses:
"200":
description: The event.
content:
application/json:
schema:
$ref: "#/components/schemas/DiagnosticEvent"
"401":
$ref: "#/components/responses/Unauthorized"
"404":
$ref: "#/components/responses/NotFound"
"429":
$ref: "#/components/responses/RateLimited"
"500":
$ref: "#/components/responses/Internal"
"503":
$ref: "#/components/responses/Unavailable"
delete:
summary: Purge one resolved event
description: |
Deletes a resolved event. An event that is still active answers 409 —
an open episode is the current state of the box, not history — and an
id no row holds answers 404. A runtime action, served in file mode too.
responses:
"204":
description: Purged.
"401":
$ref: "#/components/responses/Unauthorized"
"404":
$ref: "#/components/responses/NotFound"
"409":
$ref: "#/components/responses/Conflict"
"429":
$ref: "#/components/responses/RateLimited"
"500":
$ref: "#/components/responses/Internal"
"503":
$ref: "#/components/responses/Unavailable"
/api/stats:
get:
summary: Totals for a period
@@ -1664,11 +1789,21 @@ components:
Health:
type: object
required: [status, disk, upstreams, queries_dropped, writer_failed, refreshes_gated, snapshot_generation]
required: [status, disk, upstreams, diagnostics, queries_dropped, writer_failed, refreshes_gated, snapshot_generation]
properties:
status:
type: string
enum: [ok, degraded]
diagnostics:
type: object
required: [state, active_warnings, active_errors]
properties:
state:
type: string
enum: [recording, unavailable]
description: unavailable when the event store failed to open or its writes are failing; either state degrades health.
active_warnings: { type: integer }
active_errors: { type: integer }
disk:
type: object
required: [state, free_bytes, db_bytes, log_bytes, sample_failures]
@@ -1760,6 +1895,92 @@ components:
nullable: true
description: Cursor for the next page; null on the last page.
DiagnosticEvent:
type: object
required: [id, code, component, subject, severity, first_seen, last_seen, occurrences, resolved_at, detail]
properties:
id: { type: integer }
code:
type: string
description: |
The failure kind, as `component.name`. One of a fixed set of
fifteen; new codes are added with new releases.
enum:
- disk.space
- disk.probe
- blocklist.refresh
- blocklist.snapshot
- blocklist.storage
- certificate.reload
- query_log.write
- query_log.maintenance
- query_log.recreated
- upstream_history.write
- upstream.exchange
- client_names.storage
- clients.storage
- listener.start
- configuration.load
component:
type: string
description: The part of `code` before the dot, repeated for filtering.
subject:
type: string
description: |
What failed, as a display name: a blocklist source name, an
endpoint, an operation. Redacted where it derives from a url; the
store's internal identity for the subject is never exposed.
severity:
type: string
enum: [warning, error]
first_seen:
type: integer
description: When this episode opened, unix seconds.
last_seen:
type: integer
description: The most recent failure of this episode, unix seconds.
occurrences:
type: integer
description: How many failures this episode has held; at least 1.
resolved_at:
type: integer
nullable: true
description: |
When the subject recovered, unix seconds. Null while the episode is
still open. A subject that fails again opens a new event rather than
reopening this one.
detail:
type: string
description: The last error of this episode, truncated to 512 bytes.
DiagnosticsPage:
type: object
required: [events, next_before, active]
properties:
events:
type: array
items:
$ref: "#/components/schemas/DiagnosticEvent"
next_before:
type: integer
nullable: true
description: Cursor for the next page; null on the last page.
active:
type: object
required: [warnings, errors]
description: Episodes open right now, whatever this page filtered to.
properties:
warnings: { type: integer }
errors: { type: integer }
DiagnosticsPurge:
type: object
required: [purged]
properties:
purged:
type: integer
description: How many resolved events the purge removed; zero when there were none.
StatsTotals:
type: object
required: [period, since, until, queries, blocked, cached, clients, avg_response_time_us]