package middleware import ( "crypto/rand" "encoding/hex" "log/slog" "net/http" "time" ) // responseWriter wraps http.ResponseWriter to capture the status code. type responseWriter struct { http.ResponseWriter statusCode int } func (rw *responseWriter) WriteHeader(code int) { rw.statusCode = code rw.ResponseWriter.WriteHeader(code) } // generateRequestID creates a short random hex string for request tracing. func generateRequestID() string { b := make([]byte, 8) if _, err := rand.Read(b); err != nil { return "unknown" } return hex.EncodeToString(b) } // Logging returns middleware that logs each HTTP request with structured fields: // method, path, status, duration (ms), request-id, and remote address. func Logging() func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() requestID := generateRequestID() rw := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK} // Set request ID header for downstream correlation. w.Header().Set("X-Request-ID", requestID) next.ServeHTTP(rw, r) duration := time.Since(start) slog.Info("http request", "method", r.Method, "path", r.URL.Path, "status", rw.statusCode, "duration_ms", duration.Milliseconds(), "duration", duration.String(), "request_id", requestID, "remote", r.RemoteAddr, "user_agent", r.UserAgent(), ) }) } }