log/slog: built-in handler constructors take options as a second arg

There is now one constructor function for each built-in handler, with
signature

    NewXXXHandler(io.Writer, *HandlerOptions) *XXXHandler

Fixes #59339.

Change-Id: Ia02183c5ce0dc15c64e33ad05fd69bca09df2d2d
Reviewed-on: https://go-review.googlesource.com/c/go/+/486415
Reviewed-by: Alan Donovan <adonovan@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
This commit is contained in:
Jonathan Amsterdam 2023-04-19 14:24:33 -04:00
parent 5c51e9f45b
commit a82f69f60e
16 changed files with 44 additions and 49 deletions

View File

@ -58,10 +58,8 @@ pkg log/slog, func IntValue(int) Value #56345
pkg log/slog, func Log(context.Context, Level, string, ...interface{}) #56345 pkg log/slog, func Log(context.Context, Level, string, ...interface{}) #56345
pkg log/slog, func LogAttrs(context.Context, Level, string, ...Attr) #56345 pkg log/slog, func LogAttrs(context.Context, Level, string, ...Attr) #56345
pkg log/slog, func New(Handler) *Logger #56345 pkg log/slog, func New(Handler) *Logger #56345
pkg log/slog, func NewJSONHandler(io.Writer) *JSONHandler #56345
pkg log/slog, func NewLogLogger(Handler, Level) *log.Logger #56345 pkg log/slog, func NewLogLogger(Handler, Level) *log.Logger #56345
pkg log/slog, func NewRecord(time.Time, Level, string, uintptr) Record #56345 pkg log/slog, func NewRecord(time.Time, Level, string, uintptr) Record #56345
pkg log/slog, func NewTextHandler(io.Writer) *TextHandler #56345
pkg log/slog, func SetDefault(*Logger) #56345 pkg log/slog, func SetDefault(*Logger) #56345
pkg log/slog, func String(string, string) Attr #56345 pkg log/slog, func String(string, string) Attr #56345
pkg log/slog, func StringValue(string) Value #56345 pkg log/slog, func StringValue(string) Value #56345
@ -105,8 +103,6 @@ pkg log/slog, method (*TextHandler) WithAttrs([]Attr) Handler #56345
pkg log/slog, method (*TextHandler) WithGroup(string) Handler #56345 pkg log/slog, method (*TextHandler) WithGroup(string) Handler #56345
pkg log/slog, method (Attr) Equal(Attr) bool #56345 pkg log/slog, method (Attr) Equal(Attr) bool #56345
pkg log/slog, method (Attr) String() string #56345 pkg log/slog, method (Attr) String() string #56345
pkg log/slog, method (HandlerOptions) NewJSONHandler(io.Writer) *JSONHandler #56345
pkg log/slog, method (HandlerOptions) NewTextHandler(io.Writer) *TextHandler #56345
pkg log/slog, method (Kind) String() string #56345 pkg log/slog, method (Kind) String() string #56345
pkg log/slog, method (Level) Level() Level #56345 pkg log/slog, method (Level) Level() Level #56345
pkg log/slog, method (Level) MarshalJSON() ([]uint8, error) #56345 pkg log/slog, method (Level) MarshalJSON() ([]uint8, error) #56345

2
api/next/59339.txt Normal file
View File

@ -0,0 +1,2 @@
pkg log/slog, func NewJSONHandler(io.Writer, *HandlerOptions) *JSONHandler #59339
pkg log/slog, func NewTextHandler(io.Writer, *HandlerOptions) *TextHandler #59339

View File

@ -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 This statement uses [New] to create a new logger with a TextHandler
that writes structured records in text form to standard error: 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 [TextHandler] output is a sequence of key=value pairs, easily and unambiguously
parsed by machine. This statement: parsed by machine. This statement:
@ -57,7 +57,7 @@ produces this output:
The package also provides [JSONHandler], whose output is line-delimited JSON: 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) logger.Info("hello", "count", 3)
produces this output: produces this output:
@ -149,7 +149,7 @@ a global LevelVar:
Then use the LevelVar to construct a handler, and make it the default: 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)) slog.SetDefault(slog.New(h))
Now the program can change its logging level with a single statement: Now the program can change its logging level with a single statement:

View File

@ -25,7 +25,7 @@ func ExampleHandlerOptions_customLevels() {
LevelEmergency = slog.Level(12) 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 // Set a custom level to show all log output. The default value is
// LevelInfo, which would drop Debug and Trace logs. // LevelInfo, which would drop Debug and Trace logs.
Level: LevelTrace, Level: LevelTrace,
@ -69,7 +69,7 @@ func ExampleHandlerOptions_customLevels() {
return a return a
}, },
}.NewTextHandler(os.Stdout) })
logger := slog.New(th) logger := slog.New(th)
logger.Log(nil, LevelEmergency, "missing pilots") logger.Log(nil, LevelEmergency, "missing pilots")

View File

@ -63,7 +63,7 @@ func (h *LevelHandler) Handler() slog.Handler {
// Another typical use would be to decrease the log level (to LevelDebug, say) // 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. // during a part of the program that was suspected of containing a bug.
func ExampleHandler_levelHandler() { func ExampleHandler_levelHandler() {
th := slog.HandlerOptions{ReplaceAttr: slogtest.RemoveTime}.NewTextHandler(os.Stdout) th := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ReplaceAttr: slogtest.RemoveTime})
logger := slog.New(NewLevelHandler(slog.LevelWarn, th)) logger := slog.New(NewLevelHandler(slog.LevelWarn, th))
logger.Info("not printed") logger.Info("not printed")
logger.Warn("printed") logger.Warn("printed")

View File

@ -23,8 +23,7 @@ func (Token) LogValue() slog.Value {
// with an alternative representation to avoid revealing secrets. // with an alternative representation to avoid revealing secrets.
func ExampleLogValuer_secret() { func ExampleLogValuer_secret() {
t := Token("shhhh!") t := Token("shhhh!")
logger := slog.New(slog.HandlerOptions{ReplaceAttr: slogtest.RemoveTime}. logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ReplaceAttr: slogtest.RemoveTime}))
NewTextHandler(os.Stdout))
logger.Info("permission granted", "user", "Perry", "token", t) logger.Info("permission granted", "user", "Perry", "token", t)
// Output: // Output:

View File

@ -16,7 +16,7 @@ func ExampleGroup() {
r, _ := http.NewRequest("GET", "localhost", nil) r, _ := http.NewRequest("GET", "localhost", nil)
// ... // ...
logger := slog.New(slog.HandlerOptions{ReplaceAttr: slogtest.RemoveTime}.NewTextHandler(os.Stdout)) logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ReplaceAttr: slogtest.RemoveTime}))
logger.Info("finished", logger.Info("finished",
slog.Group("req", slog.Group("req",
slog.String("method", r.Method), slog.String("method", r.Method),

View File

@ -39,7 +39,7 @@ func Example_wrapping() {
} }
return a 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") Infof(logger, "message, %s", "formatted")
// Output: // Output:

View File

@ -338,8 +338,8 @@ func TestJSONAndTextHandlers(t *testing.T) {
h Handler h Handler
want string want string
}{ }{
{"text", opts.NewTextHandler(&buf), test.wantText}, {"text", NewTextHandler(&buf, &opts), test.wantText},
{"json", opts.NewJSONHandler(&buf), test.wantJSON}, {"json", NewJSONHandler(&buf, &opts), test.wantJSON},
} { } {
t.Run(handler.name, func(t *testing.T) { t.Run(handler.name, func(t *testing.T) {
h := handler.h h := handler.h
@ -419,7 +419,7 @@ func TestSecondWith(t *testing.T) {
// Verify that a second call to Logger.With does not corrupt // Verify that a second call to Logger.With does not corrupt
// the original. // the original.
var buf bytes.Buffer var buf bytes.Buffer
h := HandlerOptions{ReplaceAttr: removeKeys(TimeKey)}.NewTextHandler(&buf) h := NewTextHandler(&buf, &HandlerOptions{ReplaceAttr: removeKeys(TimeKey)})
logger := New(h).With( logger := New(h).With(
String("app", "playground"), String("app", "playground"),
String("role", "tester"), String("role", "tester"),
@ -445,14 +445,14 @@ func TestReplaceAttrGroups(t *testing.T) {
var got []ga var got []ga
h := HandlerOptions{ReplaceAttr: func(gs []string, a Attr) Attr { h := NewTextHandler(io.Discard, &HandlerOptions{ReplaceAttr: func(gs []string, a Attr) Attr {
v := a.Value.String() v := a.Value.String()
if a.Key == TimeKey { if a.Key == TimeKey {
v = "<now>" v = "<now>"
} }
got = append(got, ga{strings.Join(gs, ","), a.Key, v}) got = append(got, ga{strings.Join(gs, ","), a.Key, v})
return a return a
}}.NewTextHandler(io.Discard) }})
New(h). New(h).
With(Int("a", 1)). With(Int("a", 1)).
WithGroup("g1"). WithGroup("g1").

View File

@ -32,8 +32,8 @@ func BenchmarkAttrs(b *testing.B) {
{"disabled", disabledHandler{}, false}, {"disabled", disabledHandler{}, false},
{"async discard", newAsyncHandler(), true}, {"async discard", newAsyncHandler(), true},
{"fastText discard", newFastTextHandler(io.Discard), false}, {"fastText discard", newFastTextHandler(io.Discard), false},
{"Text discard", slog.NewTextHandler(io.Discard), false}, {"Text discard", slog.NewTextHandler(io.Discard, nil), false},
{"JSON discard", slog.NewJSONHandler(io.Discard), false}, {"JSON discard", slog.NewJSONHandler(io.Discard, nil), false},
} { } {
logger := slog.New(handler.h) logger := slog.New(handler.h)
b.Run(handler.name, func(b *testing.B) { b.Run(handler.name, func(b *testing.B) {

View File

@ -25,18 +25,17 @@ type JSONHandler struct {
} }
// NewJSONHandler creates a JSONHandler that writes to w, // NewJSONHandler creates a JSONHandler that writes to w,
// using the default options. // using the given options.
func NewJSONHandler(w io.Writer) *JSONHandler { // If opts is nil, the default options are used.
return (HandlerOptions{}).NewJSONHandler(w) func NewJSONHandler(w io.Writer, opts *HandlerOptions) *JSONHandler {
} if opts == nil {
opts = &HandlerOptions{}
// NewJSONHandler creates a JSONHandler with the given options that writes to w. }
func (opts HandlerOptions) NewJSONHandler(w io.Writer) *JSONHandler {
return &JSONHandler{ return &JSONHandler{
&commonHandler{ &commonHandler{
json: true, json: true,
w: w, w: w,
opts: opts, opts: *opts,
}, },
} }
} }

View File

@ -39,7 +39,7 @@ func TestJSONHandler(t *testing.T) {
} { } {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
var buf bytes.Buffer var buf bytes.Buffer
h := test.opts.NewJSONHandler(&buf) h := NewJSONHandler(&buf, &test.opts)
r := NewRecord(testTime, LevelInfo, "m", 0) r := NewRecord(testTime, LevelInfo, "m", 0)
r.AddAttrs(Int("a", 1), Any("m", map[string]int{"b": 2})) r.AddAttrs(Int("a", 1), Any("m", map[string]int{"b": 2}))
if err := h.Handle(context.Background(), r); err != nil { if err := h.Handle(context.Background(), r); err != nil {
@ -171,7 +171,7 @@ func BenchmarkJSONHandler(b *testing.B) {
}}, }},
} { } {
b.Run(bench.name, func(b *testing.B) { b.Run(bench.name, func(b *testing.B) {
l := New(bench.opts.NewJSONHandler(io.Discard)).With( l := New(NewJSONHandler(io.Discard, &bench.opts)).With(
String("program", "my-test-program"), String("program", "my-test-program"),
String("package", "log/slog"), String("package", "log/slog"),
String("traceID", "2039232309232309"), String("traceID", "2039232309232309"),
@ -236,7 +236,7 @@ func BenchmarkPreformatting(b *testing.B) {
{"struct file", outFile, structAttrs}, {"struct file", outFile, structAttrs},
} { } {
b.Run(bench.name, func(b *testing.B) { b.Run(bench.name, func(b *testing.B) {
l := New(NewJSONHandler(bench.wc)).With(bench.attrs...) l := New(NewJSONHandler(bench.wc, nil)).With(bench.attrs...)
b.ReportAllocs() b.ReportAllocs()
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {

View File

@ -27,7 +27,7 @@ const timeRE = `\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}(Z|[+-]\d{2}:\d{2})`
func TestLogTextHandler(t *testing.T) { func TestLogTextHandler(t *testing.T) {
var buf bytes.Buffer var buf bytes.Buffer
l := New(NewTextHandler(&buf)) l := New(NewTextHandler(&buf, nil))
check := func(want string) { check := func(want string) {
t.Helper() t.Helper()
@ -104,13 +104,13 @@ func TestConnections(t *testing.T) {
// Once slog.SetDefault is called, the direction is reversed: the default // Once slog.SetDefault is called, the direction is reversed: the default
// log.Logger's output goes through the handler. // log.Logger's output goes through the handler.
SetDefault(New(HandlerOptions{AddSource: true}.NewTextHandler(&slogbuf))) SetDefault(New(NewTextHandler(&slogbuf, &HandlerOptions{AddSource: true})))
log.Print("msg2") log.Print("msg2")
checkLogOutput(t, slogbuf.String(), "time="+timeRE+` level=INFO source=.*logger_test.go:\d{3} msg=msg2`) checkLogOutput(t, slogbuf.String(), "time="+timeRE+` level=INFO source=.*logger_test.go:\d{3} msg=msg2`)
// The default log.Logger always outputs at Info level. // The default log.Logger always outputs at Info level.
slogbuf.Reset() slogbuf.Reset()
SetDefault(New(HandlerOptions{Level: LevelWarn}.NewTextHandler(&slogbuf))) SetDefault(New(NewTextHandler(&slogbuf, &HandlerOptions{Level: LevelWarn})))
log.Print("should not appear") log.Print("should not appear")
if got := slogbuf.String(); got != "" { if got := slogbuf.String(); got != "" {
t.Errorf("got %q, want empty", got) t.Errorf("got %q, want empty", got)
@ -352,7 +352,7 @@ func TestLoggerError(t *testing.T) {
} }
return a return a
} }
l := New(HandlerOptions{ReplaceAttr: removeTime}.NewTextHandler(&buf)) l := New(NewTextHandler(&buf, &HandlerOptions{ReplaceAttr: removeTime}))
l.Error("msg", "err", io.EOF, "a", 1) l.Error("msg", "err", io.EOF, "a", 1)
checkLogOutput(t, buf.String(), `level=ERROR msg=msg err=EOF a=1`) checkLogOutput(t, buf.String(), `level=ERROR msg=msg err=EOF a=1`)
buf.Reset() buf.Reset()
@ -362,7 +362,7 @@ func TestLoggerError(t *testing.T) {
func TestNewLogLogger(t *testing.T) { func TestNewLogLogger(t *testing.T) {
var buf bytes.Buffer var buf bytes.Buffer
h := NewTextHandler(&buf) h := NewTextHandler(&buf, nil)
ll := NewLogLogger(h, LevelWarn) ll := NewLogLogger(h, LevelWarn)
ll.Print("hello") ll.Print("hello")
checkLogOutput(t, buf.String(), "time="+timeRE+` level=WARN msg=hello`) checkLogOutput(t, buf.String(), "time="+timeRE+` level=WARN msg=hello`)

View File

@ -22,18 +22,17 @@ type TextHandler struct {
} }
// NewTextHandler creates a TextHandler that writes to w, // NewTextHandler creates a TextHandler that writes to w,
// using the default options. // using the given options.
func NewTextHandler(w io.Writer) *TextHandler { // If opts is nil, the default options are used.
return (HandlerOptions{}).NewTextHandler(w) func NewTextHandler(w io.Writer, opts *HandlerOptions) *TextHandler {
} if opts == nil {
opts = &HandlerOptions{}
// NewTextHandler creates a TextHandler with the given options that writes to w. }
func (opts HandlerOptions) NewTextHandler(w io.Writer) *TextHandler {
return &TextHandler{ return &TextHandler{
&commonHandler{ &commonHandler{
json: false, json: false,
w: w, w: w,
opts: opts, opts: *opts,
}, },
} }
} }

View File

@ -82,7 +82,7 @@ func TestTextHandler(t *testing.T) {
} { } {
t.Run(opts.name, func(t *testing.T) { t.Run(opts.name, func(t *testing.T) {
var buf bytes.Buffer var buf bytes.Buffer
h := opts.opts.NewTextHandler(&buf) h := NewTextHandler(&buf, &opts.opts)
r := NewRecord(testTime, LevelInfo, "a message", 0) r := NewRecord(testTime, LevelInfo, "a message", 0)
r.AddAttrs(test.attr) r.AddAttrs(test.attr)
if err := h.Handle(context.Background(), r); err != nil { if err := h.Handle(context.Background(), r); err != nil {
@ -124,7 +124,7 @@ func (t text) MarshalText() ([]byte, error) {
func TestTextHandlerPreformatted(t *testing.T) { func TestTextHandlerPreformatted(t *testing.T) {
var buf bytes.Buffer var buf bytes.Buffer
var h Handler = NewTextHandler(&buf) var h Handler = NewTextHandler(&buf, nil)
h = h.WithAttrs([]Attr{Duration("dur", time.Minute), Bool("b", true)}) h = h.WithAttrs([]Attr{Duration("dur", time.Minute), Bool("b", true)})
// Also test omitting time. // Also test omitting time.
r := NewRecord(time.Time{}, 0 /* 0 Level is INFO */, "m", 0) r := NewRecord(time.Time{}, 0 /* 0 Level is INFO */, "m", 0)
@ -145,7 +145,7 @@ func TestTextHandlerAlloc(t *testing.T) {
for i := 0; i < 10; i++ { for i := 0; i < 10; i++ {
r.AddAttrs(Int("x = y", i)) r.AddAttrs(Int("x = y", i))
} }
var h Handler = NewTextHandler(io.Discard) var h Handler = NewTextHandler(io.Discard, nil)
wantAllocs(t, 0, func() { h.Handle(context.Background(), r) }) wantAllocs(t, 0, func() { h.Handle(context.Background(), r) })
h = h.WithGroup("s") h = h.WithGroup("s")

View File

@ -19,7 +19,7 @@ import (
// format when given a pointer to a map[string]any. // format when given a pointer to a map[string]any.
func Example_parsing() { func Example_parsing() {
var buf bytes.Buffer var buf bytes.Buffer
h := slog.NewJSONHandler(&buf) h := slog.NewJSONHandler(&buf, nil)
results := func() []map[string]any { results := func() []map[string]any {
var ms []map[string]any var ms []map[string]any