Skip to content

Day 145 — Correlation IDs & Log Context

Month 6 · Week 1 · ⬅ Day 144 · Day 146 ➡ · Journal index

🎯 Learning Objective

Thread a correlation (request) ID through context.Context and have a custom slog.Handler stamp it on every log line automatically, so one request's logs are searchable by a single field.

📚 Topics

  • Context values with an unexported key type; extract-or-mint middleware
  • Context-reading slog.Handler; InfoContext; echoing the ID in headers

📖 Reading / Sources

📝 Notes

  • A correlation ID (a.k.a. request ID, trace ID) is one value that tags every log/metric/event for a single request, so you can grep one request across services → [[correlation-id]].
  • Middleware does extract-or-mint: read an incoming X-Request-ID (trust upstream gateways) or generate one (crypto/rand → hex). Stash it in the request context and echo it back in the response header so the caller can correlate too.
  • Context key hygiene: use an unexported named type (type ctxKey int) for the key, never a bare string. This prevents collisions with keys other packages set in the same context → [[context]]. Don't stuff request-scoped data into structs; pass ctx.
  • Context is for request-scoped values and cancellation only — not for optional function params. Always first arg: func F(ctx context.Context, …).
  • The elegant part: a custom slog.Handler that reads the ID out of ctx in Handle(ctx, record) and adds it as an attr. Because slog passes the call-site context into Handle, no handler call site has to remember the ID — they just use InfoContext(ctx, …).
  • Wrap an inner handler: type ctxHandler struct{ slog.Handler } and override only Handle. Caveat: embedding promotes WithAttrs/WithGroup from the inner handler, which would unwrap your decorator — for a production handler, override those too to re-wrap.
  • This composes with Day 143: put the trace-id in the same slot so logs and traces share the pivot key. Forward the ID on outbound calls (set the header) to extend correlation across service hops.

💻 Code Examples

type ctxKey int
const requestIDKey ctxKey = 0 // unexported key type — no cross-package collisions

// A handler that reads the request ID from ctx and stamps every record.
type ctxHandler struct{ slog.Handler }

func (h ctxHandler) Handle(ctx context.Context, r slog.Record) error {
    if id, ok := ctx.Value(requestIDKey).(string); ok && id != "" {
        r.AddAttrs(slog.String("request_id", id))
    }
    return h.Handler.Handle(ctx, r)
}

// Middleware: extract-or-mint, stash in ctx, echo in the response header.
func requestID(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 = newID() // crypto/rand -> hex
        }
        w.Header().Set("X-Request-ID", id)
        ctx := context.WithValue(r.Context(), requestIDKey, id)
        next.ServeHTTP(w, r.WithContext(ctx)) // logger.InfoContext(ctx,…) now stamped
    })
}

Full code: examples/month-06/correlation/main.go · Run: go run ./examples/month-06/correlation

🏋️ Exercises / Practice

Exercise Status Link
redactReplaceAttr policy (pairs with log context) exercises/month-06/week-1/redact

🐛 Mistakes Made

  • Used a bare string "request_id" as the context key → risk of collision; switched to an unexported ctxKey type.
  • Logged with logger.Info (no ctx) and wondered why the ID was missing — the handler reads ctx, so must use InfoContext.

❓ Open Questions

  • Should I generate a UUID or reuse the trace-id as the correlation ID? (Reuse the trace-id when tracing is on, so all three signals share one key.)

🧠 Active Recall (answer without looking)

  1. Q: Why use an unexported named type for a context key instead of a string?
    A

Context keys are compared by equality including type, so an unexported key type can't collide with a key set by another package (even one using the same string). It also hides the key from outside packages. 2. Q: How does a slog.Handler get the request ID without every call site passing it?

A

Handle(ctx, record) receives the call's context, so a custom handler reads the ID out of ctx and adds it as an attr. Call sites only need to use the *Context log methods (InfoContext).

🪶 Feynman Reflection

A correlation ID is a wristband stamped on a request when it enters the building. You tuck the wristband into the context that travels with the request, and a smart logger peeks at the wristband on every line it writes — so later you can find every footstep of that one request by its number.

🕳️ Knowledge Gaps

  • Correctly re-wrapping WithAttrs/WithGroup in a decorating handler — revisit with the slog handler guide.

✅ Summary

I can mint/propagate a correlation ID through context with a safe key type and have a context-reading slog handler stamp it on every log line automatically.

⏭️ Next Steps / Prep for Tomorrow

  • Day 146: turn these signals into dashboards and alerts.

Time spent Difficulty Confidence
90 min 🟦🟦⬜⬜⬜ 🟦🟦🟦🟦⬜

Suggested commit: feat(examples): correlation IDs via context and a slog handler (day 145)