feat: Phase 5bc — SignalService + ObservationService + MetricsRepo/SignalRepo
Problem: signal lifecycle (upsert, resolve, health aggregation) and observe-pass orchestration (load checks, resolve targets, run probes, aggregate health) were embedded in scheduler/scheduler.go — 1095 lines of monolith with no port abstraction. Change: - app/signals.go: SignalService — ProcessCheckResult evaluates probe outcomes (upserts signals on critical/warning, resolves on ok), records metrics, computes health changes (ok/degraded/down/stale). WorstHealthForTarget aggregates open signals into entity health. - app/observation.go: ObservationService — RunPass loads enabled checks via CheckRepository, resolves targets via TargetResolver, dispatches probes through CheckerLookup (probes.Registry) with bounded concurrency (default 10), sends results through SignalService. - adapters/postgres/signals.go: MetricsRepo (InsertSamples via sqlcgen InsertMetricSample), SignalRepo (Open/UpsertWithTriggers/ Transition with inline SQL matching the scheduler's patterns). Verification: go build/vet, full test suite (18 pkgs green), DB integration (postgres + mcp — green).
This commit is contained in:
88
internal/adapters/postgres/signals.go
Normal file
88
internal/adapters/postgres/signals.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/dtoro/oikos/internal/adapters/postgres/sqlcgen"
|
||||
"github.com/dtoro/oikos/internal/core/domain"
|
||||
"github.com/dtoro/oikos/internal/core/ports"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// MetricsRepo implements ports.MetricsRepository.
|
||||
type MetricsRepo struct {
|
||||
pool *Pool
|
||||
}
|
||||
|
||||
var _ ports.MetricsRepository = (*MetricsRepo)(nil)
|
||||
|
||||
func NewMetricsRepo(pool *Pool) *MetricsRepo { return &MetricsRepo{pool: pool} }
|
||||
|
||||
func (m *MetricsRepo) InsertSamples(ctx context.Context, entityID domain.UUID, samples []ports.MetricSample) error {
|
||||
for _, s := range samples {
|
||||
if err := sqlcgen.New(m.pool).InsertMetricSample(ctx, sqlcgen.InsertMetricSampleParams{
|
||||
EntityID: mustUUID(entityID), Metric: s.Metric, Value: s.Value,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SignalRepo implements ports.SignalRepository with inline SQL.
|
||||
type SignalRepo struct {
|
||||
pool *Pool
|
||||
}
|
||||
|
||||
var _ ports.SignalRepository = (*SignalRepo)(nil)
|
||||
|
||||
func NewSignalRepo(pool *Pool) *SignalRepo { return &SignalRepo{pool: pool} }
|
||||
|
||||
func (r *SignalRepo) Open(ctx context.Context) ([]domain.Signal, error) {
|
||||
rows, err := r.pool.Query(ctx,
|
||||
`SELECT entity_id, kind, severity, state FROM signals WHERE state NOT IN ('resolved','failed') ORDER BY severity DESC`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []domain.Signal
|
||||
for rows.Next() {
|
||||
var s domain.Signal
|
||||
var id uuid.UUID
|
||||
if err := rows.Scan(&id, &s.Kind, &s.Severity, &s.State); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.EntityID = domain.UUID(id.String())
|
||||
items = append(items, s)
|
||||
}
|
||||
return items, rows.Err()
|
||||
}
|
||||
|
||||
func (r *SignalRepo) History(ctx context.Context, entityID domain.UUID, limit int) ([]domain.Signal, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *SignalRepo) UpsertWithTriggers(ctx context.Context, input ports.SignalUpsertInput) error {
|
||||
eid := mustUUID(input.Signal.EntityID)
|
||||
_, err := r.pool.Exec(ctx,
|
||||
`INSERT INTO signals (entity_id, kind, severity, target_entity_id, state)
|
||||
VALUES ($1, $2, $3, $4, 'raised')
|
||||
ON CONFLICT (target_entity_id, kind) WHERE state NOT IN ('resolved','failed')
|
||||
DO UPDATE SET occurrence_count = signals.occurrence_count + 1,
|
||||
last_seen_at = now(), updated_at = now()`,
|
||||
eid, input.Signal.Kind, input.Signal.Severity, eid)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *SignalRepo) Transition(ctx context.Context, input ports.SignalTransitionInput) (domain.Signal, error) {
|
||||
tag, err := r.pool.Exec(ctx,
|
||||
`UPDATE signals SET state = 'resolved', updated_at = now()
|
||||
WHERE entity_id = $1 AND state IN ('raised', 'acknowledged')`, mustUUID(input.SignalID))
|
||||
if err != nil {
|
||||
return domain.Signal{}, err
|
||||
}
|
||||
if tag.RowsAffected() == 0 {
|
||||
return domain.Signal{}, domain.ErrNotFound
|
||||
}
|
||||
return domain.Signal{}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user