// Package logging buffers HTTP request log entries and writes them to // SQLite in batches so the hot request path never blocks on a DB write; it // also runs the retention cleanup job and provides audit-log helpers. package logging import ( "context" "database/sql" "log/slog" "time" "github.com/owen/vibeproxy/internal/database" "github.com/owen/vibeproxy/internal/models" ) type RequestLogger struct { db *sql.DB log *slog.Logger ch chan models.RequestLog } func NewRequestLogger(db *sql.DB, log *slog.Logger) *RequestLogger { return &RequestLogger{db: db, log: log, ch: make(chan models.RequestLog, 2048)} } // Log enqueues an entry; if the buffer is full the entry is dropped rather // than blocking the proxy hot path (a slow logger must never slow down // traffic). func (l *RequestLogger) Log(entry models.RequestLog) { select { case l.ch <- entry: default: l.log.Warn("request log buffer full, dropping entry") } } // Run drains the buffer, flushing in batches, until ctx is cancelled. func (l *RequestLogger) Run(ctx context.Context) { const batchSize = 100 ticker := time.NewTicker(2 * time.Second) defer ticker.Stop() batch := make([]models.RequestLog, 0, batchSize) flush := func() { if len(batch) == 0 { return } if err := database.InsertRequestLogBatch(l.db, batch); err != nil { l.log.Error("request log flush failed", "err", err) } batch = batch[:0] } for { select { case <-ctx.Done(): flush() return case e := <-l.ch: batch = append(batch, e) if len(batch) >= batchSize { flush() } case <-ticker.C: flush() } } } // StartRetentionLoop periodically deletes request logs older than // retentionDays so the database does not grow without bound. func StartRetentionLoop(ctx context.Context, db *sql.DB, log *slog.Logger, getRetentionDays func() int) { ticker := time.NewTicker(1 * time.Hour) go func() { defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: days := getRetentionDays() if days <= 0 { continue } n, err := database.DeleteOldRequestLogs(db, days) if err != nil { log.Error("log retention cleanup failed", "err", err) } else if n > 0 { log.Info("log retention cleanup", "deleted", n) } } } }() }