slog: update package doc
Change-Id: Id892eac02438a3bc22ca30590111631fbe78990c
Reviewed-on: https://go-review.googlesource.com/c/exp/+/469857
TryBot-Result: Gopher Robot <gobot@golang.org>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
diff --git a/slog/doc.go b/slog/doc.go
index 442eaec..40430cc 100644
--- a/slog/doc.go
+++ b/slog/doc.go
@@ -22,7 +22,7 @@
pairs, where the keys are strings and the values may be of any type.
As an example,
- slog.Info("hello", "count", 3)
+ slog.Info("hello", "count", 3)
creates a record containing the time of the call,
a level of Info, the message "hello", and a single
@@ -38,31 +38,31 @@
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
+ 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))
+ 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)
+ logger.Info("hello", "count", 3)
produces this output:
- time=2022-11-08T15:28:26.000-05:00 level=INFO msg=hello count=3
+ 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)
+ 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}
+ {"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),
@@ -71,24 +71,23 @@
Setting a logger as the default with
- slog.SetDefault(logger)
+ 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))
+ slog.Info("hello", slog.Int("count", 3))
behaves the same as
- slog.Info("hello", "count", 3)
+ 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
@@ -105,11 +104,11 @@
The call
- logger.LogAttrs(slog.LevelInfo, "hello", slog.Int("count", 3))
+ 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)
+ 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
@@ -117,13 +116,12 @@
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)
+ 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.
@@ -147,17 +145,16 @@
To vary the level dynamically for an entire program, first initialize
a global LevelVar:
- var programLevel = new(slog.LevelVar) // Info by default
+ 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))
+ 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)
-
+ programLevel.Set(slog.LevelDebug)
# Groups
@@ -169,17 +166,17 @@
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))
+ 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
+ request.method=GET request.url=http://example.com
JSONHandler would display it as
- "request":{"method":"GET","url":"http://example.com"}
+ "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
@@ -191,34 +188,32 @@
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)
+ 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
-This package works with [context.Context] in two ways.
+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.
-Storing a Logger in a Context is a convenient way to pass it around,
-since many functions already take a Context as an argument.
-Create a Context that contains a Logger with [NewContext],
-and retrieve the Logger with [FromContext].
+The [Logger.Log] and [Logger.LogAttrs] methods take a context as a first
+argument, as do their corresponding top-level functions.
-Sometimes a Context holds information that a Handler would like to use.
-For example, tracing systems usually store span identifiers in Contexts.
-To provide a Context to a Handler, associate it with a Logger using [Logger.WithContext]:
+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,
- logger.WithContext(ctx).Info("hello")
+ slog.InfoCtx(ctx, "message")
-That sets [Record.Context] to ctx so the Handler can access it.
-
+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
@@ -231,39 +226,27 @@
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.
-To distinguish logger functions from application functions, the logger
-ignores the topmost few functions on the call stack.
-The number of frames to ignore is called the "depth."
+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:
-If your own packages define logging functions that wrap those provided by slog,
-you will want to adjust the depth so that source line
-information points to your wrapper, not the underlying slog function. For instance, if you define
-this function in file mylog.go:
-
- func Infof(format string, args ...any) {
- slog.Default().Info(fmt.Sprintf(format, args...))
- }
+ 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")
+ Infof(slog.Default(), "hello, %s", "world")
-then slog will use source file mylog.go, not main.go.
+then slog will report the source file as mylog.go, not main.go.
-The [LogDepth] and [LogAttrDepth] functions are designed for this case.
-Their first argument is the number of calls between your wrapper
-function and the LogDepth or LogAttrDepth call.
-The proper definition of Infof is
-
- func Infof(format string, args ...any) {
- slog.Default().LogDepth(1, slog.LevelInfo, fmt.Sprintf(format, args...))
- }
-
+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
@@ -271,14 +254,14 @@
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.AddAttrs] to add attributes)
+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,
@@ -293,12 +276,12 @@
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
+ 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
+ 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.
@@ -311,20 +294,20 @@
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))
+ 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 }
+ type expensive struct { arg int }
- func (e expensive) LogValue() slog.Value {
- return slog.AnyValue(computeExpensiveValue(e.arg))
- }
+ 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})
+ slog.Debug("frobbing", "value", expensive{arg})
Now computeExpensiveValue will only be called when the line is enabled.