This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
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), ¤t); 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), ¤t); 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
|
||||
}
|
||||
Reference in New Issue
Block a user