Files
mcp/internal/registry/events.go
Kyle Isom 1e58dcce27 Implement mcp purge command for registry cleanup
Add PurgeComponent RPC to the agent service that removes stale registry
entries for components that are both gone (observed state is removed,
unknown, or exited) and unwanted (not in any current service definition).
Refuses to purge components with running or stopped containers. When all
components of a service are purged, the service row is deleted too.
Supports --dry-run to preview without modifying the database.

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

106 lines
3.0 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
}
// DeleteComponentEvents deletes all events for a specific component.
func DeleteComponentEvents(db *sql.DB, service, component string) error {
_, err := db.Exec("DELETE FROM events WHERE service = ? AND component = ?", service, component)
if err != nil {
return fmt.Errorf("delete events %q/%q: %w", service, component, err)
}
return 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()
}