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,TextHandlervsJSONHandler,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) andNewTextHandler(key=value, for local dev). Both take anio.Writer+*slog.HandlerOptions. - Prefer typed attributes (
slog.String("k", v),slog.Int,slog.Duration) over loose"k", vpairs: 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.Levelis the minimum emitted; gaps let you add custom levels. The level can be a*slog.LevelVarto 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-levelslog.Info/Errorand bridges the oldlogpackage, 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 zeroslog.Attr{}drops it.- Use the
Contextvariants (InfoContext(ctx, …)) so a custom handler can read request-scoped values (see Day 145). slog records are cheap but not free — gate hotDebugpaths withlogger.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 |
|---|---|---|
redact — ReplaceAttr secret-masking policy |
✅ | exercises/month-06/week-1/redact |
🐛 Mistakes Made¶
- Used loose pairs and miscounted args → got a
!BADKEYfield. Switched to typedslog.String/Int. - Expected
log.Printfcalls to stay plain afterSetDefault— they actually route through slog's bridge now, which is the point.
❓ Open Questions¶
- When is a custom
slog.Handlerworth writing vs. just usingReplaceAttr? (Custom handler when you need to read context or fan out to multiple sinks.)
🧠 Active Recall (answer without looking)¶
- 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/WithGroupcorrectness) — 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)