diff options
| author | Jes Cok <xigua67damn@gmail.com> | 2025-08-27 14:27:31 +0000 |
|---|---|---|
| committer | Gopher Robot <gobot@golang.org> | 2025-09-04 11:07:44 -0700 |
| commit | e36c5aead681d8264f1fac725f2a15c1ca2b895a (patch) | |
| tree | 9fd692337f2bcf0b6f57b33b2776d30385b8b6f8 /src | |
| parent | 150fae714eb2bcf0a5fb216ac0e5c7fd76f37e02 (diff) | |
| download | go-e36c5aead681d8264f1fac725f2a15c1ca2b895a.tar.xz | |
log/slog: add multiple handlers support for logger
Fixes #65954
Change-Id: Ib01c6f47126ce290108b20c07479c82ef17c427c
GitHub-Last-Rev: 34a36ea4bf099b2ad30f35e639155853ff73ef46
GitHub-Pull-Request: golang/go#74840
Reviewed-on: https://go-review.googlesource.com/c/go/+/692237
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Michael Pratt <mpratt@google.com>
Auto-Submit: Michael Pratt <mpratt@google.com>
Diffstat (limited to 'src')
| -rw-r--r-- | src/log/slog/example_multi_handler_test.go | 39 | ||||
| -rw-r--r-- | src/log/slog/multi_handler.go | 61 | ||||
| -rw-r--r-- | src/log/slog/multi_handler_test.go | 139 |
3 files changed, 239 insertions, 0 deletions
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 0000000000..daba82c47d --- /dev/null +++ b/src/log/slog/example_multi_handler_test.go @@ -0,0 +1,39 @@ +// 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_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.NewMultiHandler(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} +} diff --git a/src/log/slog/multi_handler.go b/src/log/slog/multi_handler.go new file mode 100644 index 0000000000..4cc802b29b --- /dev/null +++ b/src/log/slog/multi_handler.go @@ -0,0 +1,61 @@ +// 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" +) + +// NewMultiHandler creates a [MultiHandler] with the given Handlers. +func NewMultiHandler(handlers ...Handler) *MultiHandler { + h := make([]Handler, len(handlers)) + copy(h, handlers) + return &MultiHandler{multi: h} +} + +// MultiHandler is 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. +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 new file mode 100644 index 0000000000..86844a661b --- /dev/null +++ b/src/log/slog/multi_handler_test.go @@ -0,0 +1,139 @@ +// 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" + "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 { + _ = h.Handler.Handle(ctx, r) + return h.err +} + +func TestMultiHandler(t *testing.T) { + 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 := NewMultiHandler(h1, h2) + logger := New(multi) + + logger.Info("hello world", "user", "test") + + 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) { + h1 := NewTextHandler(&bytes.Buffer{}, &HandlerOptions{Level: LevelError}) + h2 := NewTextHandler(&bytes.Buffer{}, &HandlerOptions{Level: LevelInfo}) + + multi := NewMultiHandler(h1, h2) + + if !multi.Enabled(context.Background(), LevelInfo) { + t.Error("Enabled should be true for INFO level, but got false") + } + if !multi.Enabled(context.Background(), 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 := NewMultiHandler(h1, h2) + + if multi.Enabled(context.Background(), 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 := NewMultiHandler(h1, h2).WithAttrs([]Attr{String("request_id", "123")}) + logger := New(multi) + + logger.Info("request processed") + + 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) { + var buf1, buf2 bytes.Buffer + h1 := NewTextHandler(&buf1, &HandlerOptions{AddSource: false}) + h2 := NewJSONHandler(&buf2, &HandlerOptions{AddSource: false}) + + multi := NewMultiHandler(h1, h2).WithGroup("req") + logger := New(multi) + + logger.Info("user login", "user_id", 42) + + 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) { + errFail := errors.New("mock failing") + + var buf1, buf2 bytes.Buffer + h1 := NewTextHandler(&buf1, nil) + h2 := &mockFailingHandler{Handler: NewJSONHandler(&buf2, nil), err: errFail} + + multi := NewMultiHandler(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, 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) { + multi := NewMultiHandler() + logger := New(multi) + + logger.Info("nothing") + + 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) + } + }) +} + +// Test that NewMultiHandler copies the input slice and is insulated from future modification. +func TestNewMultiHandlerCopy(t *testing.T) { + var buf1 bytes.Buffer + h1 := NewTextHandler(&buf1, nil) + slice := []Handler{h1} + multi := NewMultiHandler(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"`) +} |
