9da913080f
Every incident-mutation handler read userFromContext(ctx) and wrote the result's .ID into acknowledged_by/incident_events.user_id without checking the ok bool. For a team-scoped service-account caller this returned a zero-value user id, which violated the users(id) FK and 500'd on acknowledge, unacknowledge, resolve, snooze, unsnooze and create-note. handleDeleteNote didn't crash but silently matched zero rows instead (WHERE user_id = 0), so a service account could never delete its own note. Add acknowledged_by_service_account_id (incidents) and service_account_id (incident_events) as nullable FKs to service_accounts(id), parallel to and mutually exclusive with the existing human columns (migration 015, with a CHECK enforcing the exclusion). Route every one of the six handlers plus delete-note through a new callerActorIDs() helper that branches on Caller.AsHuman()/ServiceAccountID() instead of assuming a human, and thread a serviceAccountID parameter through logEvent and the new acknowledgeIncidentAs (acknowledgeIncident itself is untouched: its only other caller, the push-notification Acknowledge button, is always human). Render the new actor distinctly from both a human and "the server acted" in the web UI's incident timeline and facts card. handleIncidentAssign, handleIncidentArchive and handleIncidentUnarchive are deliberately not touched here — they track no actor at all today, for anyone, which is a separate pre-existing gap (follow-up issue to come). Fixes #25
111 lines
5.1 KiB
Go
111 lines
5.1 KiB
Go
package models
|
|
|
|
import "time"
|
|
|
|
// Incident is the human work item: the thing that gets acknowledged, assigned,
|
|
// snoozed, discussed and resolved. Alerts are the machine-owned signal records
|
|
// underneath it — many alerts map to one incident, correlated by the groupKey
|
|
// Alertmanager already computed from the operator's group_by configuration.
|
|
//
|
|
// Nothing here is ever written by the Alertmanager webhook except Status, which
|
|
// the webhook and the sweeper may flip to "resolved" once every member alert has
|
|
// stopped firing.
|
|
type Incident struct {
|
|
// EscalationLevel is which rung of its team's ladder this incident is on,
|
|
// 0 for none — either the team has no ladder, or somebody has answered.
|
|
// EscalationDueAt is when the current level runs out, so a client can say
|
|
// how long is left rather than only what already happened.
|
|
EscalationLevel int64 `json:"escalation_level"`
|
|
EscalationDueAt *time.Time `json:"escalation_due_at,omitempty"`
|
|
|
|
// TeamID is the team that owns this incident, fixed when it opens: an
|
|
// incident never moves between teams. TeamName rides along so the combined
|
|
// queue can badge each row without a second request.
|
|
TeamID int64 `json:"team_id"`
|
|
TeamName string `json:"team_name,omitempty"`
|
|
|
|
ID int64 `json:"id"`
|
|
GroupKey string `json:"group_key"`
|
|
Title string `json:"title"`
|
|
GroupLabels map[string]string `json:"group_labels"`
|
|
|
|
// Status is "triggered", "acknowledged" or "resolved".
|
|
Status string `json:"status"`
|
|
|
|
// Severity is the highest `severity` label across the alerts that were
|
|
// firing when it was last recomputed. It is deliberately not cleared when an
|
|
// incident resolves — a resolved incident should still say how bad it was.
|
|
Severity *string `json:"severity,omitempty"`
|
|
|
|
TriggeredAt time.Time `json:"triggered_at"`
|
|
|
|
AcknowledgedByID *int64 `json:"acknowledged_by_id,omitempty"`
|
|
AcknowledgedByUser *string `json:"acknowledged_by,omitempty"`
|
|
AcknowledgedAt *time.Time `json:"acknowledged_at,omitempty"`
|
|
|
|
// AcknowledgedByServiceAccountID/Name are the service-account-shaped
|
|
// parallel to AcknowledgedByID/User above: mutually exclusive with it,
|
|
// populated when a service account (not a human) acknowledged this
|
|
// incident. See migration 015 and terdut-server#25.
|
|
AcknowledgedByServiceAccountID *int64 `json:"acknowledged_by_service_account_id,omitempty"`
|
|
AcknowledgedByServiceAccountName *string `json:"acknowledged_by_service_account,omitempty"`
|
|
|
|
AssignedToID *int64 `json:"assigned_to_id,omitempty"`
|
|
AssignedToUser *string `json:"assigned_to,omitempty"`
|
|
|
|
// SnoozedUntil hides the incident from the default queue without closing it.
|
|
// A timestamp in the past reads as "not snoozed"; nothing sweeps it.
|
|
SnoozedUntil *time.Time `json:"snoozed_until,omitempty"`
|
|
|
|
ResolvedAt *time.Time `json:"resolved_at,omitempty"`
|
|
|
|
// ResolutionSource is "alerts" when every member alert stopped firing, or
|
|
// "manual" when a human closed it. Manual resolution is terminal: a later
|
|
// occurrence opens a new incident rather than reopening this one.
|
|
ResolutionSource *string `json:"resolution_source,omitempty"`
|
|
|
|
ArchivedAt *time.Time `json:"archived_at,omitempty"`
|
|
|
|
// Alerts is populated by GET /api/incidents/{id} only.
|
|
Alerts []Alert `json:"alerts,omitempty"`
|
|
}
|
|
|
|
// IncidentEvent is one entry in an incident's timeline. The table is append-only
|
|
// and is the only history this server keeps — alert rows are mutated in place.
|
|
//
|
|
// Type is one of: triggered, alert_added, alert_resolved, acknowledged,
|
|
// unacknowledged, assigned, snoozed, unsnoozed, resolved, note. UserID and
|
|
// ServiceAccountID are mutually exclusive; both nil means the server acted
|
|
// rather than any caller.
|
|
type IncidentEvent struct {
|
|
ID int64 `json:"id"`
|
|
IncidentID int64 `json:"incident_id"`
|
|
Type string `json:"type"`
|
|
UserID *int64 `json:"user_id,omitempty"`
|
|
Username *string `json:"username,omitempty"`
|
|
|
|
// ServiceAccountID/Name are the service-account-shaped parallel to
|
|
// UserID/Username above: mutually exclusive with it, populated when a
|
|
// service account (not a human, and not nil-meaning-the-server-acted)
|
|
// performed this event. Named Name, not Username — a ServiceAccount has
|
|
// a Name field, not a Username. See migration 015 and terdut-server#25.
|
|
ServiceAccountID *int64 `json:"service_account_id,omitempty"`
|
|
ServiceAccountName *string `json:"service_account_name,omitempty"`
|
|
|
|
AlertID *int64 `json:"alert_id,omitempty"`
|
|
Detail *string `json:"detail,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
// SimilarIncident is an earlier, resolved incident with the same signature as
|
|
// the one being looked at. ResolutionNotes are the "what fixed it" notes;
|
|
// NoteCount counts the plain working notes, which live on the timeline.
|
|
type SimilarIncident struct {
|
|
ID int64 `json:"id"`
|
|
Title string `json:"title"`
|
|
TriggeredAt time.Time `json:"triggered_at"`
|
|
ResolvedAt time.Time `json:"resolved_at"`
|
|
NoteCount int `json:"note_count"`
|
|
ResolutionNotes []IncidentEvent `json:"resolution_notes"`
|
|
}
|