Skip to content

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.Loggerslog.Handler (Text vs JSON) → any io.Writer
  • Levels (Debug/Info/Warn/Error) and HandlerOptions.Level
  • Typed attrs (slog.String/Int/Duration/Any) vs loose key/value pairs
  • logger.With(...) child loggers · slog.Group
  • slog.SetDefault and the log bridge

📖 Reading / Sources

📝 Notes

  • Structured logging records key/value pairs, not formatted strings, so logs are machine-queryable (filter by request_id, level, etc.). slog shipped in Go 1.21 → [[structured-logging]].
  • Architecture: a slog.Logger formats a Record and passes it to a slog.Handler. The handler decides format (NewTextHandler = key=value; NewJSONHandler = JSON) and destination (any io.Writer) → [[slog-handler]].
  • Levels are ordered ints; HandlerOptions.Level sets the threshold — messages below it are dropped cheaply. Default is Info, so Debug is hidden unless you lower the level (use a *slog.LevelVar to change it at runtime).
  • Prefer typed attribute constructors (slog.String("k", v), slog.Int, slog.Duration, slog.Any) over loose "k", v pairs: 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-level slog.Info/Error functions use your logger and routes the old log package's output through slog — one place to configure everything.
  • slog is concurrency-safe. Attribute evaluation still happens even if the line is later dropped by level — for expensive values guard with logger.Enabled(ctx, level) or use slog.LogAttrs (the lower-allocation API). Use InfoContext/LogAttrs to pass a ctx so 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 !BADKEY attribute. Switched to typed slog.Int(...).
  • Logged at Debug and saw nothing — the default level is Info; had to set Level: 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.Context into every log line — a custom handler or WithGroup plus InfoContext?

🧠 Active Recall (answer without looking)

  1. Q: What are the three things a slog.Handler controls, and what does the Logger itself 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.

  2. Q: Why prefer slog.String("k", v) over the loose "k", v form?

    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).
  • LogAttrs allocation 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)