Files
tiny-forge/internal/store/eventlog.go
T
alexei.dolgolyov c26c41e6a1 feat: enable proxy toggle on quick deploy, event log clearing, and UX fixes
- Add enable_proxy toggle to Quick Deploy form (defaults to on)
- Add DELETE /api/events/log/{id} and DELETE /api/events/log endpoints
- Add Clear All button with confirmation on Events page
- Rename "NPM Proxy" to "Enable Proxy" on stage form (provider-agnostic)
- Fix polling interval validation (min 60s) and number input trim errors
- Fix domain field no longer required in settings
2026-04-05 01:50:19 +03:00

191 lines
5.1 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.
// 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()
}