- 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
70 lines
2.4 KiB
Go
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(),
|
|
)
|
|
})
|
|
}
|
|
}
|