Day 041 — log/slog: Structured Logging¶
Month 2 · Week 2 · ⬅ Day 040 · Day 042 ➡ · Journal index
🎯 Learning Objective¶
Emit structured (key/value) logs with log/slog: pick a handler and level, attach strongly-typed attributes, build child loggers with shared context, and set the package default.
📚 Topics¶
slog.Logger→slog.Handler(Text vs JSON) → anyio.Writer- Levels (
Debug/Info/Warn/Error) andHandlerOptions.Level - Typed attrs (
slog.String/Int/Duration/Any) vs loose key/value pairs logger.With(...)child loggers ·slog.Groupslog.SetDefaultand thelogbridge
📖 Reading / Sources¶
📝 Notes¶
- Structured logging records key/value pairs, not formatted strings, so logs are machine-queryable (filter by
request_id,level, etc.).slogshipped in Go 1.21 → [[structured-logging]]. - Architecture: a
slog.Loggerformats a Record and passes it to aslog.Handler. The handler decides format (NewTextHandler=key=value;NewJSONHandler= JSON) and destination (anyio.Writer) → [[slog-handler]]. - Levels are ordered ints;
HandlerOptions.Levelsets the threshold — messages below it are dropped cheaply. Default isInfo, soDebugis hidden unless you lower the level (use a*slog.LevelVarto change it at runtime). - Prefer typed attribute constructors (
slog.String("k", v),slog.Int,slog.Duration,slog.Any) over loose"k", vpairs: they avoid the odd-number-of-args footgun (a dangling key logs a!BADKEY) and are faster → [[slog-typed-attrs]]. logger.With(attrs...)returns a child logger that stamps those attrs on every line — ideal for request-scoped fields.slog.Group("name", ...)nests attrs into a sub-object.slog.SetDefault(logger)makes the top-levelslog.Info/Errorfunctions use your logger and routes the oldlogpackage's output through slog — one place to configure everything.slogis concurrency-safe. Attribute evaluation still happens even if the line is later dropped by level — for expensive values guard withlogger.Enabled(ctx, level)or useslog.LogAttrs(the lower-allocation API). UseInfoContext/LogAttrsto pass actxso handlers can extract trace IDs.
💻 Code Examples¶
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo, // Debug is dropped
}))
logger.Info("server started",
slog.String("addr", ":8080"),
slog.Int("workers", 4),
)
// Child logger: every line carries request_id automatically.
reqLog := logger.With(slog.String("request_id", "r-123"))
reqLog.Error("upstream failed", slog.Any("err", err))
Full code:
examples/month-02/slog-structured/main.go· Run:go run ./examples/month-02/slog-structured
🏋️ Exercises / Practice¶
| Exercise | Status | Link |
|---|---|---|
| (Covered by the runnable example — no separate kata) | ✅ | examples/month-02/slog-structured |
🐛 Mistakes Made¶
- Passed an odd number of loose key/value args (
"count", n, "stray") → got a!BADKEYattribute. Switched to typedslog.Int(...). - Logged at
Debugand saw nothing — the default level isInfo; had to setLevel: slog.LevelDebug. - Logged a secret in plaintext — moved to a
slog.Group/redaction discipline and removed it.
❓ Open Questions¶
- Best pattern to pull a trace/request ID from
context.Contextinto every log line — a custom handler orWithGroupplusInfoContext?
🧠 Active Recall (answer without looking)¶
-
Q: What are the three things a
slog.Handlercontrols, and what does theLoggeritself do?
A
The handler controls the **output format** (text vs JSON), the **destination** (`io.Writer`), and **level filtering**/attribute handling. The `Logger` builds a `Record` from the message + attrs and hands it to the handler. Swap handlers to change format/destination without touching call sites. -
Q: Why prefer
slog.String("k", v)over the loose"k", vform?
A
Typed attrs are checked at compile time and avoid the odd-number-of-arguments footgun (a missing value yields a `!BADKEY` attr). They're also a bit faster since slog doesn't have to infer types at runtime.
🪶 Feynman Reflection¶
slog turns a log line into a little labeled record instead of a sentence. The Logger writes down what happened and the fields; the Handler decides how to print it (human key=value or JSON for machines) and where. A child logger made with With is like a rubber stamp that adds the same fields — say a request ID — to every line, so you can later filter the whole story of one request out of millions.
🕳️ Knowledge Gaps¶
- Writing a custom
Handler(e.g. to inject context trace IDs or redact fields). LogAttrsallocation behavior in hot paths.
✅ Summary¶
I can configure a JSON/Text slog handler with a level, log with typed attributes, build request-scoped child loggers via With, group fields, and set the package default.
⏭️ Next Steps / Prep for Tomorrow¶
- Day 042: consolidate the week (json, time, http, context, slog) with closed-book recall and the weekly review.
| Time spent | Difficulty | Confidence |
|---|---|---|
| 90 min | 🟦🟦⬜⬜⬜ | 🟦🟦🟦⬜⬜ |
Suggested commit: feat(examples): log/slog structured logging (day 041)