Skip to content

Metrics: SQLite-persisted history by default, runtime-switchable backend (ADR-0013) - #28

Merged
swimmesberger merged 1 commit into
mainfrom
wt/cpu-graphs-zero-issue-656b6a
Aug 10, 2026
Merged

Metrics: SQLite-persisted history by default, runtime-switchable backend (ADR-0013)#28
swimmesberger merged 1 commit into
mainfrom
wt/cpu-graphs-zero-issue-656b6a

Conversation

@swimmesberger

Copy link
Copy Markdown
Owner

What

Metrics history is now on by default with zero dependencies, and the metrics backend is switchable at runtime from Settings → Metrics — no restart, no compose edits.

Backend Behaviour
sqlite (new default) live ring as before + windowed history persisted into watchtower.db (default 30 days, configurable 1–365)
memory (opt-in) the old default: live-only ring, nothing written
influxdb (opt-in, BYO) unchanged reader over an externally-collected InfluxDB

Why

Two lessons from operating the ADR-0007 split (in-memory default, Influx opt-in):

  1. The external-collector contract is fragile. docker_stats renamed its CPU metric across collector releases; a bucket filled by the "wrong" version renders every CPU graph as a silent, plausible flat 0 while memory works (the incident that started this branch).
  2. History shouldn't cost an InfluxDB. For Watchtower's single-node targets, "go back to when the incident happened" is a core feature, not something worth a few hundred MB of idle RAM and a collector fleet.

ADR-0013 records the reversal of ADR-0007's "no SQLite persistence" rejection: that rejection assumed persisting the raw 10s firehose. This stores 1-minute averages kept ~72h + 10-minute rollups kept the retention window — a few hundred thousand small rows / tens of MB at default settings, trivially inside SQLite's comfort zone.

How

  • Write pathMetricsPersistenceService: accumulates sampler ticks in memory, flushes one row per series per minute; rollup + both deletion windows ride the sampler loop (no new job infra). Offline containers and unreadable host /proc persist as gaps, not zeros.
  • Read pathSqliteMetricsSource: live window from the ring; historical ranges bucketed in SQL from whichever tier covers the range. Container identity in history is the container name (stable across recreation, same convention as the Influx reader).
  • Runtime switchIMetricsSource is now MetricsSourceRouter, resolving the backend from IOptionsMonitor per call. metrics.updateConfig persists Watchtower:Metrics:* through the settings store (the same live-rebind mechanism as the automation toggles). The sampler re-checks the backend per tick and idles entirely under influxdb, preserving the single-collector invariant behaviourally. The Influx reader is built lazily from the current options, rebuilt on change, and degrades to influx-misconfigured instead of faulting DI.
  • Frontend — new Metrics card on the Settings page (backend select, retention, Influx connection with masked token — the token never travels back to the client). The metrics-history flag is evaluated per session fetch and follows the switch; a save that flips it reloads the page to rebuild the boot-frozen contribution registry (upstream reactive-capabilities enhancement filed as Elarion #137).
  • Original bug made observable — the Influx reader now logs a warning when container rows carry memory but no container.cpu.utilization values (the collector-version drift signature), and the docs gained a pinned-collector warning box with the explicit metrics: enable snippet.

Reviewer notes

  • Default change: existing deployments that never set Metrics:Backend start persisting history on upgrade (deliberate; compose comments + README document it, memory restores the old behaviour). Migration AddMetricsHistory adds the two sample tables.
  • The rollup pass is bounded per run (MaxRollupBucketsPerPass) — a large backlog converges over successive hourly passes; the tests exercise this with two passes.
  • metrics.updateConfig validates backend/retention/Influx connection before writing, so a bad save can't strand the dashboard; a null token keeps the stored one.
  • ADR-0007's "exactly one implementation registered / restart required" wording is superseded; all ~8 doc-comment sites repeating it are swept in this PR.

Testing

  • 483/483 tests pass, including 8 new ones: minute aggregation + boundary flush, gaps-not-zeros, rollup + both retention windows, step-bucketed history queries, tier selection past the raw window, router switching + misconfigured-Influx degradation, config-handler validation + token masking.
  • Verified live in the dev stack: fresh instance → History nav present with zero config; RPC switch to memorymetrics-history flag false on the next session fetch, nav gone, direct URL shows the "Enable in Settings" banner; switch back → restored. No restarts.

…e backend (ADR-0013)

Metrics history no longer requires an external InfluxDB stack. Three
backends now sit behind one runtime router: sqlite (new default) persists
windowed history into watchtower.db next to the live ring, memory keeps
the old live-only behaviour, and influxdb stays as the bring-your-own
reader for existing observability stacks.

Storage is two-tier and deliberately not a raw-sample TSDB: 1-minute
averages of the 10s sampler ticks kept ~72h, 10-minute rollups kept
Metrics:RetentionDays (default 30, clamped 1-365). Rollup and windowed
deletion ride the sampler loop; historical queries bucket either tier
into the requested step in SQL. Offline containers and unavailable host
reads persist as gaps, never as zeros.

The backend is switchable at runtime from Settings -> Metrics: the new
metrics.getConfig/updateConfig handlers persist Watchtower:Metrics:*
through the settings store (which layers over env config and re-binds
IOptionsMonitor live), the router resolves the backend per call, and the
sampler re-checks it per tick - idling entirely under influxdb so exactly
one collector runs. A misconfigured influx setup now degrades to
unavailable (influx-misconfigured) instead of faulting DI, and the reader
logs a warning when rows carry memory but no container.cpu.utilization -
the collector-version drift that silently renders every CPU graph as 0.

The metrics-history client flag follows the switch (evaluated per session
fetch); the Settings page reloads when a save flips it so the boot-frozen
contribution registry rebuilds (upstream fix filed as Elarion#137).

ADR-0013 supersedes ADR-0007's "no SQLite persistence" and
"restart required" decisions; docs and stale comments swept.
@swimmesberger
swimmesberger merged commit 7fe3ee5 into main Aug 10, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant