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¶
-
contextpackage docs - slog handler guide — reading from context
- Go blog — Contexts and structs (key types)
📝 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
grepone 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; passctx. - 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.Handlerthat reads the ID out ofctxinHandle(ctx, record)and adds it as an attr. Because slog passes the call-site context intoHandle, no handler call site has to remember the ID — they just useInfoContext(ctx, …). - Wrap an inner handler:
type ctxHandler struct{ slog.Handler }and override onlyHandle. Caveat: embedding promotesWithAttrs/WithGroupfrom 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 |
|---|---|---|
redact — ReplaceAttr 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 unexportedctxKeytype. - Logged with
logger.Info(no ctx) and wondered why the ID was missing — the handler readsctx, so must useInfoContext.
❓ 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)¶
- 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/WithGroupin 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)