mirror of
https://github.com/golang/go.git
synced 2025-12-08 06:10:04 +00:00
log/slog: add multiple handlers support for logger
Fixes #65954
Change-Id: Ib01c6f47126ce290108b20c07479c82ef17c427c
GitHub-Last-Rev: 34a36ea4bf
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>
This commit is contained in:
parent
150fae714e
commit
e36c5aead6
5 changed files with 251 additions and 0 deletions
6
api/next/65954.txt
Normal file
6
api/next/65954.txt
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
pkg log/slog, func NewMultiHandler(...Handler) *MultiHandler #65954
|
||||
pkg log/slog, method (*MultiHandler) Enabled(context.Context, Level) bool #65954
|
||||
pkg log/slog, method (*MultiHandler) Handle(context.Context, Record) error #65954
|
||||
pkg log/slog, method (*MultiHandler) WithAttrs([]Attr) Handler #65954
|
||||
pkg log/slog, method (*MultiHandler) WithGroup(string) Handler #65954
|
||||
pkg log/slog, type MultiHandler struct #65954
|
||||
6
doc/next/6-stdlib/99-minor/log/slog/65954.md
Normal file
6
doc/next/6-stdlib/99-minor/log/slog/65954.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
The [`NewMultiHandler`](/pkg/log/slog#NewMultiHandler) function creates a
|
||||
[`MultiHandler`](/pkg/log/slog#MultiHandler) 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.
|
||||
39
src/log/slog/example_multi_handler_test.go
Normal file
39
src/log/slog/example_multi_handler_test.go
Normal file
|
|
@ -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}
|
||||
}
|
||||
61
src/log/slog/multi_handler.go
Normal file
61
src/log/slog/multi_handler.go
Normal file
|
|
@ -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}
|
||||
}
|
||||
139
src/log/slog/multi_handler_test.go
Normal file
139
src/log/slog/multi_handler_test.go
Normal file
|
|
@ -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"`)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue