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
@@ -58,3 +58,23 @@ def test_expand_is_backcompat_with_old_agent_sample():
n.startswith(("net_", "disk_read", "disk_write", "psi_", "temp_", "mem_cached"))
for n in names
)
def test_every_resource_is_the_host_or_a_host_colon_part():
"""A host rename carries "<host>" and "<host>:…" (steward/core/renames.py
HOST_SOURCES); a resource named any other way would stay behind."""
from steward.core.renames import HOST_SOURCES
sample = {
"ts": "x", "cpu_pct": 1.0, "cpu_cores": [1.0],
"mem": {"total_bytes": 10, "available_bytes": 5, "cached_bytes": 1, "buffers_bytes": 1},
"load": {"1m": 0.1}, "uptime_secs": 1,
"storage": [{"mount": "/", "total_bytes": 10, "used_bytes": 5}],
"net": [{"iface": "eth0", "rx_bps": 1.0, "tx_bps": 1.0}],
"diskio": [{"device": "sda", "read_bps": 1.0, "write_bps": 1.0}],
"temps": [{"label": "Package", "celsius": 40.0}],
"psi": {"cpu_some_avg10": 0.0},
}
sep = dict(HOST_SOURCES)["host_agent"]
resources = {r.resource_name for r in _expand_sample_to_metrics(sample, "web01", datetime.now(timezone.utc))}
assert len(resources) > 5
assert {r for r in resources if r != "web01" and not r.startswith("web01" + sep)} == set()