| // 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"`) |
| } |