Skip to content

Day 141 — Structured Logging with slog

Month 6 · Week 1 · ⬅ Day 140 · Day 142 ➡ · Journal index

🎯 Learning Objective

Replace ad-hoc log.Printf strings with log/slog structured records: pick a handler, set a level, attach typed attributes, and bind per-scope context.

📚 Topics

  • slog.New, TextHandler vs JSONHandler, slog.SetDefault
  • Levels, typed attrs (slog.String/Int/Duration), With/WithGroup, ReplaceAttr

📖 Reading / Sources

📝 Notes

  • slog (Go 1.21+) is stdlib. A logger = a frontend (*slog.Logger) over a backend (slog.Handler). The handler decides format/destination; the logger just builds records → [[structured-logging]].
  • Two built-in handlers: NewJSONHandler (machine-readable, for aggregators) and NewTextHandler (key=value, for local dev). Both take an io.Writer + *slog.HandlerOptions.
  • Prefer typed attributes (slog.String("k", v), slog.Int, slog.Duration) over loose "k", v pairs: typed avoids the odd-argument bug and preserves types in JSON (numbers stay numbers). A dangling loose key logs under !BADKEY.
  • Levels are an int: Debug(-4) < Info(0) < Warn(4) < Error(8). HandlerOptions.Level is the minimum emitted; gaps let you add custom levels. The level can be a *slog.LevelVar to change it at runtime without rebuilding the logger.
  • logger.With(attrs...) returns a child logger carrying those attrs on every record (request/component scope); the parent is unchanged → [[immutability]].
  • WithGroup("db") namespaces following attrs under a sub-object ({"db":{...}} in JSON).
  • slog.SetDefault(l) rewires package-level slog.Info/Error and bridges the old log package, so library code logs through your handler.
  • ReplaceAttr(groups, a) is the single choke point to rewrite/redact every attr (mask secrets, pin/format time, rename keys). Returning the zero slog.Attr{} drops it.
  • Use the Context variants (InfoContext(ctx, …)) so a custom handler can read request-scoped values (see Day 145). slog records are cheap but not free — gate hot Debug paths with logger.Enabled(ctx, slog.LevelDebug).

💻 Code Examples

opts := &slog.HandlerOptions{
    Level: slog.LevelInfo,
    ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
        if a.Key == "password" { // redact secrets everywhere
            return slog.String(a.Key, "[REDACTED]")
        }
        return a
    },
}
log := slog.New(slog.NewJSONHandler(os.Stdout, opts))
log.Info("login", slog.String("user", "alice"), slog.String("password", "hunter2"))
// {"time":...,"level":"INFO","msg":"login","user":"alice","password":"[REDACTED]"}

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

🏋️ Exercises / Practice

Exercise Status Link
redactReplaceAttr secret-masking policy exercises/month-06/week-1/redact

🐛 Mistakes Made

  • Used loose pairs and miscounted args → got a !BADKEY field. Switched to typed slog.String/Int.
  • Expected log.Printf calls to stay plain after SetDefault — they actually route through slog's bridge now, which is the point.

❓ Open Questions

  • When is a custom slog.Handler worth writing vs. just using ReplaceAttr? (Custom handler when you need to read context or fan out to multiple sinks.)

🧠 Active Recall (answer without looking)

  1. Q: Why prefer slog.Int("n", 3) over the loose pair "n", 3?
    A

Typed attrs can't trigger the odd-argument/!BADKEY mistake and they preserve the value's type in the output (a JSON number, not a string). They're also slightly faster (no key/value type-juggling). 2. Q: What does returning slog.Attr{} from ReplaceAttr do?

A

It drops the attribute entirely — the empty (zero) Attr has an empty key, which handlers omit. Useful to delete the time field in tests or strip a field globally.

🪶 Feynman Reflection

A structured log line is a little JSON object, not a sentence. slog gives you a logger that stamps each event with a level and a bag of typed key/values; a handler decides where it goes and how it looks. With pre-fills that bag for a scope, and ReplaceAttr is a last-mile filter every field passes through.

🕳️ Knowledge Gaps

  • Writing a fully spec-compliant custom handler (WithAttrs/WithGroup correctness) — revisit Day 145.

✅ Summary

I can stand up a JSON/Text slog logger, set a minimum level, attach typed attributes, scope them with With, and enforce redaction with ReplaceAttr.

⏭️ Next Steps / Prep for Tomorrow

  • Day 142: counters, gauges, and histograms with Prometheus client_golang.

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

Suggested commit: feat(examples): structured logging with slog (day 141)