Source file src/log/slog/handler_test.go

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

View as plain text