Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 2937564

Browse files
log/slog: add multiple handlers support for logger
Fixes #65954 Change-Id: I88f880977782e632ed71699272e3e5d3985ea37b
1 parent b2960e3 commit 2937564

File tree

4 files changed

+225
-0
lines changed

4 files changed

+225
-0
lines changed

‎api/next/65954.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pkg log/slog, func MultiHandler(...Handler) Handler #65954
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
The [`MultiHandler`](/pkg/log/slog#MultiHandler) function returns a handler that
2+
invokes all the given Handlers.
3+
Its `Enable` method reports whether any of the handlers' `Enabled` methods
4+
return true.
5+
Its `Handle`, `WithAttr` and `WithGroup` methods call the corresponding method
6+
on each of the enabled handlers.

‎src/log/slog/handler.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package slog
66

77
import (
88
"context"
9+
"errors"
910
"fmt"
1011
"io"
1112
"log/slog/internal/buffer"
@@ -642,3 +643,49 @@ func (dh discardHandler) Enabled(context.Context, Level) bool { return false }
642643
func (dh discardHandler) Handle(context.Context, Record) error { return nil }
643644
func (dh discardHandler) WithAttrs(attrs []Attr) Handler { return dh }
644645
func (dh discardHandler) WithGroup(name string) Handler { return dh }
646+
647+
// MultiHandler returns a handler that invokes all the given Handlers.
648+
// Its Enable method reports whether any of the handlers' Enabled methods return true.
649+
// Its Handle, WithAttr and WithGroup methods call the corresponding method on each of the enabled handlers.
650+
func MultiHandler(handlers ...Handler) Handler {
651+
return multiHandler(handlers)
652+
}
653+
654+
type multiHandler []Handler
655+
656+
func (h multiHandler) Enabled(ctx context.Context, l Level) bool {
657+
for i := range h {
658+
if h[i].Enabled(ctx, l) {
659+
return true
660+
}
661+
}
662+
return false
663+
}
664+
665+
func (h multiHandler) Handle(ctx context.Context, r Record) error {
666+
var errs []error
667+
for i := range h {
668+
if h[i].Enabled(ctx, r.Level) {
669+
if err := h[i].Handle(ctx, r.Clone()); err != nil {
670+
errs = append(errs, err)
671+
}
672+
}
673+
}
674+
return errors.Join(errs...)
675+
}
676+
677+
func (h multiHandler) WithAttrs(attrs []Attr) Handler {
678+
handlers := make([]Handler, 0, len(h))
679+
for i := range h {
680+
handlers = append(handlers, h[i].WithAttrs(attrs))
681+
}
682+
return multiHandler(handlers)
683+
}
684+
685+
func (h multiHandler) WithGroup(name string) Handler {
686+
handlers := make([]Handler, 0, len(h))
687+
for i := range h {
688+
handlers = append(handlers, h[i].WithGroup(name))
689+
}
690+
return multiHandler(handlers)
691+
}

‎src/log/slog/multi_handler_test.go

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
// Copyright 2025 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package slog
6+
7+
import (
8+
"bytes"
9+
"context"
10+
"errors"
11+
"strings"
12+
"testing"
13+
"time"
14+
)
15+
16+
// mockFailingHandler is a handler that always returns an error from its Handle method.
17+
type mockFailingHandler struct {
18+
Handler
19+
err error
20+
}
21+
22+
func (h *mockFailingHandler) Handle(ctx context.Context, r Record) error {
23+
// It still calls the underlying handler's Handle method to ensure the log can be processed.
24+
_ = h.Handler.Handle(ctx, r)
25+
// But it always returns a predefined error.
26+
return h.err
27+
}
28+
29+
func TestMultiHandler(t *testing.T) {
30+
ctx := context.Background()
31+
32+
t.Run("Handle sends log to all handlers", func(t *testing.T) {
33+
var buf1, buf2 bytes.Buffer
34+
h1 := NewTextHandler(&buf1, nil)
35+
h2 := NewJSONHandler(&buf2, nil)
36+
37+
multi := MultiHandler(h1, h2)
38+
logger := New(multi)
39+
40+
logger.Info("hello world", "user", "test")
41+
42+
// Check the output of the Text handler.
43+
output1 := buf1.String()
44+
if !strings.Contains(output1, `level=INFO`) ||
45+
!strings.Contains(output1, `msg="hello world"`) ||
46+
!strings.Contains(output1, `user=test`) {
47+
t.Errorf("Text handler did not receive the correct log message. Got: %s", output1)
48+
}
49+
50+
// Check the output of the JSON handle.
51+
output2 := buf2.String()
52+
if !strings.Contains(output2, `"level":"INFO"`) ||
53+
!strings.Contains(output2, `"msg":"hello world"`) ||
54+
!strings.Contains(output2, `"user":"test"`) {
55+
t.Errorf("JSON handler did not receive the correct log message. Got: %s", output2)
56+
}
57+
})
58+
59+
t.Run("Enabled returns true if any handler is enabled", func(t *testing.T) {
60+
h1 := NewTextHandler(&bytes.Buffer{}, &HandlerOptions{Level: LevelError})
61+
h2 := NewTextHandler(&bytes.Buffer{}, &HandlerOptions{Level: LevelInfo})
62+
63+
multi := MultiHandler(h1, h2)
64+
65+
if !multi.Enabled(ctx, LevelInfo) {
66+
t.Error("Enabled should be true for INFO level, but got false")
67+
}
68+
if !multi.Enabled(ctx, LevelError) {
69+
t.Error("Enabled should be true for ERROR level, but got false")
70+
}
71+
})
72+
73+
t.Run("Enabled returns false if no handlers are enabled", func(t *testing.T) {
74+
h1 := NewTextHandler(&bytes.Buffer{}, &HandlerOptions{Level: LevelError})
75+
h2 := NewTextHandler(&bytes.Buffer{}, &HandlerOptions{Level: LevelInfo})
76+
77+
multi := MultiHandler(h1, h2)
78+
79+
if multi.Enabled(ctx, LevelDebug) {
80+
t.Error("Enabled should be false for DEBUG level, but got true")
81+
}
82+
})
83+
84+
t.Run("WithAttrs propagates attributes to all handlers", func(t *testing.T) {
85+
var buf1, buf2 bytes.Buffer
86+
h1 := NewTextHandler(&buf1, nil)
87+
h2 := NewJSONHandler(&buf2, nil)
88+
89+
multi := MultiHandler(h1, h2).WithAttrs([]Attr{String("request_id", "123")})
90+
logger := New(multi)
91+
92+
logger.Info("request processed")
93+
94+
// Check if the Text handler contains the attribute.
95+
if !strings.Contains(buf1.String(), "request_id=123") {
96+
t.Errorf("Text handler output missing attribute. Got: %s", buf1.String())
97+
}
98+
99+
// Check if the JSON handler contains the attribute.
100+
if !strings.Contains(buf2.String(), `"request_id":"123"`) {
101+
t.Errorf("JSON handler output missing attribute. Got: %s", buf2.String())
102+
}
103+
})
104+
105+
t.Run("WithGroup propagates group to all handlers", func(t *testing.T) {
106+
var buf1, buf2 bytes.Buffer
107+
h1 := NewTextHandler(&buf1, &HandlerOptions{AddSource: false})
108+
h2 := NewJSONHandler(&buf2, &HandlerOptions{AddSource: false})
109+
110+
multi := MultiHandler(h1, h2).WithGroup("req")
111+
logger := New(multi)
112+
113+
logger.Info("user login", "user_id", 42)
114+
115+
// Check if the Text handler contains the group.
116+
expectedText := "req.user_id=42"
117+
if !strings.Contains(buf1.String(), expectedText) {
118+
t.Errorf("Text handler output missing group. Expected to contain %q, Got: %s", expectedText, buf1.String())
119+
}
120+
121+
// Check if the JSON handler contains the group.
122+
expectedJSON := `"req":{"user_id":42}`
123+
if !strings.Contains(buf2.String(), expectedJSON) {
124+
t.Errorf("JSON handler output missing group. Expected to contain %q, Got: %s", expectedJSON, buf2.String())
125+
}
126+
})
127+
128+
t.Run("Handle propagates errors from handlers", func(t *testing.T) {
129+
var buf bytes.Buffer
130+
h1 := NewTextHandler(&buf, nil)
131+
132+
// Simulate a handler that will fail.
133+
errFail := errors.New("fake fail")
134+
h2 := &mockFailingHandler{
135+
Handler: NewTextHandler(&bytes.Buffer{}, nil),
136+
err: errFail,
137+
}
138+
139+
multi := MultiHandler(h1, h2)
140+
141+
err := multi.Handle(ctx, NewRecord(time.Now(), LevelInfo, "test message", 0))
142+
143+
// Check if the error was returned correctly.
144+
if err == nil {
145+
t.Fatal("Expected an error from Handle, but got nil")
146+
}
147+
if !errors.Is(err, errFail) {
148+
t.Errorf("Expected error: %v, but got: %v", errFail, err)
149+
}
150+
151+
// Also, check that the successful handler still output the log.
152+
if !strings.Contains(buf.String(), "test message") {
153+
t.Error("The successful handler should still have processed the log")
154+
}
155+
})
156+
157+
t.Run("Handle with no handlers", func(t *testing.T) {
158+
// Create an empty multi-handler.
159+
multi := MultiHandler()
160+
logger := New(multi)
161+
162+
// This should be safe to call and do nothing.
163+
logger.Info("this is nothing")
164+
165+
// Calling Handle directly should also be safe.
166+
err := multi.Handle(ctx, NewRecord(time.Now(), LevelInfo, "test", 0))
167+
if err != nil {
168+
t.Errorf("Handle with no sub-handlers should return nil, but got: %v", err)
169+
}
170+
})
171+
}

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /