Source file src/log/slog/multi_handler_test.go

     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  	"testing"
    12  	"time"
    13  )
    14  
    15  // mockFailingHandler is a handler that always returns an error
    16  // 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  	_ = h.Handler.Handle(ctx, r)
    24  	return h.err
    25  }
    26  
    27  func TestMultiHandler(t *testing.T) {
    28  	t.Run("Handle sends log to all handlers", func(t *testing.T) {
    29  		var buf1, buf2 bytes.Buffer
    30  		h1 := NewTextHandler(&buf1, nil)
    31  		h2 := NewJSONHandler(&buf2, nil)
    32  
    33  		multi := NewMultiHandler(h1, h2)
    34  		logger := New(multi)
    35  
    36  		logger.Info("hello world", "user", "test")
    37  
    38  		checkLogOutput(t, buf1.String(), "time="+textTimeRE+` level=INFO msg="hello world" user=test`)
    39  		checkLogOutput(t, buf2.String(), `{"time":"`+jsonTimeRE+`","level":"INFO","msg":"hello world","user":"test"}`)
    40  	})
    41  
    42  	t.Run("Enabled returns true if any handler is enabled", func(t *testing.T) {
    43  		h1 := NewTextHandler(&bytes.Buffer{}, &HandlerOptions{Level: LevelError})
    44  		h2 := NewTextHandler(&bytes.Buffer{}, &HandlerOptions{Level: LevelInfo})
    45  
    46  		multi := NewMultiHandler(h1, h2)
    47  
    48  		if !multi.Enabled(context.Background(), LevelInfo) {
    49  			t.Error("Enabled should be true for INFO level, but got false")
    50  		}
    51  		if !multi.Enabled(context.Background(), LevelError) {
    52  			t.Error("Enabled should be true for ERROR level, but got false")
    53  		}
    54  	})
    55  
    56  	t.Run("Enabled returns false if no handlers are enabled", func(t *testing.T) {
    57  		h1 := NewTextHandler(&bytes.Buffer{}, &HandlerOptions{Level: LevelError})
    58  		h2 := NewTextHandler(&bytes.Buffer{}, &HandlerOptions{Level: LevelInfo})
    59  
    60  		multi := NewMultiHandler(h1, h2)
    61  
    62  		if multi.Enabled(context.Background(), LevelDebug) {
    63  			t.Error("Enabled should be false for DEBUG level, but got true")
    64  		}
    65  	})
    66  
    67  	t.Run("WithAttrs propagates attributes to all handlers", func(t *testing.T) {
    68  		var buf1, buf2 bytes.Buffer
    69  		h1 := NewTextHandler(&buf1, nil)
    70  		h2 := NewJSONHandler(&buf2, nil)
    71  
    72  		multi := NewMultiHandler(h1, h2).WithAttrs([]Attr{String("request_id", "123")})
    73  		logger := New(multi)
    74  
    75  		logger.Info("request processed")
    76  
    77  		checkLogOutput(t, buf1.String(), "time="+textTimeRE+` level=INFO msg="request processed" request_id=123`)
    78  		checkLogOutput(t, buf2.String(), `{"time":"`+jsonTimeRE+`","level":"INFO","msg":"request processed","request_id":"123"}`)
    79  	})
    80  
    81  	t.Run("WithGroup propagates group to all handlers", func(t *testing.T) {
    82  		var buf1, buf2 bytes.Buffer
    83  		h1 := NewTextHandler(&buf1, &HandlerOptions{AddSource: false})
    84  		h2 := NewJSONHandler(&buf2, &HandlerOptions{AddSource: false})
    85  
    86  		multi := NewMultiHandler(h1, h2).WithGroup("req")
    87  		logger := New(multi)
    88  
    89  		logger.Info("user login", "user_id", 42)
    90  
    91  		checkLogOutput(t, buf1.String(), "time="+textTimeRE+` level=INFO msg="user login" req.user_id=42`)
    92  		checkLogOutput(t, buf2.String(), `{"time":"`+jsonTimeRE+`","level":"INFO","msg":"user login","req":{"user_id":42}}`)
    93  	})
    94  
    95  	t.Run("Handle propagates errors from handlers", func(t *testing.T) {
    96  		errFail := errors.New("mock failing")
    97  
    98  		var buf1, buf2 bytes.Buffer
    99  		h1 := NewTextHandler(&buf1, nil)
   100  		h2 := &mockFailingHandler{Handler: NewJSONHandler(&buf2, nil), err: errFail}
   101  
   102  		multi := NewMultiHandler(h2, h1)
   103  
   104  		err := multi.Handle(context.Background(), NewRecord(time.Now(), LevelInfo, "test message", 0))
   105  		if !errors.Is(err, errFail) {
   106  			t.Errorf("Expected error: %v, but got: %v", errFail, err)
   107  		}
   108  
   109  		checkLogOutput(t, buf1.String(), "time="+textTimeRE+` level=INFO msg="test message"`)
   110  		checkLogOutput(t, buf2.String(), `{"time":"`+jsonTimeRE+`","level":"INFO","msg":"test message"}`)
   111  	})
   112  
   113  	t.Run("Handle with no handlers", func(t *testing.T) {
   114  		multi := NewMultiHandler()
   115  		logger := New(multi)
   116  
   117  		logger.Info("nothing")
   118  
   119  		err := multi.Handle(context.Background(), NewRecord(time.Now(), LevelInfo, "test", 0))
   120  		if err != nil {
   121  			t.Errorf("Handle with no sub-handlers should return nil, but got: %v", err)
   122  		}
   123  	})
   124  }
   125  
   126  // Test that NewMultiHandler copies the input slice and is insulated from future modification.
   127  func TestNewMultiHandlerCopy(t *testing.T) {
   128  	var buf1 bytes.Buffer
   129  	h1 := NewTextHandler(&buf1, nil)
   130  	slice := []Handler{h1}
   131  	multi := NewMultiHandler(slice...)
   132  	slice[0] = nil
   133  
   134  	err := multi.Handle(context.Background(), NewRecord(time.Now(), LevelInfo, "test message", 0))
   135  	if err != nil {
   136  		t.Errorf("Expected nil error, but got: %v", err)
   137  	}
   138  	checkLogOutput(t, buf1.String(), "time="+textTimeRE+` level=INFO msg="test message"`)
   139  }
   140  

View as plain text