feat(alerts): an acknowledgement is on the record, with who made it (#5463)
#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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user