Files
mcp/internal/registry/events.go
Kyle Isom 6122123064 P1.1: Registry package with full CRUD and tests
SQLite schema (services, components, ports, volumes, cmd, events),
migrations, and complete CRUD operations. 7 tests covering:
idempotent migration, service CRUD, duplicate name rejection,
component CRUD with ports/volumes/cmd, composite PK enforcement,
cascade delete, and event insert/query/count/prune.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:18:35 -07:00

97 lines
2.7 KiB
Go

package registry
import (
"database/sql"
"fmt"
"time"
)
// Event represents a state-change event.
type Event struct {
ID int64
Service string
Component string
PrevState string
NewState string
Timestamp time.Time
}
// InsertEvent records a state transition.
func InsertEvent(db *sql.DB, service, component, prevState, newState string) error {
_, err := db.Exec(
"INSERT INTO events (service, component, prev_state, new_state) VALUES (?, ?, ?, ?)",
service, component, prevState, newState,
)
if err != nil {
return fmt.Errorf("insert event %q/%q: %w", service, component, err)
}
return nil
}
// QueryEvents returns events for a service/component within a time window.
// If service is empty, returns events for all services.
// If component is empty, returns events for all components in the service.
func QueryEvents(db *sql.DB, service, component string, since time.Time, limit int) ([]Event, error) {
query := "SELECT id, service, component, prev_state, new_state, timestamp FROM events WHERE timestamp > ?"
args := []any{since.UTC().Format("2006-01-02 15:04:05")}
if service != "" {
query += " AND service = ?"
args = append(args, service)
}
if component != "" {
query += " AND component = ?"
args = append(args, component)
}
query += " ORDER BY timestamp DESC"
if limit > 0 {
query += " LIMIT ?"
args = append(args, limit)
}
rows, err := db.Query(query, args...)
if err != nil {
return nil, fmt.Errorf("query events: %w", err)
}
defer func() { _ = rows.Close() }()
var events []Event
for rows.Next() {
var e Event
var ts string
if err := rows.Scan(&e.ID, &e.Service, &e.Component, &e.PrevState, &e.NewState, &ts); err != nil {
return nil, fmt.Errorf("scan event: %w", err)
}
e.Timestamp, _ = time.Parse("2006-01-02 15:04:05", ts)
events = append(events, e)
}
return events, rows.Err()
}
// CountEvents counts events for a component within a time window matching
// a specific new_state. Used for flap detection.
func CountEvents(db *sql.DB, service, component string, since time.Time) (int, error) {
var count int
err := db.QueryRow(
"SELECT COUNT(*) FROM events WHERE service = ? AND component = ? AND timestamp > ?",
service, component, since.UTC().Format("2006-01-02 15:04:05"),
).Scan(&count)
if err != nil {
return 0, fmt.Errorf("count events %q/%q: %w", service, component, err)
}
return count, nil
}
// PruneEvents deletes events older than the given time.
func PruneEvents(db *sql.DB, before time.Time) (int64, error) {
res, err := db.Exec(
"DELETE FROM events WHERE timestamp < ?",
before.UTC().Format("2006-01-02 15:04:05"),
)
if err != nil {
return 0, fmt.Errorf("prune events: %w", err)
}
return res.RowsAffected()
}