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

For a more sophisticated type-safe context key pattern with default values, see ctxkey.

← All articles