package v1 import ( "bufio" "fmt" "log/slog" "net" "net/http" "strings" "time" "github.com/gorilla/mux" ) // statusResponseWriter wraps http.ResponseWriter to capture the status code. // Implements http.Hijacker so WebSocket upgrades work through the proxy. type statusResponseWriter struct { http.ResponseWriter status int } func (w *statusResponseWriter) WriteHeader(code int) { w.status = code w.ResponseWriter.WriteHeader(code) } func (w *statusResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { if hj, ok := w.ResponseWriter.(http.Hijacker); ok { return hj.Hijack() } return nil, nil, fmt.Errorf("upstream ResponseWriter does not implement http.Hijacker") } func (w *statusResponseWriter) Flush() { if fl, ok := w.ResponseWriter.(http.Flusher); ok { fl.Flush() } } // RequestLoggingMiddleware logs method, path, status, and duration for each request. // Long-lived connections (SSE, WebSocket) are excluded. func RequestLoggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { path := r.URL.Path // Skip SSE and WebSocket endpoints (long-lived connections) if strings.HasSuffix(path, "/events") || strings.HasSuffix(path, "/ws") || strings.HasSuffix(path, "/stream") { next.ServeHTTP(w, r) return } start := time.Now() sw := &statusResponseWriter{ResponseWriter: w, status: http.StatusOK} next.ServeHTTP(sw, r) attrs := []any{ "status", sw.status, "method", r.Method, "path", path, "duration", time.Since(start), } // Add route params if present vars := mux.Vars(r) if name := vars["name"]; name != "" { attrs = append(attrs, "instance", name) } if app := vars["app"]; app != "" { attrs = append(attrs, "app", app) } if node := vars["node"]; node != "" { attrs = append(attrs, "node", node) } if sw.status >= 400 { slog.Error("request", attrs...) } else { slog.Info("request", attrs...) } }) }