feat(alerts): renames carry their alerts — monitor and host renames move rules, windows and history (#5065)
CI / lint (push) Successful in 3s
CI / integration (push) Successful in 50s
CI / unit (push) Successful in 1m6s
CI / publish (push) Successful in 1m2s

#5065 "Renames carry their alerts". Alert rules, maintenance-window scopes
and metric history key a thing by (source, name), so a rename used to leave
the rules watching a name nothing reported under, and split the history.

- core/renames.carry_rename moves AlertRule.resource_name,
  MaintenanceWindow.scope_resource, plugin_metrics and plugin_metrics_hourly
  to the new name in the caller's transaction. A host rename also carries
  its parts: host_agent "<host>:…" and docker "<host>/…" (HOST_SOURCES).
  Matching is exact (a prefix compare, not LIKE or a collation range).
  History left under the new name by something deleted is dropped first
  rather than merged in.
- Web monitor edit, MCP update_monitor and web host edit call it; MCP
  answers with `carried` (it used to return a warning naming stranded rules).
- A name another monitor of the same type, or another host, already has is
  refused on create and rename: two would share one history.
- The rename is audited (monitor.updated / host.renamed) with what moved.

Tests: an integration test renames a firing monitor through the web route
and finds its rule still firing and cleared by a sample on the new name,
its window and raw+hourly history moved and an orphan's history dropped; a
host rename moves agent and container parts but not a longer-named host's
or another source's; a guard that every host_agent resource is "<host>" or
"<host>:…".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-06 14:02:43 -04:00
co-authored by Claude Opus 5.5
parent 88d34a3827
commit c9f5c049db
9 changed files with 355 additions and 21 deletions
@@ -1,7 +1,7 @@
"""Integration: the monitoring-config write tools against real Postgres.
A monitor is created on a host, edited (config merged, a rename warning for
the rule that watches it by name), paused and deleted; a rule is created on
A monitor is created on a host, edited (config merged, a rename that carries
the rule watching it by name), paused and deleted; a rule is created on
that monitor, edited, acknowledged once firing, and deleted; a maintenance
window in progress is ended and an upcoming one cancelled. Every write leaves
an audit row as "<user> via MCP".
@@ -95,11 +95,15 @@ def test_monitor_rule_ack_and_maintenance_writes(app):
row = await s.get(Monitor, made["monitor_id"])
assert row.config["headers"] == {"Authorization": "Bearer s3cret"}
# A rename leaves the rule on the old name, and says so.
# A rename takes the rule with it, and says what moved.
renamed = await m.update_monitor(mon_name, name=f"{mon_name}-2")
assert rule_name in renamed["warning"]
await a.update_alert_rule(rule_name, resource=f"{mon_name}-2")
assert "warning" not in await m.update_monitor(f"{mon_name}-2", check_interval_s=30)
assert renamed["carried"]["from"] == mon_name and renamed["carried"]["rules"] == 1
async with app.db_sessionmaker() as s:
assert (await s.get(AlertRule, rule["rule_id"])).resource_name == f"{mon_name}-2"
assert "carried" not in await m.update_monitor(f"{mon_name}-2", check_interval_s=30)
# Another http monitor may not take the same name: they would share one history.
with pytest.raises(ValueError, match="already named"):
await m.create_monitor(f"{mon_name}-2", "http", "other.lan")
assert (await m.set_monitor_enabled(f"{mon_name}-2", False))["enabled"] is False
# ── acknowledge ────────────────────────────────────────────────────