package web import ( "context" "log/slog" "net/http" "time" chimw "github.com/go-chi/chi/v5/middleware" "github.com/google/uuid" ) // ctxKey is the unexported type used for context.Context keys owned by the // web package. Per Go convention, using an unexported named type prevents // accidental collisions with other packages' context keys. type ctxKey string const requestIDKey ctxKey = "request_id" // RequestIDMiddleware emits a UUIDv4 for each request, attaches it to the // request context under requestIDKey, and sets the X-Request-ID response // header. The downstream handler (and any nested middleware) can recover // the ID via LoggerFromContext for structured logging. func RequestIDMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { id := uuid.NewString() ctx := context.WithValue(r.Context(), requestIDKey, id) w.Header().Set("X-Request-ID", id) next.ServeHTTP(w, r.WithContext(ctx)) }) } // LoggerFromContext returns slog.Default() decorated with the request_id // attribute if one is present in ctx, otherwise plain slog.Default(). // Handlers should prefer this helper over slog.Default() so per-request // log lines carry the request_id correlator. func LoggerFromContext(ctx context.Context) *slog.Logger { if id, ok := ctx.Value(requestIDKey).(string); ok && id != "" { return slog.Default().With("request_id", id) } return slog.Default() } // SlogLoggerMiddleware returns chi-compatible middleware that emits one // structured log line per request. The line carries method, path, status, // duration_ms, and request_id (when present). Per RESEARCH Pitfall 6 this // REPLACES chi's built-in middleware.Logger — never register both. // // The middleware deliberately allowlists fields (V7 / T-01-09): it never // logs request bodies, Authorization headers, Cookie headers, or the DSN. func SlogLoggerMiddleware(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) { start := time.Now() ww := chimw.NewWrapResponseWriter(w, r.ProtoMajor) next.ServeHTTP(ww, r) l := logger if id, ok := r.Context().Value(requestIDKey).(string); ok && id != "" { l = l.With("request_id", id) } l.Info("request", "method", r.Method, "path", r.URL.Path, "status", ww.Status(), "duration_ms", time.Since(start).Milliseconds(), ) }) } }