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>
106 lines
3.0 KiB
Go
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()
|
|
}
|