Skip to content

Notification routes: match on alert severity (#6928) - #6936

Merged
erikdarlingdata merged 28 commits into
devfrom
feature/6928-route-severity
Oct 30, 2026
Merged

erikdarlingdata merged 28 commits into
devfrom
feature/6928-route-severity

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Oct 29, 2026 •

Copy link
Copy Markdown
Owner

Claude posting for Erik Darling.

Closes #6928.

Why

A notification route could not be limited by severity, so production warnings could not be kept out of an on-call channel.

What changes

Store (V229). config.config_notification_routes.severities is both, critical or warning, default both, so every existing route keeps its effect. V229 also adds config.external_alert_state.fired_severities and config.investigation_request_alerts.severity (see Resolutions and the agent gate below). The rung is idempotent and has its rewind artifact. StorageVersion.SchemaVersion is 229.

Matching. NotificationRouter.Resolve and ResolveAgent take an optional severity. A route whose filter excludes the alert's tier is dropped first, then the existing order and tie-breaks run unchanged, ending at the default channels. A caller with no severity is matched by every route, as before, so Lite is unaffected. The route cache key includes the severity filter, so an edit of only that filter reloads the cached routes and takes effect without a restart.

Which tier an alert has. The tier graded at the fire site when there is one, else the metric's own badge. Red is Critical. Every other badge counts as Warning, so a Warning-only route also takes alerts that are not Critical.

Resolutions and escalation.

  • A resolution goes to the channels the fire went to. It does not re-match routes, so a filter change never moves a clear.
  • When an open Warning alert climbs to Critical, it is delivered again as a Critical fire. The clear is then sent to every destination the episode fired to (PagerDuty per routing key, plus Teams, Slack, webhook and email), so a Warning incident is not left open on the first route's channel.
  • The external-alert PagerDuty close used to re-match the route by metric alone. It now closes at every tier the episode fired at, read from fired_severities.

Agent-action gate. Each alert row on an investigation request now records its tier, and the gate resolves each alert's route at that tier. Before, a Critical alert dispatched under a fleet route at Act could be lowered to Off by a server route that is Warning only. A row written before V229 resolves as before. The gate still only lowers a request's level.

Journal. A change to severities on a route with an agent setting is journaled and shown, like a match or scope change, and a non-operator is refused the same way.

MCP. create_notification_route and update_notification_route take severities (any case, anything else is invalid). get_notification_routes shows it, and the undo answers carry it.

Viewer. The route editor has a Severity choice (Both, Critical only, Warning only) and the routes window has a Severity column. The column is read only on a store at V229 or newer. Below that every route shows as Both.

Web. The Routes list has a Severity column and the route editor a Severity choice. The service accepts severities on create and edit, keeps the saved value when an edit leaves it out, and refuses any other word with a clear message.

Web route edit fix (also in v3.11.0). Saving an edit to an existing route from the web page always failed with HTTP 400 "expected_modified_at is required". The edit form dropped the route's last-saved time when it read its fields back. One line in admin-agent-setup.js fixes it. This is older than this PR (since #6274), and is fixed here because it is the same code.

Docs. alerts-and-notifications.md, configuration-mcp.md, and the V229 rollback line in docs/darling/admin.md.

Route severity is Darling only. Lite's routes have no severities field, and the Lite and Darling route field check lists it as Darling only.

Tests

  • New: NotificationRouteSeverityTests, NotificationRouteSeverityLiveTests, AgentActionGateSeverityLiveTests, ViewerNotificationRouteSeveritiesTests, a resolution test in AlertResolutionDeliveryTests, a drain test in ExternalAlertDrainLiveTests, and AdminPendingNotesTests.SavingAnEditedRoute_SendsTheVersionTheFormRead_AndTheChosenSeverity for the web edit fix (it fails without the fix).
  • The agent-gate test fails when the fix is taken out (the request is refused as not Level 2) and passes with it.
  • After merging V227 and V228 under V229: the census, ladder, rung, migration, schema-version, MCP read-timeout, route severity, pending notes, agent setup and agent-gate classes, Total 1466, Failed 0, on a throwaway PostgreSQL cluster.
  • Browser, against the service built from this branch on a throwaway store: the Routes list and editor at 1400 px and 390 px. Create Critical only, edit to Warning only, then to Both, and the list shows each. A bad word is refused with "severities is critical, warning or both." No sideways page scroll at phone width.
  • After merging dev (V227 and V228 as squashed there, V229 on top, dev's text kept for both): the rung census, ladder, rewind artifact, schema-version gate, route severity, threshold override and blocking band classes, Total 1107, Failed 0, on a throwaway PostgreSQL cluster. The PR reads MERGEABLE.
  • After the first CI run: DarlingRouteCacheReloadTests, InvestigationFeedbackMcpLiveTests and the other Investigation*Live* classes, ManagedRoles, ProvisionRoles, NotificationRouteSeverity*, AgentActionGateSeverityLiveTests and AdminPageTests on a throwaway store, Failed 0. The route cache key gained the severity filter, and the mcp role was granted INSERT on investigation_request_alerts.severity, which the practice copy writes. The routes table test and the managed-roles grant pin name the new column.
  • CI runs the full suite.

Merge order

V227 (#6939) and V228 (#6938) are on dev. This PR is V229 and goes after them.

Considered, not added

  • Hiding the viewer's Severity choice on a store below V229: the viewer's connect gate already refuses a store below the required version.
  • Rebuilding provisioned roles on a store that kept the old route functions: the managed batch drops them, and a bring-your-own store gets the same step from the existing provisioning note.

A threshold override can now be set for a server tag. Precedence is database, then server, then the deepest tag the server is under (the notification routes' own rule), then global. Adds V227, the MCP tag argument, the get_alert_settings tag source, the Viewer and web Admin tag rows, and tests.
A route gains a severities filter (both, critical or warning, default both) in
V229, the route functions and grants, the router, the MCP create, update and get
tools, and the external-alert PagerDuty close. Viewer and web Routes list
editing, tests and docs follow.
…ade and the settings round trip (#6927)

The fleet total was banded as one rate against the per-server tiers, so ten servers at 3 events an hour (none even Warning) made the tile Critical. The roll-up now takes the worst card band per signal, in the same fields. Adds live tests for the V228 upgrade on a store with a settings row and for update_alert_settings and get_alert_settings with the critical-below-warn refusal, and moves the settings column pins from 86 to 90.
Router, tier rule, the three-channel example through the deliverer, resolution routing, the PagerDuty close severity, MCP round trip and V229 on a store with routes. Docs: alerts and notifications, MCP configuration, rollback line for V229.
…he alert tier, and journal severity filter changes (part of #6928)

An alert that fired Warning and climbed to Critical now sends its clear to each destination its fires reached, on the in-process path and the external alert path. The agent-action gate re-resolves each alert at the tier it arrived at. A change to the severity filter of an agent route is journaled from the writer and the MCP tool.
… rule in the server view (#6926)

LoadAsync now throws when the tag tables could not be read, so the worker keeps the whole snapshot in force instead of installing one with no tag winners until the next config change.

get_alert_settings filtered tag rows through its own known-key and integer checks. It now calls the loader's Accept, so a deeper tag whose value is outside the key's bounds is skipped there as it is in the engine, and the view names the tag the engine applies.
#6927)

Nothing read it: the service, the viewer and the MCP tools all read the tiers through BlockingBandStore. The config fields it wrapped are still bound by the seed insert and the store load, so they stay.
…6928)

The route form's read() rebuilt its entries without modifiedAt, so every Save on an
existing route went out with no expected_modified_at and the service refused it with a 400.
The form now carries modifiedAt through. A test drives the Routes tab: Edit, choose a
severity, Save, and check the write the page sent.
…e read from the store source (#6926)

V227 adds a nullable column, two CHECKs and a partial unique index to the
operator-written config.config_threshold_overrides table, a handful of rows, so
it is declared SetsTheFloor false and the lock-wait multiple stays 5.

The tag-name read in the override tool now uses postgres.CreateCommand like its
sibling reads, which is the store receiver the read-deadline census recognises.
@erikdarlingdata
erikdarlingdata marked this pull request as ready for review October 30, 2026 01:33
The route severity test class now records its own-store exemption above the class, where the collection hygiene test looks for it. The Lite and Darling route field parity test lists severities as a Darling-only field.
@erikdarlingdata
erikdarlingdata enabled auto-merge (squash) October 30, 2026 02:04
V227 and V228 reached dev as squash commits, so the copies this branch carried
conflicted. Dev's text for both is kept and V229 sits on top.
The route cache key now includes the severity filter, so an edit of only that filter reloads the cached routes. The mcp role gets INSERT on investigation_request_alerts.severity, which the practice copy writes, so starting practice no longer fails with a permission refusal. The admin routes table test and the managed-roles grant pin name the new severities column and the new grant column.
@erikdarlingdata
erikdarlingdata merged commit 5f26d50 into dev Oct 30, 2026
25 of 26 checks passed
@erikdarlingdata
erikdarlingdata deleted the feature/6928-route-severity branch October 30, 2026 03:47
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