summaryrefslogtreecommitdiffstats
path: root/slog/json_handler_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'slog/json_handler_test.go')
-rw-r--r--slog/json_handler_test.go280
1 files changed, 280 insertions, 0 deletions
diff --git a/slog/json_handler_test.go b/slog/json_handler_test.go
new file mode 100644
index 0000000..9f27ffd
--- /dev/null
+++ b/slog/json_handler_test.go
@@ -0,0 +1,280 @@
+// Copyright 2022 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package slog
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "math"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+
+ "golang.org/x/exp/slog/internal/buffer"
+)
+
+func TestJSONHandler(t *testing.T) {
+ for _, test := range []struct {
+ name string
+ opts HandlerOptions
+ want string
+ }{
+ {
+ "none",
+ HandlerOptions{},
+ `{"time":"2000-01-02T03:04:05Z","level":"INFO","msg":"m","a":1,"m":{"b":2}}`,
+ },
+ {
+ "replace",
+ HandlerOptions{ReplaceAttr: upperCaseKey},
+ `{"TIME":"2000-01-02T03:04:05Z","LEVEL":"INFO","MSG":"m","A":1,"M":{"b":2}}`,
+ },
+ } {
+ t.Run(test.name, func(t *testing.T) {
+ var buf bytes.Buffer
+ h := NewJSONHandler(&buf, &test.opts)
+ r := NewRecord(testTime, LevelInfo, "m", 0)
+ r.AddAttrs(Int("a", 1), Any("m", map[string]int{"b": 2}))
+ if err := h.Handle(context.Background(), r); err != nil {
+ t.Fatal(err)
+ }
+ got := strings.TrimSuffix(buf.String(), "\n")
+ if got != test.want {
+ t.Errorf("\ngot %s\nwant %s", got, test.want)
+ }
+ })
+ }
+}
+
+// for testing json.Marshaler
+type jsonMarshaler struct {
+ s string
+}
+
+func (j jsonMarshaler) String() string { return j.s } // should be ignored
+
+func (j jsonMarshaler) MarshalJSON() ([]byte, error) {
+ if j.s == "" {
+ return nil, errors.New("json: empty string")
+ }
+ return []byte(fmt.Sprintf(`[%q]`, j.s)), nil
+}
+
+type jsonMarshalerError struct {
+ jsonMarshaler
+}
+
+func (jsonMarshalerError) Error() string { return "oops" }
+
+func TestAppendJSONValue(t *testing.T) {
+ // jsonAppendAttrValue should always agree with json.Marshal.
+ for _, value := range []any{
+ "hello",
+ `"[{escape}]"`,
+ "<escapeHTML&>",
+ `-123`,
+ int64(-9_200_123_456_789_123_456),
+ uint64(9_200_123_456_789_123_456),
+ -12.75,
+ 1.23e-9,
+ false,
+ time.Minute,
+ testTime,
+ jsonMarshaler{"xyz"},
+ jsonMarshalerError{jsonMarshaler{"pqr"}},
+ LevelWarn,
+ } {
+ got := jsonValueString(AnyValue(value))
+ want, err := marshalJSON(value)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got != want {
+ t.Errorf("%v: got %s, want %s", value, got, want)
+ }
+ }
+}
+
+func marshalJSON(x any) (string, error) {
+ var buf bytes.Buffer
+ enc := json.NewEncoder(&buf)
+ enc.SetEscapeHTML(false)
+ if err := enc.Encode(x); err != nil {
+ return "", err
+ }
+ return strings.TrimSpace(buf.String()), nil
+}
+
+func TestJSONAppendAttrValueSpecial(t *testing.T) {
+ // Attr values that render differently from json.Marshal.
+ for _, test := range []struct {
+ value any
+ want string
+ }{
+ {math.NaN(), `"!ERROR:json: unsupported value: NaN"`},
+ {math.Inf(+1), `"!ERROR:json: unsupported value: +Inf"`},
+ {math.Inf(-1), `"!ERROR:json: unsupported value: -Inf"`},
+ {io.EOF, `"EOF"`},
+ } {
+ got := jsonValueString(AnyValue(test.value))
+ if got != test.want {
+ t.Errorf("%v: got %s, want %s", test.value, got, test.want)
+ }
+ }
+}
+
+func jsonValueString(v Value) string {
+ var buf []byte
+ s := &handleState{h: &commonHandler{json: true}, buf: (*buffer.Buffer)(&buf)}
+ if err := appendJSONValue(s, v); err != nil {
+ s.appendError(err)
+ }
+ return string(buf)
+}
+
+func BenchmarkJSONHandler(b *testing.B) {
+ for _, bench := range []struct {
+ name string
+ opts HandlerOptions
+ }{
+ {"defaults", HandlerOptions{}},
+ {"time format", HandlerOptions{
+ ReplaceAttr: func(_ []string, a Attr) Attr {
+ v := a.Value
+ if v.Kind() == KindTime {
+ return String(a.Key, v.Time().Format(rfc3339Millis))
+ }
+ if a.Key == "level" {
+ return Attr{"severity", a.Value}
+ }
+ return a
+ },
+ }},
+ {"time unix", HandlerOptions{
+ ReplaceAttr: func(_ []string, a Attr) Attr {
+ v := a.Value
+ if v.Kind() == KindTime {
+ return Int64(a.Key, v.Time().UnixNano())
+ }
+ if a.Key == "level" {
+ return Attr{"severity", a.Value}
+ }
+ return a
+ },
+ }},
+ } {
+ b.Run(bench.name, func(b *testing.B) {
+ l := New(NewJSONHandler(io.Discard, &bench.opts)).With(
+ String("program", "my-test-program"),
+ String("package", "log/slog"),
+ String("traceID", "2039232309232309"),
+ String("URL", "https://pkg.go.dev/golang.org/x/log/slog"))
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ l.LogAttrs(nil, LevelInfo, "this is a typical log message",
+ String("module", "github.com/google/go-cmp"),
+ String("version", "v1.23.4"),
+ Int("count", 23),
+ Int("number", 123456),
+ )
+ }
+ })
+ }
+}
+
+func BenchmarkPreformatting(b *testing.B) {
+ type req struct {
+ Method string
+ URL string
+ TraceID string
+ Addr string
+ }
+
+ structAttrs := []any{
+ String("program", "my-test-program"),
+ String("package", "log/slog"),
+ Any("request", &req{
+ Method: "GET",
+ URL: "https://pkg.go.dev/golang.org/x/log/slog",
+ TraceID: "2039232309232309",
+ Addr: "127.0.0.1:8080",
+ }),
+ }
+
+ outFile, err := os.Create(filepath.Join(b.TempDir(), "bench.log"))
+ if err != nil {
+ b.Fatal(err)
+ }
+ defer func() {
+ if err := outFile.Close(); err != nil {
+ b.Fatal(err)
+ }
+ }()
+
+ for _, bench := range []struct {
+ name string
+ wc io.Writer
+ attrs []any
+ }{
+ {"separate", io.Discard, []any{
+ String("program", "my-test-program"),
+ String("package", "log/slog"),
+ String("method", "GET"),
+ String("URL", "https://pkg.go.dev/golang.org/x/log/slog"),
+ String("traceID", "2039232309232309"),
+ String("addr", "127.0.0.1:8080"),
+ }},
+ {"struct", io.Discard, structAttrs},
+ {"struct file", outFile, structAttrs},
+ } {
+ b.Run(bench.name, func(b *testing.B) {
+ l := New(NewJSONHandler(bench.wc, nil)).With(bench.attrs...)
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ l.LogAttrs(nil, LevelInfo, "this is a typical log message",
+ String("module", "github.com/google/go-cmp"),
+ String("version", "v1.23.4"),
+ Int("count", 23),
+ Int("number", 123456),
+ )
+ }
+ })
+ }
+}
+
+func BenchmarkJSONEncoding(b *testing.B) {
+ value := 3.14
+ buf := buffer.New()
+ defer buf.Free()
+ b.Run("json.Marshal", func(b *testing.B) {
+ b.ReportAllocs()
+ for i := 0; i < b.N; i++ {
+ by, err := json.Marshal(value)
+ if err != nil {
+ b.Fatal(err)
+ }
+ buf.Write(by)
+ *buf = (*buf)[:0]
+ }
+ })
+ b.Run("Encoder.Encode", func(b *testing.B) {
+ b.ReportAllocs()
+ for i := 0; i < b.N; i++ {
+ if err := json.NewEncoder(buf).Encode(value); err != nil {
+ b.Fatal(err)
+ }
+ *buf = (*buf)[:0]
+ }
+ })
+ _ = buf
+}