Wrapping output methods

May 15, 2026

Let’s talk about a feature I’ve never used, or even knew existed… I mentioned a while back that the AddSource field of HandlerOptions controls whether the log output includes the source code position of the log call. But what if that log call is wrapped by a helper, obscuring the meaningful source position?

Wrapping output methods

The logger functions use reflection over the call stack to find the file name and line number of the logging call within the application. This can produce incorrect source information for functions that wrap slog. For instance, if you define this function in file mylog.go:

func Infof(logger *slog.Logger, format string, args ...any) {
    logger.Info(fmt.Sprintf(format, args...))
}

and you call it like this in main.go:

Infof(slog.Default(), "hello, %s", "world")

then slog will report the source file as mylog.go, not main.go.

A correct implementation of Infof will obtain the source location (pc) and pass it to NewRecord. The Infof function in the package-level example called “wrapping” demonstrates how to do this.

This section of the docs just explains the problem, in a bit more detail than I did… then points you to the Example for the solution. Let’s look at that Example. Follow the link for the full example, executable in your browser. Here I’ll just quote the most interesting bit:

	var pcs [1]uintptr
	runtime.Callers(2, pcs[:]) // skip [Callers, Infof]
	r := slog.NewRecord(time.Now(), slog.LevelInfo, fmt.Sprintf(format, args...), pcs[0])
	_ = logger.Handler().Handle(context.Background(), r)

What you see here is the “low-level” construction of a log record, which gives you fine-grained control over the details. The detail that matters here, is the caller’s stack frame.

What is a log Record? That’s the next topic we’ll discuss!


Share this

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

Unsure? Browse the archive .

Related Content


slog.HandlerOptions

Now we’ll begin looking at HandlerOptions. Despite the Handler-centricity, this is something we care heavily about as mere users of the slog library. Whenever you instantiate either slog.JSONHandler or slog.TextHandler, you have the option of providing a HandlerOptions object, to tweak the default handler behavior. We’ll look at each of the config options, one at a time. type HandlerOptions type HandlerOptions struct { // AddSource causes the handler to compute the source code position // of the log statement and add a SourceKey attribute to the output.


The better test alternative to slog.DiscardHandler

Last time I described slog.DiscardHandler as a viable way to silence logs in tests. Now I want to tell you why I never use that option, and you probably shouldn’t, either. But first a story. The slog package was introduced with Go 1.21 in August of 2023. It didn’t yet have slog.DiscardHandler, so you had to roll your own. But that was easy: logger := slog.New(slog.NewTextHandler(io.Discard, nil)) Easy as it was, it was also annoying.


slog.DiscardHandler

Now that we’ve made it through the Handler interface, let’s look at the first of the implementations provided by the standard library: the DiscardHandler. var DiscardHandler Handler = discardHandler{} DiscardHandler discards all log output. DiscardHandler.Enabled returns false for all Levels. Why would you ever want a discard handler? Two likely scenarios come to mind. The one where I most frequently use it, is testing. You may not wish to log anything during a test, and this is a simple way to do that:

Get daily content like this in your inbox!

Subscribe