DocsLogFan-out and levels

Fan-out and levels

Send one record to several outputs at once, and use the custom TRACE and FATAL levels.

log.New already fans out when you pass more than one output, so the common case needs no extra work. The primitives it builds on are exported too, for when you want to assemble handlers by hand or adopt an existing *slog.Logger. This guide covers fan-out and the two custom levels.

Fan-out to several outputs

Passing several outputs to log.New writes each record to all of them. A frequent pairing is human-readable text on the console and structured JSON in a file.

logger := log.New(
	log.WithText(os.Stdout),
	log.WithJSON(file),
)

Under the hood this is a MultiHandler. Build one directly with log.NewMultiHandler when you want fan-out outside log.New.

multi := log.NewMultiHandler(
	slog.NewTextHandler(os.Stdout, log.HandlerOptions(slog.LevelDebug)),
	slog.NewJSONHandler(file, log.HandlerOptions(slog.LevelDebug)),
)

logger := log.Wrap(slog.New(multi))

A MultiHandler drops a record only when every child would discard it, and one failing output does not stop the others, so a broken file writer still lets the console keep logging. log.Wrap adopts any *slog.Logger as a log.Logger, and logger.Slog() hands the *slog.Logger back.

The custom levels

log adds two levels beyond slog's four. log.LevelTrace sits below DEBUG for the noisiest diagnostics, and log.LevelFatal sits above ERROR. Every log.Logger has Trace and Fatal helpers for them.

logger.Trace("entered loop", "i", i)
logger.Fatal("cannot recover", "err", err)

For the level names to render as TRACE and FATAL instead of slog's numeric fallback such as DEBUG-4, a handler needs the right options. log.WithText and log.WithJSON set them for you. When you build a raw handler yourself, pass log.HandlerOptions(level) as its *slog.HandlerOptions.

h := slog.NewTextHandler(os.Stdout, log.HandlerOptions(slog.LevelDebug))

Warning

Fatal does not exit the process. It logs a FATAL record and returns. Calling os.Exit inside a logging call would skip deferred cleanup and unflushed writers, including the FATAL record itself. If you want to exit, call os.Exit(1) yourself once the record is flushed.

Changing the threshold on a logger

WithLevel on a logger returns a new logger at a different minimum level while keeping the same outputs, format, and attributes. It wraps the existing handler rather than rebuilding it, so a custom output is preserved.

quiet := logger.WithLevel(slog.LevelError) // same outputs, higher threshold