feat(alerts): an acknowledgement is on the record, with who made it (#5463)
CI / lint (push) Successful in 3s
CI / integration (push) Successful in 1m7s
CI / unit (push) Successful in 1m27s
CI / publish (push) Successful in 1m8s

#5463 "Acknowledgements on record: an event and who did it", step 1 of
milestone 499 "Alerts you can trust". The live walk in #5461 found that
acknowledging changed only the alert's current state: once it resolved,
nothing said who had acknowledged it or when.

- alert_events.actor_id (migration 0040): the person behind a transition.
- Acknowledging writes a firing → acknowledged event by the acknowledger.
- A rule edit, disable or enable that resets an alert carries its actor
  (web and MCP alike).
- The history page notes "acknowledged by X" and "rule disabled by X"; MCP
  get_alert_history returns `by`.
- One usernames() helper in alerts.service, used by the active list, the
  history page and the MCP tools.
- docs/core/alerting.md: the state diagram no longer shows an acknowledged
  alert re-firing, and says what an event records.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-09 08:19:48 -04:00
co-authored by Claude Opus 5.5
parent b3a5bbfeff
commit 734d313a54
9 changed files with 133 additions and 48 deletions
+15 -6
View File
@@ -144,8 +144,9 @@ async def active_fragment():
rules_off=sum(1 for r in rules if not r.enabled))
def _history_row(event, rule) -> dict:
"""One transition as the history page shows it."""
def _history_row(event, rule, names: dict | None = None) -> dict:
"""One transition as the history page shows it. `names` maps user ids to
usernames: a transition a person made says who (#5463)."""
from steward.core.alerts import MUTED, RELEASED
from steward.core.formatting import metric_label, metric_value
notes, muted_by, released = [], None, False
@@ -156,6 +157,12 @@ def _history_row(event, rule) -> dict:
released = True
elif part:
notes.append(part)
actor = getattr(event, "actor_id", None)
by = (names or {}).get(actor) or ("a deleted user" if actor else None)
if event.to_state == "acknowledged":
notes.insert(0, f"acknowledged by {by}" if by else "acknowledged")
elif by and notes:
notes[0] = f"{notes[0]} by {by}"
metric = rule.metric_name
if event.metric_value is None:
value = f"No {metric_label(metric)} reading"
@@ -193,14 +200,15 @@ async def alert_history():
rule_id = chosen.id if chosen else None
rows = await alert_svc.history(db, since, rule_id, PAGE_SIZE, before)
show_notified, show_action = await alert_svc.history_columns(db, since, rule_id)
names = await alert_svc.usernames(db, (e.actor_id for e, _ in rows))
verdict = None
if chosen and not before:
every = await alert_svc.history(db, since, rule_id, HISTORY_SUMMARY_ROWS)
verdict = summary(episodes([_history_row(e, r) for e, r in every], newest_page=True,
now=now, since=since))
rows, after = next_page(rows, lambda row: (row[0].transitioned_at, row[0].id))
ctx = {"episodes": episodes([_history_row(e, r) for e, r in rows], newest_page=not before,
now=now, since=since),
ctx = {"episodes": episodes([_history_row(e, r, names) for e, r in rows],
newest_page=not before, now=now, since=since),
"after": after, "rules": rules, "chosen": chosen, "current_range": range_key,
"show_notified": show_notified, "show_action": show_action, "verdict": verdict}
if before:
@@ -327,7 +335,7 @@ async def update_rule(rule_id: str):
fields = await alert_svc.rule_fields(db, **_rule_form(form))
except ValueError as exc:
return str(exc), 400
await alert_svc.apply_rule_change(db, rule, fields)
await alert_svc.apply_rule_change(db, rule, fields, actor_id=session.get("user_id"))
if apply_action:
rule.ansible_action = ansible_action
await log_audit(current_app, session.get("user_id"), session.get("username", ""),
@@ -344,7 +352,8 @@ async def toggle_rule(rule_id: str):
rule = result.scalar_one_or_none()
if rule is None:
return "Not found", 404
await alert_svc.apply_rule_change(db, rule, {}, enabled=not rule.enabled)
await alert_svc.apply_rule_change(db, rule, {}, enabled=not rule.enabled,
actor_id=session.get("user_id"))
action = "alert_rule.enabled" if rule.enabled else "alert_rule.disabled"
await log_audit(current_app, session.get("user_id"), session.get("username", ""),
action, entity_type="alert_rule", entity_id=rule.name)