golang.org/x/exp@v0.0.0-20240506185415-9bf2ced13842/slog/json_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  package slog
     6  
     7  import (
     8  	"bytes"
     9  	"context"
    10  	"encoding/json"
    11  	"errors"
    12  	"fmt"
    13  	"io"
    14  	"math"
    15  	"os"
    16  	"path/filepath"
    17  	"strings"
    18  	"testing"
    19  	"time"
    20  
    21  	"golang.org/x/exp/slog/internal/buffer"
    22  )
    23  
    24  func TestJSONHandler(t *testing.T) {
    25  	for _, test := range []struct {
    26  		name string
    27  		opts HandlerOptions
    28  		want string
    29  	}{
    30  		{
    31  			"none",
    32  			HandlerOptions{},
    33  			`{"time":"2000-01-02T03:04:05Z","level":"INFO","msg":"m","a":1,"m":{"b":2}}`,
    34  		},
    35  		{
    36  			"replace",
    37  			HandlerOptions{ReplaceAttr: upperCaseKey},
    38  			`{"TIME":"2000-01-02T03:04:05Z","LEVEL":"INFO","MSG":"m","A":1,"M":{"b":2}}`,
    39  		},
    40  	} {
    41  		t.Run(test.name, func(t *testing.T) {
    42  			var buf bytes.Buffer
    43  			h := NewJSONHandler(&buf, &test.opts)
    44  			r := NewRecord(testTime, LevelInfo, "m", 0)
    45  			r.AddAttrs(Int("a", 1), Any("m", map[string]int{"b": 2}))
    46  			if err := h.Handle(context.Background(), r); err != nil {
    47  				t.Fatal(err)
    48  			}
    49  			got := strings.TrimSuffix(buf.String(), "\n")
    50  			if got != test.want {
    51  				t.Errorf("\ngot  %s\nwant %s", got, test.want)
    52  			}
    53  		})
    54  	}
    55  }
    56  
    57  // for testing json.Marshaler
    58  type jsonMarshaler struct {
    59  	s string
    60  }
    61  
    62  func (j jsonMarshaler) String() string { return j.s } // should be ignored
    63  
    64  func (j jsonMarshaler) MarshalJSON() ([]byte, error) {
    65  	if j.s == "" {
    66  		return nil, errors.New("json: empty string")
    67  	}
    68  	return []byte(fmt.Sprintf(`[%q]`, j.s)), nil
    69  }
    70  
    71  type jsonMarshalerError struct {
    72  	jsonMarshaler
    73  }
    74  
    75  func (jsonMarshalerError) Error() string { return "oops" }
    76  
    77  func TestAppendJSONValue(t *testing.T) {
    78  	// jsonAppendAttrValue should always agree with json.Marshal.
    79  	for _, value := range []any{
    80  		"hello",
    81  		`"[{escape}]"`,
    82  		"<escapeHTML&>",
    83  		`-123`,
    84  		int64(-9_200_123_456_789_123_456),
    85  		uint64(9_200_123_456_789_123_456),
    86  		-12.75,
    87  		1.23e-9,
    88  		false,
    89  		time.Minute,
    90  		testTime,
    91  		jsonMarshaler{"xyz"},
    92  		jsonMarshalerError{jsonMarshaler{"pqr"}},
    93  		LevelWarn,
    94  	} {
    95  		got := jsonValueString(AnyValue(value))
    96  		want, err := marshalJSON(value)
    97  		if err != nil {
    98  			t.Fatal(err)
    99  		}
   100  		if got != want {
   101  			t.Errorf("%v: got %s, want %s", value, got, want)
   102  		}
   103  	}
   104  }
   105  
   106  func marshalJSON(x any) (string, error) {
   107  	var buf bytes.Buffer
   108  	enc := json.NewEncoder(&buf)
   109  	enc.SetEscapeHTML(false)
   110  	if err := enc.Encode(x); err != nil {
   111  		return "", err
   112  	}
   113  	return strings.TrimSpace(buf.String()), nil
   114  }
   115  
   116  func TestJSONAppendAttrValueSpecial(t *testing.T) {
   117  	// Attr values that render differently from json.Marshal.
   118  	for _, test := range []struct {
   119  		value any
   120  		want  string
   121  	}{
   122  		{math.NaN(), `"!ERROR:json: unsupported value: NaN"`},
   123  		{math.Inf(+1), `"!ERROR:json: unsupported value: +Inf"`},
   124  		{math.Inf(-1), `"!ERROR:json: unsupported value: -Inf"`},
   125  		{io.EOF, `"EOF"`},
   126  	} {
   127  		got := jsonValueString(AnyValue(test.value))
   128  		if got != test.want {
   129  			t.Errorf("%v: got %s, want %s", test.value, got, test.want)
   130  		}
   131  	}
   132  }
   133  
   134  func jsonValueString(v Value) string {
   135  	var buf []byte
   136  	s := &handleState{h: &commonHandler{json: true}, buf: (*buffer.Buffer)(&buf)}
   137  	if err := appendJSONValue(s, v); err != nil {
   138  		s.appendError(err)
   139  	}
   140  	return string(buf)
   141  }
   142  
   143  func BenchmarkJSONHandler(b *testing.B) {
   144  	for _, bench := range []struct {
   145  		name string
   146  		opts HandlerOptions
   147  	}{
   148  		{"defaults", HandlerOptions{}},
   149  		{"time format", HandlerOptions{
   150  			ReplaceAttr: func(_ []string, a Attr) Attr {
   151  				v := a.Value
   152  				if v.Kind() == KindTime {
   153  					return String(a.Key, v.Time().Format(rfc3339Millis))
   154  				}
   155  				if a.Key == "level" {
   156  					return Attr{"severity", a.Value}
   157  				}
   158  				return a
   159  			},
   160  		}},
   161  		{"time unix", HandlerOptions{
   162  			ReplaceAttr: func(_ []string, a Attr) Attr {
   163  				v := a.Value
   164  				if v.Kind() == KindTime {
   165  					return Int64(a.Key, v.Time().UnixNano())
   166  				}
   167  				if a.Key == "level" {
   168  					return Attr{"severity", a.Value}
   169  				}
   170  				return a
   171  			},
   172  		}},
   173  	} {
   174  		b.Run(bench.name, func(b *testing.B) {
   175  			l := New(NewJSONHandler(io.Discard, &bench.opts)).With(
   176  				String("program", "my-test-program"),
   177  				String("package", "log/slog"),
   178  				String("traceID", "2039232309232309"),
   179  				String("URL", "https://pkg.go.dev/golang.org/x/log/slog"))
   180  			b.ReportAllocs()
   181  			b.ResetTimer()
   182  			for i := 0; i < b.N; i++ {
   183  				l.LogAttrs(nil, LevelInfo, "this is a typical log message",
   184  					String("module", "github.com/google/go-cmp"),
   185  					String("version", "v1.23.4"),
   186  					Int("count", 23),
   187  					Int("number", 123456),
   188  				)
   189  			}
   190  		})
   191  	}
   192  }
   193  
   194  func BenchmarkPreformatting(b *testing.B) {
   195  	type req struct {
   196  		Method  string
   197  		URL     string
   198  		TraceID string
   199  		Addr    string
   200  	}
   201  
   202  	structAttrs := []any{
   203  		String("program", "my-test-program"),
   204  		String("package", "log/slog"),
   205  		Any("request", &req{
   206  			Method:  "GET",
   207  			URL:     "https://pkg.go.dev/golang.org/x/log/slog",
   208  			TraceID: "2039232309232309",
   209  			Addr:    "127.0.0.1:8080",
   210  		}),
   211  	}
   212  
   213  	outFile, err := os.Create(filepath.Join(b.TempDir(), "bench.log"))
   214  	if err != nil {
   215  		b.Fatal(err)
   216  	}
   217  	defer func() {
   218  		if err := outFile.Close(); err != nil {
   219  			b.Fatal(err)
   220  		}
   221  	}()
   222  
   223  	for _, bench := range []struct {
   224  		name  string
   225  		wc    io.Writer
   226  		attrs []any
   227  	}{
   228  		{"separate", io.Discard, []any{
   229  			String("program", "my-test-program"),
   230  			String("package", "log/slog"),
   231  			String("method", "GET"),
   232  			String("URL", "https://pkg.go.dev/golang.org/x/log/slog"),
   233  			String("traceID", "2039232309232309"),
   234  			String("addr", "127.0.0.1:8080"),
   235  		}},
   236  		{"struct", io.Discard, structAttrs},
   237  		{"struct file", outFile, structAttrs},
   238  	} {
   239  		b.Run(bench.name, func(b *testing.B) {
   240  			l := New(NewJSONHandler(bench.wc, nil)).With(bench.attrs...)
   241  			b.ReportAllocs()
   242  			b.ResetTimer()
   243  			for i := 0; i < b.N; i++ {
   244  				l.LogAttrs(nil, LevelInfo, "this is a typical log message",
   245  					String("module", "github.com/google/go-cmp"),
   246  					String("version", "v1.23.4"),
   247  					Int("count", 23),
   248  					Int("number", 123456),
   249  				)
   250  			}
   251  		})
   252  	}
   253  }
   254  
   255  func BenchmarkJSONEncoding(b *testing.B) {
   256  	value := 3.14
   257  	buf := buffer.New()
   258  	defer buf.Free()
   259  	b.Run("json.Marshal", func(b *testing.B) {
   260  		b.ReportAllocs()
   261  		for i := 0; i < b.N; i++ {
   262  			by, err := json.Marshal(value)
   263  			if err != nil {
   264  				b.Fatal(err)
   265  			}
   266  			buf.Write(by)
   267  			*buf = (*buf)[:0]
   268  		}
   269  	})
   270  	b.Run("Encoder.Encode", func(b *testing.B) {
   271  		b.ReportAllocs()
   272  		for i := 0; i < b.N; i++ {
   273  			if err := json.NewEncoder(buf).Encode(value); err != nil {
   274  				b.Fatal(err)
   275  			}
   276  			*buf = (*buf)[:0]
   277  		}
   278  	})
   279  	_ = buf
   280  }