Skip to content

Commit

Permalink
slog: sync with log/slog
Browse files Browse the repository at this point in the history
Bring this copy of slog up to date with the changes in the standard library
log/slog package.

Change-Id: I05d4de21388c4a4761b46b89fb74a1c7258ac06c
Reviewed-on: https://go-review.googlesource.com/c/exp/+/494179
Run-TryBot: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
  • Loading branch information
jba committed May 10, 2023
1 parent 47ecfdc commit dd950f8
Show file tree
Hide file tree
Showing 21 changed files with 249 additions and 306 deletions.
22 changes: 17 additions & 5 deletions slog/attr.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,26 @@ func Duration(key string, v time.Duration) Attr {
}

// Group returns an Attr for a Group Value.
// The caller must not subsequently mutate the
// argument slice.
// The first argument is the key; the remaining arguments
// are converted to Attrs as in [Logger.Log].
//
// Use Group to collect several Attrs under a single
// Use Group to collect several key-value pairs 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 Group(key string, as ...Attr) Attr {
return Attr{key, GroupValue(as...)}
func Group(key string, args ...any) Attr {
return Attr{key, GroupValue(argsToAttrSlice(args)...)}
}

func argsToAttrSlice(args []any) []Attr {
var (
attr Attr
attrs []Attr
)
for len(args) > 0 {
attr, args = argsToAttr(args)
attrs = append(attrs, attr)
}
return attrs
}

// Any returns an Attr for the supplied value.
Expand Down
4 changes: 2 additions & 2 deletions slog/benchmarks/benchmarks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ func BenchmarkAttrs(b *testing.B) {
{"disabled", disabledHandler{}},
{"async discard", newAsyncHandler()},
{"fastText discard", newFastTextHandler(io.Discard)},
{"Text discard", slog.NewTextHandler(io.Discard)},
{"JSON discard", slog.NewJSONHandler(io.Discard)},
{"Text discard", slog.NewTextHandler(io.Discard, nil)},
{"JSON discard", slog.NewJSONHandler(io.Discard, nil)},
} {
logger := slog.New(handler.h)
b.Run(handler.name, func(b *testing.B) {
Expand Down
80 changes: 40 additions & 40 deletions slog/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ For more control over the output format, create a logger with a different handle
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, nil))
[TextHandler] output is a sequence of key=value pairs, easily and unambiguously
parsed by machine. This statement:
Expand All @@ -57,14 +57,14 @@ produces this output:
The package also provides [JSONHandler], whose output is line-delimited JSON:
logger := slog.New(slog.NewJSONHandler(os.Stdout))
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
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].
Both [TextHandler] and [JSONHandler] can be configured with [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.
Expand All @@ -78,38 +78,6 @@ will cause the top-level functions like [Info] to use it.
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.
Expand Down Expand Up @@ -149,7 +117,7 @@ a global LevelVar:
Then use the LevelVar to construct a handler, and make it the default:
h := slog.HandlerOptions{Level: programLevel}.NewJSONHandler(os.Stderr)
h := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: programLevel})
slog.SetDefault(slog.New(h))
Now the program can change its logging level with a single statement:
Expand All @@ -164,11 +132,11 @@ 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:
Use [Group] to create a Group attribute from a name and a list of key-value pairs:
slog.Group("request",
slog.String("method", r.Method),
slog.Any("url", r.URL))
"method", r.Method,
"url", r.URL)
TextHandler would display this group as
Expand Down Expand Up @@ -199,7 +167,7 @@ so even if it uses the common key "id", the log line will have distinct keys.
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.
is the identifier for the current span when tracing is enabled.
The [Logger.Log] and [Logger.LogAttrs] methods take a context as a first
argument, as do their corresponding top-level functions.
Expand All @@ -212,6 +180,38 @@ in "Ctx" do. For example,
It is recommended to pass a context to an output method if one is available.
# 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)
# Customizing a type's logging behavior
If a type implements the [LogValuer] interface, the [Value] returned from its LogValue
Expand Down
4 changes: 2 additions & 2 deletions slog/example_custom_levels_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func ExampleHandlerOptions_customLevels() {
LevelEmergency = slog.Level(12)
)

th := slog.HandlerOptions{
th := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
// Set a custom level to show all log output. The default value is
// LevelInfo, which would drop Debug and Trace logs.
Level: LevelTrace,
Expand Down Expand Up @@ -70,7 +70,7 @@ func ExampleHandlerOptions_customLevels() {

return a
},
}.NewTextHandler(os.Stdout)
})

logger := slog.New(th)
logger.Log(nil, LevelEmergency, "missing pilots")
Expand Down
2 changes: 1 addition & 1 deletion slog/example_level_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func (h *LevelHandler) Handler() slog.Handler {
// Another typical use would be to decrease the log level (to LevelDebug, say)
// during a part of the program that was suspected of containing a bug.
func ExampleHandler_levelHandler() {
th := slog.HandlerOptions{ReplaceAttr: testutil.RemoveTime}.NewTextHandler(os.Stdout)
th := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ReplaceAttr: testutil.RemoveTime})
logger := slog.New(NewLevelHandler(slog.LevelWarn, th))
logger.Info("not printed")
logger.Warn("printed")
Expand Down
4 changes: 1 addition & 3 deletions slog/example_logvaluer_group_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@

package slog_test

import (
"golang.org/x/exp/slog"
)
import "golang.org/x/exp/slog"

type Name struct {
First, Last string
Expand Down
3 changes: 1 addition & 2 deletions slog/example_logvaluer_secret_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ func (Token) LogValue() slog.Value {
// with an alternative representation to avoid revealing secrets.
func ExampleLogValuer_secret() {
t := Token("shhhh!")
logger := slog.New(slog.HandlerOptions{ReplaceAttr: testutil.RemoveTime}.
NewTextHandler(os.Stdout))
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ReplaceAttr: testutil.RemoveTime}))
logger.Info("permission granted", "user", "Perry", "token", t)

// Output:
Expand Down
2 changes: 1 addition & 1 deletion slog/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func ExampleGroup() {
r, _ := http.NewRequest("GET", "localhost", nil)
// ...

logger := slog.New(slog.HandlerOptions{ReplaceAttr: testutil.RemoveTime}.NewTextHandler(os.Stdout))
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ReplaceAttr: testutil.RemoveTime}))
slog.SetDefault(logger)

slog.Info("finished",
Expand Down
7 changes: 4 additions & 3 deletions slog/example_wrap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,14 @@ func Example_wrapping() {
}
// Remove the directory from the source's filename.
if a.Key == slog.SourceKey {
a.Value = slog.StringValue(filepath.Base(a.Value.String()))
source := a.Value.Any().(*slog.Source)
source.File = filepath.Base(source.File)
}
return a
}
logger := slog.New(slog.HandlerOptions{AddSource: true, ReplaceAttr: replace}.NewTextHandler(os.Stdout))
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{AddSource: true, ReplaceAttr: replace}))
Infof(logger, "message, %s", "formatted")

// Output:
// level=INFO source=example_wrap_test.go:43 msg="message, formatted"
// level=INFO source=example_wrap_test.go:44 msg="message, formatted"
}
56 changes: 14 additions & 42 deletions slog/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ type Handler interface {
Enabled(context.Context, Level) bool

// Handle handles the Record.
// It will only be called Enabled returns true.
// It will only be called when 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.
Expand Down Expand Up @@ -130,10 +130,8 @@ func (h *defaultHandler) WithGroup(name string) Handler {
// HandlerOptions are options for a TextHandler or JSONHandler.
// 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 causes the handler to compute the source code position
// of the log statement and add a SourceKey attribute to the output.
AddSource bool

// Level reports the minimum record level that will be logged.
Expand Down Expand Up @@ -285,22 +283,7 @@ func (h *commonHandler) handle(r Record) error {
}
// source
if h.opts.AddSource {
frame := r.frame()
if frame.File != "" {
key := SourceKey
if rep == nil {
state.appendKey(key)
state.appendSource(frame.File, frame.Line)
} else {
buf := buffer.New()
buf.WriteString(frame.File) // TODO: escape?
buf.WriteByte(':')
buf.WritePosInt(frame.Line)
s := buf.String()
buf.Free()
state.appendAttr(String(key, s))
}
}
state.appendAttr(Any(SourceKey, r.source()))
}
key = MessageKey
msg := r.Message
Expand Down Expand Up @@ -421,7 +404,6 @@ func (s *handleState) openGroup(name string) {
if s.groups != nil {
*s.groups = append(*s.groups, name)
}

}

// closeGroup ends the group with the given name.
Expand Down Expand Up @@ -455,6 +437,16 @@ func (s *handleState) appendAttr(a Attr) {
if a.isEmpty() {
return
}
// Special case: Source.
if v := a.Value; v.Kind() == KindAny {
if src, ok := v.Any().(*Source); ok {
if s.h.json {
a.Value = src.group()
} else {
a.Value = StringValue(fmt.Sprintf("%s:%d", src.File, src.Line))
}
}
}
if a.Value.Kind() == KindGroup {
attrs := a.Value.Group()
// Output only non-empty groups.
Expand Down Expand Up @@ -496,26 +488,6 @@ func (s *handleState) appendKey(key string) {
s.sep = s.h.attrSep()
}

func (s *handleState) appendSource(file string, line int) {
if s.h.json {
s.buf.WriteByte('"')
*s.buf = appendEscapedJSONString(*s.buf, file)
s.buf.WriteByte(':')
s.buf.WritePosInt(line)
s.buf.WriteByte('"')
} else {
// text
if needsQuoting(file) {
s.appendString(file + ":" + strconv.Itoa(line))
} else {
// common case: no quoting needed.
s.appendString(file)
s.buf.WriteByte(':')
s.buf.WritePosInt(line)
}
}
}

func (s *handleState) appendString(str string) {
if s.h.json {
s.buf.WriteByte('"')
Expand Down
Loading

0 comments on commit dd950f8

Please sign in to comment.