@@ -236,15 +236,14 @@ func newFormatter(opts Options, outfmt outputFormat) Formatter {
236
236
// implementation. It should be constructed with NewFormatter. Some of
237
237
// its methods directly implement logr.LogSink.
238
238
type Formatter struct {
239
- outputFormat outputFormat
240
- prefix string
241
- values []any
242
- valuesStr string
243
- parentValuesStr string
244
- depth int
245
- opts * Options
246
- group string // for slog groups
247
- groupDepth int
239
+ outputFormat outputFormat
240
+ prefix string
241
+ values []any
242
+ valuesStr string
243
+ depth int
244
+ opts * Options
245
+ groupName string // for slog groups
246
+ groups []groupDef
248
247
}
249
248
250
249
// outputFormat indicates which outputFormat to use.
@@ -257,83 +256,116 @@ const (
257
256
outputJSON
258
257
)
259
258
259
+ // groupDef represents a saved group. The values may be empty, but we don't
260
+ // know if we need to render the group until the final record is rendered.
261
+ type groupDef struct {
262
+ name string
263
+ values string
264
+ }
265
+
260
266
// PseudoStruct is a list of key-value pairs that gets logged as a struct.
261
267
type PseudoStruct []any
262
268
263
269
// render produces a log line, ready to use.
264
270
func (f Formatter ) render (builtins , args []any ) string {
265
271
// Empirically bytes.Buffer is faster than strings.Builder for this.
266
272
buf := bytes .NewBuffer (make ([]byte , 0 , 1024 ))
273
+
267
274
if f .outputFormat == outputJSON {
268
- buf .WriteByte ('{' ) // for the whole line
275
+ buf .WriteByte ('{' ) // for the whole record
269
276
}
270
277
278
+ // Render builtins
271
279
vals := builtins
272
280
if hook := f .opts .RenderBuiltinsHook ; hook != nil {
273
281
vals = hook (f .sanitize (vals ))
274
282
}
275
- f .flatten (buf , vals , false , false ) // keys are ours, no need to escape
283
+ f .flatten (buf , vals , false ) // keys are ours, no need to escape
276
284
continuing := len (builtins ) > 0
277
285
278
- if f .parentValuesStr != "" {
279
- if continuing {
280
- buf .WriteByte (f .comma ())
286
+ // Turn the inner-most group into a string
287
+ argsStr := func () string {
288
+ buf := bytes .NewBuffer (make ([]byte , 0 , 1024 ))
289
+
290
+ vals = args
291
+ if hook := f .opts .RenderArgsHook ; hook != nil {
292
+ vals = hook (f .sanitize (vals ))
281
293
}
282
- buf .WriteString (f .parentValuesStr )
283
- continuing = true
284
- }
294
+ f .flatten (buf , vals , true ) // escape user-provided keys
285
295
286
- groupDepth := f .groupDepth
287
- if f .group != "" {
288
- if f .valuesStr != "" || len (args ) != 0 {
289
- if continuing {
290
- buf .WriteByte (f .comma ())
291
- }
292
- buf .WriteString (f .quoted (f .group , true )) // escape user-provided keys
293
- buf .WriteByte (f .colon ())
294
- buf .WriteByte ('{' ) // for the group
295
- continuing = false
296
- } else {
297
- // The group was empty
298
- groupDepth --
296
+ return buf .String ()
297
+ }()
298
+
299
+ // Render the stack of groups from the inside out.
300
+ bodyStr := f .renderGroup (f .groupName , f .valuesStr , argsStr )
301
+ for i := len (f .groups ) - 1 ; i >= 0 ; i -- {
302
+ grp := & f .groups [i ]
303
+ if grp .values == "" && bodyStr == "" {
304
+ // no contents, so we must elide the whole group
305
+ continue
299
306
}
307
+ bodyStr = f .renderGroup (grp .name , grp .values , bodyStr )
300
308
}
301
309
302
- if f . valuesStr != "" {
310
+ if bodyStr != "" {
303
311
if continuing {
304
312
buf .WriteByte (f .comma ())
305
313
}
306
- buf .WriteString (f .valuesStr )
307
- continuing = true
314
+ buf .WriteString (bodyStr )
308
315
}
309
316
310
- vals = args
311
- if hook := f .opts .RenderArgsHook ; hook != nil {
312
- vals = hook (f .sanitize (vals ))
317
+ if f .outputFormat == outputJSON {
318
+ buf .WriteByte ('}' ) // for the whole record
313
319
}
314
- f .flatten (buf , vals , continuing , true ) // escape user-provided keys
315
320
316
- for i := 0 ; i < groupDepth ; i ++ {
317
- buf .WriteByte ('}' ) // for the groups
321
+ return buf .String ()
322
+ }
323
+
324
+ // renderGroup returns a string representation of the named group with rendered
325
+ // values and args. If the name is empty, this will return the values and args,
326
+ // joined. If the name is not empty, this will return a single key-value pair,
327
+ // where the value is a grouping of the values and args. If the values and
328
+ // args are both empty, this will return an empty string, even if the name was
329
+ // specified.
330
+ func (f Formatter ) renderGroup (name string , values string , args string ) string {
331
+ buf := bytes .NewBuffer (make ([]byte , 0 , 1024 ))
332
+
333
+ needClosingBrace := false
334
+ if name != "" && (values != "" || args != "" ) {
335
+ buf .WriteString (f .quoted (name , true )) // escape user-provided keys
336
+ buf .WriteByte (f .colon ())
337
+ buf .WriteByte ('{' )
338
+ needClosingBrace = true
318
339
}
319
340
320
- if f .outputFormat == outputJSON {
321
- buf .WriteByte ('}' ) // for the whole line
341
+ continuing := false
342
+ if values != "" {
343
+ buf .WriteString (values )
344
+ continuing = true
345
+ }
346
+
347
+ if args != "" {
348
+ if continuing {
349
+ buf .WriteByte (f .comma ())
350
+ }
351
+ buf .WriteString (args )
352
+ }
353
+
354
+ if needClosingBrace {
355
+ buf .WriteByte ('}' )
322
356
}
323
357
324
358
return buf .String ()
325
359
}
326
360
327
- // flatten renders a list of key-value pairs into a buffer. If continuing is
328
- // true, it assumes that the buffer has previous values and will emit a
329
- // separator (which depends on the output format) before the first pair it
330
- // writes. If escapeKeys is true, the keys are assumed to have
331
- // non-JSON-compatible characters in them and must be evaluated for escapes.
361
+ // flatten renders a list of key-value pairs into a buffer. If escapeKeys is
362
+ // true, the keys are assumed to have non-JSON-compatible characters in them
363
+ // and must be evaluated for escapes.
332
364
//
333
365
// This function returns a potentially modified version of kvList, which
334
366
// ensures that there is a value for every key (adding a value if needed) and
335
367
// that each key is a string (substituting a key if needed).
336
- func (f Formatter ) flatten (buf * bytes.Buffer , kvList []any , continuing bool , escapeKeys bool ) []any {
368
+ func (f Formatter ) flatten (buf * bytes.Buffer , kvList []any , escapeKeys bool ) []any {
337
369
// This logic overlaps with sanitize() but saves one type-cast per key,
338
370
// which can be measurable.
339
371
if len (kvList )% 2 != 0 {
@@ -354,7 +386,7 @@ func (f Formatter) flatten(buf *bytes.Buffer, kvList []any, continuing bool, esc
354
386
}
355
387
v := kvList [i + 1 ]
356
388
357
- if i > 0 || continuing {
389
+ if i > 0 {
358
390
if f .outputFormat == outputJSON {
359
391
buf .WriteByte (f .comma ())
360
392
} else {
@@ -766,46 +798,17 @@ func (f Formatter) sanitize(kvList []any) []any {
766
798
// startGroup opens a new group scope (basically a sub-struct), which locks all
767
799
// the current saved values and starts them anew. This is needed to satisfy
768
800
// slog.
769
- func (f * Formatter ) startGroup (group string ) {
801
+ func (f * Formatter ) startGroup (name string ) {
770
802
// Unnamed groups are just inlined.
771
- if group == "" {
803
+ if name == "" {
772
804
return
773
805
}
774
806
775
- // Any saved values can no longer be changed.
776
- buf := bytes .NewBuffer (make ([]byte , 0 , 1024 ))
777
- continuing := false
778
-
779
- if f .parentValuesStr != "" {
780
- buf .WriteString (f .parentValuesStr )
781
- continuing = true
782
- }
783
-
784
- if f .group != "" && f .valuesStr != "" {
785
- if continuing {
786
- buf .WriteByte (f .comma ())
787
- }
788
- buf .WriteString (f .quoted (f .group , true )) // escape user-provided keys
789
- buf .WriteByte (f .colon ())
790
- buf .WriteByte ('{' ) // for the group
791
- continuing = false
792
- }
793
-
794
- if f .valuesStr != "" {
795
- if continuing {
796
- buf .WriteByte (f .comma ())
797
- }
798
- buf .WriteString (f .valuesStr )
799
- }
800
-
801
- // NOTE: We don't close the scope here - that's done later, when a log line
802
- // is actually rendered (because we have N scopes to close).
803
-
804
- f .parentValuesStr = buf .String ()
807
+ n := len (f .groups )
808
+ f .groups = append (f .groups [:n :n ], groupDef {f .groupName , f .valuesStr })
805
809
806
810
// Start collecting new values.
807
- f .group = group
808
- f .groupDepth ++
811
+ f .groupName = name
809
812
f .valuesStr = ""
810
813
f .values = nil
811
814
}
@@ -900,7 +903,7 @@ func (f *Formatter) AddValues(kvList []any) {
900
903
901
904
// Pre-render values, so we don't have to do it on each Info/Error call.
902
905
buf := bytes .NewBuffer (make ([]byte , 0 , 1024 ))
903
- f .flatten (buf , vals , false , true ) // escape user-provided keys
906
+ f .flatten (buf , vals , true ) // escape user-provided keys
904
907
f .valuesStr = buf .String ()
905
908
}
906
909
0 commit comments