Structured Logging Basics
log/slog logs structured records — a message plus key/value fields — instead
of formatted strings. Once a program has more than a couple of log calls
that all belong to the same piece of work, a different problem shows up: how
do you make sure every one of them tags itself consistently, without
retyping the same fields at every call site?
logger.Info("processing", "component", "importer", "item", item)
logger.Info("done", "component", "importer", "item", item)
This works, until a third call site forgets "component", or a fourth
spells it "module" instead. Nothing catches that — they're just string
keys — and the cost lands later, on whoever runs grep component=importer
against the logs and silently gets an incomplete answer, because one call
site's records don't carry the field being searched on.
Logger.With: attach it once, keep it attached
func ProcessItems(logger *slog.Logger, component string, items []string) {
log := logger.With("component", component)
for _, item := range items {
log.Info("processing", "item", item)
}
}
With returns a new *slog.Logger that carries "component" on every
record it produces, from here on, without it being repeated at each call
site. The loop only has to think about what's specific to this call —
"item" — because what's constant across the whole function was attached
once, to log, before the loop even started. Every record log produces
downstream — now or after this function grows to five more log calls —
automatically carries "component". There's no way for a future call site
to forget it, because there's nothing left for that call site to type.
Why this is the shape that scales
The value isn't visible with two log calls; it's visible once a function —
or a whole subsystem — has a dozen. logger.With(...) composes, too:
calling .With on an already-With'd logger adds fields on top of the
ones already attached, which is exactly how you'd thread a request ID
through a handler and then a component name through a piece of business
logic it calls, each layer adding its own context without needing to know
what the layer above already attached. The alternative — a shared constant
slice of key/value pairs spliced into every call — is the same idea done
manually, and manual is where the missed field creeps back in.
Try it
Implement ProcessItems(logger *slog.Logger, component string, items []string).
It logs one "processing" record per item, and every record it produces
must include "component" set to the given value.
Practice
1. Attach Context Once with Logger.With
IntermediateImplement
ProcessItems(logger *slog.Logger, component string, items []string).- For each item in
items, log oneInforecord with the message"processing"and a field"item"set to that item. - Every record must also carry a field
"component"set to the givencomponentvalue — attached once, not repeated at each call. - No return value.
package main import "log/slog" // ProcessItems logs one "processing" record per item; every record must // carry a "component" field set to the given value. func ProcessItems(logger *slog.Logger, component string, items []string) { // TODO: implement } func main() {}Sign in to write and run your solution in the browser.
Sign in for hints and the solution.
- For each item in