Self-contained HTTP/HTTPS/TCP/UDP reverse proxy manager written in Go, with SQLite storage, a Bootstrap 5 admin UI (Go templates, no JS framework), and a REST API under /api/v1. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QTXFrNP1imCW7jtfL5SZrw
95 lines
2.2 KiB
Go
95 lines
2.2 KiB
Go
// 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)
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
}
|