Day 100 — Per-Request Structured Logging¶
Month 4 · Week 3 · ⬅ Day 099 · Day 101 ➡ · Journal index
🎯 Learning Objective¶
Emit one structured log line per HTTP request — method, path, status, duration,
request id — using log/slog, and thread the request id through context.
📚 Topics¶
slog.Logger, JSON handler,LogAttrs- A
ResponseWriterwrapper to capture status/bytes; request-id incontext
📖 Reading / Sources¶
📝 Notes¶
slogis stdlib (Go 1.21+). ALoggerwraps aHandler;NewJSONHandleremits one machine-parseable object per line — filter bystatus>=500later.ResponseWriterexposes no status getter. Wrap it: embed the interface, overrideWriteHeader/Writeto recordstatusandbytes. → [[responsewriter-wrap]]- Default the recorded status to 200. A handler that writes a body without
calling
WriteHeaderstill sends200; if you default to 0 you'll mislog it. - Log on
defer/afternext.ServeHTTPso status and duration are final. - Request id rides in
contextunder an unexported key type so no other package can collide. Never use a barestringas a context key. → [[context-keys]] context.Contextis the first arg ofslog's*Contextmethods (InfoContext,LogAttrs), matching the wider context-first convention.LogAttrsavoids allocation vs the variadickey, value...form — it takes pre-builtslog.Attrvalues. Prefer it on hot paths. → [[slog-attrs]]
💻 Code Examples¶
type statusRecorder struct {
http.ResponseWriter
status int
bytes int
}
func (r *statusRecorder) WriteHeader(code int) { r.status = code; r.ResponseWriter.WriteHeader(code) }
func (r *statusRecorder) Write(b []byte) (int, error) {
n, err := r.ResponseWriter.Write(b)
r.bytes += n
return n, err
}
func Logging(logger *slog.Logger, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK} // default 200!
next.ServeHTTP(rec, r)
logger.LogAttrs(r.Context(), slog.LevelInfo, "http_request",
slog.String("method", r.Method),
slog.Int("status", rec.status),
slog.Duration("duration", time.Since(start)))
})
}
Full code:
examples/month-04/reqlog/main.go· Run:go run ./examples/month-04/reqlog
🏋️ Exercises / Practice¶
| Exercise | Status | Link |
|---|---|---|
Wrap ResponseWriter to capture status (reused in week-1 tests) |
✅ | examples/month-04/reqlog |
🐛 Mistakes Made¶
- Logged before
ServeHTTPreturned → every request showed status 0. - Used a
stringcontext key and shadowed another package's value. Switched to an unexportedtype ctxKey int.
❓ Open Questions¶
- How do I propagate the request id to a downstream service? (Trace headers like
traceparent; revisit with distributed tracing.)
🧠 Active Recall (answer without looking)¶
-
Q: Why default the wrapped recorder's status to
200?
A
A handler that writes a body without ever calling `WriteHeader` still emits a `200` on the wire. Defaulting to 0 would mislabel every such response. -
Q: Why use an unexported type for the context key?
A
Context keys are compared by value AND type. An unexported key type makes collisions with other packages impossible; a bare `string` key can clash.
🪶 Feynman Reflection¶
The HTTP package hands me a write-only ResponseWriter, so to know what status I
sent I slip a small recorder in front of it. After the handler finishes I emit
one tidy JSON line a machine can grep. A short random id ties that line to any
deeper logs the handler produced.
🕳️ Knowledge Gaps¶
sloggroups,WithGroup, and per-request child loggers vialogger.With.
✅ Summary¶
I can build a logging middleware that records status/bytes/duration and a
request id, and emit it as structured JSON with slog.
⏭️ Next Steps / Prep for Tomorrow¶
- Day 101: pagination & filtering of list endpoints.
| Time spent | Difficulty | Confidence |
|---|---|---|
| 90 min | 🟦🟦⬜⬜⬜ | 🟦🟦🟦⬜⬜ |
Suggested commit: feat(examples): per-request slog middleware with request id (day 100)