package httpapi import ( "context" "fmt" "github.com/dtoro/oikos/internal/domain" "github.com/dtoro/oikos/internal/httpapi/gen" "github.com/jackc/pgx/v5" ) func (s *Server) ListSignals(ctx context.Context, req gen.ListSignalsRequestObject) (gen.ListSignalsResponseObject, error) { limit := clampLimit(req.Params.Limit) rows, err := s.pool.Query(ctx, ` SELECT sig.entity_id, se.slug, sig.kind, sig.severity, sig.state, te.slug, sig.check_id::text, sig.evidence, sig.likely_cause, sig.occurrence_count, sig.flap_count, sig.hold_down_until, sig.mute_until, sig.first_seen_at, sig.last_seen_at FROM signals sig JOIN entities se ON se.id = sig.entity_id LEFT JOIN entities te ON te.id = sig.target_entity_id WHERE ($1::text IS NULL OR sig.state = $1) AND ($2::text IS NULL OR sig.severity = $2) AND ($3::text IS NULL OR te.slug = $3) AND ($4::text IS NULL OR sig.kind = $4) AND ($5::text IS NULL OR se.slug > $5) ORDER BY se.slug LIMIT $6`, req.Params.State, (*string)(req.Params.Severity), req.Params.EntityId, req.Params.Kind, req.Params.Cursor, limit+1) if err != nil { return nil, err } defer rows.Close() items := []gen.Signal{} for rows.Next() { var sig gen.Signal var flap int if err := rows.Scan(&sig.Id, &sig.Slug, &sig.Kind, &sig.Severity, &sig.State, &sig.Target, &sig.CheckId, &sig.Evidence, &sig.LikelyCause, &sig.OccurrenceCount, &flap, &sig.HoldDownUntil, &sig.MuteUntil, &sig.FirstSeenAt, &sig.LastSeenAt); err != nil { return nil, err } sig.FlapCount = &flap items = append(items, sig) } if rows.Err() != nil { return nil, rows.Err() } var next *string if len(items) > limit { items = items[:limit] next = &items[len(items)-1].Slug } return gen.ListSignals200JSONResponse{Items: items, NextCursor: next}, nil } func (s *Server) AckSignal(ctx context.Context, req gen.AckSignalRequestObject) (gen.AckSignalResponseObject, error) { id, err := s.resolveEntityID(ctx, req.Id) if err != nil { return nil, err } tx, err := s.pool.Begin(ctx) if err != nil { return nil, err } defer tx.Rollback(ctx) var sig gen.Signal err = tx.QueryRow(ctx, ` UPDATE signals SET state = 'acknowledged', updated_at = now() WHERE entity_id = $1 AND state IN ('raised','failed') RETURNING entity_id, (SELECT slug FROM entities WHERE id = $1), kind, severity, 'acknowledged', (SELECT slug FROM entities WHERE id = target_entity_id), check_id::text, evidence, likely_cause, occurrence_count, flap_count, hold_down_until, mute_until, first_seen_at, last_seen_at`, id).Scan(&sig.Id, &sig.Slug, &sig.Kind, &sig.Severity, &sig.State, &sig.Target, &sig.CheckId, &sig.Evidence, &sig.LikelyCause, &sig.OccurrenceCount, &sig.FlapCount, &sig.HoldDownUntil, &sig.MuteUntil, &sig.FirstSeenAt, &sig.LastSeenAt) if err != nil { if err == pgx.ErrNoRows { return nil, fmt.Errorf("%w: signal %s not in a state that can be acknowledged", domain.ErrInvalidTransition, req.Id) } return nil, err } if err := tx.Commit(ctx); err != nil { return nil, err } return gen.AckSignal200JSONResponse{SignalUpdatedJSONResponse: gen.SignalUpdatedJSONResponse(sig)}, nil } func (s *Server) ResolveSignal(ctx context.Context, req gen.ResolveSignalRequestObject) (gen.ResolveSignalResponseObject, error) { id, err := s.resolveEntityID(ctx, req.Id) if err != nil { return nil, err } tx, err := s.pool.Begin(ctx) if err != nil { return nil, err } defer tx.Rollback(ctx) var sig gen.Signal err = tx.QueryRow(ctx, ` UPDATE signals SET state = 'resolved', updated_at = now() WHERE entity_id = $1 AND state IN ('raised','acknowledged','acting','failed') RETURNING entity_id, (SELECT slug FROM entities WHERE id = $1), kind, severity, 'resolved', (SELECT slug FROM entities WHERE id = target_entity_id), check_id::text, evidence, likely_cause, occurrence_count, flap_count, hold_down_until, mute_until, first_seen_at, last_seen_at`, id).Scan(&sig.Id, &sig.Slug, &sig.Kind, &sig.Severity, &sig.State, &sig.Target, &sig.CheckId, &sig.Evidence, &sig.LikelyCause, &sig.OccurrenceCount, &sig.FlapCount, &sig.HoldDownUntil, &sig.MuteUntil, &sig.FirstSeenAt, &sig.LastSeenAt) if err != nil { if err == pgx.ErrNoRows { return nil, fmt.Errorf("%w: signal %s not in a state that can be resolved", domain.ErrInvalidTransition, req.Id) } return nil, err } if err := tx.Commit(ctx); err != nil { return nil, err } return gen.ResolveSignal200JSONResponse{SignalUpdatedJSONResponse: gen.SignalUpdatedJSONResponse(sig)}, nil } func (s *Server) MuteSignal(ctx context.Context, req gen.MuteSignalRequestObject) (gen.MuteSignalResponseObject, error) { id, err := s.resolveEntityID(ctx, req.Id) if err != nil { return nil, err } tx, err := s.pool.Begin(ctx) if err != nil { return nil, err } defer tx.Rollback(ctx) var sig gen.Signal err = tx.QueryRow(ctx, ` UPDATE signals SET state = 'muted', mute_until = $2, updated_at = now() WHERE entity_id = $1 AND state IN ('raised','acknowledged') RETURNING entity_id, (SELECT slug FROM entities WHERE id = $1), kind, severity, 'muted', (SELECT slug FROM entities WHERE id = target_entity_id), check_id::text, evidence, likely_cause, occurrence_count, flap_count, hold_down_until, mute_until, first_seen_at, last_seen_at`, id, req.Body.MuteUntil).Scan(&sig.Id, &sig.Slug, &sig.Kind, &sig.Severity, &sig.State, &sig.Target, &sig.CheckId, &sig.Evidence, &sig.LikelyCause, &sig.OccurrenceCount, &sig.FlapCount, &sig.HoldDownUntil, &sig.MuteUntil, &sig.FirstSeenAt, &sig.LastSeenAt) if err != nil { if err == pgx.ErrNoRows { return nil, fmt.Errorf("%w: signal %s not in a state that can be muted", domain.ErrInvalidTransition, req.Id) } return nil, err } if err := tx.Commit(ctx); err != nil { return nil, err } return gen.MuteSignal200JSONResponse{SignalUpdatedJSONResponse: gen.SignalUpdatedJSONResponse(sig)}, nil }