Proposal: Structured Logging

Author: Jonathan Amsterdam

Date: 2022-10-19

Issue: https://go.dev/issue/56345

Discussion: https://github.com/golang/go/discussions/54763

Preliminary implementation: https://go.googlesource.com/exp/+/refs/heads/master/slog

Package documentation: https://pkg.go.dev/golang.org/x/exp/slog

We propose adding structured logging with levels to the standard library, to reside in a new package with import path log/slog.

Structured logging is the ability to output logs with machine-readable structure, typically key-value pairs, in addition to a human-readable message. Structured logs can be parsed, filtered, searched and analyzed faster and more reliably than logs designed only for people to read. For many programs that aren't run directly by a user, like servers, logging is the main way for developers to observe the detailed behavior of the system, and often the first place they go to debug it. Logs therefore tend to be voluminous, and the ability to search and filter them quickly is essential.

In theory, one can produce structured logs with any logging package:

log.Printf(`{"message": %q, "count": %d}`, msg, count)

In practice, this is too tedious and error-prone, so structured logging packages provide an API for expressing key-value pairs. This proposal contains such an API.

We also propose generalizing the logging “backend.” The log package provides control only over the io.Writer that logs are written to. In the new package, every logger has a handler that can process a log event however it wishes. Although it is possible to have a structured logger with a fixed backend (for instance, zerolog outputs only JSON), having a flexible backend provides several benefits: programs can display the logs in a variety of formats, convert them to an RPC message for a network logging service, store them for later processing, and add to or modify the data.

Lastly, the design incorporates levels in a way that accommodates both traditional named levels and logr-style verbosities.

The goals of this design are:

  • Ease of use. A survey of the existing logging packages shows that programmers want an API that is light on the page and easy to understand. This proposal adopts the most popular way to express key-value pairs: alternating keys and values.

  • High performance. The API has been designed to minimize allocation and locking. It provides an alternative to alternating keys and values that is more cumbersome but faster (similar to Zap's Fields).

  • Integration with runtime tracing. The Go team is developing an improved runtime tracing system. Logs from this package will be incorporated seamlessly into those traces, giving developers the ability to correlate their program's actions with the behavior of the runtime.

What does success look like?

Go has many popular structured logging packages, all good at what they do. We do not expect developers to rewrite their existing third-party structured logging code to use this new package. We expect existing logging packages to coexist with this one for the foreseeable future.

We have tried to provide an API that is pleasant enough that users will prefer it to existing packages in new code, if only to avoid a dependency. (Some developers may find the runtime tracing integration compelling.) We also expect newcomers to Go to encounter this package before learning third-party packages, so they will likely be most familiar with it.

But more important than any traction gained by the “frontend” is the promise of a common “backend.” An application with many dependencies may find that it has linked in many logging packages. When all the logging packages support the standard handler interface proposed here, then the application can create a single handler and install it once for each logging library to get consistent logging across all its dependencies. Since this happens in the application‘s main function, the benefits of a unified backend can be obtained with minimal code churn. We expect that this proposal’s handlers will be implemented for all popular logging formats and network protocols, and that every common logging framework will provide a shim from their own backend to a handler. Then the Go logging community can work together to build high-quality backends that all can share.

Prior Work

The existing log package has been in the standard library since the release of Go 1 in March 2012. It provides formatted logging, but not structured logging or levels.

Logrus, one of the first structured logging packages, showed how an API could add structure while preserving the formatted printing of the log package. It uses maps to hold key-value pairs, which is relatively inefficient.

Zap grew out of Uber's frustration with the slow log times of their high-performance servers. It showed how a logger that avoided allocations could be very fast.

Zerolog reduced allocations even further, but at the cost of reducing the flexibility of the logging backend.

All the above loggers include named levels along with key-value pairs. Logr and Google's own glog use integer verbosities instead of named levels, providing a more fine-grained approach to filtering high-detail logs.

Other popular logging packages are Go-kit‘s log, HashiCorp’s hclog, and klog.

Design

Overview

Here is a short program that uses some of the new API:

import "log/slog"

func main() {
    slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr)))
    slog.Info("hello", "name", "Al")
    slog.Error("oops", "err", net.ErrClosed, "status", 500)
    slog.LogAttrs(slog.LevelError, "oops",
        slog.Any("err", net.ErrClosed), slog.Int("status", 500))
}

This program generates the following output on standard error:

time=2022-10-24T16:05:48.054-04:00 level=INFO msg=hello name=Al
time=2022-10-24T16:05:48.054-04:00 level=ERROR err="use of closed network connection" msg=oops status=500
time=2022-10-24T16:05:48.054-04:00 level=ERROR err="use of closed network connection" msg=oops status=500

It begins by setting the default logger to one that writes log records in an easy-to-read format similar to logfmt. (There is also a built-in handler for JSON.)

If the slog.SetDefault line is omitted, the output is sent to the standard log package, producing mostly structured output:

2022/10/24 16:07:00 INFO hello name=Al
2022/10/24 16:07:00 ERROR oops err="use of closed network connection" status=500
2022/10/24 16:07:00 ERROR oops  err="use of closed network connection" status=500

The program outputs three log messages augmented with key-value pairs. The first logs at the Info level, passing a single key-value pair along with the message. The second logs at the Error level, passing two key-value pairs.

The third produces the same output as the second, but more efficiently. Functions like Any and Int construct slog.Attr values, which are key-value pairs that avoid memory allocation for most values.

Main Types

The slog package contains three main types:

  • Logger is the frontend, providing output methods like Info and LogAttrs that developers call to produce logs.

  • Each call to a Logger output method creates a Record.

  • The Record is passed to a Handler for output.

We cover these bottom-up, beginning with Handler.

Handlers

A Handler describes the logging backend. It handles log records produced by a Logger.

A typical handler may print log records to standard error, or write them to a file, database or network service, or perhaps augment them with additional attributes and pass them on to another handler.

type Handler interface {
	// Enabled reports whether the handler handles records at the given level.
	// The handler ignores records whose level is lower.
	// It is called early, before any arguments are processed,
	// to save effort if the log event should be discarded.
	// The Logger's context is passed so Enabled can use its values
	// to make a decision. The context may be nil.
	Enabled(context.Context, Level) bool

	// Handle handles the Record.
	// It will only be called if Enabled returns true.
	//
	// The first argument is the context of the Logger that created the Record,
	// which may be nil.
	// It is present solely to provide Handlers access to the context's values.
	// Canceling the context should not affect record processing.
	// (Among other things, log messages may be necessary to debug a
	// cancellation-related problem.)
	//
	// Handle methods that produce output should observe the following rules:
	//   - If r.Time is the zero time, ignore the time.
	//   - If an Attr's key is the empty string and the value is not a group,
	//     ignore the Attr.
	//   - If a group's key is empty, inline the group's Attrs.
	//   - If a group has no Attrs (even if it has a non-empty key),
	//     ignore it.
	Handle(ctx context.Context, r Record) error

	// WithAttrs returns a new Handler whose attributes consist of
	// both the receiver's attributes and the arguments.
	// The Handler owns the slice: it may retain, modify or discard it.
	WithAttrs(attrs []Attr) Handler

	// WithGroup returns a new Handler with the given group appended to
	// the receiver's existing groups.
	// The keys of all subsequent attributes, whether added by With or in a
	// Record, should be qualified by the sequence of group names.
	//
	// How this qualification happens is up to the Handler, so long as
	// this Handler's attribute keys differ from those of another Handler
	// with a different sequence of group names.
	//
	// A Handler should treat WithGroup as starting a Group of Attrs that ends
	// at the end of the log event. That is,
	//
	//     logger.WithGroup("s").LogAttrs(level, msg, slog.Int("a", 1), slog.Int("b", 2))
	//
	// should behave like
	//
	//     logger.LogAttrs(level, msg, slog.Group("s", slog.Int("a", 1), slog.Int("b", 2)))
	//
	// If the name is empty, WithGroup returns the receiver.
	WithGroup(name string) Handler
}

The slog package provides two handlers, one for simple textual output and one for JSON. They are described in more detail below.

The Record Type

A Record holds information about a log event.

type Record struct {
	// The time at which the output method (Log, Info, etc.) was called.
	Time time.Time

	// The log message.
	Message string

	// The level of the event.
	Level Level

	// The program counter at the time the record was constructed, as determined
	// by runtime.Callers. If zero, no program counter is available.
	//
	// The only valid use for this value is as an argument to
	// [runtime.CallersFrames]. In particular, it must not be passed to
	// [runtime.FuncForPC].
	PC uintptr

	// Has unexported fields.
}

Records have two methods for accessing the sequence of Attrs. This API allows an efficient implementation of the Attr sequence that avoids copying and minimizes allocation.

func (r Record) Attrs(f func(Attr))
    Attrs calls f on each Attr in the Record.

func (r Record) NumAttrs() int
    NumAttrs returns the number of attributes in the Record.

So that other logging backends can wrap Handlers, it is possible to construct a Record directly and add attributes to it:

func NewRecord(t time.Time, level Level, msg string, pc uintptr) Record
    NewRecord creates a Record from the given arguments. Use Record.AddAttrs to
    add attributes to the Record.

    NewRecord is intended for logging APIs that want to support a Handler as a
    backend.

func (r *Record) AddAttrs(attrs ...Attr)
    AddAttrs appends the given Attrs to the Record's list of Attrs. It resolves
    the Attrs before doing so.

func (r *Record) Add(args ...any)
    Add converts the args to Attrs as described in Logger.Log, then appends the
    Attrs to the Record's list of Attrs. It resolves the Attrs before doing so.

Copies of a Record share state. A Record should not be modified after handing out a copy to it. Use Clone for that:

func (r Record) Clone() Record
    Clone returns a copy of the record with no shared state. The original record
    and the clone can both be modified without interfering with each other.

The Attr and Value Types

An Attr is a key-value pair.

type Attr struct {
	Key   string
	Value Value
}

There are convenience functions for constructing Attrs with various value types, as well as Equal and String methods.

func Any(key string, value any) Attr
    Any returns an Attr for the supplied value. See Value.AnyValue for how
    values are treated.

func Bool(key string, v bool) Attr
    Bool returns an Attr for a bool.

func Duration(key string, v time.Duration) Attr
    Duration returns an Attr for a time.Duration.

func Float64(key string, v float64) Attr
    Float64 returns an Attr for a floating-point number.

func Group(key string, as ...Attr) Attr
    Group returns an Attr for a Group Value. The caller must not subsequently
    mutate the argument slice.

    Use Group to collect several Attrs under a single key on a log line, or as
    the result of LogValue in order to log a single value as multiple Attrs.

func Int(key string, value int) Attr
    Int converts an int to an int64 and returns an Attr with that value.

func Int64(key string, value int64) Attr
    Int64 returns an Attr for an int64.

func String(key, value string) Attr
    String returns an Attr for a string value.

func Time(key string, v time.Time) Attr
    Time returns an Attr for a time.Time. It discards the monotonic portion.

func Uint64(key string, v uint64) Attr
    Uint64 returns an Attr for a uint64.

func (a Attr) Equal(b Attr) bool
    Equal reports whether a and b have equal keys and values.

func (a Attr) String() string

A Value can represent any Go value, but unlike type any, it can represent most small values without an allocation. In particular, integer types and strings, which account for the vast majority of values in log messages, do not require allocation. The default version of Value uses package unsafe to store any value in three machine words. The version without unsafe requires five.

There are constructor functions for common types, and a general one, AnyValue, that dispatches on its argument type.

type Value struct {
	// Has unexported fields.
}

func AnyValue(v any) Value
    AnyValue returns a Value for the supplied value.

    Given a value of one of Go's predeclared string, bool, or (non-complex)
    numeric types, AnyValue returns a Value of kind String, Bool, Uint64, Int64,
    or Float64. The width of the original numeric type is not preserved.

    Given a time.Time or time.Duration value, AnyValue returns a Value of kind
    TimeKind or DurationKind. The monotonic time is not preserved.

    For nil, or values of all other types, including named types whose
    underlying type is numeric, AnyValue returns a value of kind AnyKind.

func BoolValue(v bool) Value
    BoolValue returns a Value for a bool.

func DurationValue(v time.Duration) Value
    DurationValue returns a Value for a time.Duration.

func Float64Value(v float64) Value
    Float64Value returns a Value for a floating-point number.

func GroupValue(as ...Attr) Value
    GroupValue returns a new Value for a list of Attrs. The caller must not
    subsequently mutate the argument slice.

func Int64Value(v int64) Value
    Int64Value returns a Value for an int64.

func IntValue(v int) Value
    IntValue returns a Value for an int.

func StringValue(value string) Value
    String returns a new Value for a string.

func TimeValue(v time.Time) Value
    TimeValue returns a Value for a time.Time. It discards the monotonic
    portion.

func Uint64Value(v uint64) Value
    Uint64Value returns a Value for a uint64.

Extracting Go values from a Value is reminiscent of reflect.Value: there is a Kind method that returns an enum of type Kind, and a method for each Kind that returns the value or panics if it is the wrong kind.

type Kind int
    Kind is the kind of a Value.

const (
	AnyKind Kind = iota
	BoolKind
	DurationKind
	Float64Kind
	Int64Kind
	StringKind
	TimeKind
	Uint64Kind
	GroupKind
	LogValuerKind
)

func (v Value) Any() any
    Any returns v's value as an any.

func (v Value) Bool() bool
    Bool returns v's value as a bool. It panics if v is not a bool.

func (a Value) Duration() time.Duration
    Duration returns v's value as a time.Duration. It panics if v is not a
    time.Duration.

func (v Value) Equal(w Value) bool
    Equal reports whether v and w have equal keys and values.

func (v Value) Float64() float64
    Float64 returns v's value as a float64. It panics if v is not a float64.

func (v Value) Group() []Attr
    Group returns v's value as a []Attr. It panics if v's Kind is not GroupKind.

func (v Value) Int64() int64
    Int64 returns v's value as an int64. It panics if v is not a signed integer.

func (v Value) Kind() Kind
    Kind returns v's Kind.

func (v Value) LogValuer() LogValuer
    LogValuer returns v's value as a LogValuer. It panics if v is not a
    LogValuer.

func (v Value) Resolve() Value
    Resolve repeatedly calls LogValue on v while it implements LogValuer, and
    returns the result. If the number of LogValue calls exceeds a threshold, a
    Value containing an error is returned. Resolve's return value is guaranteed
    not to be of Kind LogValuerKind.

func (v Value) String() string
    String returns Value's value as a string, formatted like fmt.Sprint.
    Unlike the methods Int64, Float64, and so on, which panic if v is of the
    wrong kind, String never panics.

func (v Value) Time() time.Time
    Time returns v's value as a time.Time. It panics if v is not a time.Time.

func (v Value) Uint64() uint64
    Uint64 returns v's value as a uint64. It panics if v is not an unsigned
    integer.

The LogValuer interface

A LogValuer is any Go value that can convert itself into a Value for logging.

This mechanism may be used to defer expensive operations until they are needed, or to expand a single value into a sequence of components.

type LogValuer interface {
	LogValue() Value
}

Value.Resolve can be used to call the LogValue method.

func (v Value) Resolve() Value
    Resolve repeatedly calls LogValue on v while it implements LogValuer, and
    returns the result. If the number of LogValue calls exceeds a threshold, a
    Value containing an error is returned. Resolve's return value is guaranteed
    not to be of Kind LogValuerKind.

The Attrs passed to a Handler.WithAttrs, and the Attrs obtained via Record.Attrs, have already been resolved, that is, replaced with a call to Resolve.

As an example of LogValuer, a type could obscure its value in log output like so:

type Password string

func (p Password) LogValue() slog.Value {
    return slog.StringValue("REDACTED")
}

Loggers

A Logger records structured information about each call to its Log, Debug, Info, Warn, and Error methods. For each call, it creates a Record and passes it to a Handler.

type Logger struct {
	// Has unexported fields.
}

A Logger consists of a Handler. Use New to create Logger with a Handler, and the Handler method to retrieve it.

func New(h Handler) *Logger
    New creates a new Logger with the given Handler.

func (l *Logger) Handler() Handler
    Handler returns l's Handler.

There is a single, global default Logger. It can be set and retrieved with the SetDefault and Default functions.

func SetDefault(l *Logger)
    SetDefault makes l the default Logger. After this call, output from the
    log package's default Logger (as with log.Print, etc.) will be logged at
    LevelInfo using l's Handler.

func Default() *Logger
    Default returns the default Logger.

The slog package works to ensure consistent output with the log package. Writing to slog's default logger without setting a handler will write structured text to log's default logger. Once a handler is set with SetDefault, as in the example above, the default log logger will send its text output to the structured handler.

Output methods

Logger's output methods produce log output by constructing a Record and passing it to the Logger‘s handler. There are two output methods for each of four most common levels, one which takes a context and one which doesn’t. There is also a Log method that takes any level, and a LogAttrs method that accepts only Attrs as an optimization, both of which take a context.

These methods first call Handler.Enabled to see if they should proceed.

Each of these methods has a corresponding top-level function that uses the default logger.

The context is passed to Handler.Enabled and Handler.Handle. Handlers sometimes need to retrieve values from a context, tracing spans being a prime example.

We will provide a vet check for the methods that take a list of any arguments to catch problems with missing keys or values.

func (l *Logger) Log(ctx context.Context, level Level, msg string, args ...any)
    Log emits a log record with the current time and the given level and
    message. The Record's Attrs consist of the Logger's attributes followed by
    the Attrs specified by args.

    The attribute arguments are processed as follows:
      - If an argument is an Attr, it is used as is.
      - If an argument is a string and this is not the last argument, the
        following argument is treated as the value and the two are combined into
        an Attr.
      - Otherwise, the argument is treated as a value with key "!BADKEY".

func (l *Logger) LogAttrs(ctx context.Context, level Level, msg string, attrs ...Attr)
    LogAttrs is a more efficient version of Logger.Log that accepts only Attrs.

func (l *Logger) Debug(msg string, args ...any)
    Debug logs at LevelDebug.

func (l *Logger) Info(msg string, args ...any)
    Info logs at LevelInfo.

func (l *Logger) Warn(msg string, args ...any)
    Warn logs at LevelWarn.

func (l *Logger) Error(msg string, args ...any)
    Error logs at LevelError.

func (l *Logger) DebugCtx(ctx context.Context, msg string, args ...any)
    DebugCtx logs at LevelDebug with the given context.

func (l *Logger) InfoCtx(ctx context.Context, msg string, args ...any)
    InfoCtx logs at LevelInfo with the given context.

func (l *Logger) WarnCtx(ctx context.Context, msg string, args ...any)
    WarnCtx logs at LevelWarn with the given context.

func (l *Logger) ErrorCtx(ctx context.Context, msg string, args ...any)
    ErrorCtx logs at LevelError with the given context.

Loggers can have attributes as well, added by the With method.

func (l *Logger) With(args ...any) *Logger
    With returns a new Logger that includes the given arguments, converted to
    Attrs as in Logger.Log. The Attrs will be added to each output from the
    Logger.

    The new Logger's handler is the result of calling WithAttrs on the
    receiver's handler.

Groups

Although most attribute values are simple types like strings and integers, sometimes aggregate or composite values are desired. For example, consider

type Name struct {
    First, Last string
}

To handle values like this we include GroupKind for groups of Attrs. To log a Name n as a group, we could write

slog.Info("message",
    slog.Group("name",
        slog.String("first", n.First),
        slog.String("last", n.Last),
    ),
)

Handlers should qualify a group's members by its name. What “qualify” means depends on the handler. A handler that supports recursive data, like the built-in JSONHandler, can use the group name as a key to a nested object:

"name": {"first": "Ren", "last": "Hoek"}

Handlers that use a flat output representation, like the built-in TextHandler, could prefix the group member's keys with the group name. This is TextHandler's output:

name.first=Ren name.last=Hoek

If the author of the Name type wanted to arrange matters so that Names always logged in this way, they could implement the LogValuer interface discussed above:

func (n Name) LogValue() slog.Value {
    return slog.GroupValue(
        slog.String("first", n.First),
        slog.String("last", n.Last),
    )
}

Now, if n is a Name, the log line

slog.Info("message", "name", n)

will render exactly like the example with an explicit slog.Group above.

Logger groups

Sometimes it is useful to qualify all the attribute keys from a Logger. For example, an application may be composed of multiple subsystems, some of which may use the same attribute keys. Qualifying each subsystem's keys is one way to avoid duplicates. This can be done with Logger.WithGroup. Duplicate keys can be avoided by handing each subsystem a Logger with a different group.

func (l *Logger) WithGroup(name string) *Logger
    WithGroup returns a new Logger that starts a group. The keys of all
    attributes added to the Logger will be qualified by the given name.

Levels

A Level is the importance or severity of a log event. The higher the level, the more important or severe the event.

type Level int

The slog package provides names for common levels.

The level numbers below don't really matter too much. Any system can map them to another numbering scheme if it wishes. We picked them to satisfy three constraints.

First, we wanted the default level to be Info. Since Levels are ints, Info is the default value for int, zero.

Second, we wanted to make it easy to work with verbosities instead of levels. As discussed above, some logging packages like glog and Logr use verbosities instead, where a verbosity of 0 corresponds to the Info level and higher values represent less important messages. Negating a verbosity converts it into a Level. To use a verbosity of v with this design, pass -v to Log or LogAttrs.

Third, we wanted some room between levels to accommodate schemes with named levels between ours. For example, Google Cloud Logging defines a Notice level between Info and Warn. Since there are only a few of these intermediate levels, the gap between the numbers need not be large. Our gap of 4 matches OpenTelemetry's mapping. Subtracting 9 from an OpenTelemetry level in the DEBUG, INFO, WARN and ERROR ranges converts it to the corresponding slog Level range. OpenTelemetry also has the names TRACE and FATAL, which slog does not. But those OpenTelemetry levels can still be represented as slog Levels by using the appropriate integers.

const (
	LevelDebug Level = -4
	LevelInfo  Level = 0
	LevelWarn  Level = 4
	LevelError Level = 8
)

The Leveler interface generalizes Level, so that a Handler.Enabled implementation can vary its behavior. One way to get dynamic behavior is to use LevelVar.

type Leveler interface {
	Level() Level
}
    A Leveler provides a Level value.

    As Level itself implements Leveler, clients typically supply a Level value
    wherever a Leveler is needed, such as in HandlerOptions. Clients who need to
    vary the level dynamically can provide a more complex Leveler implementation
    such as *LevelVar.

func (l Level) Level() Level
    Level returns the receiver. It implements Leveler.

type LevelVar struct {
	// Has unexported fields.
}
    A LevelVar is a Level variable, to allow a Handler level to change
    dynamically. It implements Leveler as well as a Set method, and it is safe
    for use by multiple goroutines. The zero LevelVar corresponds to LevelInfo.

func (v *LevelVar) Level() Level
    Level returns v's level.

func (v *LevelVar) Set(l Level)
    Set sets v's level to l.

func (v *LevelVar) String() string

Provided Handlers

The slog package includes two handlers, which behave similarly except for their output format. TextHandler emits attributes as KEY=VALUE, and JSONHandler writes line-delimited JSON objects. Both can be configured using a HandlerOptions. A zero HandlerOptions consists entirely of default values.

type HandlerOptions struct {
	// When AddSource is true, the handler adds a ("source", "file:line")
	// attribute to the output indicating the source code position of the log
	// statement. AddSource is false by default to skip the cost of computing
	// this information.
	AddSource bool

	// 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.
	Level Leveler

	// 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 an Attr with Key == "", 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.
	//
	// The first argument is a list of currently open groups that contain the
	// Attr. It must not be retained or modified. ReplaceAttr is never called
	// for Group attributes, only their contents. For example, the attribute
	// list
	//
	//     Int("a", 1), Group("g", Int("b", 2)), Int("c", 3)
	//
	// results in consecutive calls to ReplaceAttr with the following arguments:
	//
	//     nil, Int("a", 1)
	//     []string{"g"}, Int("b", 2)
	//     nil, Int("c", 3)
	//
	// ReplaceAttr can be used to change the default keys of the built-in
	// attributes, convert types (for example, to replace a `time.Time` with the
	// integer seconds since the Unix epoch), sanitize personal information, or
	// remove attributes from the output.
	ReplaceAttr func(groups []string, a Attr) Attr
}

Interoperating with Other Log Packages

As stated earlier, we expect that this package will interoperate with other log packages.

One way that could happen is for another package's frontend to send slog.Records to a slog.Handler. For instance, a logr.LogSink implementation could construct a Record from a message and list of keys and values, and pass it to a Handler. That is facilitated by NewRecord, Record.Add and Record.AddAttrs, described above.

Another way for two log packages to work together is for the other package to wrap its backend as a slog.Handler, so users could write code with the slog package‘s API but connect the results to an existing logr.LogSink, for example. This involves writing a slog.Handler that wraps the other logger’s backend. Doing so doesn't seem to require any additional support from this package.

Testing Package

To verify that a Handler's behavior matches the specification, we propose a package testing/slogtest with one exported function:

// TestHandler tests a [slog.Handler].
// If TestHandler finds any misbehaviors, it returns an error for each,
// combined into a single error with errors.Join.
//
// TestHandler installs the given Handler in a [slog.Logger] and
// makes several calls to the Logger's output methods.
//
// The results function is invoked after all such calls.
// It should return a slice of map[string]any, one for each call to a Logger output method.
// The keys and values of the map should correspond to the keys and values of the Handler's
// output. Each group in the output should be represented as its own nested map[string]any.
//
// If the Handler outputs JSON, then calling [encoding/json.Unmarshal] with a `map[string]any`
// will create the right data structure.
func TestHandler(h slog.Handler, results func() []map[string]any) error

Acknowledgements

Ian Cottrell's ideas about high-performance observability, captured in the golang.org/x/exp/event package, informed a great deal of the design and implementation of this proposal.

Seth Vargo’s ideas on logging were a source of motivation and inspiration. His comments on an earlier draft helped improve the proposal.

Michael Knyszek explained how logging could work with runtime tracing.

Tim Hockin helped us understand logr's design choices, which led to significant improvements.

Abhinav Gupta helped me understand Zap in depth, which informed the design.

Russ Cox provided valuable feedback and helped shape the final design.

Alan Donovan's CL reviews greatly improved the implementation.

The participants in the GitHub discussion helped us confirm we were on the right track, and called our attention to important features we had overlooked (and have since added).

Appendix: API

package slog

Package slog provides structured logging, in which log records include a
message, a severity level, and various other attributes expressed as key-value
pairs.

It defines a type, Logger, which provides several methods (such as Logger.Info
and Logger.Error) for reporting events of interest.

Each Logger is associated with a Handler. A Logger output method creates a
Record from the method arguments and passes it to the Handler, which decides how
to handle it. There is a default Logger accessible through top-level functions
(such as Info and Error) that call the corresponding Logger methods.

A log record consists of a time, a level, a message, and a set of key-value
pairs, where the keys are strings and the values may be of any type. As an
example,

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

creates a record containing the time of the call, a level of Info, the message
"hello", and a single pair with key "count" and value 3.

The Info top-level function calls the Logger.Info method on the default Logger.
In addition to Logger.Info, there are methods for Debug, Warn and Error levels.
Besides these convenience methods for common levels, there is also a Logger.Log
method which takes the level as an argument. Each of these methods has a
corresponding top-level function that uses the default logger.

The default handler formats the log record's message, time, level, and
attributes as a string and passes it to the log package.

    2022/11/08 15:28:26 INFO hello count=3

For more control over the output format, create a logger with a different
handler. This statement uses New to create a new logger with a TextHandler that
writes structured records in text form to standard error:

    logger := slog.New(slog.NewTextHandler(os.Stderr))

TextHandler output is a sequence of key=value pairs, easily and unambiguously
parsed by machine. This statement:

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

produces this output:

    time=2022-11-08T15:28:26.000-05:00 level=INFO msg=hello count=3

The package also provides JSONHandler, whose output is line-delimited JSON:

    logger := slog.New(slog.NewJSONHandler(os.Stdout))
    logger.Info("hello", "count", 3)

produces this output:

    {"time":"2022-11-08T15:28:26.000000000-05:00","level":"INFO","msg":"hello","count":3}

Both TextHandler and JSONHandler can be configured with a HandlerOptions.
There are options for setting the minimum level (see Levels, below), displaying
the source file and line of the log call, and modifying attributes before they
are logged.

Setting a logger as the default with

    slog.SetDefault(logger)

will cause the top-level functions like Info to use it. SetDefault also updates
the default logger used by the log package, so that existing applications that
use log.Printf and related functions will send log records to the logger's
handler without needing to be rewritten.

# Attrs and Values

An Attr is a key-value pair. The Logger output methods accept Attrs as well as
alternating keys and values. The statement

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

behaves the same as

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

There are convenience constructors for Attr such as Int, String, and Bool for
common types, as well as the function Any for constructing Attrs of any type.

The value part of an Attr is a type called Value. Like an [any], a Value can
hold any Go value, but it can represent typical values, including all numbers
and strings, without an allocation.

For the most efficient log output, use Logger.LogAttrs. It is similar to
Logger.Log but accepts only Attrs, not alternating keys and values; this allows
it, too, to avoid allocation.

The call

    logger.LogAttrs(nil, slog.LevelInfo, "hello", slog.Int("count", 3))

is the most efficient way to achieve the same output as

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

Some attributes are common to many log calls. For example, you may wish to
include the URL or trace identifier of a server request with all log events
arising from the request. Rather than repeat the attribute with every log call,
you can use Logger.With to construct a new Logger containing the attributes:

    logger2 := logger.With("url", r.URL)

The arguments to With are the same key-value pairs used in Logger.Info.
The result is a new Logger with the same handler as the original, but additional
attributes that will appear in the output of every call.

# Levels

A Level is an integer representing the importance or severity of a log event.
The higher the level, the more severe the event. This package defines constants
for the most common levels, but any int can be used as a level.

In an application, you may wish to log messages only at a certain level or
greater. One common configuration is to log messages at Info or higher levels,
suppressing debug logging until it is needed. The built-in handlers can be
configured with the minimum level to output by setting [HandlerOptions.Level].
The program's `main` function typically does this. The default value is
LevelInfo.

Setting the [HandlerOptions.Level] field to a Level value fixes the handler's
minimum level throughout its lifetime. Setting it to a LevelVar allows the level
to be varied dynamically. A LevelVar holds a Level and is safe to read or write
from multiple goroutines. To vary the level dynamically for an entire program,
first initialize a global LevelVar:

    var programLevel = new(slog.LevelVar) // Info by default

Then use the LevelVar to construct a handler, and make it the default:

    h := slog.HandlerOptions{Level: programLevel}.NewJSONHandler(os.Stderr)
    slog.SetDefault(slog.New(h))

Now the program can change its logging level with a single statement:

    programLevel.Set(slog.LevelDebug)

# Groups

Attributes can be collected into groups. A group has a name that is used to
qualify the names of its attributes. How this qualification is displayed depends
on the handler. TextHandler separates the group and attribute names with a dot.
JSONHandler treats each group as a separate JSON object, with the group name as
the key.

Use Group to create a Group Attr from a name and a list of Attrs:

    slog.Group("request",
        slog.String("method", r.Method),
        slog.Any("url", r.URL))

TextHandler would display this group as

    request.method=GET request.url=http://example.com

JSONHandler would display it as

    "request":{"method":"GET","url":"http://example.com"}

Use Logger.WithGroup to qualify all of a Logger's output with a group name.
Calling WithGroup on a Logger results in a new Logger with the same Handler as
the original, but with all its attributes qualified by the group name.

This can help prevent duplicate attribute keys in large systems, where
subsystems might use the same keys. Pass each subsystem a different Logger with
its own group name so that potential duplicates are qualified:

    logger := slog.Default().With("id", systemID)
    parserLogger := logger.WithGroup("parser")
    parseInput(input, parserLogger)

When parseInput logs with parserLogger, its keys will be qualified with
"parser", so even if it uses the common key "id", the log line will have
distinct keys.

# Contexts

Some handlers may wish to include information from the context.Context that is
available at the call site. One example of such information is the identifier
for the current span when tracing is is enabled.

The Logger.Log and Logger.LogAttrs methods take a context as a first argument,
as do their corresponding top-level functions.

Although the convenience methods on Logger (Info and so on) and the
corresponding top-level functions do not take a context, the alternatives ending
in "Ctx" do. For example,

    slog.InfoCtx(ctx, "message")

It is recommended to pass a context to an output method if one is available.

# Advanced topics

## Customizing a type's logging behavior

If a type implements the LogValuer interface, the Value returned from its
LogValue method is used for logging. You can use this to control how values
of the type appear in logs. For example, you can redact secret information
like passwords, or gather a struct's fields in a Group. See the examples under
LogValuer for details.

A LogValue method may return a Value that itself implements LogValuer. The
Value.Resolve method handles these cases carefully, avoiding infinite loops and
unbounded recursion. Handler authors and others may wish to use Value.Resolve
instead of calling LogValue directly.

## 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(format string, args ...any) {
        slog.Default().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.

## Working with Records

Sometimes a Handler will need to modify a Record before passing it on to another
Handler or backend. A Record contains a mixture of simple public fields (e.g.
Time, Level, Message) and hidden fields that refer to state (such as attributes)
indirectly. This means that modifying a simple copy of a Record (e.g. by calling
Record.Add or Record.AddAttrs to add attributes) may have unexpected effects
on the original. Before modifying a Record, use [Clone] to create a copy that
shares no state with the original, or create a new Record with NewRecord and
build up its Attrs by traversing the old ones with Record.Attrs.

## Performance considerations

If profiling your application demonstrates that logging is taking significant
time, the following suggestions may help.

If many log lines have a common attribute, use Logger.With to create a Logger
with that attribute. The built-in handlers will format that attribute only once,
at the call to Logger.With. The Handler interface is designed to allow that
optimization, and a well-written Handler should take advantage of it.

The arguments to a log call are always evaluated, even if the log event is
discarded. If possible, defer computation so that it happens only if the value
is actually logged. For example, consider the call

    slog.Info("starting request", "url", r.URL.String())  // may compute String unnecessarily

The URL.String method will be called even if the logger discards Info-level
events. Instead, pass the URL directly:

    slog.Info("starting request", "url", &r.URL) // calls URL.String only if needed

The built-in TextHandler will call its String method, but only if the log event
is enabled. Avoiding the call to String also preserves the structure of the
underlying value. For example JSONHandler emits the components of the parsed
URL as a JSON object. If you want to avoid eagerly paying the cost of the String
call without causing the handler to potentially inspect the structure of the
value, wrap the value in a fmt.Stringer implementation that hides its Marshal
methods.

You can also use the LogValuer interface to avoid unnecessary work in disabled
log calls. Say you need to log some expensive value:

    slog.Debug("frobbing", "value", computeExpensiveValue(arg))

Even if this line is disabled, computeExpensiveValue will be called. To avoid
that, define a type implementing LogValuer:

    type expensive struct { arg int }

    func (e expensive) LogValue() slog.Value {
        return slog.AnyValue(computeExpensiveValue(e.arg))
    }

Then use a value of that type in log calls:

    slog.Debug("frobbing", "value", expensive{arg})

Now computeExpensiveValue will only be called when the line is enabled.

The built-in handlers acquire a lock before calling io.Writer.Write to ensure
that each record is written in one piece. User-defined handlers are responsible
for their own locking.

CONSTANTS

const (
	// TimeKey is the key used by the built-in handlers for the time
	// when the log method is called. The associated Value is a [time.Time].
	TimeKey = "time"
	// LevelKey is the key used by the built-in handlers for the level
	// of the log call. The associated value is a [Level].
	LevelKey = "level"
	// MessageKey is the key used by the built-in handlers for the
	// message of the log call. The associated value is a string.
	MessageKey = "msg"
	// SourceKey is the key used by the built-in handlers for the source file
	// and line of the log call. The associated value is a string.
	SourceKey = "source"
)
    Keys for "built-in" attributes.


FUNCTIONS

func Debug(msg string, args ...any)
    Debug calls Logger.Debug on the default logger.

func DebugCtx(ctx context.Context, msg string, args ...any)
    DebugCtx calls Logger.DebugCtx on the default logger.

func Error(msg string, args ...any)
    Error calls Logger.Error on the default logger.

func ErrorCtx(ctx context.Context, msg string, args ...any)
    ErrorCtx calls Logger.ErrorCtx on the default logger.

func Info(msg string, args ...any)
    Info calls Logger.Info on the default logger.

func InfoCtx(ctx context.Context, msg string, args ...any)
    InfoCtx calls Logger.InfoCtx on the default logger.

func Log(ctx context.Context, level Level, msg string, args ...any)
    Log calls Logger.Log on the default logger.

func LogAttrs(ctx context.Context, level Level, msg string, attrs ...Attr)
    LogAttrs calls Logger.LogAttrs on the default logger.

func NewLogLogger(h Handler, level Level) *log.Logger
    NewLogLogger returns a new log.Logger such that each call to its Output
    method dispatches a Record to the specified handler. The logger acts as a
    bridge from the older log API to newer structured logging handlers.

func SetDefault(l *Logger)
    SetDefault makes l the default Logger. After this call, output from the
    log package's default Logger (as with log.Print, etc.) will be logged at
    LevelInfo using l's Handler.

func Warn(msg string, args ...any)
    Warn calls Logger.Warn on the default logger.

func WarnCtx(ctx context.Context, msg string, args ...any)
    WarnCtx calls Logger.WarnCtx on the default logger.


TYPES

type Attr struct {
	Key   string
	Value Value
}
    An Attr is a key-value pair.

func Any(key string, value any) Attr
    Any returns an Attr for the supplied value. See [Value.AnyValue] for how
    values are treated.

func Bool(key string, v bool) Attr
    Bool returns an Attr for a bool.

func Duration(key string, v time.Duration) Attr
    Duration returns an Attr for a time.Duration.

func Float64(key string, v float64) Attr
    Float64 returns an Attr for a floating-point number.

func Group(key string, as ...Attr) Attr
    Group returns an Attr for a Group Value. The caller must not subsequently
    mutate the argument slice.

    Use Group to collect several Attrs under a single key on a log line, or as
    the result of LogValue in order to log a single value as multiple Attrs.

func Int(key string, value int) Attr
    Int converts an int to an int64 and returns an Attr with that value.

func Int64(key string, value int64) Attr
    Int64 returns an Attr for an int64.

func String(key, value string) Attr
    String returns an Attr for a string value.

func Time(key string, v time.Time) Attr
    Time returns an Attr for a time.Time. It discards the monotonic portion.

func Uint64(key string, v uint64) Attr
    Uint64 returns an Attr for a uint64.

func (a Attr) Equal(b Attr) bool
    Equal reports whether a and b have equal keys and values.

func (a Attr) String() string

type Handler interface {
	// Enabled reports whether the handler handles records at the given level.
	// The handler ignores records whose level is lower.
	// It is called early, before any arguments are processed,
	// to save effort if the log event should be discarded.
	// If called from a Logger method, the first argument is the context
	// passed to that method, or context.Background() if nil was passed
	// or the method does not take a context.
	// The context is passed so Enabled can use its values
	// to make a decision.
	Enabled(context.Context, Level) bool

	// Handle handles the Record.
	// It will only be called Enabled returns true.
	// The Context argument is as for Enabled.
	// It is present solely to provide Handlers access to the context's values.
	// Canceling the context should not affect record processing.
	// (Among other things, log messages may be necessary to debug a
	// cancellation-related problem.)
	//
	// Handle methods that produce output should observe the following rules:
	//   - If r.Time is the zero time, ignore the time.
	//   - If r.PC is zero, ignore it.
	//   - If an Attr's key is the empty string and the value is not a group,
	//     ignore the Attr.
	//   - If a group's key is empty, inline the group's Attrs.
	//   - If a group has no Attrs (even if it has a non-empty key),
	//     ignore it.
	Handle(context.Context, Record) error

	// WithAttrs returns a new Handler whose attributes consist of
	// both the receiver's attributes and the arguments.
	// The Handler owns the slice: it may retain, modify or discard it.
	// [Logger.With] will resolve the Attrs.
	WithAttrs(attrs []Attr) Handler

	// WithGroup returns a new Handler with the given group appended to
	// the receiver's existing groups.
	// The keys of all subsequent attributes, whether added by With or in a
	// Record, should be qualified by the sequence of group names.
	//
	// How this qualification happens is up to the Handler, so long as
	// this Handler's attribute keys differ from those of another Handler
	// with a different sequence of group names.
	//
	// A Handler should treat WithGroup as starting a Group of Attrs that ends
	// at the end of the log event. That is,
	//
	//     logger.WithGroup("s").LogAttrs(level, msg, slog.Int("a", 1), slog.Int("b", 2))
	//
	// should behave like
	//
	//     logger.LogAttrs(level, msg, slog.Group("s", slog.Int("a", 1), slog.Int("b", 2)))
	//
	// If the name is empty, WithGroup returns the receiver.
	WithGroup(name string) Handler
}
    A Handler handles log records produced by a Logger..

    A typical handler may print log records to standard error, or write them to
    a file or database, or perhaps augment them with additional attributes and
    pass them on to another handler.

    Any of the Handler's methods may be called concurrently with itself or
    with other methods. It is the responsibility of the Handler to manage this
    concurrency.

    Users of the slog package should not invoke Handler methods directly.
    They should use the methods of Logger instead.

type HandlerOptions struct {
	// When AddSource is true, the handler adds a ("source", "file:line")
	// attribute to the output indicating the source code position of the log
	// statement. AddSource is false by default to skip the cost of computing
	// this information.
	AddSource bool

	// 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.
	Level Leveler

	// 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 an Attr with Key == "", 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.
	//
	// The first argument is a list of currently open groups that contain the
	// Attr. It must not be retained or modified. ReplaceAttr is never called
	// for Group attributes, only their contents. For example, the attribute
	// list
	//
	//     Int("a", 1), Group("g", Int("b", 2)), Int("c", 3)
	//
	// results in consecutive calls to ReplaceAttr with the following arguments:
	//
	//     nil, Int("a", 1)
	//     []string{"g"}, Int("b", 2)
	//     nil, Int("c", 3)
	//
	// ReplaceAttr can be used to change the default keys of the built-in
	// attributes, convert types (for example, to replace a `time.Time` with the
	// integer seconds since the Unix epoch), sanitize personal information, or
	// remove attributes from the output.
	ReplaceAttr func(groups []string, a Attr) Attr
}
    HandlerOptions are options for a TextHandler or JSONHandler. A zero
    HandlerOptions consists entirely of default values.

func (opts HandlerOptions) NewJSONHandler(w io.Writer) *JSONHandler
    NewJSONHandler creates a JSONHandler with the given options that writes to
    w.

func (opts HandlerOptions) NewTextHandler(w io.Writer) *TextHandler
    NewTextHandler creates a TextHandler with the given options that writes to
    w.

type JSONHandler struct {
	// Has unexported fields.
}
    JSONHandler is a Handler that writes Records to an io.Writer as
    line-delimited JSON objects.

func NewJSONHandler(w io.Writer) *JSONHandler
    NewJSONHandler creates a JSONHandler that writes to w, using the default
    options.

func (h *JSONHandler) Enabled(_ context.Context, level Level) bool
    Enabled reports whether the handler handles records at the given level.
    The handler ignores records whose level is lower.

func (h *JSONHandler) Handle(_ context.Context, r Record) error
    Handle formats its argument Record as a JSON object on a single line.

    If the Record's time is zero, the time is omitted. Otherwise, the key is
    "time" and the value is output as with json.Marshal.

    If the Record's level is zero, the level is omitted. Otherwise, the key is
    "level" and the value of Level.String is output.

    If the AddSource option is set and source information is available, the key
    is "source" and the value is output as "FILE:LINE".

    The message's key is "msg".

    To modify these or other attributes, or remove them from the output,
    use [HandlerOptions.ReplaceAttr].

    Values are formatted as with encoding/json.Marshal, with the following
    exceptions:
      - Floating-point NaNs and infinities are formatted as one of the strings
        "NaN", "+Inf" or "-Inf".
      - Levels are formatted as with Level.String.
      - HTML characters are not escaped.

    Each call to Handle results in a single serialized call to io.Writer.Write.

func (h *JSONHandler) WithAttrs(attrs []Attr) Handler
    WithAttrs returns a new JSONHandler whose attributes consists of h's
    attributes followed by attrs.

func (h *JSONHandler) WithGroup(name string) Handler

type Kind int
    Kind is the kind of a Value.

const (
	KindAny Kind = iota
	KindBool
	KindDuration
	KindFloat64
	KindInt64
	KindString
	KindTime
	KindUint64
	KindGroup
	KindLogValuer
)
func (k Kind) String() string

type Level int
    A Level is the importance or severity of a log event. The higher the level,
    the more important or severe the event.

const (
	LevelDebug Level = -4
	LevelInfo  Level = 0
	LevelWarn  Level = 4
	LevelError Level = 8
)
    Second, we wanted to make it easy to use levels to specify logger verbosity.
    Since a larger level means a more severe event, a logger that accepts events
    with smaller (or more negative) level means a more verbose logger. Logger
    verbosity is thus the negation of event severity, and the default verbosity
    of 0 accepts all events at least as severe as INFO.

    Third, we wanted some room between levels to accommodate schemes with
    named levels between ours. For example, Google Cloud Logging defines a
    Notice level between Info and Warn. Since there are only a few of these
    intermediate levels, the gap between the numbers need not be large.
    Our gap of 4 matches OpenTelemetry's mapping. Subtracting 9 from an
    OpenTelemetry level in the DEBUG, INFO, WARN and ERROR ranges converts it to
    the corresponding slog Level range. OpenTelemetry also has the names TRACE
    and FATAL, which slog does not. But those OpenTelemetry levels can still be
    represented as slog Levels by using the appropriate integers.

    Names for common levels.

func (l Level) Level() Level
    Level returns the receiver. It implements Leveler.

func (l Level) MarshalJSON() ([]byte, error)
    MarshalJSON implements encoding/json.Marshaler by quoting the output of
    Level.String.

func (l Level) MarshalText() ([]byte, error)
    MarshalText implements encoding.TextMarshaler by calling Level.String.

func (l Level) String() string
    String returns a name for the level. If the level has a name, then that
    name in uppercase is returned. If the level is between named values, then an
    integer is appended to the uppercased name. Examples:

        LevelWarn.String() => "WARN"
        (LevelInfo+2).String() => "INFO+2"

func (l *Level) UnmarshalJSON(data []byte) error
    UnmarshalJSON implements encoding/json.Unmarshaler It accepts any string
    produced by Level.MarshalJSON, ignoring case. It also accepts numeric
    offsets that would result in a different string on output. For example,
    "Error-8" would marshal as "INFO".

func (l *Level) UnmarshalText(data []byte) error
    UnmarshalText implements encoding.TextUnmarshaler. It accepts any string
    produced by Level.MarshalText, ignoring case. It also accepts numeric
    offsets that would result in a different string on output. For example,
    "Error-8" would marshal as "INFO".

type LevelVar struct {
	// Has unexported fields.
}
    A LevelVar is a Level variable, to allow a Handler level to change
    dynamically. It implements Leveler as well as a Set method, and it is safe
    for use by multiple goroutines. The zero LevelVar corresponds to LevelInfo.

func (v *LevelVar) Level() Level
    Level returns v's level.

func (v *LevelVar) MarshalText() ([]byte, error)
    MarshalText implements encoding.TextMarshaler by calling Level.MarshalText.

func (v *LevelVar) Set(l Level)
    Set sets v's level to l.

func (v *LevelVar) String() string

func (v *LevelVar) UnmarshalText(data []byte) error
    UnmarshalText implements encoding.TextUnmarshaler by calling
    Level.UnmarshalText.

type Leveler interface {
	Level() Level
}
    A Leveler provides a Level value.

    As Level itself implements Leveler, clients typically supply a Level value
    wherever a Leveler is needed, such as in HandlerOptions. Clients who need to
    vary the level dynamically can provide a more complex Leveler implementation
    such as *LevelVar.

type LogValuer interface {
	LogValue() Value
}
    A LogValuer is any Go value that can convert itself into a Value for
    logging.

    This mechanism may be used to defer expensive operations until they are
    needed, or to expand a single value into a sequence of components.

type Logger struct {
	// Has unexported fields.
}
    A Logger records structured information about each call to its Log, Debug,
    Info, Warn, and Error methods. For each call, it creates a Record and passes
    it to a Handler.

    To create a new Logger, call New or a Logger method that begins "With".

func Default() *Logger
    Default returns the default Logger.

func New(h Handler) *Logger
    New creates a new Logger with the given non-nil Handler and a nil context.

func With(args ...any) *Logger
    With calls Logger.With on the default logger.

func (l *Logger) Debug(msg string, args ...any)
    Debug logs at LevelDebug.

func (l *Logger) DebugCtx(ctx context.Context, msg string, args ...any)
    DebugCtx logs at LevelDebug with the given context.

func (l *Logger) Enabled(ctx context.Context, level Level) bool
    Enabled reports whether l emits log records at the given context and level.

func (l *Logger) Error(msg string, args ...any)
    Error logs at LevelError.

func (l *Logger) ErrorCtx(ctx context.Context, msg string, args ...any)
    ErrorCtx logs at LevelError with the given context.

func (l *Logger) Handler() Handler
    Handler returns l's Handler.

func (l *Logger) Info(msg string, args ...any)
    Info logs at LevelInfo.

func (l *Logger) InfoCtx(ctx context.Context, msg string, args ...any)
    InfoCtx logs at LevelInfo with the given context.

func (l *Logger) Log(ctx context.Context, level Level, msg string, args ...any)
    Log emits a log record with the current time and the given level and
    message. The Record's Attrs consist of the Logger's attributes followed by
    the Attrs specified by args.

    The attribute arguments are processed as follows:
      - If an argument is an Attr, it is used as is.
      - If an argument is a string and this is not the last argument, the
        following argument is treated as the value and the two are combined into
        an Attr.
      - Otherwise, the argument is treated as a value with key "!BADKEY".

func (l *Logger) LogAttrs(ctx context.Context, level Level, msg string, attrs ...Attr)
    LogAttrs is a more efficient version of Logger.Log that accepts only Attrs.

func (l *Logger) Warn(msg string, args ...any)
    Warn logs at LevelWarn.

func (l *Logger) WarnCtx(ctx context.Context, msg string, args ...any)
    WarnCtx logs at LevelWarn with the given context.

func (l *Logger) With(args ...any) *Logger
    With returns a new Logger that includes the given arguments, converted
    to Attrs as in Logger.Log and resolved. The Attrs will be added to each
    output from the Logger. The new Logger shares the old Logger's context. The
    new Logger's handler is the result of calling WithAttrs on the receiver's
    handler.

func (l *Logger) WithGroup(name string) *Logger
    WithGroup returns a new Logger that starts a group. The keys of all
    attributes added to the Logger will be qualified by the given name. The new
    Logger shares the old Logger's context.

    The new Logger's handler is the result of calling WithGroup on the
    receiver's handler.

type Record struct {
	// The time at which the output method (Log, Info, etc.) was called.
	Time time.Time

	// The log message.
	Message string

	// The level of the event.
	Level Level

	// The program counter at the time the record was constructed, as determined
	// by runtime.Callers. If zero, no program counter is available.
	//
	// The only valid use for this value is as an argument to
	// [runtime.CallersFrames]. In particular, it must not be passed to
	// [runtime.FuncForPC].
	PC uintptr

	// Has unexported fields.
}
    A Record holds information about a log event. Copies of a Record share
    state. Do not modify a Record after handing out a copy to it. Use
    Record.Clone to create a copy with no shared state.

func NewRecord(t time.Time, level Level, msg string, pc uintptr) Record
    NewRecord creates a Record from the given arguments. Use Record.AddAttrs to
    add attributes to the Record.

    NewRecord is intended for logging APIs that want to support a Handler as a
    backend.

func (r *Record) Add(args ...any)
    Add converts the args to Attrs as described in Logger.Log, then appends the
    Attrs to the Record's list of Attrs. It resolves the Attrs before doing so.

func (r *Record) AddAttrs(attrs ...Attr)
    AddAttrs appends the given Attrs to the Record's list of Attrs. It resolves
    the Attrs before doing so.

func (r Record) Attrs(f func(Attr))
    Attrs calls f on each Attr in the Record. The Attrs are already resolved.

func (r Record) Clone() Record
    Clone returns a copy of the record with no shared state. The original record
    and the clone can both be modified without interfering with each other.

func (r Record) NumAttrs() int
    NumAttrs returns the number of attributes in the Record.

type TextHandler struct {
	// Has unexported fields.
}
    TextHandler is a Handler that writes Records to an io.Writer as a sequence
    of key=value pairs separated by spaces and followed by a newline.

func NewTextHandler(w io.Writer) *TextHandler
    NewTextHandler creates a TextHandler that writes to w, using the default
    options.

func (h *TextHandler) Enabled(_ context.Context, level Level) bool
    Enabled reports whether the handler handles records at the given level.
    The handler ignores records whose level is lower.

func (h *TextHandler) Handle(_ context.Context, r Record) error
    Handle formats its argument Record as a single line of space-separated
    key=value items.

    If the Record's time is zero, the time is omitted. Otherwise, the key is
    "time" and the value is output in RFC3339 format with millisecond precision.

    If the Record's level is zero, the level is omitted. Otherwise, the key is
    "level" and the value of Level.String is output.

    If the AddSource option is set and source information is available, the key
    is "source" and the value is output as FILE:LINE.

    The message's key "msg".

    To modify these or other attributes, or remove them from the output,
    use [HandlerOptions.ReplaceAttr].

    If a value implements encoding.TextMarshaler, the result of MarshalText is
    written. Otherwise, the result of fmt.Sprint is written.

    Keys and values are quoted with strconv.Quote if they contain Unicode space
    characters, non-printing characters, '"' or '='.

    Keys inside groups consist of components (keys or group names) separated by
    dots. No further escaping is performed. If it is necessary to reconstruct
    the group structure of a key even in the presence of dots inside components,
    use [HandlerOptions.ReplaceAttr] to escape the keys.

    Each call to Handle results in a single serialized call to io.Writer.Write.

func (h *TextHandler) WithAttrs(attrs []Attr) Handler
    WithAttrs returns a new TextHandler whose attributes consists of h's
    attributes followed by attrs.

func (h *TextHandler) WithGroup(name string) Handler

type Value struct {
	// Has unexported fields.
}
    A Value can represent any Go value, but unlike type any, it can represent
    most small values without an allocation. The zero Value corresponds to nil.

func AnyValue(v any) Value
    AnyValue returns a Value for the supplied value.

    If the supplied value is of type Value, it is returned unmodified.

    Given a value of one of Go's predeclared string, bool, or (non-complex)
    numeric types, AnyValue returns a Value of kind String, Bool, Uint64, Int64,
    or Float64. The width of the original numeric type is not preserved.

    Given a time.Time or time.Duration value, AnyValue returns a Value of kind
    KindTime or KindDuration. The monotonic time is not preserved.

    For nil, or values of all other types, including named types whose
    underlying type is numeric, AnyValue returns a value of kind KindAny.

func BoolValue(v bool) Value
    BoolValue returns a Value for a bool.

func DurationValue(v time.Duration) Value
    DurationValue returns a Value for a time.Duration.

func Float64Value(v float64) Value
    Float64Value returns a Value for a floating-point number.

func GroupValue(as ...Attr) Value
    GroupValue returns a new Value for a list of Attrs. The caller must not
    subsequently mutate the argument slice.

func Int64Value(v int64) Value
    Int64Value returns a Value for an int64.

func IntValue(v int) Value
    IntValue returns a Value for an int.

func StringValue(value string) Value
    StringValue returns a new Value for a string.

func TimeValue(v time.Time) Value
    TimeValue returns a Value for a time.Time. It discards the monotonic
    portion.

func Uint64Value(v uint64) Value
    Uint64Value returns a Value for a uint64.

func (v Value) Any() any
    Any returns v's value as an any.

func (v Value) Bool() bool
    Bool returns v's value as a bool. It panics if v is not a bool.

func (a Value) Duration() time.Duration
    Duration returns v's value as a time.Duration. It panics if v is not a
    time.Duration.

func (v Value) Equal(w Value) bool
    Equal reports whether v and w have equal keys and values.

func (v Value) Float64() float64
    Float64 returns v's value as a float64. It panics if v is not a float64.

func (v Value) Group() []Attr
    Group returns v's value as a []Attr. It panics if v's Kind is not KindGroup.

func (v Value) Int64() int64
    Int64 returns v's value as an int64. It panics if v is not a signed integer.

func (v Value) Kind() Kind
    Kind returns v's Kind.

func (v Value) LogValuer() LogValuer
    LogValuer returns v's value as a LogValuer. It panics if v is not a
    LogValuer.

func (v Value) Resolve() Value
    Resolve repeatedly calls LogValue on v while it implements LogValuer, and
    returns the result. If v resolves to a group, the group's attributes' values
    are also resolved. If the number of LogValue calls exceeds a threshold, a
    Value containing an error is returned. Resolve's return value is guaranteed
    not to be of Kind KindLogValuer.

func (v Value) String() string
    String returns Value's value as a string, formatted like fmt.Sprint.
    Unlike the methods Int64, Float64, and so on, which panic if v is of the
    wrong kind, String never panics.

func (v Value) Time() time.Time
    Time returns v's value as a time.Time. It panics if v is not a time.Time.

func (v Value) Uint64() uint64
    Uint64 returns v's value as a uint64. It panics if v is not an unsigned
    integer.


package slogtest

FUNCTIONS

func TestHandler(h slog.Handler, results func() []map[string]any) error
    TestHandler tests a slog.Handler. If TestHandler finds any misbehaviors,
    it returns an error for each, combined into a single error with errors.Join.

    TestHandler installs the given Handler in a slog.Logger and makes several
    calls to the Logger's output methods.

    The results function is invoked after all such calls. It should return
    a slice of map[string]any, one for each call to a Logger output method.
    The keys and values of the map should correspond to the keys and values of
    the Handler's output. Each group in the output should be represented as its
    own nested map[string]any.

    If the Handler outputs JSON, then calling encoding/json.Unmarshal with a
    `map[string]any` will create the right data structure.