Phase 3 — Alerts (P3-05/06/07) #7

Merged
steve merged 34 commits from p3-alerts into main 2026-05-04 22:51:17 +01:00
2 changed files with 68 additions and 2 deletions
Showing only changes of commit 9be3cead8e - Show all commits
+54
View File
@@ -68,6 +68,60 @@ func (e *Engine) raiseAndNotify(ctx context.Context, hostID, kind, severity, mes
})
}
// Acknowledge updates the alert row and fans out alert.acknowledged to
// every enabled channel. Best-effort: store errors are logged but the
// dispatch still fires only when the store update succeeds.
func (e *Engine) Acknowledge(ctx context.Context, alertID, userID string, when time.Time) error {
if err := e.store.Acknowledge(ctx, alertID, userID, when); err != nil {
return err
}
a, lerr := e.store.GetAlert(ctx, alertID)
if lerr != nil || a == nil {
// Acknowledge already succeeded; dispatch is best-effort.
return nil //nolint:nilerr
}
p := alertPayload(ctx, e.store, notification.EventAcknowledged, a)
go e.hub.Dispatch(context.WithoutCancel(ctx), p)
return nil
}
// Resolve marks the alert resolved and fans out alert.resolved.
func (e *Engine) Resolve(ctx context.Context, alertID string, when time.Time) error {
a, _ := e.store.GetAlert(ctx, alertID)
if err := e.store.Resolve(ctx, alertID, when); err != nil {
return err
}
if a == nil {
return nil
}
p := alertPayload(ctx, e.store, notification.EventResolved, a)
go e.hub.Dispatch(context.WithoutCancel(ctx), p)
return nil
}
// alertPayload builds a Payload from a stored Alert, looking up the host
// name when HostID is set.
func alertPayload(ctx context.Context, st *store.Store, ev notification.Event, a *store.Alert) notification.Payload {
hostID, hostName := "", ""
if a.HostID != nil {
hostID = *a.HostID
hostName = hostID
if h, err := st.GetHost(ctx, hostID); err == nil && h != nil {
hostName = h.Name
}
}
return notification.Payload{
Event: ev,
AlertID: a.ID,
Severity: a.Severity,
Kind: a.Kind,
HostID: hostID,
HostName: hostName,
Message: a.Message,
RaisedAt: a.CreatedAt,
}
}
// resolveAndNotify clears every open (or acknowledged) alert for
// (host_id, kind) via store.AutoResolve, then fires alert.resolved
// for each row that was actually open. Best-effort — errors are
+14 -2
View File
@@ -119,7 +119,13 @@ func (s *Server) handleUIAlertAcknowledge(w stdhttp.ResponseWriter, r *stdhttp.R
stdhttp.Error(w, "missing id", stdhttp.StatusBadRequest)
return
}
if err := s.deps.Store.Acknowledge(r.Context(), id, u.ID, time.Now().UTC()); err != nil {
var err error
if s.deps.AlertEngine != nil {
err = s.deps.AlertEngine.Acknowledge(r.Context(), id, u.ID, time.Now().UTC())
} else {
err = s.deps.Store.Acknowledge(r.Context(), id, u.ID, time.Now().UTC())
}
if err != nil {
slog.Warn("ui alerts: ack", "err", err)
}
_ = s.deps.Store.AppendAudit(r.Context(), store.AuditEntry{
@@ -147,7 +153,13 @@ func (s *Server) handleUIAlertResolve(w stdhttp.ResponseWriter, r *stdhttp.Reque
stdhttp.Error(w, "missing id", stdhttp.StatusBadRequest)
return
}
if err := s.deps.Store.Resolve(r.Context(), id, time.Now().UTC()); err != nil {
var err error
if s.deps.AlertEngine != nil {
err = s.deps.AlertEngine.Resolve(r.Context(), id, time.Now().UTC())
} else {
err = s.deps.Store.Resolve(r.Context(), id, time.Now().UTC())
}
if err != nil {
slog.Warn("ui alerts: resolve", "err", err)
}
_ = s.deps.Store.AppendAudit(r.Context(), store.AuditEntry{