// Package middleware contains chi-compatible HTTP middleware: functions // matching the shape func(http.Handler) http.Handler, each wrapping the // next handler in the chain to add some cross-cutting behavior (logging, // authentication, ...) before and/or after the real handler runs. package middleware import ( "log/slog" "net/http" "time" // Aliased to chimw so it doesn't collide with this package's own name // ("middleware") when referenced from other files/packages. chimw "github.com/go-chi/chi/v5/middleware" ) // RequestLogger is a middleware FACTORY: a function that takes the // dependencies it needs (here, just a logger) and returns the actual // middleware function chi expects. This extra layer exists because chi's // r.Use() only accepts func(http.Handler) http.Handler - there's no room // to pass in a logger directly, so we wrap it in an outer function that // captures the logger in a closure instead. // // There are three layers of function here, each running at a different // time: // // RequestLogger(logger) -> runs ONCE, when building the router // func(next http.Handler) ... -> runs ONCE, when chi wires up the chain // func(w, r) { ... } -> runs on EVERY request func RequestLogger(logger *slog.Logger) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Capture the start time BEFORE the request is handled, so we // can measure total duration afterward. start := time.Now() // A plain http.ResponseWriter only lets you WRITE a status // code/body - it doesn't let you read back what was written // afterward. WrapResponseWriter adds that: once the handler // below has run, ww.Status() and ww.BytesWritten() become // available. We must pass ww (not w) to next.ServeHTTP so the // wrapping actually captures what gets written downstream. ww := chimw.NewWrapResponseWriter(w, r.ProtoMajor) // Run the rest of the middleware chain / the final handler. // Everything above this line happens BEFORE the request is // handled; everything below happens AFTER the response has // been written. next.ServeHTTP(ww, r) // Emit one structured JSON log line per request, with typed // fields (slog.String, slog.Int, slog.Duration) so each one // becomes an independently queryable key once these logs land // in Loki via Grafana Alloy. logger.Info("http_request", // GetReqID reads back the request ID that chi's own // RequestID middleware (registered earlier in the chain, // see router.go) attached to the request's context - this // lets you correlate every log line belonging to one // specific request. slog.String("request_id", chimw.GetReqID(r.Context())), slog.String("method", r.Method), slog.String("path", r.URL.Path), slog.Int("status", ww.Status()), slog.Int("bytes", ww.BytesWritten()), slog.Duration("duration_ms", time.Since(start)), slog.String("remote_addr", r.RemoteAddr), ) }) } }