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:
2026-08-17 10:28:59 -05:00
co-authored by Claude Sonnet 5
commit ba1f7c4c03
105 changed files with 9142 additions and 0 deletions
+94
View File
@@ -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)
}
}
}
}()
}