From 2051b7853529ce57afd7642e75fda53c59bfee8b Mon Sep 17 00:00:00 2001 From: Jes Cok Date: Fri, 1 Aug 2025 21:59:45 +0800 Subject: [PATCH 1/6] log/slog: add multiple handlers support for logger Fixes #65954 Change-Id: I88f880977782e632ed71699272e3e5d3985ea37b --- api/next/65954.txt | 1 + doc/next/6-stdlib/99-minor/log/slog/65954.md | 6 + src/log/slog/handler.go | 47 +++++ src/log/slog/multi_handler_test.go | 171 +++++++++++++++++++ 4 files changed, 225 insertions(+) create mode 100644 api/next/65954.txt create mode 100644 doc/next/6-stdlib/99-minor/log/slog/65954.md create mode 100644 src/log/slog/multi_handler_test.go diff --git a/api/next/65954.txt b/api/next/65954.txt new file mode 100644 index 00000000000000..c82d2c9542bb16 --- /dev/null +++ b/api/next/65954.txt @@ -0,0 +1 @@ +pkg log/slog, func MultiHandler(...Handler) Handler #65954 diff --git a/doc/next/6-stdlib/99-minor/log/slog/65954.md b/doc/next/6-stdlib/99-minor/log/slog/65954.md new file mode 100644 index 00000000000000..3676658c281a29 --- /dev/null +++ b/doc/next/6-stdlib/99-minor/log/slog/65954.md @@ -0,0 +1,6 @@ +The [`MultiHandler`](/pkg/log/slog#MultiHandler) function returns a handler that +invokes all the given Handlers. +Its `Enable` method reports whether any of the handlers' `Enabled` methods +return true. +Its `Handle`, `WithAttr` and `WithGroup` methods call the corresponding method +on each of the enabled handlers. diff --git a/src/log/slog/handler.go b/src/log/slog/handler.go index 26eb4b82fc8b57..efd7e8e882a43a 100644 --- a/src/log/slog/handler.go +++ b/src/log/slog/handler.go @@ -6,6 +6,7 @@ package slog import ( "context" + "errors" "fmt" "io" "log/slog/internal/buffer" @@ -642,3 +643,49 @@ func (dh discardHandler) Enabled(context.Context, Level) bool { return false } func (dh discardHandler) Handle(context.Context, Record) error { return nil } func (dh discardHandler) WithAttrs(attrs []Attr) Handler { return dh } func (dh discardHandler) WithGroup(name string) Handler { return dh } + +// MultiHandler returns a handler that invokes all the given Handlers. +// Its Enable method reports whether any of the handlers' Enabled methods return true. +// Its Handle, WithAttr and WithGroup methods call the corresponding method on each of the enabled handlers. +func MultiHandler(handlers ...Handler) Handler { + return multiHandler(handlers) +} + +type multiHandler []Handler + +func (h multiHandler) Enabled(ctx context.Context, l Level) bool { + for i := range h { + if h[i].Enabled(ctx, l) { + return true + } + } + return false +} + +func (h multiHandler) Handle(ctx context.Context, r Record) error { + var errs []error + for i := range h { + if h[i].Enabled(ctx, r.Level) { + if err := h[i].Handle(ctx, r.Clone()); err != nil { + errs = append(errs, err) + } + } + } + return errors.Join(errs...) +} + +func (h multiHandler) WithAttrs(attrs []Attr) Handler { + handlers := make([]Handler, 0, len(h)) + for i := range h { + handlers = append(handlers, h[i].WithAttrs(attrs)) + } + return multiHandler(handlers) +} + +func (h multiHandler) WithGroup(name string) Handler { + handlers := make([]Handler, 0, len(h)) + for i := range h { + handlers = append(handlers, h[i].WithGroup(name)) + } + return multiHandler(handlers) +} diff --git a/src/log/slog/multi_handler_test.go b/src/log/slog/multi_handler_test.go new file mode 100644 index 00000000000000..cf05ae593d88a7 --- /dev/null +++ b/src/log/slog/multi_handler_test.go @@ -0,0 +1,171 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package slog + +import ( + "bytes" + "context" + "errors" + "strings" + "testing" + "time" +) + +// mockFailingHandler is a handler that always returns an error from its Handle method. +type mockFailingHandler struct { + Handler + err error +} + +func (h *mockFailingHandler) Handle(ctx context.Context, r Record) error { + // It still calls the underlying handler's Handle method to ensure the log can be processed. + _ = h.Handler.Handle(ctx, r) + // But it always returns a predefined error. + return h.err +} + +func TestMultiHandler(t *testing.T) { + ctx := context.Background() + + t.Run("Handle sends log to all handlers", func(t *testing.T) { + var buf1, buf2 bytes.Buffer + h1 := NewTextHandler(&buf1, nil) + h2 := NewJSONHandler(&buf2, nil) + + multi := MultiHandler(h1, h2) + logger := New(multi) + + logger.Info("hello world", "user", "test") + + // Check the output of the Text handler. + output1 := buf1.String() + if !strings.Contains(output1, `level=INFO`) || + !strings.Contains(output1, `msg="hello world"`) || + !strings.Contains(output1, `user=test`) { + t.Errorf("Text handler did not receive the correct log message. Got: %s", output1) + } + + // Check the output of the JSON handle. + output2 := buf2.String() + if !strings.Contains(output2, `"level":"INFO"`) || + !strings.Contains(output2, `"msg":"hello world"`) || + !strings.Contains(output2, `"user":"test"`) { + t.Errorf("JSON handler did not receive the correct log message. Got: %s", output2) + } + }) + + t.Run("Enabled returns true if any handler is enabled", func(t *testing.T) { + h1 := NewTextHandler(&bytes.Buffer{}, &HandlerOptions{Level: LevelError}) + h2 := NewTextHandler(&bytes.Buffer{}, &HandlerOptions{Level: LevelInfo}) + + multi := MultiHandler(h1, h2) + + if !multi.Enabled(ctx, LevelInfo) { + t.Error("Enabled should be true for INFO level, but got false") + } + if !multi.Enabled(ctx, LevelError) { + t.Error("Enabled should be true for ERROR level, but got false") + } + }) + + t.Run("Enabled returns false if no handlers are enabled", func(t *testing.T) { + h1 := NewTextHandler(&bytes.Buffer{}, &HandlerOptions{Level: LevelError}) + h2 := NewTextHandler(&bytes.Buffer{}, &HandlerOptions{Level: LevelInfo}) + + multi := MultiHandler(h1, h2) + + if multi.Enabled(ctx, LevelDebug) { + t.Error("Enabled should be false for DEBUG level, but got true") + } + }) + + t.Run("WithAttrs propagates attributes to all handlers", func(t *testing.T) { + var buf1, buf2 bytes.Buffer + h1 := NewTextHandler(&buf1, nil) + h2 := NewJSONHandler(&buf2, nil) + + multi := MultiHandler(h1, h2).WithAttrs([]Attr{String("request_id", "123")}) + logger := New(multi) + + logger.Info("request processed") + + // Check if the Text handler contains the attribute. + if !strings.Contains(buf1.String(), "request_id=123") { + t.Errorf("Text handler output missing attribute. Got: %s", buf1.String()) + } + + // Check if the JSON handler contains the attribute. + if !strings.Contains(buf2.String(), `"request_id":"123"`) { + t.Errorf("JSON handler output missing attribute. Got: %s", buf2.String()) + } + }) + + t.Run("WithGroup propagates group to all handlers", func(t *testing.T) { + var buf1, buf2 bytes.Buffer + h1 := NewTextHandler(&buf1, &HandlerOptions{AddSource: false}) + h2 := NewJSONHandler(&buf2, &HandlerOptions{AddSource: false}) + + multi := MultiHandler(h1, h2).WithGroup("req") + logger := New(multi) + + logger.Info("user login", "user_id", 42) + + // Check if the Text handler contains the group. + expectedText := "req.user_id=42" + if !strings.Contains(buf1.String(), expectedText) { + t.Errorf("Text handler output missing group. Expected to contain %q, Got: %s", expectedText, buf1.String()) + } + + // Check if the JSON handler contains the group. + expectedJSON := `"req":{"user_id":42}` + if !strings.Contains(buf2.String(), expectedJSON) { + t.Errorf("JSON handler output missing group. Expected to contain %q, Got: %s", expectedJSON, buf2.String()) + } + }) + + t.Run("Handle propagates errors from handlers", func(t *testing.T) { + var buf bytes.Buffer + h1 := NewTextHandler(&buf, nil) + + // Simulate a handler that will fail. + errFail := errors.New("fake fail") + h2 := &mockFailingHandler{ + Handler: NewTextHandler(&bytes.Buffer{}, nil), + err: errFail, + } + + multi := MultiHandler(h1, h2) + + err := multi.Handle(ctx, NewRecord(time.Now(), LevelInfo, "test message", 0)) + + // Check if the error was returned correctly. + if err == nil { + t.Fatal("Expected an error from Handle, but got nil") + } + if !errors.Is(err, errFail) { + t.Errorf("Expected error: %v, but got: %v", errFail, err) + } + + // Also, check that the successful handler still output the log. + if !strings.Contains(buf.String(), "test message") { + t.Error("The successful handler should still have processed the log") + } + }) + + t.Run("Handle with no handlers", func(t *testing.T) { + // Create an empty multi-handler. + multi := MultiHandler() + logger := New(multi) + + // This should be safe to call and do nothing. + logger.Info("this is nothing") + + // Calling Handle directly should also be safe. + err := multi.Handle(ctx, NewRecord(time.Now(), LevelInfo, "test", 0)) + if err != nil { + t.Errorf("Handle with no sub-handlers should return nil, but got: %v", err) + } + }) +} From 9efdf276fa3eb8e204d8d14677adc7ee15901bd7 Mon Sep 17 00:00:00 2001 From: Jes Cok Date: Mon, 4 Aug 2025 21:38:47 +0800 Subject: [PATCH 2/6] 2 Change-Id: I2effccfd296366065ee9626a9674cd637b702218 --- src/log/slog/handler.go | 47 -------------------- src/log/slog/multi_handler.go | 58 +++++++++++++++++++++++++ src/log/slog/multi_handler_test.go | 70 ++++++------------------------ 3 files changed, 71 insertions(+), 104 deletions(-) create mode 100644 src/log/slog/multi_handler.go diff --git a/src/log/slog/handler.go b/src/log/slog/handler.go index efd7e8e882a43a..26eb4b82fc8b57 100644 --- a/src/log/slog/handler.go +++ b/src/log/slog/handler.go @@ -6,7 +6,6 @@ package slog import ( "context" - "errors" "fmt" "io" "log/slog/internal/buffer" @@ -643,49 +642,3 @@ func (dh discardHandler) Enabled(context.Context, Level) bool { return false } func (dh discardHandler) Handle(context.Context, Record) error { return nil } func (dh discardHandler) WithAttrs(attrs []Attr) Handler { return dh } func (dh discardHandler) WithGroup(name string) Handler { return dh } - -// MultiHandler returns a handler that invokes all the given Handlers. -// Its Enable method reports whether any of the handlers' Enabled methods return true. -// Its Handle, WithAttr and WithGroup methods call the corresponding method on each of the enabled handlers. -func MultiHandler(handlers ...Handler) Handler { - return multiHandler(handlers) -} - -type multiHandler []Handler - -func (h multiHandler) Enabled(ctx context.Context, l Level) bool { - for i := range h { - if h[i].Enabled(ctx, l) { - return true - } - } - return false -} - -func (h multiHandler) Handle(ctx context.Context, r Record) error { - var errs []error - for i := range h { - if h[i].Enabled(ctx, r.Level) { - if err := h[i].Handle(ctx, r.Clone()); err != nil { - errs = append(errs, err) - } - } - } - return errors.Join(errs...) -} - -func (h multiHandler) WithAttrs(attrs []Attr) Handler { - handlers := make([]Handler, 0, len(h)) - for i := range h { - handlers = append(handlers, h[i].WithAttrs(attrs)) - } - return multiHandler(handlers) -} - -func (h multiHandler) WithGroup(name string) Handler { - handlers := make([]Handler, 0, len(h)) - for i := range h { - handlers = append(handlers, h[i].WithGroup(name)) - } - return multiHandler(handlers) -} diff --git a/src/log/slog/multi_handler.go b/src/log/slog/multi_handler.go new file mode 100644 index 00000000000000..99b53528b02703 --- /dev/null +++ b/src/log/slog/multi_handler.go @@ -0,0 +1,58 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package slog + +import ( + "context" + "errors" +) + +// MultiHandler returns a handler that invokes all the given Handlers. +// Its Enable method reports whether any of the handlers' Enabled methods return true. +// Its Handle, WithAttr and WithGroup methods call the corresponding method on each of the enabled handlers. +func MultiHandler(handlers ...Handler) Handler { + return &multiHandler{multi: handlers} +} + +type multiHandler struct { + multi []Handler +} + +func (h *multiHandler) Enabled(ctx context.Context, l Level) bool { + for i := range h.multi { + if h.multi[i].Enabled(ctx, l) { + return true + } + } + return false +} + +func (h *multiHandler) Handle(ctx context.Context, r Record) error { + var errs []error + for i := range h.multi { + if h.multi[i].Enabled(ctx, r.Level) { + if err := h.multi[i].Handle(ctx, r.Clone()); err != nil { + errs = append(errs, err) + } + } + } + return errors.Join(errs...) +} + +func (h *multiHandler) WithAttrs(attrs []Attr) Handler { + handlers := make([]Handler, 0, len(h.multi)) + for i := range h.multi { + handlers = append(handlers, h.multi[i].WithAttrs(attrs)) + } + return &multiHandler{multi: handlers} +} + +func (h *multiHandler) WithGroup(name string) Handler { + handlers := make([]Handler, 0, len(h.multi)) + for i := range h.multi { + handlers = append(handlers, h.multi[i].WithGroup(name)) + } + return &multiHandler{multi: handlers} +} diff --git a/src/log/slog/multi_handler_test.go b/src/log/slog/multi_handler_test.go index cf05ae593d88a7..8ddd894803cf12 100644 --- a/src/log/slog/multi_handler_test.go +++ b/src/log/slog/multi_handler_test.go @@ -8,7 +8,6 @@ import ( "bytes" "context" "errors" - "strings" "testing" "time" ) @@ -27,8 +26,6 @@ func (h *mockFailingHandler) Handle(ctx context.Context, r Record) error { } func TestMultiHandler(t *testing.T) { - ctx := context.Background() - t.Run("Handle sends log to all handlers", func(t *testing.T) { var buf1, buf2 bytes.Buffer h1 := NewTextHandler(&buf1, nil) @@ -39,21 +36,8 @@ func TestMultiHandler(t *testing.T) { logger.Info("hello world", "user", "test") - // Check the output of the Text handler. - output1 := buf1.String() - if !strings.Contains(output1, `level=INFO`) || - !strings.Contains(output1, `msg="hello world"`) || - !strings.Contains(output1, `user=test`) { - t.Errorf("Text handler did not receive the correct log message. Got: %s", output1) - } - - // Check the output of the JSON handle. - output2 := buf2.String() - if !strings.Contains(output2, `"level":"INFO"`) || - !strings.Contains(output2, `"msg":"hello world"`) || - !strings.Contains(output2, `"user":"test"`) { - t.Errorf("JSON handler did not receive the correct log message. Got: %s", output2) - } + checkLogOutput(t, buf1.String(), "time="+textTimeRE+` level=INFO msg="hello world" user=test`) + checkLogOutput(t, buf2.String(), `{"time":"`+jsonTimeRE+`","level":"INFO","msg":"hello world","user":"test"}`) }) t.Run("Enabled returns true if any handler is enabled", func(t *testing.T) { @@ -62,10 +46,10 @@ func TestMultiHandler(t *testing.T) { multi := MultiHandler(h1, h2) - if !multi.Enabled(ctx, LevelInfo) { + if !multi.Enabled(context.Background(), LevelInfo) { t.Error("Enabled should be true for INFO level, but got false") } - if !multi.Enabled(ctx, LevelError) { + if !multi.Enabled(context.Background(), LevelError) { t.Error("Enabled should be true for ERROR level, but got false") } }) @@ -76,7 +60,7 @@ func TestMultiHandler(t *testing.T) { multi := MultiHandler(h1, h2) - if multi.Enabled(ctx, LevelDebug) { + if multi.Enabled(context.Background(), LevelDebug) { t.Error("Enabled should be false for DEBUG level, but got true") } }) @@ -91,15 +75,8 @@ func TestMultiHandler(t *testing.T) { logger.Info("request processed") - // Check if the Text handler contains the attribute. - if !strings.Contains(buf1.String(), "request_id=123") { - t.Errorf("Text handler output missing attribute. Got: %s", buf1.String()) - } - - // Check if the JSON handler contains the attribute. - if !strings.Contains(buf2.String(), `"request_id":"123"`) { - t.Errorf("JSON handler output missing attribute. Got: %s", buf2.String()) - } + checkLogOutput(t, buf1.String(), "time="+textTimeRE+` level=INFO msg="request processed" request_id=123`) + checkLogOutput(t, buf2.String(), `{"time":"`+jsonTimeRE+`","level":"INFO","msg":"request processed","request_id":"123"}`) }) t.Run("WithGroup propagates group to all handlers", func(t *testing.T) { @@ -112,24 +89,14 @@ func TestMultiHandler(t *testing.T) { logger.Info("user login", "user_id", 42) - // Check if the Text handler contains the group. - expectedText := "req.user_id=42" - if !strings.Contains(buf1.String(), expectedText) { - t.Errorf("Text handler output missing group. Expected to contain %q, Got: %s", expectedText, buf1.String()) - } - - // Check if the JSON handler contains the group. - expectedJSON := `"req":{"user_id":42}` - if !strings.Contains(buf2.String(), expectedJSON) { - t.Errorf("JSON handler output missing group. Expected to contain %q, Got: %s", expectedJSON, buf2.String()) - } + checkLogOutput(t, buf1.String(), "time="+textTimeRE+` level=INFO msg="user login" req.user_id=42`) + checkLogOutput(t, buf2.String(), `{"time":"`+jsonTimeRE+`","level":"INFO","msg":"user login","req":{"user_id":42}}`) }) t.Run("Handle propagates errors from handlers", func(t *testing.T) { var buf bytes.Buffer h1 := NewTextHandler(&buf, nil) - // Simulate a handler that will fail. errFail := errors.New("fake fail") h2 := &mockFailingHandler{ Handler: NewTextHandler(&bytes.Buffer{}, nil), @@ -138,32 +105,21 @@ func TestMultiHandler(t *testing.T) { multi := MultiHandler(h1, h2) - err := multi.Handle(ctx, NewRecord(time.Now(), LevelInfo, "test message", 0)) - - // Check if the error was returned correctly. - if err == nil { - t.Fatal("Expected an error from Handle, but got nil") - } + err := multi.Handle(context.Background(), NewRecord(time.Now(), LevelInfo, "test message", 0)) if !errors.Is(err, errFail) { t.Errorf("Expected error: %v, but got: %v", errFail, err) } - // Also, check that the successful handler still output the log. - if !strings.Contains(buf.String(), "test message") { - t.Error("The successful handler should still have processed the log") - } + checkLogOutput(t, buf.String(), "time="+textTimeRE+` level=INFO msg="test message"`) }) t.Run("Handle with no handlers", func(t *testing.T) { - // Create an empty multi-handler. multi := MultiHandler() logger := New(multi) - // This should be safe to call and do nothing. - logger.Info("this is nothing") + logger.Info("nothing") - // Calling Handle directly should also be safe. - err := multi.Handle(ctx, NewRecord(time.Now(), LevelInfo, "test", 0)) + err := multi.Handle(context.Background(), NewRecord(time.Now(), LevelInfo, "test", 0)) if err != nil { t.Errorf("Handle with no sub-handlers should return nil, but got: %v", err) } From 06aeeb3b9610d05adb9110b0369532fcd5694f4d Mon Sep 17 00:00:00 2001 From: Jes Cok Date: Mon, 4 Aug 2025 22:04:08 +0800 Subject: [PATCH 3/6] 3 Change-Id: I98eda168e7630f523184b23b38c629b42eebedde --- src/log/slog/multi_handler_test.go | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/log/slog/multi_handler_test.go b/src/log/slog/multi_handler_test.go index 8ddd894803cf12..77f6c0b6a72010 100644 --- a/src/log/slog/multi_handler_test.go +++ b/src/log/slog/multi_handler_test.go @@ -12,16 +12,15 @@ import ( "time" ) -// mockFailingHandler is a handler that always returns an error from its Handle method. +// mockFailingHandler is a handler that always returns an error +// from its Handle method. type mockFailingHandler struct { Handler err error } func (h *mockFailingHandler) Handle(ctx context.Context, r Record) error { - // It still calls the underlying handler's Handle method to ensure the log can be processed. _ = h.Handler.Handle(ctx, r) - // But it always returns a predefined error. return h.err } @@ -94,23 +93,21 @@ func TestMultiHandler(t *testing.T) { }) t.Run("Handle propagates errors from handlers", func(t *testing.T) { - var buf bytes.Buffer - h1 := NewTextHandler(&buf, nil) + errFail := errors.New("mock failing") - errFail := errors.New("fake fail") - h2 := &mockFailingHandler{ - Handler: NewTextHandler(&bytes.Buffer{}, nil), - err: errFail, - } + var buf1, buf2 bytes.Buffer + h1 := NewTextHandler(&buf1, nil) + h2 := &mockFailingHandler{Handler: NewJSONHandler(&buf2, nil), err: errFail} - multi := MultiHandler(h1, h2) + multi := MultiHandler(h2, h1) err := multi.Handle(context.Background(), NewRecord(time.Now(), LevelInfo, "test message", 0)) if !errors.Is(err, errFail) { t.Errorf("Expected error: %v, but got: %v", errFail, err) } - checkLogOutput(t, buf.String(), "time="+textTimeRE+` level=INFO msg="test message"`) + checkLogOutput(t, buf1.String(), "time="+textTimeRE+` level=INFO msg="test message"`) + checkLogOutput(t, buf2.String(), `{"time":"`+jsonTimeRE+`","level":"INFO","msg":"test message"}`) }) t.Run("Handle with no handlers", func(t *testing.T) { From 2b23315385bdd5e53ed8c0180e9cd958472e2a31 Mon Sep 17 00:00:00 2001 From: Jes Cok Date: Sat, 9 Aug 2025 15:36:49 +0800 Subject: [PATCH 4/6] 4 Change-Id: I4830410ed2ea9e74550eaa5e5722e3b9aeb80d16 --- src/log/slog/multi_handler.go | 4 +++- src/log/slog/multi_handler_test.go | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/log/slog/multi_handler.go b/src/log/slog/multi_handler.go index 99b53528b02703..82a4dafad24ee9 100644 --- a/src/log/slog/multi_handler.go +++ b/src/log/slog/multi_handler.go @@ -13,7 +13,9 @@ import ( // Its Enable method reports whether any of the handlers' Enabled methods return true. // Its Handle, WithAttr and WithGroup methods call the corresponding method on each of the enabled handlers. func MultiHandler(handlers ...Handler) Handler { - return &multiHandler{multi: handlers} + h := make([]Handler, len(handlers)) + copy(h, handlers) + return &multiHandler{multi: h} } type multiHandler struct { diff --git a/src/log/slog/multi_handler_test.go b/src/log/slog/multi_handler_test.go index 77f6c0b6a72010..7a4713dfa49dab 100644 --- a/src/log/slog/multi_handler_test.go +++ b/src/log/slog/multi_handler_test.go @@ -122,3 +122,18 @@ func TestMultiHandler(t *testing.T) { } }) } + +// Test that MultiHandler copies the input slice and is insulated from future modification. +func TestMultiHandlerCopy(t *testing.T) { + var buf1 bytes.Buffer + h1 := NewTextHandler(&buf1, nil) + slice := []Handler{h1} + multi := MultiHandler(slice...) + slice[0] = nil + + err := multi.Handle(context.Background(), NewRecord(time.Now(), LevelInfo, "test message", 0)) + if err != nil { + t.Errorf("Expected nil error, but got: %v", err) + } + checkLogOutput(t, buf1.String(), "time="+textTimeRE+` level=INFO msg="test message"`) +} From 9c40d1c60718e98af69c26fb3ed4c14b60577431 Mon Sep 17 00:00:00 2001 From: Jes Cok Date: Tue, 19 Aug 2025 21:51:35 +0800 Subject: [PATCH 5/6] 5 Change-Id: I8dc2a9e008e945e7338b3f1ab14216c99442f15e --- src/log/slog/example_multi_handler_test.go | 39 ++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/log/slog/example_multi_handler_test.go diff --git a/src/log/slog/example_multi_handler_test.go b/src/log/slog/example_multi_handler_test.go new file mode 100644 index 00000000000000..48e4d28d960c97 --- /dev/null +++ b/src/log/slog/example_multi_handler_test.go @@ -0,0 +1,39 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package slog_test + +import ( + "bytes" + "log/slog" + "os" +) + +func ExampleMultiHandler() { + removeTime := func(groups []string, a slog.Attr) slog.Attr { + if a.Key == slog.TimeKey && len(groups) == 0 { + return slog.Attr{} + } + return a + } + + var textBuf, jsonBuf bytes.Buffer + textHandler := slog.NewTextHandler(&textBuf, &slog.HandlerOptions{ReplaceAttr: removeTime}) + jsonHandler := slog.NewJSONHandler(&jsonBuf, &slog.HandlerOptions{ReplaceAttr: removeTime}) + + multiHandler := slog.MultiHandler(textHandler, jsonHandler) + logger := slog.New(multiHandler) + + logger.Info("login", + slog.String("name", "whoami"), + slog.Int("id", 42), + ) + + os.Stdout.WriteString(textBuf.String()) + os.Stdout.WriteString(jsonBuf.String()) + + // Output: + // level=INFO msg=login name=whoami id=42 + // {"level":"INFO","msg":"login","name":"whoami","id":42} +} From 0e7e7451fd0b483ce780a1d11d949b8d61e956e9 Mon Sep 17 00:00:00 2001 From: Jes Cok Date: Tue, 19 Aug 2025 21:56:57 +0800 Subject: [PATCH 6/6] 6 Change-Id: I5d5d1d70b323bf83e61f186d5ab8b6ebc087346e --- src/log/slog/example_multi_handler_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/log/slog/example_multi_handler_test.go b/src/log/slog/example_multi_handler_test.go index 48e4d28d960c97..1014ce289ef180 100644 --- a/src/log/slog/example_multi_handler_test.go +++ b/src/log/slog/example_multi_handler_test.go @@ -1,4 +1,4 @@ -// Copyright 2022 The Go Authors. All rights reserved. +// Copyright 2025 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file.