Skip to content

Commit 2937564

Browse files
committed
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)