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