...

Source file src/log/slog/handler_test.go

Documentation: log/slog

     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  	"encoding/json"
    11  	"fmt"
    12  	"io"
    13  	"log/slog/internal/buffer"
    14  	"os"
    15  	"path/filepath"
    16  	"slices"
    17  	"strconv"
    18  	"strings"
    19  	"sync"
    20  	"testing"
    21  	"time"
    22  )
    23  
    24  func TestDefaultHandle(t *testing.T) {
    25  	ctx := context.Background()
    26  	preAttrs := []Attr{Int("pre", 0)}
    27  	attrs := []Attr{Int("a", 1), String("b", "two")}
    28  	for _, test := range []struct {
    29  		name  string
    30  		with  func(Handler) Handler
    31  		attrs []Attr
    32  		want  string
    33  	}{
    34  		{
    35  			name: "no attrs",
    36  			want: "INFO message",
    37  		},
    38  		{
    39  			name:  "attrs",
    40  			attrs: attrs,
    41  			want:  "INFO message a=1 b=two",
    42  		},
    43  		{
    44  			name:  "preformatted",
    45  			with:  func(h Handler) Handler { return h.WithAttrs(preAttrs) },
    46  			attrs: attrs,
    47  			want:  "INFO message pre=0 a=1 b=two",
    48  		},
    49  		{
    50  			name: "groups",
    51  			attrs: []Attr{
    52  				Int("a", 1),
    53  				Group("g",
    54  					Int("b", 2),
    55  					Group("h", Int("c", 3)),
    56  					Int("d", 4)),
    57  				Int("e", 5),
    58  			},
    59  			want: "INFO message a=1 g.b=2 g.h.c=3 g.d=4 e=5",
    60  		},
    61  		{
    62  			name:  "group",
    63  			with:  func(h Handler) Handler { return h.WithAttrs(preAttrs).WithGroup("s") },
    64  			attrs: attrs,
    65  			want:  "INFO message pre=0 s.a=1 s.b=two",
    66  		},
    67  		{
    68  			name: "preformatted groups",
    69  			with: func(h Handler) Handler {
    70  				return h.WithAttrs([]Attr{Int("p1", 1)}).
    71  					WithGroup("s1").
    72  					WithAttrs([]Attr{Int("p2", 2)}).
    73  					WithGroup("s2")
    74  			},
    75  			attrs: attrs,
    76  			want:  "INFO message p1=1 s1.p2=2 s1.s2.a=1 s1.s2.b=two",
    77  		},
    78  		{
    79  			name: "two with-groups",
    80  			with: func(h Handler) Handler {
    81  				return h.WithAttrs([]Attr{Int("p1", 1)}).
    82  					WithGroup("s1").
    83  					WithGroup("s2")
    84  			},
    85  			attrs: attrs,
    86  			want:  "INFO message p1=1 s1.s2.a=1 s1.s2.b=two",
    87  		},
    88  	} {
    89  		t.Run(test.name, func(t *testing.T) {
    90  			var got string
    91  			var h Handler = newDefaultHandler(func(_ uintptr, b []byte) error {
    92  				got = string(b)
    93  				return nil
    94  			})
    95  			if test.with != nil {
    96  				h = test.with(h)
    97  			}
    98  			r := NewRecord(time.Time{}, LevelInfo, "message", 0)
    99  			r.AddAttrs(test.attrs...)
   100  			if err := h.Handle(ctx, r); err != nil {
   101  				t.Fatal(err)
   102  			}
   103  			if got != test.want {
   104  				t.Errorf("\ngot  %s\nwant %s", got, test.want)
   105  			}
   106  		})
   107  	}
   108  }
   109  
   110  func TestConcurrentWrites(t *testing.T) {
   111  	ctx := context.Background()
   112  	count := 1000
   113  	for _, handlerType := range []string{"text", "json"} {
   114  		t.Run(handlerType, func(t *testing.T) {
   115  			var buf bytes.Buffer
   116  			var h Handler
   117  			switch handlerType {
   118  			case "text":
   119  				h = NewTextHandler(&buf, nil)
   120  			case "json":
   121  				h = NewJSONHandler(&buf, nil)
   122  			default:
   123  				t.Fatalf("unexpected handlerType %q", handlerType)
   124  			}
   125  			sub1 := h.WithAttrs([]Attr{Bool("sub1", true)})
   126  			sub2 := h.WithAttrs([]Attr{Bool("sub2", true)})
   127  			var wg sync.WaitGroup
   128  			for i := 0; i < count; i++ {
   129  				sub1Record := NewRecord(time.Time{}, LevelInfo, "hello from sub1", 0)
   130  				sub1Record.AddAttrs(Int("i", i))
   131  				sub2Record := NewRecord(time.Time{}, LevelInfo, "hello from sub2", 0)
   132  				sub2Record.AddAttrs(Int("i", i))
   133  				wg.Add(1)
   134  				go func() {
   135  					defer wg.Done()
   136  					if err := sub1.Handle(ctx, sub1Record); err != nil {
   137  						t.Error(err)
   138  					}
   139  					if err := sub2.Handle(ctx, sub2Record); err != nil {
   140  						t.Error(err)
   141  					}
   142  				}()
   143  			}
   144  			wg.Wait()
   145  			for i := 1; i <= 2; i++ {
   146  				want := "hello from sub" + strconv.Itoa(i)
   147  				n := strings.Count(buf.String(), want)
   148  				if n != count {
   149  					t.Fatalf("want %d occurrences of %q, got %d", count, want, n)
   150  				}
   151  			}
   152  		})
   153  	}
   154  }
   155  
   156  // Verify the common parts of TextHandler and JSONHandler.
   157  func TestJSONAndTextHandlers(t *testing.T) {
   158  	// remove all Attrs
   159  	removeAll := func(_ []string, a Attr) Attr { return Attr{} }
   160  
   161  	attrs := []Attr{String("a", "one"), Int("b", 2), Any("", nil)}
   162  	preAttrs := []Attr{Int("pre", 3), String("x", "y")}
   163  
   164  	for _, test := range []struct {
   165  		name      string
   166  		replace   func([]string, Attr) Attr
   167  		addSource bool
   168  		with      func(Handler) Handler
   169  		preAttrs  []Attr
   170  		attrs     []Attr
   171  		wantText  string
   172  		wantJSON  string
   173  	}{
   174  		{
   175  			name:     "basic",
   176  			attrs:    attrs,
   177  			wantText: "time=2000-01-02T03:04:05.000Z level=INFO msg=message a=one b=2",
   178  			wantJSON: `{"time":"2000-01-02T03:04:05Z","level":"INFO","msg":"message","a":"one","b":2}`,
   179  		},
   180  		{
   181  			name:     "empty key",
   182  			attrs:    append(slices.Clip(attrs), Any("", "v")),
   183  			wantText: `time=2000-01-02T03:04:05.000Z level=INFO msg=message a=one b=2 ""=v`,
   184  			wantJSON: `{"time":"2000-01-02T03:04:05Z","level":"INFO","msg":"message","a":"one","b":2,"":"v"}`,
   185  		},
   186  		{
   187  			name:     "cap keys",
   188  			replace:  upperCaseKey,
   189  			attrs:    attrs,
   190  			wantText: "TIME=2000-01-02T03:04:05.000Z LEVEL=INFO MSG=message A=one B=2",
   191  			wantJSON: `{"TIME":"2000-01-02T03:04:05Z","LEVEL":"INFO","MSG":"message","A":"one","B":2}`,
   192  		},
   193  		{
   194  			name:     "remove all",
   195  			replace:  removeAll,
   196  			attrs:    attrs,
   197  			wantText: "",
   198  			wantJSON: `{}`,
   199  		},
   200  		{
   201  			name:     "preformatted",
   202  			with:     func(h Handler) Handler { return h.WithAttrs(preAttrs) },
   203  			preAttrs: preAttrs,
   204  			attrs:    attrs,
   205  			wantText: "time=2000-01-02T03:04:05.000Z level=INFO msg=message pre=3 x=y a=one b=2",
   206  			wantJSON: `{"time":"2000-01-02T03:04:05Z","level":"INFO","msg":"message","pre":3,"x":"y","a":"one","b":2}`,
   207  		},
   208  		{
   209  			name:     "preformatted cap keys",
   210  			replace:  upperCaseKey,
   211  			with:     func(h Handler) Handler { return h.WithAttrs(preAttrs) },
   212  			preAttrs: preAttrs,
   213  			attrs:    attrs,
   214  			wantText: "TIME=2000-01-02T03:04:05.000Z LEVEL=INFO MSG=message PRE=3 X=y A=one B=2",
   215  			wantJSON: `{"TIME":"2000-01-02T03:04:05Z","LEVEL":"INFO","MSG":"message","PRE":3,"X":"y","A":"one","B":2}`,
   216  		},
   217  		{
   218  			name:     "preformatted remove all",
   219  			replace:  removeAll,
   220  			with:     func(h Handler) Handler { return h.WithAttrs(preAttrs) },
   221  			preAttrs: preAttrs,
   222  			attrs:    attrs,
   223  			wantText: "",
   224  			wantJSON: "{}",
   225  		},
   226  		{
   227  			name:     "remove built-in",
   228  			replace:  removeKeys(TimeKey, LevelKey, MessageKey),
   229  			attrs:    attrs,
   230  			wantText: "a=one b=2",
   231  			wantJSON: `{"a":"one","b":2}`,
   232  		},
   233  		{
   234  			name:     "preformatted remove built-in",
   235  			replace:  removeKeys(TimeKey, LevelKey, MessageKey),
   236  			with:     func(h Handler) Handler { return h.WithAttrs(preAttrs) },
   237  			attrs:    attrs,
   238  			wantText: "pre=3 x=y a=one b=2",
   239  			wantJSON: `{"pre":3,"x":"y","a":"one","b":2}`,
   240  		},
   241  		{
   242  			name:    "groups",
   243  			replace: removeKeys(TimeKey, LevelKey), // to simplify the result
   244  			attrs: []Attr{
   245  				Int("a", 1),
   246  				Group("g",
   247  					Int("b", 2),
   248  					Group("h", Int("c", 3)),
   249  					Int("d", 4)),
   250  				Int("e", 5),
   251  			},
   252  			wantText: "msg=message a=1 g.b=2 g.h.c=3 g.d=4 e=5",
   253  			wantJSON: `{"msg":"message","a":1,"g":{"b":2,"h":{"c":3},"d":4},"e":5}`,
   254  		},
   255  		{
   256  			name:     "empty group",
   257  			replace:  removeKeys(TimeKey, LevelKey),
   258  			attrs:    []Attr{Group("g"), Group("h", Int("a", 1))},
   259  			wantText: "msg=message h.a=1",
   260  			wantJSON: `{"msg":"message","h":{"a":1}}`,
   261  		},
   262  		{
   263  			name:    "nested empty group",
   264  			replace: removeKeys(TimeKey, LevelKey),
   265  			attrs: []Attr{
   266  				Group("g",
   267  					Group("h",
   268  						Group("i"), Group("j"))),
   269  			},
   270  			wantText: `msg=message`,
   271  			wantJSON: `{"msg":"message"}`,
   272  		},
   273  		{
   274  			name:    "nested non-empty group",
   275  			replace: removeKeys(TimeKey, LevelKey),
   276  			attrs: []Attr{
   277  				Group("g",
   278  					Group("h",
   279  						Group("i"), Group("j", Int("a", 1)))),
   280  			},
   281  			wantText: `msg=message g.h.j.a=1`,
   282  			wantJSON: `{"msg":"message","g":{"h":{"j":{"a":1}}}}`,
   283  		},
   284  		{
   285  			name:    "escapes",
   286  			replace: removeKeys(TimeKey, LevelKey),
   287  			attrs: []Attr{
   288  				String("a b", "x\t\n\000y"),
   289  				Group(" b.c=\"\\x2E\t",
   290  					String("d=e", "f.g\""),
   291  					Int("m.d", 1)), // dot is not escaped
   292  			},
   293  			wantText: `msg=message "a b"="x\t\n\x00y" " b.c=\"\\x2E\t.d=e"="f.g\"" " b.c=\"\\x2E\t.m.d"=1`,
   294  			wantJSON: `{"msg":"message","a b":"x\t\n\u0000y"," b.c=\"\\x2E\t":{"d=e":"f.g\"","m.d":1}}`,
   295  		},
   296  		{
   297  			name:    "LogValuer",
   298  			replace: removeKeys(TimeKey, LevelKey),
   299  			attrs: []Attr{
   300  				Int("a", 1),
   301  				Any("name", logValueName{"Ren", "Hoek"}),
   302  				Int("b", 2),
   303  			},
   304  			wantText: "msg=message a=1 name.first=Ren name.last=Hoek b=2",
   305  			wantJSON: `{"msg":"message","a":1,"name":{"first":"Ren","last":"Hoek"},"b":2}`,
   306  		},
   307  		{
   308  			// Test resolution when there is no ReplaceAttr function.
   309  			name: "resolve",
   310  			attrs: []Attr{
   311  				Any("", &replace{Value{}}), // should be elided
   312  				Any("name", logValueName{"Ren", "Hoek"}),
   313  			},
   314  			wantText: "time=2000-01-02T03:04:05.000Z level=INFO msg=message name.first=Ren name.last=Hoek",
   315  			wantJSON: `{"time":"2000-01-02T03:04:05Z","level":"INFO","msg":"message","name":{"first":"Ren","last":"Hoek"}}`,
   316  		},
   317  		{
   318  			name:     "with-group",
   319  			replace:  removeKeys(TimeKey, LevelKey),
   320  			with:     func(h Handler) Handler { return h.WithAttrs(preAttrs).WithGroup("s") },
   321  			attrs:    attrs,
   322  			wantText: "msg=message pre=3 x=y s.a=one s.b=2",
   323  			wantJSON: `{"msg":"message","pre":3,"x":"y","s":{"a":"one","b":2}}`,
   324  		},
   325  		{
   326  			name:    "preformatted with-groups",
   327  			replace: removeKeys(TimeKey, LevelKey),
   328  			with: func(h Handler) Handler {
   329  				return h.WithAttrs([]Attr{Int("p1", 1)}).
   330  					WithGroup("s1").
   331  					WithAttrs([]Attr{Int("p2", 2)}).
   332  					WithGroup("s2").
   333  					WithAttrs([]Attr{Int("p3", 3)})
   334  			},
   335  			attrs:    attrs,
   336  			wantText: "msg=message p1=1 s1.p2=2 s1.s2.p3=3 s1.s2.a=one s1.s2.b=2",
   337  			wantJSON: `{"msg":"message","p1":1,"s1":{"p2":2,"s2":{"p3":3,"a":"one","b":2}}}`,
   338  		},
   339  		{
   340  			name:    "two with-groups",
   341  			replace: removeKeys(TimeKey, LevelKey),
   342  			with: func(h Handler) Handler {
   343  				return h.WithAttrs([]Attr{Int("p1", 1)}).
   344  					WithGroup("s1").
   345  					WithGroup("s2")
   346  			},
   347  			attrs:    attrs,
   348  			wantText: "msg=message p1=1 s1.s2.a=one s1.s2.b=2",
   349  			wantJSON: `{"msg":"message","p1":1,"s1":{"s2":{"a":"one","b":2}}}`,
   350  		},
   351  		{
   352  			name:    "empty with-groups",
   353  			replace: removeKeys(TimeKey, LevelKey),
   354  			with: func(h Handler) Handler {
   355  				return h.WithGroup("x").WithGroup("y")
   356  			},
   357  			wantText: "msg=message",
   358  			wantJSON: `{"msg":"message"}`,
   359  		},
   360  		{
   361  			name:    "empty with-groups, no non-empty attrs",
   362  			replace: removeKeys(TimeKey, LevelKey),
   363  			with: func(h Handler) Handler {
   364  				return h.WithGroup("x").WithAttrs([]Attr{Group("g")}).WithGroup("y")
   365  			},
   366  			wantText: "msg=message",
   367  			wantJSON: `{"msg":"message"}`,
   368  		},
   369  		{
   370  			name:    "one empty with-group",
   371  			replace: removeKeys(TimeKey, LevelKey),
   372  			with: func(h Handler) Handler {
   373  				return h.WithGroup("x").WithAttrs([]Attr{Int("a", 1)}).WithGroup("y")
   374  			},
   375  			attrs:    []Attr{Group("g", Group("h"))},
   376  			wantText: "msg=message x.a=1",
   377  			wantJSON: `{"msg":"message","x":{"a":1}}`,
   378  		},
   379  		{
   380  			name:     "GroupValue as Attr value",
   381  			replace:  removeKeys(TimeKey, LevelKey),
   382  			attrs:    []Attr{{"v", AnyValue(IntValue(3))}},
   383  			wantText: "msg=message v=3",
   384  			wantJSON: `{"msg":"message","v":3}`,
   385  		},
   386  		{
   387  			name:     "byte slice",
   388  			replace:  removeKeys(TimeKey, LevelKey),
   389  			attrs:    []Attr{Any("bs", []byte{1, 2, 3, 4})},
   390  			wantText: `msg=message bs="\x01\x02\x03\x04"`,
   391  			wantJSON: `{"msg":"message","bs":"AQIDBA=="}`,
   392  		},
   393  		{
   394  			name:     "json.RawMessage",
   395  			replace:  removeKeys(TimeKey, LevelKey),
   396  			attrs:    []Attr{Any("bs", json.RawMessage([]byte("1234")))},
   397  			wantText: `msg=message bs="1234"`,
   398  			wantJSON: `{"msg":"message","bs":1234}`,
   399  		},
   400  		{
   401  			name:    "inline group",
   402  			replace: removeKeys(TimeKey, LevelKey),
   403  			attrs: []Attr{
   404  				Int("a", 1),
   405  				Group("", Int("b", 2), Int("c", 3)),
   406  				Int("d", 4),
   407  			},
   408  			wantText: `msg=message a=1 b=2 c=3 d=4`,
   409  			wantJSON: `{"msg":"message","a":1,"b":2,"c":3,"d":4}`,
   410  		},
   411  		{
   412  			name: "Source",
   413  			replace: func(gs []string, a Attr) Attr {
   414  				if a.Key == SourceKey {
   415  					s := a.Value.Any().(*Source)
   416  					s.File = filepath.Base(s.File)
   417  					return Any(a.Key, s)
   418  				}
   419  				return removeKeys(TimeKey, LevelKey)(gs, a)
   420  			},
   421  			addSource: true,
   422  			wantText:  `source=handler_test.go:$LINE msg=message`,
   423  			wantJSON:  `{"source":{"function":"log/slog.TestJSONAndTextHandlers","file":"handler_test.go","line":$LINE},"msg":"message"}`,
   424  		},
   425  		{
   426  			name: "replace built-in with group",
   427  			replace: func(_ []string, a Attr) Attr {
   428  				if a.Key == TimeKey {
   429  					return Group(TimeKey, "mins", 3, "secs", 2)
   430  				}
   431  				if a.Key == LevelKey {
   432  					return Attr{}
   433  				}
   434  				return a
   435  			},
   436  			wantText: `time.mins=3 time.secs=2 msg=message`,
   437  			wantJSON: `{"time":{"mins":3,"secs":2},"msg":"message"}`,
   438  		},
   439  		{
   440  			name:     "replace empty",
   441  			replace:  func([]string, Attr) Attr { return Attr{} },
   442  			attrs:    []Attr{Group("g", Int("a", 1))},
   443  			wantText: "",
   444  			wantJSON: `{}`,
   445  		},
   446  		{
   447  			name: "replace empty 1",
   448  			with: func(h Handler) Handler {
   449  				return h.WithGroup("g").WithAttrs([]Attr{Int("a", 1)})
   450  			},
   451  			replace:  func([]string, Attr) Attr { return Attr{} },
   452  			attrs:    []Attr{Group("h", Int("b", 2))},
   453  			wantText: "",
   454  			wantJSON: `{}`,
   455  		},
   456  		{
   457  			name: "replace empty 2",
   458  			with: func(h Handler) Handler {
   459  				return h.WithGroup("g").WithAttrs([]Attr{Int("a", 1)}).WithGroup("h").WithAttrs([]Attr{Int("b", 2)})
   460  			},
   461  			replace:  func([]string, Attr) Attr { return Attr{} },
   462  			attrs:    []Attr{Group("i", Int("c", 3))},
   463  			wantText: "",
   464  			wantJSON: `{}`,
   465  		},
   466  		{
   467  			name:     "replace empty 3",
   468  			with:     func(h Handler) Handler { return h.WithGroup("g") },
   469  			replace:  func([]string, Attr) Attr { return Attr{} },
   470  			attrs:    []Attr{Int("a", 1)},
   471  			wantText: "",
   472  			wantJSON: `{}`,
   473  		},
   474  		{
   475  			name: "replace empty inline",
   476  			with: func(h Handler) Handler {
   477  				return h.WithGroup("g").WithAttrs([]Attr{Int("a", 1)}).WithGroup("h").WithAttrs([]Attr{Int("b", 2)})
   478  			},
   479  			replace:  func([]string, Attr) Attr { return Attr{} },
   480  			attrs:    []Attr{Group("", Int("c", 3))},
   481  			wantText: "",
   482  			wantJSON: `{}`,
   483  		},
   484  		{
   485  			name: "replace partial empty attrs 1",
   486  			with: func(h Handler) Handler {
   487  				return h.WithGroup("g").WithAttrs([]Attr{Int("a", 1)}).WithGroup("h").WithAttrs([]Attr{Int("b", 2)})
   488  			},
   489  			replace: func(groups []string, attr Attr) Attr {
   490  				return removeKeys(TimeKey, LevelKey, MessageKey, "a")(groups, attr)
   491  			},
   492  			attrs:    []Attr{Group("i", Int("c", 3))},
   493  			wantText: "g.h.b=2 g.h.i.c=3",
   494  			wantJSON: `{"g":{"h":{"b":2,"i":{"c":3}}}}`,
   495  		},
   496  		{
   497  			name: "replace partial empty attrs 2",
   498  			with: func(h Handler) Handler {
   499  				return h.WithGroup("g").WithAttrs([]Attr{Int("a", 1)}).WithAttrs([]Attr{Int("n", 4)}).WithGroup("h").WithAttrs([]Attr{Int("b", 2)})
   500  			},
   501  			replace: func(groups []string, attr Attr) Attr {
   502  				return removeKeys(TimeKey, LevelKey, MessageKey, "a", "b")(groups, attr)
   503  			},
   504  			attrs:    []Attr{Group("i", Int("c", 3))},
   505  			wantText: "g.n=4 g.h.i.c=3",
   506  			wantJSON: `{"g":{"n":4,"h":{"i":{"c":3}}}}`,
   507  		},
   508  		{
   509  			name: "replace partial empty attrs 3",
   510  			with: func(h Handler) Handler {
   511  				return h.WithGroup("g").WithAttrs([]Attr{Int("x", 0)}).WithAttrs([]Attr{Int("a", 1)}).WithAttrs([]Attr{Int("n", 4)}).WithGroup("h").WithAttrs([]Attr{Int("b", 2)})
   512  			},
   513  			replace: func(groups []string, attr Attr) Attr {
   514  				return removeKeys(TimeKey, LevelKey, MessageKey, "a", "c")(groups, attr)
   515  			},
   516  			attrs:    []Attr{Group("i", Int("c", 3))},
   517  			wantText: "g.x=0 g.n=4 g.h.b=2",
   518  			wantJSON: `{"g":{"x":0,"n":4,"h":{"b":2}}}`,
   519  		},
   520  		{
   521  			name: "replace resolved group",
   522  			replace: func(groups []string, a Attr) Attr {
   523  				if a.Value.Kind() == KindGroup {
   524  					return Attr{"bad", IntValue(1)}
   525  				}
   526  				return removeKeys(TimeKey, LevelKey, MessageKey)(groups, a)
   527  			},
   528  			attrs:    []Attr{Any("name", logValueName{"Perry", "Platypus"})},
   529  			wantText: "name.first=Perry name.last=Platypus",
   530  			wantJSON: `{"name":{"first":"Perry","last":"Platypus"}}`,
   531  		},
   532  		{
   533  			name:    "group and key (or both) needs quoting",
   534  			replace: removeKeys(TimeKey, LevelKey),
   535  			attrs: []Attr{
   536  				Group("prefix",
   537  					String(" needs quoting ", "v"), String("NotNeedsQuoting", "v"),
   538  				),
   539  				Group("prefix needs quoting",
   540  					String(" needs quoting ", "v"), String("NotNeedsQuoting", "v"),
   541  				),
   542  			},
   543  			wantText: `msg=message "prefix. needs quoting "=v prefix.NotNeedsQuoting=v "prefix needs quoting. needs quoting "=v "prefix needs quoting.NotNeedsQuoting"=v`,
   544  			wantJSON: `{"msg":"message","prefix":{" needs quoting ":"v","NotNeedsQuoting":"v"},"prefix needs quoting":{" needs quoting ":"v","NotNeedsQuoting":"v"}}`,
   545  		},
   546  	} {
   547  		r := NewRecord(testTime, LevelInfo, "message", callerPC(2))
   548  		source := r.Source()
   549  		if source == nil {
   550  			t.Fatal("source is nil")
   551  		}
   552  		line := strconv.Itoa(source.Line)
   553  		r.AddAttrs(test.attrs...)
   554  		var buf bytes.Buffer
   555  		opts := HandlerOptions{ReplaceAttr: test.replace, AddSource: test.addSource}
   556  		t.Run(test.name, func(t *testing.T) {
   557  			for _, handler := range []struct {
   558  				name string
   559  				h    Handler
   560  				want string
   561  			}{
   562  				{"text", NewTextHandler(&buf, &opts), test.wantText},
   563  				{"json", NewJSONHandler(&buf, &opts), test.wantJSON},
   564  			} {
   565  				t.Run(handler.name, func(t *testing.T) {
   566  					h := handler.h
   567  					if test.with != nil {
   568  						h = test.with(h)
   569  					}
   570  					buf.Reset()
   571  					if err := h.Handle(nil, r); err != nil {
   572  						t.Fatal(err)
   573  					}
   574  					want := strings.ReplaceAll(handler.want, "$LINE", line)
   575  					got := strings.TrimSuffix(buf.String(), "\n")
   576  					if got != want {
   577  						t.Errorf("\ngot  %s\nwant %s\n", got, want)
   578  					}
   579  				})
   580  			}
   581  		})
   582  	}
   583  }
   584  
   585  // removeKeys returns a function suitable for HandlerOptions.ReplaceAttr
   586  // that removes all Attrs with the given keys.
   587  func removeKeys(keys ...string) func([]string, Attr) Attr {
   588  	return func(_ []string, a Attr) Attr {
   589  		for _, k := range keys {
   590  			if a.Key == k {
   591  				return Attr{}
   592  			}
   593  		}
   594  		return a
   595  	}
   596  }
   597  
   598  func upperCaseKey(_ []string, a Attr) Attr {
   599  	a.Key = strings.ToUpper(a.Key)
   600  	return a
   601  }
   602  
   603  type logValueName struct {
   604  	first, last string
   605  }
   606  
   607  func (n logValueName) LogValue() Value {
   608  	return GroupValue(
   609  		String("first", n.first),
   610  		String("last", n.last))
   611  }
   612  
   613  func TestHandlerEnabled(t *testing.T) {
   614  	levelVar := func(l Level) *LevelVar {
   615  		var al LevelVar
   616  		al.Set(l)
   617  		return &al
   618  	}
   619  
   620  	for _, test := range []struct {
   621  		leveler Leveler
   622  		want    bool
   623  	}{
   624  		{nil, true},
   625  		{LevelWarn, false},
   626  		{&LevelVar{}, true}, // defaults to Info
   627  		{levelVar(LevelWarn), false},
   628  		{LevelDebug, true},
   629  		{levelVar(LevelDebug), true},
   630  	} {
   631  		h := &commonHandler{opts: HandlerOptions{Level: test.leveler}}
   632  		got := h.enabled(LevelInfo)
   633  		if got != test.want {
   634  			t.Errorf("%v: got %t, want %t", test.leveler, got, test.want)
   635  		}
   636  	}
   637  }
   638  
   639  func TestJSONAndTextHandlersWithUnavailableSource(t *testing.T) {
   640  	// Verify that a nil source does not cause a panic.
   641  	// and that the source is empty.
   642  	var buf bytes.Buffer
   643  	opts := &HandlerOptions{
   644  		ReplaceAttr: removeKeys(LevelKey),
   645  		AddSource:   true,
   646  	}
   647  
   648  	for _, test := range []struct {
   649  		name string
   650  		h    Handler
   651  		want string
   652  	}{
   653  		{"text", NewTextHandler(&buf, opts), "msg=message"},
   654  		{"json", NewJSONHandler(&buf, opts), `{"msg":"message"}`},
   655  	} {
   656  		t.Run(test.name, func(t *testing.T) {
   657  			buf.Reset()
   658  			r := NewRecord(time.Time{}, LevelInfo, "message", 0)
   659  			err := test.h.Handle(t.Context(), r)
   660  			if err != nil {
   661  				t.Fatal(err)
   662  			}
   663  
   664  			want := strings.TrimSpace(test.want)
   665  			got := strings.TrimSpace(buf.String())
   666  			if got != want {
   667  				t.Errorf("\ngot  %s\nwant %s", got, want)
   668  			}
   669  		})
   670  	}
   671  }
   672  
   673  func TestSecondWith(t *testing.T) {
   674  	// Verify that a second call to Logger.With does not corrupt
   675  	// the original.
   676  	var buf bytes.Buffer
   677  	h := NewTextHandler(&buf, &HandlerOptions{ReplaceAttr: removeKeys(TimeKey)})
   678  	logger := New(h).With(
   679  		String("app", "playground"),
   680  		String("role", "tester"),
   681  		Int("data_version", 2),
   682  	)
   683  	appLogger := logger.With("type", "log") // this becomes type=met
   684  	_ = logger.With("type", "metric")
   685  	appLogger.Info("foo")
   686  	got := strings.TrimSpace(buf.String())
   687  	want := `level=INFO msg=foo app=playground role=tester data_version=2 type=log`
   688  	if got != want {
   689  		t.Errorf("\ngot  %s\nwant %s", got, want)
   690  	}
   691  }
   692  
   693  func TestReplaceAttrGroups(t *testing.T) {
   694  	// Verify that ReplaceAttr is called with the correct groups.
   695  	type ga struct {
   696  		groups string
   697  		key    string
   698  		val    string
   699  	}
   700  
   701  	var got []ga
   702  
   703  	h := NewTextHandler(io.Discard, &HandlerOptions{ReplaceAttr: func(gs []string, a Attr) Attr {
   704  		v := a.Value.String()
   705  		if a.Key == TimeKey {
   706  			v = "<now>"
   707  		}
   708  		got = append(got, ga{strings.Join(gs, ","), a.Key, v})
   709  		return a
   710  	}})
   711  	New(h).
   712  		With(Int("a", 1)).
   713  		WithGroup("g1").
   714  		With(Int("b", 2)).
   715  		WithGroup("g2").
   716  		With(
   717  			Int("c", 3),
   718  			Group("g3", Int("d", 4)),
   719  			Int("e", 5)).
   720  		Info("m",
   721  			Int("f", 6),
   722  			Group("g4", Int("h", 7)),
   723  			Int("i", 8))
   724  
   725  	want := []ga{
   726  		{"", "a", "1"},
   727  		{"g1", "b", "2"},
   728  		{"g1,g2", "c", "3"},
   729  		{"g1,g2,g3", "d", "4"},
   730  		{"g1,g2", "e", "5"},
   731  		{"", "time", "<now>"},
   732  		{"", "level", "INFO"},
   733  		{"", "msg", "m"},
   734  		{"g1,g2", "f", "6"},
   735  		{"g1,g2,g4", "h", "7"},
   736  		{"g1,g2", "i", "8"},
   737  	}
   738  	if !slices.Equal(got, want) {
   739  		t.Errorf("\ngot  %v\nwant %v", got, want)
   740  	}
   741  }
   742  
   743  const rfc3339Millis = "2006-01-02T15:04:05.000Z07:00"
   744  
   745  func TestWriteTimeRFC3339(t *testing.T) {
   746  	for _, tm := range []time.Time{
   747  		time.Date(2000, 1, 2, 3, 4, 5, 0, time.UTC),
   748  		time.Date(2000, 1, 2, 3, 4, 5, 400, time.Local),
   749  		time.Date(2000, 11, 12, 3, 4, 500, 5e7, time.UTC),
   750  	} {
   751  		got := string(appendRFC3339Millis(nil, tm))
   752  		want := tm.Format(rfc3339Millis)
   753  		if got != want {
   754  			t.Errorf("got %s, want %s", got, want)
   755  		}
   756  	}
   757  }
   758  
   759  func BenchmarkWriteTime(b *testing.B) {
   760  	tm := time.Date(2022, 3, 4, 5, 6, 7, 823456789, time.Local)
   761  	var buf []byte
   762  	for b.Loop() {
   763  		buf = appendRFC3339Millis(buf[:0], tm)
   764  	}
   765  }
   766  
   767  func TestDiscardHandler(t *testing.T) {
   768  	ctx := context.Background()
   769  	stdout, stderr := os.Stdout, os.Stderr
   770  	os.Stdout, os.Stderr = nil, nil // panic on write
   771  	t.Cleanup(func() {
   772  		os.Stdout, os.Stderr = stdout, stderr
   773  	})
   774  
   775  	// Just ensure nothing panics during normal usage
   776  	l := New(DiscardHandler)
   777  	l.Info("msg", "a", 1, "b", 2)
   778  	l.Debug("bg", Int("a", 1), "b", 2)
   779  	l.Warn("w", Duration("dur", 3*time.Second))
   780  	l.Error("bad", "a", 1)
   781  	l.Log(ctx, LevelWarn+1, "w", Int("a", 1), String("b", "two"))
   782  	l.LogAttrs(ctx, LevelInfo+1, "a b c", Int("a", 1), String("b", "two"))
   783  	l.Info("info", "a", []Attr{Int("i", 1)})
   784  	l.Info("info", "a", GroupValue(Int("i", 1)))
   785  }
   786  
   787  func BenchmarkAppendKey(b *testing.B) {
   788  	for _, size := range []int{5, 10, 30, 50, 100} {
   789  		for _, quoting := range []string{"no_quoting", "pre_quoting", "key_quoting", "both_quoting"} {
   790  			b.Run(fmt.Sprintf("%s_prefix_size_%d", quoting, size), func(b *testing.B) {
   791  				var (
   792  					hs     = NewJSONHandler(io.Discard, nil).newHandleState(buffer.New(), false, "")
   793  					prefix = bytes.Repeat([]byte("x"), size)
   794  					key    = "key"
   795  				)
   796  
   797  				if quoting == "pre_quoting" || quoting == "both_quoting" {
   798  					prefix[0] = '"'
   799  				}
   800  				if quoting == "key_quoting" || quoting == "both_quoting" {
   801  					key = "ke\""
   802  				}
   803  
   804  				hs.prefix = (*buffer.Buffer)(&prefix)
   805  
   806  				for b.Loop() {
   807  					hs.appendKey(key)
   808  					hs.buf.Reset()
   809  				}
   810  			})
   811  		}
   812  	}
   813  }
   814  

View as plain text