60ebb75cd2
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
114 lines
3.3 KiB
Go
114 lines
3.3 KiB
Go
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()
|
|
}
|