Files
ITWorx-Pulse-Public/internal/alert/operations.go
T
ITWorx Pulse release export bd774932d5
Public source validation / validate (push) Failing after 3m8s
Publish ITWorx Pulse source
2026-09-03 02:09:19 +02:00

100 lines
5.1 KiB
Go

package alert
import (
"context"
"errors"
"fmt"
"time"
"github.com/jackc/pgx/v5"
)
func Unacknowledge(current Snapshot, actor string, at time.Time) (TransitionResult, error) {
current = current.normalized()
if actor == "" || len(actor) > 160 || at.IsZero() {
return TransitionResult{}, ErrInvalidObservation
}
if current.State != StateAcknowledged {
return TransitionResult{}, ErrStateConflict
}
result := current
result.State = StateFiring
result.RetainedState = StateFiring
result.AcknowledgedBy = ""
result.AcknowledgedAt = nil
result.Reason = "unacknowledged"
return finish(current, result, StateFiring, "unacknowledge"), nil
}
func (r StateRepository) AcknowledgeRevision(ctx context.Context, instanceID, actor, evaluationKey string, at time.Time, expectedRevision int64) (Instance, Occurrence, bool, error) {
return r.applyOperation(ctx, instanceID, actor, evaluationKey, at, expectedRevision, true)
}
func (r StateRepository) Unacknowledge(ctx context.Context, instanceID, actor, evaluationKey string, at time.Time, expectedRevision int64) (Instance, Occurrence, bool, error) {
return r.applyOperation(ctx, instanceID, actor, evaluationKey, at, expectedRevision, false)
}
func (r StateRepository) applyOperation(ctx context.Context, instanceID, actor, evaluationKey string, at time.Time, expectedRevision int64, acknowledge bool) (Instance, Occurrence, bool, error) {
if r.Pool == nil {
return Instance{}, Occurrence{}, false, ErrUnavailable
}
if instanceID == "" || actor == "" || len(actor) > 160 || evaluationKey == "" || len(evaluationKey) > 160 || at.IsZero() || expectedRevision < 1 {
return Instance{}, Occurrence{}, false, ErrInvalidObservation
}
tx, err := r.Pool.BeginTx(ctx, pgx.TxOptions{})
if err != nil {
return Instance{}, Occurrence{}, false, fmt.Errorf("begin alert operation: %w", err)
}
defer func() { _ = tx.Rollback(ctx) }()
var current Instance
if err := scanInstance(tx.QueryRow(ctx, `SELECT id,rule_id,rule_version_id,fingerprint,COALESCE(entity_id::text,''),current_state,retained_state,active_since,recovery_since,cooldown_until,last_evaluated_at,last_known_at,last_value,reason,source_health,COALESCE(acknowledged_by,''),acknowledged_at,revision,created_at,updated_at FROM alert_instances WHERE id=$1 FOR UPDATE`, instanceID), &current); errors.Is(err, ErrInstanceNotFound) {
return Instance{}, Occurrence{}, false, ErrInstanceNotFound
} else if err != nil {
return Instance{}, Occurrence{}, false, err
}
if occurrence, err := scanOccurrence(tx.QueryRow(ctx, `SELECT id,instance_id,evaluation_key,event_type,from_state,to_state,observed_at,value,reason,source_health,created_at FROM alert_occurrences WHERE instance_id=$1 AND evaluation_key=$2`, instanceID, evaluationKey)); err == nil {
if err := tx.Commit(ctx); err != nil {
return Instance{}, Occurrence{}, false, fmt.Errorf("commit idempotent alert operation: %w", err)
}
return current, occurrence, true, nil
} else if !errors.Is(err, pgx.ErrNoRows) {
return Instance{}, Occurrence{}, false, err
}
if current.Revision != expectedRevision {
return Instance{}, Occurrence{}, false, ErrRevisionConflict
}
var transition TransitionResult
if acknowledge {
transition, err = Acknowledge(snapshotFromInstance(current), actor, at.UTC())
} else {
transition, err = Unacknowledge(snapshotFromInstance(current), actor, at.UTC())
}
if err != nil {
return Instance{}, Occurrence{}, false, err
}
valueJSON, err := boundedJSON(current.LastValue, 128<<10)
if err != nil {
return Instance{}, Occurrence{}, false, err
}
healthJSON, err := boundedJSON(nonNilMap(current.SourceHealth), 64<<10)
if err != nil {
return Instance{}, Occurrence{}, false, err
}
acknowledgedBy := nullableText(transition.Snapshot.AcknowledgedBy)
acknowledgedAt := transition.Snapshot.AcknowledgedAt
if _, err := tx.Exec(ctx, `UPDATE alert_instances SET current_state=$1,retained_state=$2,reason=$3,acknowledged_by=$4,acknowledged_at=$5,revision=revision+1,updated_at=now() WHERE id=$6 AND revision=$7`, transition.Snapshot.State, transition.Snapshot.RetainedState, transition.Snapshot.Reason, acknowledgedBy, acknowledgedAt, instanceID, expectedRevision); err != nil {
return Instance{}, Occurrence{}, false, mapStateError(fmt.Errorf("update alert operation: %w", err))
}
occurrence, err := insertOccurrence(ctx, tx, instanceID, evaluationKey, transition, Observation{ObservedAt: at.UTC(), Reason: transition.Snapshot.Reason, Value: current.LastValue}, valueJSON, healthJSON)
if err != nil {
return Instance{}, Occurrence{}, false, err
}
if err := scanInstance(tx.QueryRow(ctx, `SELECT id,rule_id,rule_version_id,fingerprint,COALESCE(entity_id::text,''),current_state,retained_state,active_since,recovery_since,cooldown_until,last_evaluated_at,last_known_at,last_value,reason,source_health,COALESCE(acknowledged_by,''),acknowledged_at,revision,created_at,updated_at FROM alert_instances WHERE id=$1`, instanceID), &current); err != nil {
return Instance{}, Occurrence{}, false, err
}
if err := tx.Commit(ctx); err != nil {
return Instance{}, Occurrence{}, false, fmt.Errorf("commit alert operation: %w", err)
}
return current, occurrence, false, nil
}