github.com/searKing/golang/go@v1.2.117/log/slog/handler_test.go (about)

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