Learn Go← Dashboard

fmt, io, os, bufio, encoding/json, net/http, time, slog, regexp, flag, embed.

log/slog — Structured Logging

Go 1.21 added log/slog to the standard library. It's the stdlib's answer to zap/zerolog/logrus: structured, levelled logging with no third-party dependency. New code should reach for slog, not the older log package.

The four levels and the default logger

go playground
Loading...

The trailing arguments are key/value pairs. The default handler prints human-readable lines:

2026/05/20 11:02:14 INFO server starting port=8080 env=dev

For machine-parseable output, switch to JSON:

h := slog.NewJSONHandler(os.Stdout, nil)
slog.SetDefault(slog.New(h))
slog.Info("server starting", "port", 8080)
// {"time":"...","level":"INFO","msg":"server starting","port":8080}

Typed attrs avoid mistakes

"port", 8080 is convenient but a fat-fingered odd number of args silently becomes a !BADKEY attr. Prefer the typed helpers in hot paths:

slog.Info("server starting",
    slog.Int("port", 8080),
    slog.String("env", "dev"),
    slog.Duration("startup", time.Since(start)),
)

A scoped logger with context fields

With returns a child logger that carries pre-baked fields — handy for request-scoped logs:

log := slog.With("request_id", reqID, "user_id", uid)
log.Info("login")
log.Info("loaded profile", "ms", 17)
// every line includes request_id and user_id automatically

Levels and filtering

The default level is Info. To raise or lower it, give the handler options:

opts := &slog.HandlerOptions{Level: slog.LevelDebug}
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, opts)))

A slog.LevelVar lets you flip levels at runtime (handy for a SIGHUP reload):

var lvl slog.LevelVar // starts at Info
lvl.Set(slog.LevelDebug)

Don't log secrets

slog will happily print whatever you give it. Strip tokens, passwords, and PII before you hand the value to the logger — there's no built-in redaction.

Why prefer `slog.Int("port", 8080)` over the variadic `"port", 8080`?