diff --git a/internal/middleware/logging.go b/internal/middleware/logging.go index 7a1043d..00ccc2e 100644 --- a/internal/middleware/logging.go +++ b/internal/middleware/logging.go @@ -1,6 +1,8 @@ package middleware import ( + "crypto/rand" + "encoding/hex" "log/slog" "net/http" "time" @@ -17,21 +19,39 @@ func (rw *responseWriter) WriteHeader(code int) { rw.ResponseWriter.WriteHeader(code) } -// Logging returns middleware that logs each HTTP request with structured logging. +// 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", time.Since(start).String(), + "duration_ms", duration.Milliseconds(), + "duration", duration.String(), + "request_id", requestID, "remote", r.RemoteAddr, + "user_agent", r.UserAgent(), ) }) }