Show notes from similar earlier incidents
Each incident gets a signature: the alert name plus the group labels that
say what is broken, minus the ones that only say where it ran (instance,
pod, container, ...). GET /api/incidents/{id}/similar returns resolved
incidents in the same team with the same signature that have notes.
Notes can be marked as the resolution note, "what fixed it", either with a
resolution field on resolve or pinned on a note. Those lead the similar
list, show on the incident page as "Seen before", and the triggered
notification carries the latest one.
Claude-Session: https://claude.ai/code/session_01MMados3BD1oSjevHxbmVqU
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.ryuvia.com/niklas/terdut-server/internal/models"
|
||||
)
|
||||
|
||||
const (
|
||||
similarDefaultLimit = 5
|
||||
similarMaxLimit = 20
|
||||
)
|
||||
|
||||
// handleIncidentSimilar lists earlier, resolved incidents in the same team with
|
||||
// the same signature that someone left notes on, incidents with a resolution
|
||||
// note first. This is the "have we seen this before" answer for a responder
|
||||
// looking at a fresh incident; the plain notes are one timeline fetch away.
|
||||
func handleIncidentSimilar(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
id, ok := incidentIDParam(w, r, db)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
limit := similarDefaultLimit
|
||||
if v := r.URL.Query().Get("limit"); v != "" {
|
||||
n, err := strconv.Atoi(v)
|
||||
if err != nil || n < 1 {
|
||||
respond(w, http.StatusBadRequest, errResp("invalid limit"))
|
||||
return
|
||||
}
|
||||
limit = min(n, similarMaxLimit)
|
||||
}
|
||||
|
||||
out, err := similarIncidents(r.Context(), db, id, limit)
|
||||
if err != nil {
|
||||
respond(w, http.StatusInternalServerError, errResp("internal error"))
|
||||
return
|
||||
}
|
||||
respond(w, http.StatusOK, out)
|
||||
}
|
||||
}
|
||||
|
||||
func similarIncidents(ctx context.Context, q querier, id int64, limit int) ([]models.SimilarIncident, error) {
|
||||
rows, err := q.QueryContext(ctx, `
|
||||
SELECT o.id, o.title, o.triggered_at, o.resolved_at,
|
||||
(SELECT COUNT(*) FROM incident_events e
|
||||
WHERE e.incident_id = o.id AND e.type = $3)
|
||||
FROM incidents i
|
||||
JOIN incidents o ON o.team_id = i.team_id AND o.signature = i.signature
|
||||
WHERE i.id = $1 AND o.id <> i.id AND o.resolved_at IS NOT NULL
|
||||
AND EXISTS (SELECT 1 FROM incident_events e
|
||||
WHERE e.incident_id = o.id AND e.type IN ($3, $4))
|
||||
ORDER BY EXISTS (SELECT 1 FROM incident_events e
|
||||
WHERE e.incident_id = o.id AND e.type = $4) DESC,
|
||||
o.triggered_at DESC
|
||||
LIMIT $2`, id, limit, evNote, evResolutionNote)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
out := []models.SimilarIncident{}
|
||||
ids := []int64{}
|
||||
for rows.Next() {
|
||||
var s models.SimilarIncident
|
||||
var triggered, resolved int64
|
||||
if err := rows.Scan(&s.ID, &s.Title, &triggered, &resolved, &s.NoteCount); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.TriggeredAt = time.Unix(triggered, 0).UTC()
|
||||
s.ResolvedAt = time.Unix(resolved, 0).UTC()
|
||||
s.ResolutionNotes = []models.IncidentEvent{}
|
||||
out = append(out, s)
|
||||
ids = append(ids, s.ID)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
nrows, err := q.QueryContext(ctx, `
|
||||
SELECT e.id, e.incident_id, e.type, e.user_id, u.username, e.detail, e.created_at
|
||||
FROM incident_events e
|
||||
LEFT JOIN users u ON u.id = e.user_id
|
||||
WHERE e.incident_id = ANY($1) AND e.type = $2
|
||||
ORDER BY e.created_at, e.id`, ids, evResolutionNote)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer nrows.Close()
|
||||
|
||||
byID := make(map[int64]*models.SimilarIncident, len(out))
|
||||
for i := range out {
|
||||
byID[out[i].ID] = &out[i]
|
||||
}
|
||||
for nrows.Next() {
|
||||
var e models.IncidentEvent
|
||||
var ts int64
|
||||
if err := nrows.Scan(&e.ID, &e.IncidentID, &e.Type, &e.UserID, &e.Username, &e.Detail, &ts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
e.CreatedAt = time.Unix(ts, 0).UTC()
|
||||
s := byID[e.IncidentID]
|
||||
s.ResolutionNotes = append(s.ResolutionNotes, e)
|
||||
}
|
||||
return out, nrows.Err()
|
||||
}
|
||||
Reference in New Issue
Block a user