Building a logger
Assemble outputs, a level, and attributes with log.New, inject the Logger interface, and install a process-wide default.
log.New takes a handful of options and assembles the handlers for you. With no options it writes text to stdout at DEBUG. Each option adds an output, sets the level, or wraps the outputs in filters. This guide covers building a logger, passing it around, and installing one as the global default.
Outputs
An output is where records are written. Add a text or JSON output with a writer, or hand over any slog.Handler you already have.
logger := log.New(
log.WithText(os.Stdout), // human-readable text on the console
log.WithLevel(slog.LevelInfo),
)log.WithText(w) and log.WithJSON(w) each take an io.Writer, so a rotating file writer or an in-memory buffer works without this package importing it. log.WithOutput(h) adds an arbitrary handler, such as a memory sink or an exporter. Every output you add is written to, so passing more than one fans a record out to all of them.
Note
log.WithLevel sets the minimum level for the Text and JSON outputs only. A handler passed through log.WithOutput controls its own level and is not affected.
Attributes
With returns a logger that carries extra attributes on every subsequent record. Build a base logger once and derive per-component loggers from it.
logger := log.New(log.WithText(os.Stdout))
api := logger.With("svc", "api")
api.Info("ready") // record carries svc=apiThe Logger interface
log.New returns a log.Logger. It is the interface you pass around, and it embeds slog.Handler, so it drops into any code that expects a handler. Depend on the interface rather than a concrete logger to keep your types testable.
type Server struct {
log log.Logger
}
func NewServer(l log.Logger) *Server {
return &Server{log: l.With("component", "server")}
}
func (s *Server) handle() {
s.log.Debug("handling request")
}Pass log.New(...) in production. In tests pass a buffer-backed logger, or log.Discard() when the test needs no output at all.
srv := NewServer(log.Discard()) // logs nothinglog.Discard() is the null logger for this package, equal to wiring up slog.New(slog.DiscardHandler) yourself. It reports itself disabled at every level, so callers skip building records.
The global default
The package keeps one process-wide logger, created at startup, writing text to stdout. The package-level helpers log.Info, log.Error, and the rest route through it. Build the logger you want once and install it with log.SetDefault, and the helpers follow.
func setupLogging(w io.Writer) {
logger := log.New(
log.WithText(os.Stdout),
log.WithJSON(w),
)
log.SetDefault(logger)
}After SetDefault, log.Info(...) writes to both outputs. log.SetLevel moves the threshold of the built-in default only, so once you install your own logger, set its level when you build it with log.WithLevel.
Warning
Prefer injecting a log.Logger into the code you care about and treat the global as a quick start. Reaching for log.Info everywhere hides the dependency and makes a type harder to test in isolation.