- 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>
115 lines
2.9 KiB
Go
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()
|
|
}
|
|
}
|
|
|