Skip to the content.

slog

Package slog provides structured and leveled logging. A log record consists of a time, a level, a message, and a set of key-value pairs. For example,

slog.Info("hello", "count", 3)

// output:
//  2024/07/31 19:18:28 INFO hello count=3 

Design

Package slog defines a type, Logger, which provides output methods for reporting events of interest.

Each Logger is associated with a Handler. A Logger output method, such as Logger.Info, creates a Record from the method arguments and passes it to the Handler, which decides how to handle it.

In summary, slog has a two-part design.

This design allows to replace the “backend” handler.

slog provides TextHandler and JSONHandler. It’s default Logger uses a defaultHandler. They have different behavior:

// defaultLogger with defaultHandler
2024/07/31 19:18:28 INFO hello count=3

// a logger with TextHandler
time=2024-08-01T18:16:37.637+08:00 level=INFO msg=hello count=3

// a logger with JSONHandler
{"time":"2024-08-01T18:40:19.5315339+08:00","level":"INFO","msg":"hello"}

For more control over the output format, create a user-specific handler. And then create a logger via New.

Logger, Record & Handler

Here’s the details that show how a “log” is transferred to a handler.

// Suppose we run a Logger.Info() method.
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
logger.Info("hello", "count", 3)
// Output:
//   time=2024-08-01T18:16:37.637+08:00 level=INFO msg=hello count=3

// pkg slog source code
// Info logs at [LevelInfo].
func (l *Logger) Info(msg string, args ...any) {
	l.log(context.Background(), LevelInfo, msg, args...)
}

func (l *Logger) log(ctx context.Context, level Level, msg string, args ...any) {
	if !l.Enabled(ctx, level) {
		return
	}

	var pcs [1]uintptr
	// skip [runtime.Callers, this function, this function's caller]
	runtime.Callers(3, pcs[:])
	r := NewRecord(time.Now(), level, msg, pcs[0])
	r.Add(args...)

	_ = l.Handler().Handle(ctx, r)
}

Options

Options help to customize the handler’s behavior. HandlerOptions are options for a TextHandler or JSONHandler. A zero HandlerOptions consists entirely of default values.

Suppose we set the [HandlerOptions.Level] field to control the minimum level for logging.

// a Level value fixes the handler's minimum level throughout its lifetime.
h := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelWarn})
slog.SetDefault(slog.New(h))
slog.Info("hello")
slog.Warn("Shanghai")
// Output:
//  {"time":"2024-08-01T18:40:19.5315339+08:00","level":"WARN","msg":"Shanghai"}

// Setting it to a LevelVar allows the level to be varied dynamically.
var programLevel = new(slog.LevelVar) // Info by default
h = slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: programLevel})
slog.SetDefault(slog.New(h))
programLevel.Set(slog.LevelError)
slog.Info("hi")
slog.Warn("NewYork")
slog.Error("America")
// Output:
//  {"time":"2024-08-01T18:40:19.5315339+08:00","level":"ERROR","msg":"America"}   

// pkg slog source code
//
// enabled reports whether l is greater than or equal to the
// minimum level.
func (h *commonHandler) enabled(l Level) bool {
	minLevel := LevelInfo
	if h.opts.Level != nil {
		minLevel = h.opts.Level.Level()
	}
	return l >= minLevel
}

Assemble the output

handleState holds state for a single call to commonHandler.handle. It uses a Buffer, of the type []byte, to construct and hold the output. All the “appendxx” methods will call s.buf.WriteByte() or s.buf.WriteString() to append msg to the output buffer.

// pkg slog source code
//
// handleState holds state for a single call to commonHandler.handle.
// The initial value of sep determines whether to emit a separator
// before the next key, after which it stays true.
type handleState struct {
	h       *commonHandler
	buf     *buffer.Buffer // <------- used for constructing the output
	freeBuf bool           // should buf be freed?
	sep     string         // separator to write before next key
	prefix  *buffer.Buffer // for text: key prefix
	groups  *[]string      // pool-allocated slice of active groups, for ReplaceAttr
}

func (h *commonHandler) newHandleState(buf *buffer.Buffer, freeBuf bool, sep string) handleState {
	s := handleState{
		h:       h,
		buf:     buf,
		freeBuf: freeBuf,
		sep:     sep,
		prefix:  buffer.New(),
	}
	if h.opts.ReplaceAttr != nil {
		s.groups = groupPool.Get().(*[]string)
		*s.groups = append(*s.groups, h.groups[:h.nOpenGroups]...)
	}
	return s
}

// handle is the internal implementation of Handler.Handle
// used by TextHandler and JSONHandler.
func (h *commonHandler) handle(r Record) error {
	state := h.newHandleState(buffer.New(), true, "")
	defer state.free()
	if h.json {
		state.buf.WriteByte('{')
	}

	// ...

	// level
	key := LevelKey
	val := r.Level
	if rep == nil {
		state.appendKey(key)
		state.appendString(val.String())
	} else {
		state.appendAttr(Any(key, val))
	}

	// ...

	key = MessageKey
	msg := r.Message
	if rep == nil {
		state.appendKey(key)
		state.appendString(msg)
	} else {
		state.appendAttr(String(key, msg))
	}

	state.groups = stateGroups // Restore groups passed to ReplaceAttrs.
	state.appendNonBuiltIns(r)
	state.buf.WriteByte('\n')

	h.mu.Lock()
	defer h.mu.Unlock()
	_, err := h.w.Write(*state.buf) // <---- buf holds the output
	return err
}

func (s *handleState) appendNonBuiltIns(r Record) {
	// preformatted Attrs
	if pfa := s.h.preformattedAttrs; len(pfa) > 0 {
		s.buf.WriteString(s.sep)
		s.buf.Write(pfa)
		s.sep = s.h.attrSep()
		if s.h.json && pfa[len(pfa)-1] == '{' {
			s.sep = ""
		}
	}

	// Attrs in Record -- unlike the built-in ones, they are in groups started
	// from WithGroup.
	r.Attrs(func(a Attr) bool {
		if s.appendAttr(a) {
			empty = false
		}
		return true
	})

	// ...

}