fix(alerts): an alert on a count says how many of what, and names them (#6400)
CI / lint (push) Successful in 2s
CI / unit (push) Successful in 1m36s
CI / integration (push) Successful in 1m48s
CI / publish (push) Successful in 1m8s

"swarm services short on swarm is 6." becomes "6 swarm services are
short of their replicas.", with a Which line naming each service and how
many of its tasks run. The swarm figures get labels, and the counting
figures (swarm, mounts, drives, updates) read as sentences in the email,
its subject and the Alerts page. A plugin names what its count counts
through core.alerts.names_behind; the Docker plugin does for the three
swarm figures, from the stored swarm. Webhooks get alert.which too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 13:00:56 -04:00
co-authored by Claude Opus 5.5
parent ae789bc0f1
commit 0b787a0676
17 changed files with 308 additions and 24 deletions
+46
View File
@@ -106,6 +106,52 @@ def test_each_state_reads_as_a_sentence():
assert (down["headline"], down["condition"]) == ("web01 is down.", "is down")
SHORT = {**ALERT, "rule_name": "Swarm services short of replicas", "metric": "swarm_services_short",
"value": 6.0, "operator": ">", "threshold": 0.0, "resource": "swarm", "source_module": "docker",
"which": ["api_app (0 of 1 running)", "db (0 of 1 running)", "proxy (1 of 2 running)",
"web_app (0 of 1 running)"]}
def test_a_count_on_the_swarm_says_how_many_of_what_and_names_them():
a = mail.alert_facts(SHORT, timezone.utc)
assert a["headline"] == "6 swarm services are short of their replicas."
assert a["which"] == ("api_app (0 of 1 running), db (0 of 1 running), proxy (1 of 2 running)"
" and web_app (0 of 1 running)")
assert a["subject"] == ("[Steward] Firing: Swarm services short of replicas — 6 (api_app (0 of 1 running),"
" db (0 of 1 running), proxy (1 of 2 running), …)")
one = mail.alert_facts({**SHORT, "value": 1.0, "which": ["db (0 of 1 running)"]}, timezone.utc)
assert one["headline"] == "1 swarm service is short of its replicas."
clear = mail.alert_facts({**SHORT, "state": "RESOLVED", "value": 0.0, "which": []}, timezone.utc)
assert (clear["headline"], clear["subject"], clear["which"]) == (
"Every swarm service has its replicas again.",
"[Steward] Resolved: Swarm services short of replicas — swarm", "")
silent = mail.alert_facts({**SHORT, "state": "NO_DATA", "value": None}, timezone.utc)
assert silent["headline"] == "Steward has had no swarm services short of replicas reading from the swarm."
def test_a_count_on_a_host_says_where():
stalled = {**ALERT, "metric": "mounts_stalled", "value": 2.0, "operator": ">", "threshold": 0.0}
assert mail.alert_facts(stalled, timezone.utc)["headline"] == "2 mounts aren't answering on web01."
assert mail.alert_facts({**stalled, "state": "RESOLVED", "value": 1.0}, timezone.utc)["headline"] == \
"1 mount isn't answering on web01, back within its limit."
async def test_the_names_reach_both_parts_and_a_burst(monkeypatch):
msg = await _sent(monkeypatch, CFG, SHORT)
for part in (msg.get_body(("plain",)).get_content(), msg.get_body(("html",)).get_content()):
assert "Which" in part and "web_app (0 of 1 running)" in part, part
plain = await _sent(monkeypatch, CFG, ALERT)
assert "Which" not in plain.get_body(("plain",)).get_content() + plain.get_body(("html",)).get_content()
_, text, html = mail.burst_email(CFG, [SHORT, ALERT], EXTRA_NY)
assert text.count("Which: ") == 1 and html.count("Which: ") == 1
def test_a_long_list_is_cut_short():
names = [f"svc{i:02d}" for i in range(15)]
said = mail.alert_facts({**SHORT, "value": 15.0, "which": names}, timezone.utc)["which"]
assert said.endswith("svc11 and 3 more") and "svc12" not in said
def test_the_report_has_its_sections_in_both_parts():
subject, text, html = mail.report_email(CFG, REPORT, PERSON_NY)
assert subject == "[Steward] Weekly report, 4 Oct – 11 Oct 2026"