This is a discussion that is intended to lead to a proposal.
We would like to add structured logging with levels to the standard library. 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 person, 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 draft 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 (tentative name: log/slog ), 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, we include levels in our design, in a way that accommodates both traditional named levels and logr-style verbosities.
Our goals 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 Field s).
-
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 en masse 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 to prefer 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 become familiar with this package before learning third-party packages, so they will naturally prefer 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. If all of the packages support the handler interface we propose, 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 hope 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.
Overview of the Design
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", net.ErrClosed, "status", 500)
slog.LogAttrs(slog.ErrorLevel, "oops",
slog.Int("status", 500), slog.Any("err", net.ErrClosed))
}
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.)
The program then 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 an error and a key-value pair.
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 some values. slog.Attr is modeled on zap.Field .
The Design
Interaction Between Existing and New Behavior
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, as in the example above, the default log logger will send its text output to the structured handler.
Handlers
A slog.Handler describes the logging backend. It is defined as:
type Handler interface {
// Enabled reports whether this handler is accepting records.
Enabled(Level) bool
// Handle processes the Record.
Handle(Record) error
// With returns a new Handler whose attributes consist of
// the receiver's attributes concatenated with the arguments.
With(attrs []Attr) Handler
}
The main method is Handle . It accepts a slog.Record with the timestamp, message, level, caller source position, and key-value pairs of the log event. Each call to a Logger output method, like Info , Error or LogAttrs , creates a Record and invokes the Handle method.
The Enabled method is an optimization that can save effort if the log event should be discarded. Enabled is called early, before any arguments are processed.
The With method is called by Logger.With , discussed below.
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
The Record passed to a handler exports Time , Message and Level methods, as well as four methods for accessing the sequence of Attr s:
Attrs() []Attr returns a copy of the Attr s as a slice.
NumAttrs() int returns the number of Attr s.
Attr(int) Attr returns the i'th Attr .
SetAttrs([]Attr) replaces the sequence of Attr s with the given slice.
This API allows an efficient implementation of the Attr sequence that avoids copying and minimizes allocation. SetAttrs supports "middleware" handlers that want to alter the Attr s, say by removing those that contain sensitive data.
The Attr Type
The Attr type efficiently represents a key-value pair. The key is a string. The value can be any type, but Attr improves on any by storing common types without allocating memory. 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 Attr uses package unsafe to store any value in three machine words. The version without unsafe requires five.
There are convenience functions for constructing Attr s with various value types:
Int(k string, v int) Attr
Int64(k string, v int64) Attr
Uint64(k string, v uint64) Attr
Float64(k string, v float64) Attr
String(k, v string) Attr
Bool(k string, v bool) Attr
Duration(k string, v time.Duration) Attr
Time(k string, v time.Time) Attr
Any(k string, v any) Attr
The last of these dispatches on the type of v , using a more efficient representation if Attr supports it and falling back to an any field in Attr if not.
The Attr.Key method returns the key. Extracting values from an Attr is reminiscent of reflect.Value : there is a Kind method that returns an enum, and a variety of methods like Int64() int64 and Bool() bool that return the value or panic if it is the wrong kind.
Attr also has an Equal method, and an AppendValue method that efficiently appends a string representation of the value to a []byte , in the manner of the strconv.AppendX functions.
Loggers
A Logger consists of a handler and a list of Attr s. There is a default logger with no attributes whose handler writes to the default log.Logger , as explained above. Create a Logger with New :
func New(h Handler) *Logger
To add attributes to a Logger, use With :
l2 := l1.With("url", "http://example.com/")
The arguments are interpreted as alternating string keys and and arbitrary values, which are converted to Attr s. Attr s can also be passed directly. Loggers are immutable, so this actually creates a new Logger with the additional attributes. To allow handlers to preprocess attributes, the new Logger’s handler is obtained by calling Handler.With on the old one.
You can obtain a logger's handler with Logger.Handler .
The basic logging methods are
func (*Logger) Log(level Level, message string, kvs ...any)
which logs a message at the given level with a list of attributes that are interpreted just as in Logger.With , and the more efficient
func (Logger) LogAttrs(level Level, message string, attrs ...Attr)
These functions first call Handler.Enabled(level) to see if they should proceed. If so, they create a Record with the current time, the given level and message, and a list of attributes that consists of the receiver's attributes followed by the argument attributes. They then pass the Record to Handler.Handle .
Each of these methods has an alternative form that takes a call depth, so other functions can wrap them and adjust the source line information.
There are four convenience methods for common levels:
func (*Logger) Info(message string, kvs ...any)
func (*Logger) Warn(message string, kvs ...any)
func (*Logger) Debug(message string, kvs ...any)
func (*Logger) Error(message string, err error, kvs ...any)
They all call Log with the appropriate level. Error first appends Any("err", err) to the attributes.
There are no convenience methods for LogAttrs . We expect that most programmers will use the more convenient API; those few who need the extra speed will have to type more, or provide wrapper functions.
All the methods described in this section are also names of top-level functions that call the corresponding method on the default logger.
Context Support
Passing a logger in a context.Context is a common practice and a good way to include dynamically scoped information in log messages. For instance, you could construct a Logger with information from an http.Request and pass it through the code that handles the request by adding it to r.Context() .
The slog package has two functions to support this pattern. One adds a Logger to a context:
func NewContext(ctx context.Context, l *Logger) context.Context
As an example, an HTTP server might want to create a new Logger for each request. The logger would contain request-wide attributes and be stored in the context for the request:
func handle(w http.ResponseWriter, r *http.Request) {
rlogger := slog.With(
"method", r.Method,
"url", r.URL,
"traceID", getTraceID(r))
ctx := slog.NewContext(r.Context(), rlogger)
// ... use ctx ...
}
To retrieve a Logger from a context, call FromContext :
slog.FromContext(ctx).Info(...)
FromContext returns the default logger if it can't find one in the context.
Levels
A level is a positive integer, where lower numbers designate more severe or important log events. The slog package provides names for common levels, with gaps between the assigned numbers to accommodate other level schemes. (For example, Google Cloud Platform supports a Notice level between Info and Warn.)
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. To use a verbosity of v with this design, pass slog.InfoLevel + v to Log or LogAttrs .
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 with the same options:
-
The boolean AddSource option controls whether the file and line of the log call. It is false by default, because there is a small cost to extracting this information.
-
The LevelRef option, of type LevelRef , provides control over the maximum level that the handler will output. For example, setting a handler's LevelRef to Info will suppress output at Debug and higher levels. A LevelRef is a safely mutable pointer to a level, which makes it easy to dynamically and atomically change the logging level for an entire program.
-
To provide fine control over output, the ReplaceAttr option is a function that both accepts and returns an Attr . If present, it is called for every attribute in the log record, including the four built-in ones for time, message, level and (if AddSource is true) the source position. 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.
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.Record s 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 . To facilitate that, slog provides a way to construct Record s directly and add attributes to it:
func NewRecord(t time.Time, level Level, msg string, calldepth int) Record
func (*Record) AddAttr(Attr)
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.
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.
Appendix: API
package slog // import "golang.org/x/exp/slog"
FUNCTIONS
func Debug(msg string, args ...any)
Debug calls Logger.Debug on the default logger.
func Error(msg string, err error, args ...any)
Error calls Logger.Error on the default logger.
func Info(msg string, args ...any)
Info calls Logger.Info on the default logger.
func Log(level Level, msg string, args ...any)
Log calls Logger.Log on the default logger.
func LogAttrs(level Level, msg string, attrs ...Attr)
LogAttrs calls Logger.LogAttrs on the default logger.
func NewContext(ctx context.Context, l *Logger) context.Context
NewContext returns a context that contains the given Logger. Use FromContext
to retrieve the Logger.
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
InfoLevel using l's Handler.
func Warn(msg string, args ...any)
Warn calls Logger.Warn on the default logger.
TYPES
type Attr struct {
// Has unexported fields.
}
An Attr is a key-value pair. It can represent some small values without an
allocation. The zero Attr has a key of "" and a value of nil.
func Any(key string, value any) Attr
Any returns an Attr for the supplied value.
Any does not preserve the exact type of integral values. All signed integers
are converted to int64 and all unsigned integers to uint64. Similarly,
float32s are converted to float64.
However, named types are preserved. So given
type Int int
the expression
log.Any("k", Int(1)).Value()
will return Int(1).
func Bool(key string, value bool) Attr
Bool returns an Attr for a bool.
func Duration(key string, value time.Duration) Attr
Duration returns an Attr for a time.Duration.
func Float64(key string, value float64) Attr
Float64 returns an Attr for a floating-point number.
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 a new Attr for a string.
func Time(key string, value time.Time) Attr
Time returns an Attr for a time.Time.
func Uint64(key string, value uint64) Attr
Uint64 returns an Attr for a uint64.
func (a Attr) AppendValue(dst []byte) []byte
AppendValue appends a text representation of the Attr's value to dst.
The value is formatted as with fmt.Sprint.
func (a Attr) Bool() bool
Bool returns the Attr's value as a bool. It panics if the value is not a
bool.
func (a Attr) Duration() time.Duration
Duration returns the Attr's value as a time.Duration. It panics if the value
is not a time.Duration.
func (a1 Attr) Equal(a2 Attr) bool
Equal reports whether two Attrs have equal keys and values.
func (a Attr) Float64() float64
Float64 returns the Attr's value as a float64. It panics if the value is not
a float64.
func (a Attr) Format(s fmt.State, verb rune)
Format implements fmt.Formatter. It formats a Attr as "KEY=VALUE".
func (a Attr) HasValue() bool
HasValue returns true if the Attr has a value.
func (a Attr) Int64() int64
Int64 returns the Attr's value as an int64. It panics if the value is not a
signed integer.
func (a Attr) Key() string
Key returns the Attr's key.
func (a Attr) Kind() Kind
Kind returns the Attr's Kind.
func (a Attr) String() string
String returns Attr's value as a string, formatted like fmt.Sprint.
Unlike the methods Int64, Float64, and so on, which panic if the Attr is of
the wrong kind, String never panics.
func (a Attr) Time() time.Time
Time returns the Attr's value as a time.Time. It panics if the value is not
a time.Duration.
func (a Attr) Uint64() uint64
Uint64 returns the Attr's value as a uint64. It panics if the value is not
an unsigned integer.
func (a Attr) Value() any
Value returns the Attr's value as an any. If the Attr does not have a value,
it returns nil.
func (a Attr) WithKey(key string) Attr
WithKey returns an attr with the given key and the receiver's value.
type Handler interface {
// Enabled reports whether this handler is accepting records
// at the given level.
Enabled(Level) bool
// Handle processes the Record.
// Handle methods that produce output should observe the following rules:
// - If r.Time() is the zero time, do not output it.
// - If r.Level() is Level(0), do not output it.
Handle(Record) error
// With returns a new Handler whose attributes consist of
// the receiver's attributes concatenated with the arguments.
With(attrs []Attr) Handler
}
A Handler processes log records produced by Logger output. 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.
type HandlerOptions struct {
// Add a "source" attributes to the output whose value is of the form
// "file:line".
AddSource bool
// Ignore records with levels above LevelRef.Level.
// If nil, accept all levels.
LevelRef *LevelRef
// If set, ReplaceAttr is called on each attribute of the message,
// and the returned value is used instead of the original. If the returned
// key is empty, the attribute is omitted from the output.
//
// The built-in attributes with keys "time", "level", "source", and "msg"
// are passed to this function first, except that time and level are omitted
// if zero, and source is omitted if AddSource is false.
ReplaceAttr func(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(l Level) bool
Enabled reports whether l is less than or equal to the maximum level.
func (h *JSONHandler) Handle(r Record) error
Handle formats its argument Record as a JSON object on a single line.
If the Record's time is zero, it 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, it 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.
Each call to Handle results in a single, mutex-protected call to
io.Writer.Write.
func (h *JSONHandler) With(attrs []Attr) Handler
With returns a new JSONHandler whose attributes consists of h's attributes
followed by attrs.
type Kind int
Kind is the kind of an Attr's value.
const (
AnyKind Kind = iota
BoolKind
DurationKind
Float64Kind
Int64Kind
StringKind
TimeKind
Uint64Kind
)
func (k Kind) String() string
type Level int
A Level is the importance or severity of a log event. The higher the level,
the less important or severe the event.
const (
ErrorLevel Level = 10
WarnLevel Level = 20
InfoLevel Level = 30
DebugLevel Level = 31
)
Names for common levels.
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:
WarnLevel.String() => "WARN"
(WarnLevel-2).String() => "WARN-2"
type LevelRef struct {
// Has unexported fields.
}
A LevelRef is a reference to a level. LevelRefs are safe for use by multiple
goroutines. Use NewLevelRef to create a LevelRef.
If all the Handlers of a program use the same LevelRef, then a single Set on
that LevelRef will change the level for all of them.
func NewLevelRef(l Level) *LevelRef
NewLevelRef creates a LevelRef initialized to the given Level.
func (r *LevelRef) Level() Level
Level returns the LevelRef's level. If LevelRef is nil, it returns the
maximum level.
func (r *LevelRef) Set(l Level)
Set sets the LevelRef's level to l.
type Logger struct {
// Has unexported fields.
}
A Logger generates Records and passes them to a Handler.
Loggers are immutable; to create a new one, call New or Logger.With.
func Default() *Logger
Default returns the default Logger.
func FromContext(ctx context.Context) *Logger
FromContext returns the Logger stored in ctx by NewContext, or the default
Logger if there is none.
func New(h Handler) *Logger
New creates a new Logger with the given Handler.
func With(attrs ...any) *Logger
With calls Logger.With on the default logger.
func (l *Logger) Debug(msg string, args ...any)
Debug logs at DebugLevel.
func (l *Logger) Enabled(level Level) bool
Enabled reports whether l emits log records at level.
func (l *Logger) Error(msg string, err error, args ...any)
Error logs at ErrorLevel. If err is non-nil, Error appends Any("err",
err) to the list of attributes.
func (l *Logger) Handler() Handler
Handler returns l's Handler.
func (l *Logger) Info(msg string, args ...any)
Info logs at InfoLevel.
func (l *Logger) Log(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(level Level, msg string, attrs ...Attr)
LogAttrs is a more efficient version of Logger.Log that accepts only Attrs.
func (l *Logger) LogAttrsDepth(calldepth int, level Level, msg string, attrs ...Attr)
LogAttrsDepth is like Logger.LogAttrs, but accepts a call depth argument
which it interprets like Logger.LogDepth.
func (l *Logger) LogDepth(calldepth int, level Level, msg string, args ...any)
LogDepth is like Logger.Log, but accepts a call depth to adjust the file
and line number in the log record. 0 refers to the caller of LogDepth;
1 refers to the caller's caller; and so on.
func (l *Logger) Warn(msg string, args ...any)
Warn logs at WarnLevel.
func (l *Logger) With(attrs ...any) *Logger
With returns a new Logger whose handler's attributes are a concatenation of
l's attributes and the given arguments, converted to Attrs as in Logger.Log.
type Record struct {
// Has unexported fields.
}
A Record holds information about a log event.
func NewRecord(t time.Time, level Level, msg string, calldepth int) Record
NewRecord creates a new Record from the given arguments. Use Record.AddAttr
to add attributes to the Record. If calldepth is greater than zero,
Record.SourceLine will return the file and line number at that depth.
NewRecord is intended for logging APIs that want to support a Handler as a
backend. Most users won't need it.
func (r *Record) AddAttr(a Attr)
AddAttr appends a to the list of r's attributes. It does not check for
duplicate keys.
func (r *Record) Attr(i int) Attr
Attr returns the i'th Attr in r.
func (r *Record) Attrs() []Attr
Attrs returns a copy of the sequence of Attrs in r.
func (r *Record) Level() Level
Level returns the level of the log event.
func (r *Record) Message() string
Message returns the log message.
func (r *Record) NumAttrs() int
NumAttrs returns the number of Attrs in r.
func (r *Record) SourceLine() (file string, line int)
SourceLine returns the file and line of the log event. If the Record
was created without the necessary information, or if the location is
unavailable, it returns ("", 0).
func (r *Record) Time() time.Time
Time returns the time of the log event.
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(l Level) bool
Enabled reports whether l is less than or equal to the maximum level.
func (h *TextHandler) Handle(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, it 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, it 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].
Keys are written as unquoted strings. Values are written according to their
type:
- Strings are quoted if they contain Unicode space characters or are over
80 bytes long.
- If a value implements [encoding.TextMarshaler], the result of
MarshalText is used.
- Otherwise, the result of fmt.Sprint is used.
Each call to Handle results in a single, mutex-protected call to
io.Writer.Write.
func (h *TextHandler) With(attrs []Attr) Handler
With returns a new TextHandler whose attributes consists of h's attributes
followed by attrs.
|