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. WorkloadID string // Filter by owning workload (exact match). 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, workload_id, severity, message, metadata, created_at) VALUES (?, ?, ?, ?, ?, ?)`, evt.Source, evt.WorkloadID, 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.WorkloadID != "" { conditions = append(conditions, "workload_id = ?") args = append(args, filter.WorkloadID) } 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, workload_id, 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.WorkloadID, &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. // DeleteEvent removes a single event log entry by ID. func (s *Store) DeleteEvent(id int64) error { result, err := s.db.Exec(`DELETE FROM event_log WHERE id = ?`, id) if err != nil { return fmt.Errorf("delete event: %w", err) } n, _ := result.RowsAffected() if n == 0 { return fmt.Errorf("event %d: %w", id, ErrNotFound) } return nil } // ClearAllEvents removes all event log entries. func (s *Store) ClearAllEvents() (int64, error) { result, err := s.db.Exec(`DELETE FROM event_log`) if err != nil { return 0, fmt.Errorf("clear events: %w", err) } return result.RowsAffected() } 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() }