// Package reqid stamps each request with an id and carries it into every log record made with a // context. It is its own package so that layers below HTTP (auth, ingest) can correlate their // errors with an access-log line without importing the HTTP layer. package reqid import ( "context" "crypto/rand" "encoding/base32" "log/slog" "net/http" ) // Header is where the id is echoed. It is generated here, never taken from the request: an id // accepted from a caller lets them poison our logs and correlate other users' lines. const Header = "X-Request-Id" // Key is the log attribute name. const Key = "request_id" type ctxKey struct{} // Middleware stamps the request and the response. func Middleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { id := New() w.Header().Set(Header, id) next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), ctxKey{}, id))) }) } // New mints an id. func New() string { var b [10]byte rand.Read(b[:]) return base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(b[:]) } // FromContext returns the id stamped by Middleware, or "". func FromContext(ctx context.Context) string { id, _ := ctx.Value(ctxKey{}).(string) return id } // WithContext wraps a slog handler so that every *Context log call inside a request carries its id. // Without it each call site has to remember the attribute, and the ones that forget are exactly the // error paths nobody exercises. func WithContext(h slog.Handler) slog.Handler { return &handler{h} } type handler struct{ slog.Handler } func (h *handler) Handle(ctx context.Context, r slog.Record) error { if id := FromContext(ctx); id != "" { r.AddAttrs(slog.String(Key, id)) } return h.Handler.Handle(ctx, r) } func (h *handler) WithAttrs(attrs []slog.Attr) slog.Handler { return &handler{h.Handler.WithAttrs(attrs)} } func (h *handler) WithGroup(name string) slog.Handler { return &handler{h.Handler.WithGroup(name)} }