65 lines
2 KiB
Go
65 lines
2 KiB
Go
// 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)}
|
|
}
|