github.com/twelsh-aw/go/src@v0.0.0-20230516233729-a56fe86a7c81/log/slog/logger_test.go (about)

     1  // Copyright 2022 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  	"internal/race"
    11  	"internal/testenv"
    12  	"io"
    13  	"log"
    14  	loginternal "log/internal"
    15  	"path/filepath"
    16  	"regexp"
    17  	"runtime"
    18  	"slices"
    19  	"strings"
    20  	"sync"
    21  	"testing"
    22  	"time"
    23  )
    24  
    25  const timeRE = `\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}(Z|[+-]\d{2}:\d{2})`
    26  
    27  func TestLogTextHandler(t *testing.T) {
    28  	var buf bytes.Buffer
    29  
    30  	l := New(NewTextHandler(&buf, nil))
    31  
    32  	check := func(want string) {
    33  		t.Helper()
    34  		if want != "" {
    35  			want = "time=" + timeRE + " " + want
    36  		}
    37  		checkLogOutput(t, buf.String(), want)
    38  		buf.Reset()
    39  	}
    40  
    41  	l.Info("msg", "a", 1, "b", 2)
    42  	check(`level=INFO msg=msg a=1 b=2`)
    43  
    44  	// By default, debug messages are not printed.
    45  	l.Debug("bg", Int("a", 1), "b", 2)
    46  	check("")
    47  
    48  	l.Warn("w", Duration("dur", 3*time.Second))
    49  	check(`level=WARN msg=w dur=3s`)
    50  
    51  	l.Error("bad", "a", 1)
    52  	check(`level=ERROR msg=bad a=1`)
    53  
    54  	l.Log(nil, LevelWarn+1, "w", Int("a", 1), String("b", "two"))
    55  	check(`level=WARN\+1 msg=w a=1 b=two`)
    56  
    57  	l.LogAttrs(nil, LevelInfo+1, "a b c", Int("a", 1), String("b", "two"))
    58  	check(`level=INFO\+1 msg="a b c" a=1 b=two`)
    59  
    60  	l.Info("info", "a", []Attr{Int("i", 1)})
    61  	check(`level=INFO msg=info a.i=1`)
    62  
    63  	l.Info("info", "a", GroupValue(Int("i", 1)))
    64  	check(`level=INFO msg=info a.i=1`)
    65  }
    66  
    67  func TestConnections(t *testing.T) {
    68  	var logbuf, slogbuf bytes.Buffer
    69  
    70  	// Revert any changes to the default logger. This is important because other
    71  	// tests might change the default logger using SetDefault. Also ensure we
    72  	// restore the default logger at the end of the test.
    73  	currentLogger := Default()
    74  	SetDefault(New(newDefaultHandler(loginternal.DefaultOutput)))
    75  	t.Cleanup(func() {
    76  		SetDefault(currentLogger)
    77  	})
    78  
    79  	// The default slog.Logger's handler uses the log package's default output.
    80  	log.SetOutput(&logbuf)
    81  	log.SetFlags(log.Lshortfile &^ log.LstdFlags)
    82  	Info("msg", "a", 1)
    83  	checkLogOutput(t, logbuf.String(), `logger_test.go:\d+: INFO msg a=1`)
    84  	logbuf.Reset()
    85  	Warn("msg", "b", 2)
    86  	checkLogOutput(t, logbuf.String(), `logger_test.go:\d+: WARN msg b=2`)
    87  	logbuf.Reset()
    88  	Error("msg", "err", io.EOF, "c", 3)
    89  	checkLogOutput(t, logbuf.String(), `logger_test.go:\d+: ERROR msg err=EOF c=3`)
    90  
    91  	// Levels below Info are not printed.
    92  	logbuf.Reset()
    93  	Debug("msg", "c", 3)
    94  	checkLogOutput(t, logbuf.String(), "")
    95  
    96  	t.Run("wrap default handler", func(t *testing.T) {
    97  		// It should be possible to wrap the default handler and get the right output.
    98  		// This works because the default handler uses the pc in the Record
    99  		// to get the source line, rather than a call depth.
   100  		logger := New(wrappingHandler{Default().Handler()})
   101  		logger.Info("msg", "d", 4)
   102  		checkLogOutput(t, logbuf.String(), `logger_test.go:\d+: INFO msg d=4`)
   103  	})
   104  
   105  	// Once slog.SetDefault is called, the direction is reversed: the default
   106  	// log.Logger's output goes through the handler.
   107  	SetDefault(New(NewTextHandler(&slogbuf, &HandlerOptions{AddSource: true})))
   108  	log.Print("msg2")
   109  	checkLogOutput(t, slogbuf.String(), "time="+timeRE+` level=INFO source=.*logger_test.go:\d{3} msg=msg2`)
   110  
   111  	// The default log.Logger always outputs at Info level.
   112  	slogbuf.Reset()
   113  	SetDefault(New(NewTextHandler(&slogbuf, &HandlerOptions{Level: LevelWarn})))
   114  	log.Print("should not appear")
   115  	if got := slogbuf.String(); got != "" {
   116  		t.Errorf("got %q, want empty", got)
   117  	}
   118  
   119  	// Setting log's output again breaks the connection.
   120  	logbuf.Reset()
   121  	slogbuf.Reset()
   122  	log.SetOutput(&logbuf)
   123  	log.SetFlags(log.Lshortfile &^ log.LstdFlags)
   124  	log.Print("msg3")
   125  	checkLogOutput(t, logbuf.String(), `logger_test.go:\d+: msg3`)
   126  	if got := slogbuf.String(); got != "" {
   127  		t.Errorf("got %q, want empty", got)
   128  	}
   129  }
   130  
   131  type wrappingHandler struct {
   132  	h Handler
   133  }
   134  
   135  func (h wrappingHandler) Enabled(ctx context.Context, level Level) bool {
   136  	return h.h.Enabled(ctx, level)
   137  }
   138  func (h wrappingHandler) WithGroup(name string) Handler              { return h.h.WithGroup(name) }
   139  func (h wrappingHandler) WithAttrs(as []Attr) Handler                { return h.h.WithAttrs(as) }
   140  func (h wrappingHandler) Handle(ctx context.Context, r Record) error { return h.h.Handle(ctx, r) }
   141  
   142  func TestAttrs(t *testing.T) {
   143  	check := func(got []Attr, want ...Attr) {
   144  		t.Helper()
   145  		if !attrsEqual(got, want) {
   146  			t.Errorf("got %v, want %v", got, want)
   147  		}
   148  	}
   149  
   150  	l1 := New(&captureHandler{}).With("a", 1)
   151  	l2 := New(l1.Handler()).With("b", 2)
   152  	l2.Info("m", "c", 3)
   153  	h := l2.Handler().(*captureHandler)
   154  	check(h.attrs, Int("a", 1), Int("b", 2))
   155  	check(attrsSlice(h.r), Int("c", 3))
   156  }
   157  
   158  func TestCallDepth(t *testing.T) {
   159  	h := &captureHandler{}
   160  	var startLine int
   161  
   162  	check := func(count int) {
   163  		t.Helper()
   164  		const wantFunc = "log/slog.TestCallDepth"
   165  		const wantFile = "logger_test.go"
   166  		wantLine := startLine + count*2
   167  		got := h.r.source()
   168  		gotFile := filepath.Base(got.File)
   169  		if got.Function != wantFunc || gotFile != wantFile || got.Line != wantLine {
   170  			t.Errorf("got (%s, %s, %d), want (%s, %s, %d)",
   171  				got.Function, gotFile, got.Line, wantFunc, wantFile, wantLine)
   172  		}
   173  	}
   174  
   175  	logger := New(h)
   176  	SetDefault(logger)
   177  
   178  	// Calls to check must be one line apart.
   179  	// Determine line where calls start.
   180  	f, _ := runtime.CallersFrames([]uintptr{callerPC(2)}).Next()
   181  	startLine = f.Line + 4
   182  	// Do not change the number of lines between here and the call to check(0).
   183  
   184  	logger.Log(nil, LevelInfo, "")
   185  	check(0)
   186  	logger.LogAttrs(nil, LevelInfo, "")
   187  	check(1)
   188  	logger.Debug("")
   189  	check(2)
   190  	logger.Info("")
   191  	check(3)
   192  	logger.Warn("")
   193  	check(4)
   194  	logger.Error("")
   195  	check(5)
   196  	Debug("")
   197  	check(6)
   198  	Info("")
   199  	check(7)
   200  	Warn("")
   201  	check(8)
   202  	Error("")
   203  	check(9)
   204  	Log(nil, LevelInfo, "")
   205  	check(10)
   206  	LogAttrs(nil, LevelInfo, "")
   207  	check(11)
   208  }
   209  
   210  func TestAlloc(t *testing.T) {
   211  	dl := New(discardHandler{})
   212  	defer func(d *Logger) { SetDefault(d) }(Default())
   213  	SetDefault(dl)
   214  
   215  	t.Run("Info", func(t *testing.T) {
   216  		wantAllocs(t, 0, func() { Info("hello") })
   217  	})
   218  	t.Run("Error", func(t *testing.T) {
   219  		wantAllocs(t, 0, func() { Error("hello") })
   220  	})
   221  	t.Run("logger.Info", func(t *testing.T) {
   222  		wantAllocs(t, 0, func() { dl.Info("hello") })
   223  	})
   224  	t.Run("logger.Log", func(t *testing.T) {
   225  		wantAllocs(t, 0, func() { dl.Log(nil, LevelDebug, "hello") })
   226  	})
   227  	t.Run("2 pairs", func(t *testing.T) {
   228  		s := "abc"
   229  		i := 2000
   230  		wantAllocs(t, 2, func() {
   231  			dl.Info("hello",
   232  				"n", i,
   233  				"s", s,
   234  			)
   235  		})
   236  	})
   237  	t.Run("2 pairs disabled inline", func(t *testing.T) {
   238  		l := New(discardHandler{disabled: true})
   239  		s := "abc"
   240  		i := 2000
   241  		wantAllocs(t, 2, func() {
   242  			l.Log(nil, LevelInfo, "hello",
   243  				"n", i,
   244  				"s", s,
   245  			)
   246  		})
   247  	})
   248  	t.Run("2 pairs disabled", func(t *testing.T) {
   249  		l := New(discardHandler{disabled: true})
   250  		s := "abc"
   251  		i := 2000
   252  		wantAllocs(t, 0, func() {
   253  			if l.Enabled(nil, LevelInfo) {
   254  				l.Log(nil, LevelInfo, "hello",
   255  					"n", i,
   256  					"s", s,
   257  				)
   258  			}
   259  		})
   260  	})
   261  	t.Run("9 kvs", func(t *testing.T) {
   262  		s := "abc"
   263  		i := 2000
   264  		d := time.Second
   265  		wantAllocs(t, 11, func() {
   266  			dl.Info("hello",
   267  				"n", i, "s", s, "d", d,
   268  				"n", i, "s", s, "d", d,
   269  				"n", i, "s", s, "d", d)
   270  		})
   271  	})
   272  	t.Run("pairs", func(t *testing.T) {
   273  		wantAllocs(t, 0, func() { dl.Info("", "error", io.EOF) })
   274  	})
   275  	t.Run("attrs1", func(t *testing.T) {
   276  		wantAllocs(t, 0, func() { dl.LogAttrs(nil, LevelInfo, "", Int("a", 1)) })
   277  		wantAllocs(t, 0, func() { dl.LogAttrs(nil, LevelInfo, "", Any("error", io.EOF)) })
   278  	})
   279  	t.Run("attrs3", func(t *testing.T) {
   280  		wantAllocs(t, 0, func() {
   281  			dl.LogAttrs(nil, LevelInfo, "hello", Int("a", 1), String("b", "two"), Duration("c", time.Second))
   282  		})
   283  	})
   284  	t.Run("attrs3 disabled", func(t *testing.T) {
   285  		logger := New(discardHandler{disabled: true})
   286  		wantAllocs(t, 0, func() {
   287  			logger.LogAttrs(nil, LevelInfo, "hello", Int("a", 1), String("b", "two"), Duration("c", time.Second))
   288  		})
   289  	})
   290  	t.Run("attrs6", func(t *testing.T) {
   291  		wantAllocs(t, 1, func() {
   292  			dl.LogAttrs(nil, LevelInfo, "hello",
   293  				Int("a", 1), String("b", "two"), Duration("c", time.Second),
   294  				Int("d", 1), String("e", "two"), Duration("f", time.Second))
   295  		})
   296  	})
   297  	t.Run("attrs9", func(t *testing.T) {
   298  		wantAllocs(t, 1, func() {
   299  			dl.LogAttrs(nil, LevelInfo, "hello",
   300  				Int("a", 1), String("b", "two"), Duration("c", time.Second),
   301  				Int("d", 1), String("e", "two"), Duration("f", time.Second),
   302  				Int("d", 1), String("e", "two"), Duration("f", time.Second))
   303  		})
   304  	})
   305  }
   306  
   307  func TestSetAttrs(t *testing.T) {
   308  	for _, test := range []struct {
   309  		args []any
   310  		want []Attr
   311  	}{
   312  		{nil, nil},
   313  		{[]any{"a", 1}, []Attr{Int("a", 1)}},
   314  		{[]any{"a", 1, "b", "two"}, []Attr{Int("a", 1), String("b", "two")}},
   315  		{[]any{"a"}, []Attr{String(badKey, "a")}},
   316  		{[]any{"a", 1, "b"}, []Attr{Int("a", 1), String(badKey, "b")}},
   317  		{[]any{"a", 1, 2, 3}, []Attr{Int("a", 1), Int(badKey, 2), Int(badKey, 3)}},
   318  	} {
   319  		r := NewRecord(time.Time{}, 0, "", 0)
   320  		r.Add(test.args...)
   321  		got := attrsSlice(r)
   322  		if !attrsEqual(got, test.want) {
   323  			t.Errorf("%v:\ngot  %v\nwant %v", test.args, got, test.want)
   324  		}
   325  	}
   326  }
   327  
   328  func TestSetDefault(t *testing.T) {
   329  	// Verify that setting the default to itself does not result in deadlock.
   330  	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
   331  	defer cancel()
   332  	defer func(w io.Writer) { log.SetOutput(w) }(log.Writer())
   333  	log.SetOutput(io.Discard)
   334  	go func() {
   335  		Info("A")
   336  		SetDefault(Default())
   337  		Info("B")
   338  		cancel()
   339  	}()
   340  	<-ctx.Done()
   341  	if err := ctx.Err(); err != context.Canceled {
   342  		t.Errorf("wanted canceled, got %v", err)
   343  	}
   344  }
   345  
   346  func TestLoggerError(t *testing.T) {
   347  	var buf bytes.Buffer
   348  
   349  	removeTime := func(_ []string, a Attr) Attr {
   350  		if a.Key == TimeKey {
   351  			return Attr{}
   352  		}
   353  		return a
   354  	}
   355  	l := New(NewTextHandler(&buf, &HandlerOptions{ReplaceAttr: removeTime}))
   356  	l.Error("msg", "err", io.EOF, "a", 1)
   357  	checkLogOutput(t, buf.String(), `level=ERROR msg=msg err=EOF a=1`)
   358  	buf.Reset()
   359  	l.Error("msg", "err", io.EOF, "a")
   360  	checkLogOutput(t, buf.String(), `level=ERROR msg=msg err=EOF !BADKEY=a`)
   361  }
   362  
   363  func TestNewLogLogger(t *testing.T) {
   364  	var buf bytes.Buffer
   365  	h := NewTextHandler(&buf, nil)
   366  	ll := NewLogLogger(h, LevelWarn)
   367  	ll.Print("hello")
   368  	checkLogOutput(t, buf.String(), "time="+timeRE+` level=WARN msg=hello`)
   369  }
   370  
   371  func checkLogOutput(t *testing.T, got, wantRegexp string) {
   372  	t.Helper()
   373  	got = clean(got)
   374  	wantRegexp = "^" + wantRegexp + "$"
   375  	matched, err := regexp.MatchString(wantRegexp, got)
   376  	if err != nil {
   377  		t.Fatal(err)
   378  	}
   379  	if !matched {
   380  		t.Errorf("\ngot  %s\nwant %s", got, wantRegexp)
   381  	}
   382  }
   383  
   384  // clean prepares log output for comparison.
   385  func clean(s string) string {
   386  	if len(s) > 0 && s[len(s)-1] == '\n' {
   387  		s = s[:len(s)-1]
   388  	}
   389  	return strings.ReplaceAll(s, "\n", "~")
   390  }
   391  
   392  type captureHandler struct {
   393  	mu     sync.Mutex
   394  	r      Record
   395  	attrs  []Attr
   396  	groups []string
   397  }
   398  
   399  func (h *captureHandler) Handle(ctx context.Context, r Record) error {
   400  	h.mu.Lock()
   401  	defer h.mu.Unlock()
   402  	h.r = r
   403  	return nil
   404  }
   405  
   406  func (*captureHandler) Enabled(context.Context, Level) bool { return true }
   407  
   408  func (c *captureHandler) WithAttrs(as []Attr) Handler {
   409  	c.mu.Lock()
   410  	defer c.mu.Unlock()
   411  	var c2 captureHandler
   412  	c2.r = c.r
   413  	c2.groups = c.groups
   414  	c2.attrs = concat(c.attrs, as)
   415  	return &c2
   416  }
   417  
   418  func (c *captureHandler) WithGroup(name string) Handler {
   419  	c.mu.Lock()
   420  	defer c.mu.Unlock()
   421  	var c2 captureHandler
   422  	c2.r = c.r
   423  	c2.attrs = c.attrs
   424  	c2.groups = append(slices.Clip(c.groups), name)
   425  	return &c2
   426  }
   427  
   428  type discardHandler struct {
   429  	disabled bool
   430  	attrs    []Attr
   431  }
   432  
   433  func (d discardHandler) Enabled(context.Context, Level) bool { return !d.disabled }
   434  func (discardHandler) Handle(context.Context, Record) error  { return nil }
   435  func (d discardHandler) WithAttrs(as []Attr) Handler {
   436  	d.attrs = concat(d.attrs, as)
   437  	return d
   438  }
   439  func (h discardHandler) WithGroup(name string) Handler {
   440  	return h
   441  }
   442  
   443  // concat returns a new slice with the elements of s1 followed
   444  // by those of s2. The slice has no additional capacity.
   445  func concat[T any](s1, s2 []T) []T {
   446  	s := make([]T, len(s1)+len(s2))
   447  	copy(s, s1)
   448  	copy(s[len(s1):], s2)
   449  	return s
   450  }
   451  
   452  // This is a simple benchmark. See the benchmarks subdirectory for more extensive ones.
   453  func BenchmarkNopLog(b *testing.B) {
   454  	ctx := context.Background()
   455  	l := New(&captureHandler{})
   456  	b.Run("no attrs", func(b *testing.B) {
   457  		b.ReportAllocs()
   458  		for i := 0; i < b.N; i++ {
   459  			l.LogAttrs(nil, LevelInfo, "msg")
   460  		}
   461  	})
   462  	b.Run("attrs", func(b *testing.B) {
   463  		b.ReportAllocs()
   464  		for i := 0; i < b.N; i++ {
   465  			l.LogAttrs(nil, LevelInfo, "msg", Int("a", 1), String("b", "two"), Bool("c", true))
   466  		}
   467  	})
   468  	b.Run("attrs-parallel", func(b *testing.B) {
   469  		b.ReportAllocs()
   470  		b.RunParallel(func(pb *testing.PB) {
   471  			for pb.Next() {
   472  				l.LogAttrs(nil, LevelInfo, "msg", Int("a", 1), String("b", "two"), Bool("c", true))
   473  			}
   474  		})
   475  	})
   476  	b.Run("keys-values", func(b *testing.B) {
   477  		b.ReportAllocs()
   478  		for i := 0; i < b.N; i++ {
   479  			l.Log(nil, LevelInfo, "msg", "a", 1, "b", "two", "c", true)
   480  		}
   481  	})
   482  	b.Run("WithContext", func(b *testing.B) {
   483  		b.ReportAllocs()
   484  		for i := 0; i < b.N; i++ {
   485  			l.LogAttrs(ctx, LevelInfo, "msg2", Int("a", 1), String("b", "two"), Bool("c", true))
   486  		}
   487  	})
   488  	b.Run("WithContext-parallel", func(b *testing.B) {
   489  		b.ReportAllocs()
   490  		b.RunParallel(func(pb *testing.PB) {
   491  			for pb.Next() {
   492  				l.LogAttrs(ctx, LevelInfo, "msg", Int("a", 1), String("b", "two"), Bool("c", true))
   493  			}
   494  		})
   495  	})
   496  }
   497  
   498  // callerPC returns the program counter at the given stack depth.
   499  func callerPC(depth int) uintptr {
   500  	var pcs [1]uintptr
   501  	runtime.Callers(depth, pcs[:])
   502  	return pcs[0]
   503  }
   504  
   505  func wantAllocs(t *testing.T, want int, f func()) {
   506  	if race.Enabled {
   507  		t.Skip("skipping test in race mode")
   508  	}
   509  	testenv.SkipIfOptimizationOff(t)
   510  	t.Helper()
   511  	got := int(testing.AllocsPerRun(5, f))
   512  	if got != want {
   513  		t.Errorf("got %d allocs, want %d", got, want)
   514  	}
   515  }