e0a648fb0c
Critical fixes: - Fix StaleContainer frontend type to match nested backend response shape - Guard ContainerID[:12] slice against empty/short IDs in ListAllProxies High-priority fixes: - Support comma-separated severity/source in event log filtering (IN clause) - Eliminate N+1 queries in ListAllProxies and FindStaleInstances (pre-load maps) - Stop leaking internal error messages to API clients (use slog + generic msgs)
169 lines
4.5 KiB
Go
169 lines
4.5 KiB
Go
package store
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// EventLogFilter holds optional filters for listing event log entries.
|
|
type EventLogFilter struct {
|
|
Severity string // Filter by severity (info, warn, error).
|
|
Source string // Filter by source.
|
|
Since string // Only events created at or after this timestamp.
|
|
Until string // Only events created at or before this timestamp.
|
|
Limit int // Maximum number of results (default 50).
|
|
Offset int // Offset for pagination.
|
|
}
|
|
|
|
// EventLogStats holds counts of event log entries by severity.
|
|
type EventLogStats struct {
|
|
Info int `json:"info"`
|
|
Warn int `json:"warn"`
|
|
Error int `json:"error"`
|
|
Total int `json:"total"`
|
|
}
|
|
|
|
// InsertEvent inserts a new event log entry.
|
|
func (s *Store) InsertEvent(evt EventLog) (EventLog, error) {
|
|
evt.CreatedAt = Now()
|
|
if evt.Metadata == "" {
|
|
evt.Metadata = "{}"
|
|
}
|
|
|
|
result, err := s.db.Exec(
|
|
`INSERT INTO event_log (source, severity, message, metadata, created_at)
|
|
VALUES (?, ?, ?, ?, ?)`,
|
|
evt.Source, evt.Severity, evt.Message, evt.Metadata, evt.CreatedAt,
|
|
)
|
|
if err != nil {
|
|
return EventLog{}, fmt.Errorf("insert event: %w", err)
|
|
}
|
|
|
|
id, err := result.LastInsertId()
|
|
if err != nil {
|
|
return EventLog{}, fmt.Errorf("get event id: %w", err)
|
|
}
|
|
evt.ID = id
|
|
|
|
return evt, nil
|
|
}
|
|
|
|
// ListEvents returns event log entries matching the given filter.
|
|
func (s *Store) ListEvents(filter EventLogFilter) ([]EventLog, error) {
|
|
var conditions []string
|
|
var args []any
|
|
|
|
if filter.Severity != "" {
|
|
parts := strings.Split(filter.Severity, ",")
|
|
if len(parts) == 1 {
|
|
conditions = append(conditions, "severity = ?")
|
|
args = append(args, filter.Severity)
|
|
} else {
|
|
placeholders := make([]string, len(parts))
|
|
for i, p := range parts {
|
|
placeholders[i] = "?"
|
|
args = append(args, strings.TrimSpace(p))
|
|
}
|
|
conditions = append(conditions, "severity IN ("+strings.Join(placeholders, ",")+")")
|
|
}
|
|
}
|
|
if filter.Source != "" {
|
|
parts := strings.Split(filter.Source, ",")
|
|
if len(parts) == 1 {
|
|
conditions = append(conditions, "source = ?")
|
|
args = append(args, filter.Source)
|
|
} else {
|
|
placeholders := make([]string, len(parts))
|
|
for i, p := range parts {
|
|
placeholders[i] = "?"
|
|
args = append(args, strings.TrimSpace(p))
|
|
}
|
|
conditions = append(conditions, "source IN ("+strings.Join(placeholders, ",")+")")
|
|
}
|
|
}
|
|
if filter.Since != "" {
|
|
conditions = append(conditions, "created_at >= ?")
|
|
args = append(args, filter.Since)
|
|
}
|
|
if filter.Until != "" {
|
|
conditions = append(conditions, "created_at <= ?")
|
|
args = append(args, filter.Until)
|
|
}
|
|
|
|
query := "SELECT id, source, severity, message, metadata, created_at FROM event_log"
|
|
if len(conditions) > 0 {
|
|
query += " WHERE " + strings.Join(conditions, " AND ")
|
|
}
|
|
query += " ORDER BY created_at DESC"
|
|
|
|
limit := filter.Limit
|
|
if limit <= 0 {
|
|
limit = 50
|
|
}
|
|
if limit > 500 {
|
|
limit = 500
|
|
}
|
|
query += fmt.Sprintf(" LIMIT %d OFFSET %d", limit, filter.Offset)
|
|
|
|
rows, err := s.db.Query(query, args...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("query events: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
events := []EventLog{}
|
|
for rows.Next() {
|
|
var evt EventLog
|
|
if err := rows.Scan(&evt.ID, &evt.Source, &evt.Severity, &evt.Message, &evt.Metadata, &evt.CreatedAt); err != nil {
|
|
return nil, fmt.Errorf("scan event: %w", err)
|
|
}
|
|
events = append(events, evt)
|
|
}
|
|
return events, rows.Err()
|
|
}
|
|
|
|
// GetEventStats returns counts of event log entries grouped by severity.
|
|
func (s *Store) GetEventStats() (EventLogStats, error) {
|
|
rows, err := s.db.Query(
|
|
`SELECT severity, COUNT(*) FROM event_log GROUP BY severity`,
|
|
)
|
|
if err != nil {
|
|
return EventLogStats{}, fmt.Errorf("query event stats: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var stats EventLogStats
|
|
for rows.Next() {
|
|
var severity string
|
|
var count int
|
|
if err := rows.Scan(&severity, &count); err != nil {
|
|
return EventLogStats{}, fmt.Errorf("scan event stats: %w", err)
|
|
}
|
|
switch severity {
|
|
case "info":
|
|
stats.Info = count
|
|
case "warn":
|
|
stats.Warn = count
|
|
case "error":
|
|
stats.Error = count
|
|
}
|
|
stats.Total += count
|
|
}
|
|
return stats, rows.Err()
|
|
}
|
|
|
|
// PruneEvents deletes event log entries older than the given number of days.
|
|
func (s *Store) PruneEvents(olderThanDays int) (int64, error) {
|
|
if olderThanDays < 1 {
|
|
return 0, fmt.Errorf("prune events: olderThanDays must be >= 1, got %d", olderThanDays)
|
|
}
|
|
result, err := s.db.Exec(
|
|
`DELETE FROM event_log WHERE created_at < datetime('now', ?)`,
|
|
fmt.Sprintf("-%d days", olderThanDays),
|
|
)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("prune events: %w", err)
|
|
}
|
|
return result.RowsAffected()
|
|
}
|