github.com/AndrienkoAleksandr/go@v0.0.19/src/log/slog/handler_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  // TODO: verify that the output of Marshal{Text,JSON} is suitably escaped.
     6  
     7  package slog
     8  
     9  import (
    10  	"bytes"
    11  	"context"
    12  	"encoding/json"
    13  	"io"
    14  	"log/slog/internal/buffer"
    15  	"path/filepath"
    16  	"slices"
    17  	"strconv"
    18  	"strings"
    19  	"testing"
    20  	"time"
    21  )
    22  
    23  func TestDefaultHandle(t *testing.T) {
    24  	ctx := context.Background()
    25  	preAttrs := []Attr{Int("pre", 0)}
    26  	attrs := []Attr{Int("a", 1), String("b", "two")}
    27  	for _, test := range []struct {
    28  		name  string
    29  		with  func(Handler) Handler
    30  		attrs []Attr
    31  		want  string
    32  	}{
    33  		{
    34  			name: "no attrs",
    35  			want: "INFO message",
    36  		},
    37  		{
    38  			name:  "attrs",
    39  			attrs: attrs,
    40  			want:  "INFO message a=1 b=two",
    41  		},
    42  		{
    43  			name:  "preformatted",
    44  			with:  func(h Handler) Handler { return h.WithAttrs(preAttrs) },
    45  			attrs: attrs,
    46  			want:  "INFO message pre=0 a=1 b=two",
    47  		},
    48  		{
    49  			name: "groups",
    50  			attrs: []Attr{
    51  				Int("a", 1),
    52  				Group("g",
    53  					Int("b", 2),
    54  					Group("h", Int("c", 3)),
    55  					Int("d", 4)),
    56  				Int("e", 5),
    57  			},
    58  			want: "INFO message a=1 g.b=2 g.h.c=3 g.d=4 e=5",
    59  		},
    60  		{
    61  			name:  "group",
    62  			with:  func(h Handler) Handler { return h.WithAttrs(preAttrs).WithGroup("s") },
    63  			attrs: attrs,
    64  			want:  "INFO message pre=0 s.a=1 s.b=two",
    65  		},
    66  		{
    67  			name: "preformatted groups",
    68  			with: func(h Handler) Handler {
    69  				return h.WithAttrs([]Attr{Int("p1", 1)}).
    70  					WithGroup("s1").
    71  					WithAttrs([]Attr{Int("p2", 2)}).
    72  					WithGroup("s2")
    73  			},
    74  			attrs: attrs,
    75  			want:  "INFO message p1=1 s1.p2=2 s1.s2.a=1 s1.s2.b=two",
    76  		},
    77  		{
    78  			name: "two with-groups",
    79  			with: func(h Handler) Handler {
    80  				return h.WithAttrs([]Attr{Int("p1", 1)}).
    81  					WithGroup("s1").
    82  					WithGroup("s2")
    83  			},
    84  			attrs: attrs,
    85  			want:  "INFO message p1=1 s1.s2.a=1 s1.s2.b=two",
    86  		},
    87  	} {
    88  		t.Run(test.name, func(t *testing.T) {
    89  			var got string
    90  			var h Handler = newDefaultHandler(func(_ uintptr, b []byte) error {
    91  				got = string(b)
    92  				return nil
    93  			})
    94  			if test.with != nil {
    95  				h = test.with(h)
    96  			}
    97  			r := NewRecord(time.Time{}, LevelInfo, "message", 0)
    98  			r.AddAttrs(test.attrs...)
    99  			if err := h.Handle(ctx, r); err != nil {
   100  				t.Fatal(err)
   101  			}
   102  			if got != test.want {
   103  				t.Errorf("\ngot  %s\nwant %s", got, test.want)
   104  			}
   105  		})
   106  	}
   107  }
   108  
   109  // Verify the common parts of TextHandler and JSONHandler.
   110  func TestJSONAndTextHandlers(t *testing.T) {
   111  	ctx := context.Background()
   112  
   113  	// remove all Attrs
   114  	removeAll := func(_ []string, a Attr) Attr { return Attr{} }
   115  
   116  	attrs := []Attr{String("a", "one"), Int("b", 2), Any("", nil)}
   117  	preAttrs := []Attr{Int("pre", 3), String("x", "y")}
   118  
   119  	for _, test := range []struct {
   120  		name      string
   121  		replace   func([]string, Attr) Attr
   122  		addSource bool
   123  		with      func(Handler) Handler
   124  		preAttrs  []Attr
   125  		attrs     []Attr
   126  		wantText  string
   127  		wantJSON  string
   128  	}{
   129  		{
   130  			name:     "basic",
   131  			attrs:    attrs,
   132  			wantText: "time=2000-01-02T03:04:05.000Z level=INFO msg=message a=one b=2",
   133  			wantJSON: `{"time":"2000-01-02T03:04:05Z","level":"INFO","msg":"message","a":"one","b":2}`,
   134  		},
   135  		{
   136  			name:     "empty key",
   137  			attrs:    append(slices.Clip(attrs), Any("", "v")),
   138  			wantText: `time=2000-01-02T03:04:05.000Z level=INFO msg=message a=one b=2 ""=v`,
   139  			wantJSON: `{"time":"2000-01-02T03:04:05Z","level":"INFO","msg":"message","a":"one","b":2,"":"v"}`,
   140  		},
   141  		{
   142  			name:     "cap keys",
   143  			replace:  upperCaseKey,
   144  			attrs:    attrs,
   145  			wantText: "TIME=2000-01-02T03:04:05.000Z LEVEL=INFO MSG=message A=one B=2",
   146  			wantJSON: `{"TIME":"2000-01-02T03:04:05Z","LEVEL":"INFO","MSG":"message","A":"one","B":2}`,
   147  		},
   148  		{
   149  			name:     "remove all",
   150  			replace:  removeAll,
   151  			attrs:    attrs,
   152  			wantText: "",
   153  			wantJSON: `{}`,
   154  		},
   155  		{
   156  			name:     "preformatted",
   157  			with:     func(h Handler) Handler { return h.WithAttrs(preAttrs) },
   158  			preAttrs: preAttrs,
   159  			attrs:    attrs,
   160  			wantText: "time=2000-01-02T03:04:05.000Z level=INFO msg=message pre=3 x=y a=one b=2",
   161  			wantJSON: `{"time":"2000-01-02T03:04:05Z","level":"INFO","msg":"message","pre":3,"x":"y","a":"one","b":2}`,
   162  		},
   163  		{
   164  			name:     "preformatted cap keys",
   165  			replace:  upperCaseKey,
   166  			with:     func(h Handler) Handler { return h.WithAttrs(preAttrs) },
   167  			preAttrs: preAttrs,
   168  			attrs:    attrs,
   169  			wantText: "TIME=2000-01-02T03:04:05.000Z LEVEL=INFO MSG=message PRE=3 X=y A=one B=2",
   170  			wantJSON: `{"TIME":"2000-01-02T03:04:05Z","LEVEL":"INFO","MSG":"message","PRE":3,"X":"y","A":"one","B":2}`,
   171  		},
   172  		{
   173  			name:     "preformatted remove all",
   174  			replace:  removeAll,
   175  			with:     func(h Handler) Handler { return h.WithAttrs(preAttrs) },
   176  			preAttrs: preAttrs,
   177  			attrs:    attrs,
   178  			wantText: "",
   179  			wantJSON: "{}",
   180  		},
   181  		{
   182  			name:     "remove built-in",
   183  			replace:  removeKeys(TimeKey, LevelKey, MessageKey),
   184  			attrs:    attrs,
   185  			wantText: "a=one b=2",
   186  			wantJSON: `{"a":"one","b":2}`,
   187  		},
   188  		{
   189  			name:     "preformatted remove built-in",
   190  			replace:  removeKeys(TimeKey, LevelKey, MessageKey),
   191  			with:     func(h Handler) Handler { return h.WithAttrs(preAttrs) },
   192  			attrs:    attrs,
   193  			wantText: "pre=3 x=y a=one b=2",
   194  			wantJSON: `{"pre":3,"x":"y","a":"one","b":2}`,
   195  		},
   196  		{
   197  			name:    "groups",
   198  			replace: removeKeys(TimeKey, LevelKey), // to simplify the result
   199  			attrs: []Attr{
   200  				Int("a", 1),
   201  				Group("g",
   202  					Int("b", 2),
   203  					Group("h", Int("c", 3)),
   204  					Int("d", 4)),
   205  				Int("e", 5),
   206  			},
   207  			wantText: "msg=message a=1 g.b=2 g.h.c=3 g.d=4 e=5",
   208  			wantJSON: `{"msg":"message","a":1,"g":{"b":2,"h":{"c":3},"d":4},"e":5}`,
   209  		},
   210  		{
   211  			name:     "empty group",
   212  			replace:  removeKeys(TimeKey, LevelKey),
   213  			attrs:    []Attr{Group("g"), Group("h", Int("a", 1))},
   214  			wantText: "msg=message h.a=1",
   215  			wantJSON: `{"msg":"message","h":{"a":1}}`,
   216  		},
   217  		{
   218  			name:    "escapes",
   219  			replace: removeKeys(TimeKey, LevelKey),
   220  			attrs: []Attr{
   221  				String("a b", "x\t\n\000y"),
   222  				Group(" b.c=\"\\x2E\t",
   223  					String("d=e", "f.g\""),
   224  					Int("m.d", 1)), // dot is not escaped
   225  			},
   226  			wantText: `msg=message "a b"="x\t\n\x00y" " b.c=\"\\x2E\t.d=e"="f.g\"" " b.c=\"\\x2E\t.m.d"=1`,
   227  			wantJSON: `{"msg":"message","a b":"x\t\n\u0000y"," b.c=\"\\x2E\t":{"d=e":"f.g\"","m.d":1}}`,
   228  		},
   229  		{
   230  			name:    "LogValuer",
   231  			replace: removeKeys(TimeKey, LevelKey),
   232  			attrs: []Attr{
   233  				Int("a", 1),
   234  				Any("name", logValueName{"Ren", "Hoek"}),
   235  				Int("b", 2),
   236  			},
   237  			wantText: "msg=message a=1 name.first=Ren name.last=Hoek b=2",
   238  			wantJSON: `{"msg":"message","a":1,"name":{"first":"Ren","last":"Hoek"},"b":2}`,
   239  		},
   240  		{
   241  			// Test resolution when there is no ReplaceAttr function.
   242  			name: "resolve",
   243  			attrs: []Attr{
   244  				Any("", &replace{Value{}}), // should be elided
   245  				Any("name", logValueName{"Ren", "Hoek"}),
   246  			},
   247  			wantText: "time=2000-01-02T03:04:05.000Z level=INFO msg=message name.first=Ren name.last=Hoek",
   248  			wantJSON: `{"time":"2000-01-02T03:04:05Z","level":"INFO","msg":"message","name":{"first":"Ren","last":"Hoek"}}`,
   249  		},
   250  		{
   251  			name:     "with-group",
   252  			replace:  removeKeys(TimeKey, LevelKey),
   253  			with:     func(h Handler) Handler { return h.WithAttrs(preAttrs).WithGroup("s") },
   254  			attrs:    attrs,
   255  			wantText: "msg=message pre=3 x=y s.a=one s.b=2",
   256  			wantJSON: `{"msg":"message","pre":3,"x":"y","s":{"a":"one","b":2}}`,
   257  		},
   258  		{
   259  			name:    "preformatted with-groups",
   260  			replace: removeKeys(TimeKey, LevelKey),
   261  			with: func(h Handler) Handler {
   262  				return h.WithAttrs([]Attr{Int("p1", 1)}).
   263  					WithGroup("s1").
   264  					WithAttrs([]Attr{Int("p2", 2)}).
   265  					WithGroup("s2").
   266  					WithAttrs([]Attr{Int("p3", 3)})
   267  			},
   268  			attrs:    attrs,
   269  			wantText: "msg=message p1=1 s1.p2=2 s1.s2.p3=3 s1.s2.a=one s1.s2.b=2",
   270  			wantJSON: `{"msg":"message","p1":1,"s1":{"p2":2,"s2":{"p3":3,"a":"one","b":2}}}`,
   271  		},
   272  		{
   273  			name:    "two with-groups",
   274  			replace: removeKeys(TimeKey, LevelKey),
   275  			with: func(h Handler) Handler {
   276  				return h.WithAttrs([]Attr{Int("p1", 1)}).
   277  					WithGroup("s1").
   278  					WithGroup("s2")
   279  			},
   280  			attrs:    attrs,
   281  			wantText: "msg=message p1=1 s1.s2.a=one s1.s2.b=2",
   282  			wantJSON: `{"msg":"message","p1":1,"s1":{"s2":{"a":"one","b":2}}}`,
   283  		},
   284  		{
   285  			name:     "GroupValue as Attr value",
   286  			replace:  removeKeys(TimeKey, LevelKey),
   287  			attrs:    []Attr{{"v", AnyValue(IntValue(3))}},
   288  			wantText: "msg=message v=3",
   289  			wantJSON: `{"msg":"message","v":3}`,
   290  		},
   291  		{
   292  			name:     "byte slice",
   293  			replace:  removeKeys(TimeKey, LevelKey),
   294  			attrs:    []Attr{Any("bs", []byte{1, 2, 3, 4})},
   295  			wantText: `msg=message bs="\x01\x02\x03\x04"`,
   296  			wantJSON: `{"msg":"message","bs":"AQIDBA=="}`,
   297  		},
   298  		{
   299  			name:     "json.RawMessage",
   300  			replace:  removeKeys(TimeKey, LevelKey),
   301  			attrs:    []Attr{Any("bs", json.RawMessage([]byte("1234")))},
   302  			wantText: `msg=message bs="1234"`,
   303  			wantJSON: `{"msg":"message","bs":1234}`,
   304  		},
   305  		{
   306  			name:    "inline group",
   307  			replace: removeKeys(TimeKey, LevelKey),
   308  			attrs: []Attr{
   309  				Int("a", 1),
   310  				Group("", Int("b", 2), Int("c", 3)),
   311  				Int("d", 4),
   312  			},
   313  			wantText: `msg=message a=1 b=2 c=3 d=4`,
   314  			wantJSON: `{"msg":"message","a":1,"b":2,"c":3,"d":4}`,
   315  		},
   316  		{
   317  			name: "Source",
   318  			replace: func(gs []string, a Attr) Attr {
   319  				if a.Key == SourceKey {
   320  					s := a.Value.Any().(*Source)
   321  					s.File = filepath.Base(s.File)
   322  					return Any(a.Key, s)
   323  				}
   324  				return removeKeys(TimeKey, LevelKey)(gs, a)
   325  			},
   326  			addSource: true,
   327  			wantText:  `source=handler_test.go:$LINE msg=message`,
   328  			wantJSON:  `{"source":{"function":"log/slog.TestJSONAndTextHandlers","file":"handler_test.go","line":$LINE},"msg":"message"}`,
   329  		},
   330  		{
   331  			name: "replace built-in with group",
   332  			replace: func(_ []string, a Attr) Attr {
   333  				if a.Key == TimeKey {
   334  					return Group(TimeKey, "mins", 3, "secs", 2)
   335  				}
   336  				if a.Key == LevelKey {
   337  					return Attr{}
   338  				}
   339  				return a
   340  			},
   341  			wantText: `time.mins=3 time.secs=2 msg=message`,
   342  			wantJSON: `{"time":{"mins":3,"secs":2},"msg":"message"}`,
   343  		},
   344  	} {
   345  		r := NewRecord(testTime, LevelInfo, "message", callerPC(2))
   346  		line := strconv.Itoa(r.source().Line)
   347  		r.AddAttrs(test.attrs...)
   348  		var buf bytes.Buffer
   349  		opts := HandlerOptions{ReplaceAttr: test.replace, AddSource: test.addSource}
   350  		t.Run(test.name, func(t *testing.T) {
   351  			for _, handler := range []struct {
   352  				name string
   353  				h    Handler
   354  				want string
   355  			}{
   356  				{"text", NewTextHandler(&buf, &opts), test.wantText},
   357  				{"json", NewJSONHandler(&buf, &opts), test.wantJSON},
   358  			} {
   359  				t.Run(handler.name, func(t *testing.T) {
   360  					h := handler.h
   361  					if test.with != nil {
   362  						h = test.with(h)
   363  					}
   364  					buf.Reset()
   365  					if err := h.Handle(ctx, r); err != nil {
   366  						t.Fatal(err)
   367  					}
   368  					want := strings.ReplaceAll(handler.want, "$LINE", line)
   369  					got := strings.TrimSuffix(buf.String(), "\n")
   370  					if got != want {
   371  						t.Errorf("\ngot  %s\nwant %s\n", got, want)
   372  					}
   373  				})
   374  			}
   375  		})
   376  	}
   377  }
   378  
   379  // removeKeys returns a function suitable for HandlerOptions.ReplaceAttr
   380  // that removes all Attrs with the given keys.
   381  func removeKeys(keys ...string) func([]string, Attr) Attr {
   382  	return func(_ []string, a Attr) Attr {
   383  		for _, k := range keys {
   384  			if a.Key == k {
   385  				return Attr{}
   386  			}
   387  		}
   388  		return a
   389  	}
   390  }
   391  
   392  func upperCaseKey(_ []string, a Attr) Attr {
   393  	a.Key = strings.ToUpper(a.Key)
   394  	return a
   395  }
   396  
   397  type logValueName struct {
   398  	first, last string
   399  }
   400  
   401  func (n logValueName) LogValue() Value {
   402  	return GroupValue(
   403  		String("first", n.first),
   404  		String("last", n.last))
   405  }
   406  
   407  func TestHandlerEnabled(t *testing.T) {
   408  	levelVar := func(l Level) *LevelVar {
   409  		var al LevelVar
   410  		al.Set(l)
   411  		return &al
   412  	}
   413  
   414  	for _, test := range []struct {
   415  		leveler Leveler
   416  		want    bool
   417  	}{
   418  		{nil, true},
   419  		{LevelWarn, false},
   420  		{&LevelVar{}, true}, // defaults to Info
   421  		{levelVar(LevelWarn), false},
   422  		{LevelDebug, true},
   423  		{levelVar(LevelDebug), true},
   424  	} {
   425  		h := &commonHandler{opts: HandlerOptions{Level: test.leveler}}
   426  		got := h.enabled(LevelInfo)
   427  		if got != test.want {
   428  			t.Errorf("%v: got %t, want %t", test.leveler, got, test.want)
   429  		}
   430  	}
   431  }
   432  
   433  func TestSecondWith(t *testing.T) {
   434  	// Verify that a second call to Logger.With does not corrupt
   435  	// the original.
   436  	var buf bytes.Buffer
   437  	h := NewTextHandler(&buf, &HandlerOptions{ReplaceAttr: removeKeys(TimeKey)})
   438  	logger := New(h).With(
   439  		String("app", "playground"),
   440  		String("role", "tester"),
   441  		Int("data_version", 2),
   442  	)
   443  	appLogger := logger.With("type", "log") // this becomes type=met
   444  	_ = logger.With("type", "metric")
   445  	appLogger.Info("foo")
   446  	got := strings.TrimSpace(buf.String())
   447  	want := `level=INFO msg=foo app=playground role=tester data_version=2 type=log`
   448  	if got != want {
   449  		t.Errorf("\ngot  %s\nwant %s", got, want)
   450  	}
   451  }
   452  
   453  func TestReplaceAttrGroups(t *testing.T) {
   454  	// Verify that ReplaceAttr is called with the correct groups.
   455  	type ga struct {
   456  		groups string
   457  		key    string
   458  		val    string
   459  	}
   460  
   461  	var got []ga
   462  
   463  	h := NewTextHandler(io.Discard, &HandlerOptions{ReplaceAttr: func(gs []string, a Attr) Attr {
   464  		v := a.Value.String()
   465  		if a.Key == TimeKey {
   466  			v = "<now>"
   467  		}
   468  		got = append(got, ga{strings.Join(gs, ","), a.Key, v})
   469  		return a
   470  	}})
   471  	New(h).
   472  		With(Int("a", 1)).
   473  		WithGroup("g1").
   474  		With(Int("b", 2)).
   475  		WithGroup("g2").
   476  		With(
   477  			Int("c", 3),
   478  			Group("g3", Int("d", 4)),
   479  			Int("e", 5)).
   480  		Info("m",
   481  			Int("f", 6),
   482  			Group("g4", Int("h", 7)),
   483  			Int("i", 8))
   484  
   485  	want := []ga{
   486  		{"", "a", "1"},
   487  		{"g1", "b", "2"},
   488  		{"g1,g2", "c", "3"},
   489  		{"g1,g2,g3", "d", "4"},
   490  		{"g1,g2", "e", "5"},
   491  		{"", "time", "<now>"},
   492  		{"", "level", "INFO"},
   493  		{"", "msg", "m"},
   494  		{"g1,g2", "f", "6"},
   495  		{"g1,g2,g4", "h", "7"},
   496  		{"g1,g2", "i", "8"},
   497  	}
   498  	if !slices.Equal(got, want) {
   499  		t.Errorf("\ngot  %v\nwant %v", got, want)
   500  	}
   501  }
   502  
   503  const rfc3339Millis = "2006-01-02T15:04:05.000Z07:00"
   504  
   505  func TestWriteTimeRFC3339(t *testing.T) {
   506  	for _, tm := range []time.Time{
   507  		time.Date(2000, 1, 2, 3, 4, 5, 0, time.UTC),
   508  		time.Date(2000, 1, 2, 3, 4, 5, 400, time.Local),
   509  		time.Date(2000, 11, 12, 3, 4, 500, 5e7, time.UTC),
   510  	} {
   511  		want := tm.Format(rfc3339Millis)
   512  		buf := buffer.New()
   513  		defer buf.Free()
   514  		writeTimeRFC3339Millis(buf, tm)
   515  		got := buf.String()
   516  		if got != want {
   517  			t.Errorf("got %s, want %s", got, want)
   518  		}
   519  	}
   520  }
   521  
   522  func BenchmarkWriteTime(b *testing.B) {
   523  	buf := buffer.New()
   524  	defer buf.Free()
   525  	tm := time.Date(2022, 3, 4, 5, 6, 7, 823456789, time.Local)
   526  	b.ResetTimer()
   527  	for i := 0; i < b.N; i++ {
   528  		writeTimeRFC3339Millis(buf, tm)
   529  		buf.Reset()
   530  	}
   531  }