zai-proxy/dashboard/api/middleware.go
jedarden dee82a76a3 chore: update module paths and add evaluation package
- proxy/go.mod: github.com/ardenone/zai-proxy → git.ardenone.com/jedarden/zai-proxy
- dashboard/go.mod: github.com/ardenone/ardenone-cluster/containers/zai-proxy-dashboard → git.ardenone.com/jedarden/zai-proxy/dashboard
- Update all Go import paths in proxy/ and dashboard/ to match new module paths
- Add proxy/evaluation/ package (was missing from initial commit)
- Add docs/plan/plan.md with architecture, security model, telemetry design, and migration checklist

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 16:03:50 -04:00

115 lines
2.9 KiB
Go

// Package api implements HTTP middleware for the dashboard.
package api
import (
"fmt"
"net/http"
"runtime"
"time"
"git.ardenone.com/jedarden/zai-proxy/dashboard/logger"
)
// Middleware is a function that wraps an http.Handler.
type Middleware func(http.Handler) http.Handler
// Chain applies multiple middleware in order.
func Chain(handler http.Handler, middlewares ...Middleware) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
handler = middlewares[i](handler)
}
return handler
}
// LoggingMiddleware logs all requests with structured JSON logging.
func LoggingMiddleware(next http.Handler) http.Handler {
log := logger.Component("http")
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Create response wrapper to capture status code
rw := &responseWriter{ResponseWriter: w, status: http.StatusOK}
// Process request
next.ServeHTTP(rw, r)
// Calculate duration
duration := time.Since(start)
// Log the request
log.Info("request",
"method", r.Method,
"path", r.URL.Path,
"status", rw.status,
"duration_ms", duration.Milliseconds(),
"remote_addr", r.RemoteAddr,
"user_agent", r.UserAgent(),
"content_length", r.ContentLength,
)
})
}
// RecoveryMiddleware recovers from panics and logs them.
func RecoveryMiddleware(next http.Handler) http.Handler {
log := logger.Component("http")
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
// Get stack trace
buf := make([]byte, 4096)
n := runtime.Stack(buf, false)
stackTrace := string(buf[:n])
log.Error("panic recovered",
"error", fmt.Sprintf("%v", err),
"path", r.URL.Path,
"method", r.Method,
"stack", stackTrace,
)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
// CORSMiddleware adds CORS headers to responses.
func CORSMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Set CORS headers
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
// Handle preflight request
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
// responseWriter wraps http.ResponseWriter to capture status code.
type responseWriter struct {
http.ResponseWriter
status int
}
func (rw *responseWriter) WriteHeader(statusCode int) {
rw.status = statusCode
rw.ResponseWriter.WriteHeader(statusCode)
}
// Flush implements http.Flusher, required for SSE streaming.
func (rw *responseWriter) Flush() {
if f, ok := rw.ResponseWriter.(http.Flusher); ok {
f.Flush()
}
}