1
2
3
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
58 type jsonMarshaler struct {
59 s string
60 }
61
62 func (j jsonMarshaler) String() string { return j.s }
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
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
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 }
281
View as plain text