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

log/slog: add multiple handlers support for logger #74840

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
callthingsoff wants to merge 7 commits into golang:master
base: master
Choose a base branch
Loading
from callthingsoff:multi_handler
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions api/next/65954.txt
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -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 changes: 6 additions & 0 deletions doc/next/6-stdlib/99-minor/log/slog/65954.md
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -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 changes: 39 additions & 0 deletions src/log/slog/example_multi_handler_test.go
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -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 changes: 61 additions & 0 deletions src/log/slog/multi_handler.go
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -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 changes: 139 additions & 0 deletions src/log/slog/multi_handler_test.go
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -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"`)
}

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