xtablo-source/backend/internal/web/middleware.go
Arthur Belleville 36e96015f5
feat(01-03): pgxpool wrapper, RequestID/slog middleware, slog handler switch
- Remove //go:build red_gate tag from internal/web/handlers_test.go and
  internal/db/pool_test.go now that consumer symbols are about to exist
- go mod tidy after real importers land (deferred from Plan 01-01 per
  Codex concern #1) — chi/v5, templ, pgx/v5, google/uuid now in require list
- internal/db/pool.go: NewPool(ctx, dsn) builds a pgxpool.Pool with
  MaxConns=10, MinConns=1; no eager Ping (RESEARCH Pitfall 2)
- internal/web/slog.go: NewSlogHandler returns JSON when env='production',
  text otherwise; pure helper, no slog.SetDefault side effect
- internal/web/middleware.go: RequestIDMiddleware (UUIDv4 → ctx +
  X-Request-ID header), LoggerFromContext helper, SlogLoggerMiddleware
  factory using chi WrapResponseWriter; field allowlist per V7/T-01-09
2026-05-14 19:24:16 +02:00

70 lines
2.4 KiB
Go

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(),
)
})
}
}