72 lines
3.0 KiB
Go
72 lines
3.0 KiB
Go
// 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),
|
||
|
|
)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|