Let's talk about logging

March 31, 2026

I’ve been absent far too long. Most days I think about writing something again, then… I don’t. 🤷‍♂️ I’m going to try to get back into the habit… and this time, I thought I’d talk about one of my favorite packages in the standard library: log/slog.

To kick off, I’ll give a general description of the package, then starting tomorrow (I promise! I won’t forget again!) we’ll start into the particulars.

First there was log

The log package has been with us since the very beginning of Go. But few use it for serious applications, because it’s so limited. No real log levels–just “info” (or info + panic, or info + os.Exit), plain text output (no JSON or other formats), and thus no meaningful middleware capabilities.

These limitations lead to the proliferation of many third-party logging libraries such as Logrus, Zap, and ZeroLog.

Introducing log/slog

Finally, in June, 2023, with Go 1.21, log/slog was introduced. And now, except for very special cases, it should probably be your go-to logger. It offers:

  • Levels — Debug, Info, Warn, and Error by default, but it’s customizable, so you can also have Super Duper Important I really Mean it, I do! as a level, if you want!
  • Structured output — Key/value pairs for the win!
  • Pluggable handlers — Want JSON output? Handled. Standard text more to your liking? You got it. Want color on the console? No problem.
  • Composable handlers — Want JSON sent to your logging service, plain text to your log file, and color to your console, all at the same time? We have that, too!
  • Middleware — Want to filter passwords out of your logs? Or add special annotations to every error? Or replace every occurrence of the word “hippopotamus” with “rhinoceros” for unexplicable reasons? You can.

If you’ve never used log/slog, this series will be for you.

If you already use log/slog, you’ll probably learn something new anyway!

So let’s go!


Share this

Direct to your inbox, daily. I respect your privacy .

Unsure? Browse the archive .

Related Content


Expanding errors with ReplaceAttr

Let’s look at one other example where I reach for ReplaceAttr regularly: Logging additional error detail. (I talk more about this approach in my Boot.Dev course Learn Logging and Observability in Go (see below for a discount code).) For this technique to be meaningful, I first need errors that contain additional details. And this can take many forms, but probably the most ubiquitous form is an error that contains a stack trace, such as those produced by the popular package github.


Test logging with ReplaceAttr

Today’s topic is probably the most interesting, subtle, and complicated of the three HandlerOptions fields: ReplaceAttr. type HandlerOptions type HandlerOptions struct { … // ReplaceAttr is called to rewrite each non-group attribute before it is logged. // The attribute's value has been resolved (see [Value.Resolve]). // If ReplaceAttr returns a zero Attr, the attribute is discarded. // // The built-in attributes with keys "time", "level", "source", and "msg" // are passed to this function, except that time is omitted // if zero, and source is omitted if AddSource is false.


Handler level

Each handler has an optional Level value, which controls which logs to drop, and which to actually log: type HandlerOptions type HandlerOptions struct { … // Level reports the minimum record level that will be logged. // The handler discards records with lower levels. // If Level is nil, the handler assumes LevelInfo. // The handler calls Level.Level for each record processed; // to adjust the minimum level dynamically, use a LevelVar.

Get daily content like this in your inbox!

Subscribe