package middleware import ( "log/slog" "net/http" "time" ) // statusRecorder wraps http.ResponseWriter to capture the response status // code so the Logger middleware can include it in the structured log line. // // The default status is initialised to http.StatusOK because net/http // implicitly emits a 200 when a handler writes a body without calling // WriteHeader explicitly; mirroring that behaviour keeps logs accurate. type statusRecorder struct { http.ResponseWriter status int } // WriteHeader records the status code before delegating to the wrapped // ResponseWriter so the original response semantics are preserved. func (s *statusRecorder) WriteHeader(code int) { s.status = code s.ResponseWriter.WriteHeader(code) } // Logger returns HTTP middleware that emits a single structured log line // per request once the downstream handler has finished. The line includes // method, path, response status, elapsed duration in milliseconds, and the // request ID (if RequestID middleware is mounted upstream). // // The middleware is intentionally placed *inside* RequestID so the // generated identifier is available via context; placing it outside would // log an empty request_id. func Logger(log *slog.Logger) 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() rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK} next.ServeHTTP(rec, r) ctx := r.Context() log.LogAttrs(ctx, slog.LevelInfo, "http", slog.String("method", r.Method), slog.String("path", r.URL.Path), slog.Int("status", rec.status), slog.Int64("duration_ms", time.Since(start).Milliseconds()), slog.String("request_id", RequestIDFromContext(ctx)), ) }) } }