go / reqid
I use request ID middleware to trace requests through logs and across services.
The pattern
Generate a random ID, store it in context, and add it to logs:
package reqid
import (
"context"
"crypto/rand"
"encoding/hex"
"log/slog"
"net/http"
)
type ctxKey struct{}
// New generates a random 20-character request ID.
func New() string {
b := make([]byte, 10)
rand.Read(b)
return hex.EncodeToString(b)
}
// FromContext returns the request ID from ctx, or empty string if none.
func FromContext(ctx context.Context) string {
id, _ := ctx.Value(ctxKey{}).(string)
return id
}
// WithContext returns a new context containing the request ID.
func WithContext(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, ctxKey{}, id)
}
// Middleware adds a request ID to each request's context
// and includes it in the response headers.
func Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := New()
ctx := WithContext(r.Context(), id)
// Add to response header for client correlation
w.Header().Set("X-Request-ID", id)
// Add to logger context
ctx = WithLogger(ctx, slog.With("request_id", id))
next.ServeHTTP(w, r.WithContext(ctx))
})
}
Add a logger to context so all logs include the request ID:
type loggerKey struct{}
// WithLogger returns a context with the given logger attached.
func WithLogger(ctx context.Context, l *slog.Logger) context.Context {
return context.WithValue(ctx, loggerKey{}, l)
}
// Logger returns the logger from ctx, or the default logger.
func Logger(ctx context.Context) *slog.Logger {
if l, ok := ctx.Value(loggerKey{}).(*slog.Logger); ok {
return l
}
return slog.Default()
}
Usage
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/users", handleUsers)
http.ListenAndServe(":8080", reqid.Middleware(mux))
}
func handleUsers(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
log := reqid.Logger(ctx)
log.Info("fetching users")
// {"time":"...","level":"INFO","msg":"fetching users","request_id":"a1b2c3d4e5"}
users, err := db.GetUsers(ctx)
if err != nil {
log.Error("query failed", "error", err)
http.Error(w, "internal error", 500)
return
}
// ...
}
Accept client-provided IDs
For distributed tracing, accept an ID from the client but generate one if missing:
func Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := r.Header.Get("X-Request-ID")
if id == "" {
id = New()
}
// ... rest unchanged
})
}
When to use
- Any HTTP service that needs request tracing
- Microservices that call each other (propagate the ID)
- Debugging production issues via logs
For a more sophisticated type-safe context key pattern with default values, see ctxkey.