package alert import ( "context" "errors" "fmt" "strings" "github.com/jackc/pgx/v5" ) type Alert struct { Instance RuleName string `json:"ruleName"` Severity string `json:"severity"` EntityType string `json:"entityType,omitempty"` EntityName string `json:"entityName,omitempty"` Occurrences []Occurrence `json:"occurrences,omitempty"` } type AlertReader interface { ListAlerts(context.Context, int, string) ([]Alert, error) GetAlert(context.Context, string, int) (Alert, error) } func (r StateRepository) ListAlerts(ctx context.Context, limit int, state string) ([]Alert, error) { if r.Pool == nil { return nil, ErrUnavailable } if limit < 1 || limit > 100 { return nil, errors.New("alert limit is invalid") } if state != "" && !validState(State(state)) { return nil, errors.New("alert state filter is invalid") } rows, err := r.Pool.Query(ctx, `SELECT i.id,i.rule_id,i.rule_version_id,i.fingerprint,COALESCE(i.entity_id::text,''),i.current_state,i.retained_state,i.active_since,i.recovery_since,i.cooldown_until,i.last_evaluated_at,i.last_known_at,i.last_value,i.reason,i.source_health,COALESCE(i.acknowledged_by,''),i.acknowledged_at,i.revision,i.created_at,i.updated_at,r.name,r.severity,COALESCE(e.entity_type,''),COALESCE(e.display_name,'') FROM alert_instances i JOIN alert_rules r ON r.id=i.rule_id LEFT JOIN entities e ON e.id=i.entity_id WHERE (($2='' AND i.current_state <> 'inactive') OR ($2<>'' AND i.current_state=$2)) ORDER BY CASE i.current_state WHEN 'firing' THEN 1 WHEN 'acknowledged' THEN 2 WHEN 'pending' THEN 3 WHEN 'unknown' THEN 4 WHEN 'resolved' THEN 5 ELSE 6 END,i.updated_at DESC,i.id ASC LIMIT $1`, limit, state) if err != nil { return nil, fmt.Errorf("list alerts: %w", err) } defer rows.Close() items := make([]Alert, 0, limit) for rows.Next() { item, err := scanAlert(rows) if err != nil { return nil, err } items = append(items, item) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("iterate alerts: %w", err) } return items, nil } func (r StateRepository) GetAlert(ctx context.Context, id string, occurrenceLimit int) (Alert, error) { if r.Pool == nil { return Alert{}, ErrUnavailable } if strings.TrimSpace(id) == "" { return Alert{}, ErrInstanceNotFound } if occurrenceLimit < 1 || occurrenceLimit > 500 { return Alert{}, errors.New("alert occurrence limit is invalid") } row := r.Pool.QueryRow(ctx, `SELECT i.id,i.rule_id,i.rule_version_id,i.fingerprint,COALESCE(i.entity_id::text,''),i.current_state,i.retained_state,i.active_since,i.recovery_since,i.cooldown_until,i.last_evaluated_at,i.last_known_at,i.last_value,i.reason,i.source_health,COALESCE(i.acknowledged_by,''),i.acknowledged_at,i.revision,i.created_at,i.updated_at,r.name,r.severity,COALESCE(e.entity_type,''),COALESCE(e.display_name,'') FROM alert_instances i JOIN alert_rules r ON r.id=i.rule_id LEFT JOIN entities e ON e.id=i.entity_id WHERE i.id=$1`, id) item, err := scanAlert(row) if errors.Is(err, ErrInstanceNotFound) || errors.Is(err, pgx.ErrNoRows) { return Alert{}, ErrInstanceNotFound } if err != nil { return Alert{}, fmt.Errorf("get alert: %w", err) } occurrences, err := r.ListOccurrences(ctx, id, occurrenceLimit) if err != nil { return Alert{}, err } item.Occurrences = occurrences return item, nil } type alertRow interface{ Scan(...any) error } func scanAlert(row alertRow) (Alert, error) { var item Alert var valueJSON, healthJSON []byte var state, retained State err := row.Scan(&item.ID, &item.RuleID, &item.RuleVersionID, &item.Fingerprint, &item.EntityID, &state, &retained, &item.ActiveSince, &item.RecoverySince, &item.CooldownUntil, &item.LastEvaluatedAt, &item.LastKnownAt, &valueJSON, &item.Reason, &healthJSON, &item.AcknowledgedBy, &item.AcknowledgedAt, &item.Revision, &item.CreatedAt, &item.UpdatedAt, &item.RuleName, &item.Severity, &item.EntityType, &item.EntityName) if errors.Is(err, pgx.ErrNoRows) { return Alert{}, ErrInstanceNotFound } if err != nil { return Alert{}, fmt.Errorf("scan alert: %w", err) } item.State, item.RetainedState = state, retained item.LastValue, err = decodeJSON(valueJSON) if err != nil { return Alert{}, fmt.Errorf("decode alert value: %w", err) } item.SourceHealth, err = decodeMap(healthJSON) if err != nil { return Alert{}, fmt.Errorf("decode alert source health: %w", err) } return item, nil }