feat(observability): phase 1 - schema, models & event log backend
Add database foundation for observability features: - event_log table with severity/source filtering and pagination - standalone_proxies table for user-created reverse proxies - stale_threshold_days setting (default 7 days) - Auto-persist warn/error events from event bus to database - SSE broadcast of persistent events for real-time UI updates - Frontend types and API functions for downstream UI phases
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
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 != "" {
|
||||
conditions = append(conditions, "severity = ?")
|
||||
args = append(args, filter.Severity)
|
||||
}
|
||||
if filter.Source != "" {
|
||||
conditions = append(conditions, "source = ?")
|
||||
args = append(args, filter.Source)
|
||||
}
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user