Initial vibeproxy implementation
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
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
Reference in New Issue
Block a user