Skip to content

Check stability records (noise-damping stage 2) + the stages 2–4 plan#372

Merged
passcod merged 9 commits into
mainfrom
claude/alerting-noise-damping-plan
Jul 20, 2026
Merged

Check stability records (noise-damping stage 2) + the stages 2–4 plan#372
passcod merged 9 commits into
mainfrom
claude/alerting-noise-damping-plan

Conversation

@passcod

@passcod passcod commented Jul 18, 2026

Copy link
Copy Markdown
Member

🤖 Stacked on #371 (stage 1, the static linger). Implements stage 2 — transition aggregates of the noise-damping plan, plus the plan document itself (docs/plans/alerting-hysteresis.md, covering stages 2–4, the off-the-shelf survey, and the augurs maintenance check). No behaviour hooks into the incident pipeline yet — everything here is recording and read surfaces, immediately useful for analysis on its own.

The stability record (spec: CHK gains a "Stability" section)

Every stamped filing feeds a fixed-size check_stability row, 1:1 with the check state, built from observed results — pre-policy, so operator grading and any damping later built on these numbers can never feed back into them. Skipped observations carry no signal. The row holds:

  • lifetime observation counters (total / degraded);
  • a 32-entry healthy↔degraded transition ring — a stable check remembers far into the past, a flapper remembers only its recent churn, which is exactly the signal;
  • a 168-bucket hour-of-week duty profile (UTC, Monday first) whose bucket counters halve at a cap (512 ≈ two months at minute cadence), so the profile leans towards recent weeks without unbounded growth.

Deliberately not a new append-only log (we just deleted events for being one): the record is a bounded summary per state, updated in place inside the existing filing transaction. Flap statistics — flips in 24h/7d, ring coverage, typical degraded-run and healthy-gap durations — are derived at read time, not stored.

Backfill — designed to not block ingestion, and marked for deletion

A one-shot pass on the monitor pod (not a data migration) replays the last 30 days of status history into the records, so duty profiles and rings are useful from day one. Why not a migration: migrations run concurrently with the live workload, and a single fleet-wide INSERT…SELECT would hold FK row locks (FOR KEY SHARE) on most live issues rows until commit — the filing path's SELECT … FOR UPDATE conflicts with those, so the whole replay would stall ingestion. Instead the pod replays one server per statement (riding the (server_id, created_at) statuses index; row locks held for milliseconds), gated by a completion-marker table plus an advisory lock so concurrent pods don't duplicate the scan; a crash mid-run resumes on the next startup, and rows that already exist are never overwritten. The table-creation migration carries a lock_timeout = 5s guard for the FK's momentary SHARE ROW EXCLUSIVE on issues, failing the migrate run fast instead of queueing ingestion writes behind it.

Everything transitional is tagged TODO(backfill-removal) (replay code, monitor-pod call, its test, marker table) — one grep finds the whole cleanup once every deployment has run it. Approximations vs live recording, noted in the code: only checks present in a push's health array are replayed (recovery-by-omission isn't), and canopy's own checks have no status history so they start cold.

The check detail page (renamed from "attention")

The per-check page at /healthchecks/:source/:check is now the check detail page — it was already the check's home (policy chips, documentation, now stability), and "attention" only describes one part of it. Three sections:

  • About this check — the operator documentation (unchanged);
  • Fleet stability (new) — every target's duty profile summed into one heatmap plus aggregate flap counts ("Across 12 servers, 1 group with a record: …"), so shared load-dependent patterns read at a glance even when no single target stands out;
  • Needs attention — now in the standard list shape used across the UI: rank buckets in display order (unranked last), groups alphabetical within a bucket with the group name as a linked section heading, servers by kind then name, ungrouped trailing. Position no longer encodes urgency — the result chip does. Each row carries the Stability column and expands to the check's data plus the full stability record.

All scopes now show: check_state_for_check returns group- and canopy-scoped states too. A group's own state (backup health and the like) files under its group heading as a whole group row, bucketed by the group's status-page rank; canopy's self-monitoring state gets a trailing canopy section. Both feed the fleet rollup. Server rows gained rank/kind on the wire so the client reuses the shared ordering helpers.

The settings editor at /settings/healthchecks/:check is renamed HealthcheckSettings to free the name; the private endpoint follows (status_check_attentionstatus_check_detail, breaking only for the UI, which is its sole consumer).

MCP

get_check_stability: takes up to 32 (source, check) pairs, optionally narrowed to one server or group, and returns each matching state (all scopes) with its full record and derived stats — the raw material for agents to tell a flap from a load-dependent pattern from a real change.

Tests

check_stability suite: counters/ring/duty recording, skipped-carries-no-signal, observed-not-effective, ring bounding, bucket halving, backfill replay + marker gating + no-overwrite, pure derived-stats. MCP tool covered end-to-end. Playwright coverage for the stability column, per-server heatmap, no-record state, fleet rollup, and the group/canopy sections. Full database (167), private-server (242), statuses, and e2e (144) suites green.

Plan context

docs/plans/alerting-hysteresis.md tracks the staging: stage 1 ✅ #371, stage 2 ✅ this PR, stage 3 (adaptive linger + open grace from these aggregates) and stage 4 (flapping state, operator-confirmed pattern suggestions) still to come.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AEZZ9HMkKAFqrJzTg3FrE7

The exploration and staging plan for reducing close-reopen alert noise
beyond the static linger: rolling transition aggregates recorded at
filing time, adaptive linger and open-grace windows derived from them,
Nagios-style flapping state, and operator-in-the-loop pattern
suggestions. Includes the off-the-shelf survey (pipeline services vs
embedded analysis libraries) and the augurs/changepoint maintenance
check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AEZZ9HMkKAFqrJzTg3FrE7
@passcod
passcod force-pushed the claude/alerting-noise-damping-plan branch from c8d06c0 to d9ea7f6 Compare July 18, 2026 23:58
Every stamped filing now feeds a fixed-size check_stability row (1:1
with the check state) from *observed* results, so policy and any future
damping never feed back into the statistics: lifetime observation
counters, a 32-entry healthy<->degraded transition ring, and a
168-bucket hour-of-week duty profile whose buckets halve at a cap so it
leans towards recent weeks. Skipped observations carry no signal.

A data migration backfills the records from the last 30 days of status
history (present-in-push checks only; canopy's own checks start cold),
so profiles are useful immediately.

Read surfaces, no behaviour hooks yet:
- the check attention page gains a stability column (flips/24h-7d) and,
  in the expanded row, the derived stats plus a 7x24 UTC duty-cycle
  heatmap — a load-dependent check reads at a glance.
- the get_check_stability MCP tool returns full records with derived
  flap statistics for a set of (source, check) pairs, optionally
  narrowed to a server or group.

Spec: CHK gains a Stability section; MCP spec lists the tool.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AEZZ9HMkKAFqrJzTg3FrE7
@passcod passcod changed the title Plan: trend-based alerting noise damping (stages 2–4) Check stability records (noise-damping stage 2) + the stages 2–4 plan Jul 19, 2026
claude added 6 commits July 19, 2026 01:22
The status-history backfill moves out of migrations onto the monitor
pod: a single fleet-wide INSERT..SELECT would hold FK row locks
(FOR KEY SHARE) on most live issues rows until commit, and the filing
path's SELECT .. FOR UPDATE conflicts with those — so the whole replay
would stall ingestion. Instead the pod replays one server per statement
(riding the (server_id, created_at) statuses index, row locks held for
milliseconds), gated by a completion-marker table and an advisory lock;
a crash mid-run resumes on the next startup and existing rows are never
overwritten.

The table-creation migration also gains a lock_timeout guard: attaching
the FK takes SHARE ROW EXCLUSIVE on issues, which is instant to hold
but queues behind long transactions — and stalls ingestion writes
queued behind it. Better to fail the migrate run fast and retry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AEZZ9HMkKAFqrJzTg3FrE7
Everything transitional — the replay code, the monitor-pod startup
call, its test, and the marker table — carries TODO(backfill-removal),
to be deleted (marker table via a migration) once every deployment has
run the backfill.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AEZZ9HMkKAFqrJzTg3FrE7
The per-check page at /healthchecks/:source/:check is really the
check's home — policy chips, documentation, and now stability — so name
it that: the CheckDetail page, with three sections (About this check,
Fleet stability, Needs attention). The needs-attention server list is
unchanged; the settings editor at /settings/healthchecks/:check is
renamed HealthcheckSettings to free the name. The private API endpoint
follows: status_check_attention -> status_check_detail (breaking for
API consumers; the UI is the only one).

New Fleet stability section answers 'is this the whole fleet or one
server': every server's duty profile summed into one heatmap plus
aggregate flap counts, computed client-side from the per-server records
already in the response — a shared load-dependent pattern reads at a
glance even when no single server stands out.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AEZZ9HMkKAFqrJzTg3FrE7
The needs-attention list adopts the grouping and ordering every other
list uses: rank buckets in display order (unranked last, no heading),
groups alphabetical within a bucket with the group name as a linked
section heading, servers by kind then name, ungrouped servers trailing.
Position no longer encodes urgency — the result chip does.

Group- and canopy-scoped states of the check now show too:
check_state_for_check returns all scopes, and the response carries
groups (with their status-page rank bucket) and the canopy-wide state.
A group's own state files under its heading as a 'whole group' row;
canopy's self-monitoring state gets a trailing section. Both carry the
same expandable detail + stability record as server rows, and both
feed the fleet stability rollup ('Across 12 servers, 1 group…').

Server rows gain rank and kind on the wire so the client reuses the
shared ordering helpers rather than reimplementing them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AEZZ9HMkKAFqrJzTg3FrE7
Each group heading on the check detail list now carries the group's
stability aggregate — flip counts summed over every member's record
plus the group's own state, untouched by the healthy toggle — and
expands to the combined duty-cycle heatmap and statistics: the same
rollup the fleet section shows, scoped to one group. With per-row
records and the fleet section, stability now reads at all three
levels: server, group, fleet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AEZZ9HMkKAFqrJzTg3FrE7
ReturnType<typeof useTheme> resolves to unknown under project-reference
build mode (tsc -b, what CI runs). Row is a component, so it calls
useTheme() directly instead of receiving theme as a typed prop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AEZZ9HMkKAFqrJzTg3FrE7
Base automatically changed from claude/alerting-hysteresis-noise-el56zc to main July 19, 2026 23:36
@passcod
passcod marked this pull request as ready for review July 20, 2026 00:20
Broken shared amber with warning, so the two chips read identically.
Give broken amber/charcoal hazard tape instead — instantly distinct
from a plain warning, and apt for 'the check itself couldn't run'. The
white bold label with a dark halo stays legible over both stripe
colours, in light and dark themes; the tooltip and label still carry
the meaning (status is never colour-alone).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AEZZ9HMkKAFqrJzTg3FrE7
@passcod
passcod added this pull request to the merge queue Jul 20, 2026
Merged via the queue into main with commit f03fb13 Jul 20, 2026
13 of 28 checks passed
@passcod
passcod deleted the claude/alerting-noise-damping-plan branch July 20, 2026 02:18
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.

2 participants