zai-proxy/dashboard/api/middleware.go
jedarden e7c24a0c08 feat: initial zai-proxy ecosystem repo
Extracted from ardenone-cluster/containers/zai-proxy and
ardenone-cluster/containers/zai-proxy-dashboard.

- proxy/: OpenAI-compatible ZAI reverse proxy (Go, v1.10.0)
  - Token counting, rate limiting, Prometheus metrics, canary support
- dashboard/: Metrics dashboard backend + React frontend (Go, v1.0.0)
  - Prometheus collector, SQLite storage, SSE live updates
- docs/: Operational notes, research, and plan subdirs

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

115 lines
2.9 KiB
Go

// Package api implements HTTP middleware for the dashboard.
package api
import (
"fmt"
"net/http"
"runtime"
"time"
"github.com/ardenone/ardenone-cluster/containers/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()
}
}