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
+220
View File
@@ -0,0 +1,220 @@
package database
import (
"database/sql"
"github.com/owen/vibeproxy/internal/models"
)
func InsertRequestLog(db *sql.DB, l models.RequestLog) error {
_, err := db.Exec(`INSERT INTO request_logs (timestamp, proxy_host_id, proxy_host_name, client_ip, method, host,
path, status, response_size, backend, response_time_ms, user_agent) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`,
l.Timestamp.UTC().Format(timeFmt), nullInt64(l.ProxyHostID), l.ProxyHostName, l.ClientIP, l.Method, l.Host,
l.Path, l.Status, l.ResponseSize, l.Backend, l.ResponseTimeMS, l.UserAgent)
return err
}
func InsertRequestLogBatch(db *sql.DB, logs []models.RequestLog) error {
if len(logs) == 0 {
return nil
}
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
stmt, err := tx.Prepare(`INSERT INTO request_logs (timestamp, proxy_host_id, proxy_host_name, client_ip, method, host,
path, status, response_size, backend, response_time_ms, user_agent) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`)
if err != nil {
return err
}
defer stmt.Close()
for _, l := range logs {
if _, err := stmt.Exec(l.Timestamp.UTC().Format(timeFmt), nullInt64(l.ProxyHostID), l.ProxyHostName, l.ClientIP,
l.Method, l.Host, l.Path, l.Status, l.ResponseSize, l.Backend, l.ResponseTimeMS, l.UserAgent); err != nil {
return err
}
}
return tx.Commit()
}
type LogFilter struct {
ProxyHostID *int64
Status int
Search string
Limit int
Offset int
}
func QueryRequestLogs(db *sql.DB, f LogFilter) ([]models.RequestLog, int, error) {
where := "WHERE 1=1"
var args []any
if f.ProxyHostID != nil {
where += " AND proxy_host_id = ?"
args = append(args, *f.ProxyHostID)
}
if f.Status > 0 {
where += " AND status = ?"
args = append(args, f.Status)
}
if f.Search != "" {
where += " AND (host LIKE ? OR path LIKE ? OR client_ip LIKE ?)"
s := "%" + f.Search + "%"
args = append(args, s, s, s)
}
var total int
if err := db.QueryRow(`SELECT COUNT(*) FROM request_logs `+where, args...).Scan(&total); err != nil {
return nil, 0, err
}
limit := f.Limit
if limit <= 0 || limit > 500 {
limit = 50
}
q := `SELECT id, timestamp, proxy_host_id, proxy_host_name, client_ip, method, host, path, status, response_size,
backend, response_time_ms, user_agent FROM request_logs ` + where + ` ORDER BY id DESC LIMIT ? OFFSET ?`
args = append(args, limit, f.Offset)
rows, err := db.Query(q, args...)
if err != nil {
return nil, 0, err
}
defer rows.Close()
var out []models.RequestLog
for rows.Next() {
var l models.RequestLog
var ts string
var hostID sql.NullInt64
if err := rows.Scan(&l.ID, &ts, &hostID, &l.ProxyHostName, &l.ClientIP, &l.Method, &l.Host, &l.Path, &l.Status,
&l.ResponseSize, &l.Backend, &l.ResponseTimeMS, &l.UserAgent); err != nil {
return nil, 0, err
}
l.Timestamp = parseTime(ts)
if hostID.Valid {
v := hostID.Int64
l.ProxyHostID = &v
}
out = append(out, l)
}
return out, total, rows.Err()
}
func DeleteOldRequestLogs(db *sql.DB, retentionDays int) (int64, error) {
res, err := db.Exec(`DELETE FROM request_logs WHERE timestamp < strftime('%Y-%m-%dT%H:%M:%fZ', 'now', ?)`, "-"+itoa(retentionDays)+" days")
if err != nil {
return 0, err
}
return res.RowsAffected()
}
func itoa(i int) string {
if i < 0 {
i = 0
}
digits := []byte{}
if i == 0 {
return "0"
}
for i > 0 {
digits = append([]byte{byte('0' + i%10)}, digits...)
i /= 10
}
return string(digits)
}
// Stats
func CountRequestLogsSince(db *sql.DB, sinceExpr string) (int, error) {
var n int
err := db.QueryRow(`SELECT COUNT(*) FROM request_logs WHERE timestamp >= strftime('%Y-%m-%dT%H:%M:%fZ', 'now', ?)`, sinceExpr).Scan(&n)
return n, err
}
type StatusCount struct {
Status int
Count int
}
func StatusBreakdownSince(db *sql.DB, sinceExpr string) ([]StatusCount, error) {
rows, err := db.Query(`SELECT status, COUNT(*) FROM request_logs WHERE timestamp >= strftime('%Y-%m-%dT%H:%M:%fZ', 'now', ?) GROUP BY status ORDER BY status`, sinceExpr)
if err != nil {
return nil, err
}
defer rows.Close()
var out []StatusCount
for rows.Next() {
var s StatusCount
if err := rows.Scan(&s.Status, &s.Count); err != nil {
return nil, err
}
out = append(out, s)
}
return out, rows.Err()
}
type HostCount struct {
Host string
Count int
}
func TopHostsSince(db *sql.DB, sinceExpr string, limit int) ([]HostCount, error) {
rows, err := db.Query(`SELECT host, COUNT(*) c FROM request_logs WHERE timestamp >= strftime('%Y-%m-%dT%H:%M:%fZ', 'now', ?) GROUP BY host ORDER BY c DESC LIMIT ?`, sinceExpr, limit)
if err != nil {
return nil, err
}
defer rows.Close()
var out []HostCount
for rows.Next() {
var h HostCount
if err := rows.Scan(&h.Host, &h.Count); err != nil {
return nil, err
}
out = append(out, h)
}
return out, rows.Err()
}
func TopClientsSince(db *sql.DB, sinceExpr string, limit int) ([]HostCount, error) {
rows, err := db.Query(`SELECT client_ip, COUNT(*) c FROM request_logs WHERE timestamp >= strftime('%Y-%m-%dT%H:%M:%fZ', 'now', ?) GROUP BY client_ip ORDER BY c DESC LIMIT ?`, sinceExpr, limit)
if err != nil {
return nil, err
}
defer rows.Close()
var out []HostCount
for rows.Next() {
var h HostCount
if err := rows.Scan(&h.Host, &h.Count); err != nil {
return nil, err
}
out = append(out, h)
}
return out, rows.Err()
}
func RecentErrorsSince(db *sql.DB, sinceExpr string, limit int) ([]models.RequestLog, error) {
rows, err := db.Query(`SELECT id, timestamp, proxy_host_id, proxy_host_name, client_ip, method, host, path, status,
response_size, backend, response_time_ms, user_agent FROM request_logs
WHERE status >= 400 AND timestamp >= strftime('%Y-%m-%dT%H:%M:%fZ', 'now', ?) ORDER BY id DESC LIMIT ?`, sinceExpr, limit)
if err != nil {
return nil, err
}
defer rows.Close()
var out []models.RequestLog
for rows.Next() {
var l models.RequestLog
var ts string
var hostID sql.NullInt64
if err := rows.Scan(&l.ID, &ts, &hostID, &l.ProxyHostName, &l.ClientIP, &l.Method, &l.Host, &l.Path, &l.Status,
&l.ResponseSize, &l.Backend, &l.ResponseTimeMS, &l.UserAgent); err != nil {
return nil, err
}
l.Timestamp = parseTime(ts)
if hostID.Valid {
v := hostID.Int64
l.ProxyHostID = &v
}
out = append(out, l)
}
return out, rows.Err()
}