@@ -97,6 +97,29 @@ type Options struct {
97
97
// more logs. Info logs at or below this level will be written, while logs
98
98
// above this level will be discarded.
99
99
Verbosity int
100
+
101
+ // RenderBuiltinsHook allows users to mutate the list of key-value pairs
102
+ // while a log line is being rendered. The kvList argument follows logr
103
+ // conventions - each pair of slice elements is comprised of a string key
104
+ // and an arbitrary value (verified and sanitized before calling this
105
+ // hook). The value returned must follow the same conventions. This hook
106
+ // can be used to audit or modify logged data. For example, you might want
107
+ // to prefix all of funcr's built-in keys with some string. This hook is
108
+ // only called for built-in (provided by funcr itself) key-value pairs.
109
+ // Equivalent hooks are offered for key-value pairs saved via
110
+ // logr.Logger.WithValues or Formatter.AddValues (see RenderValuesHook) and
111
+ // for user-provided pairs (see RenderArgsHook).
112
+ RenderBuiltinsHook func (kvList []interface {}) []interface {}
113
+
114
+ // RenderValuesHook is the same as RenderBuiltinsHook, except that it is
115
+ // only called for key-value pairs saved via logr.Logger.WithValues. See
116
+ // RenderBuiltinsHook for more details.
117
+ RenderValuesHook func (kvList []interface {}) []interface {}
118
+
119
+ // RenderArgsHook is the same as RenderBuiltinsHook, except that it is only
120
+ // called for key-value pairs passed directly to Info and Error. See
121
+ // RenderBuiltinsHook for more details.
122
+ RenderArgsHook func (kvList []interface {}) []interface {}
100
123
}
101
124
102
125
// MessageClass indicates which category or categories of messages to consider.
@@ -199,14 +222,21 @@ const (
199
222
outputJSON
200
223
)
201
224
202
- // render produces a log-line, ready to use.
225
+ // PseudoStruct is a list of key-value pairs that gets logged as a struct.
226
+ type PseudoStruct []interface {}
227
+
228
+ // render produces a log line, ready to use.
203
229
func (f Formatter ) render (builtins , args []interface {}) string {
204
230
// Empirically bytes.Buffer is faster than strings.Builder for this.
205
231
buf := bytes .NewBuffer (make ([]byte , 0 , 1024 ))
206
232
if f .outputFormat == outputJSON {
207
233
buf .WriteByte ('{' )
208
234
}
209
- f .flatten (buf , builtins , false )
235
+ vals := builtins
236
+ if hook := f .opts .RenderBuiltinsHook ; hook != nil {
237
+ vals = hook (f .sanitize (vals ))
238
+ }
239
+ f .flatten (buf , vals , false )
210
240
continuing := len (builtins ) > 0
211
241
if len (f .valuesStr ) > 0 {
212
242
if continuing {
@@ -219,7 +249,11 @@ func (f Formatter) render(builtins, args []interface{}) string {
219
249
continuing = true
220
250
buf .WriteString (f .valuesStr )
221
251
}
222
- f .flatten (buf , args , continuing )
252
+ vals = args
253
+ if hook := f .opts .RenderArgsHook ; hook != nil {
254
+ vals = hook (f .sanitize (vals ))
255
+ }
256
+ f .flatten (buf , vals , continuing )
223
257
if f .outputFormat == outputJSON {
224
258
buf .WriteByte ('}' )
225
259
}
@@ -233,17 +267,15 @@ func (f Formatter) render(builtins, args []interface{}) string {
233
267
// ensures that there is a value for every key (adding a value if needed) and
234
268
// that each key is a string (substituting a key if needed).
235
269
func (f Formatter ) flatten (buf * bytes.Buffer , kvList []interface {}, continuing bool ) []interface {} {
270
+ // This logic overlaps with sanitize() but saves one type-cast per key,
271
+ // which can be measurable.
236
272
if len (kvList )% 2 != 0 {
237
- kvList = append (kvList , "<no-value>" )
273
+ kvList = append (kvList , noValue )
238
274
}
239
275
for i := 0 ; i < len (kvList ); i += 2 {
240
276
k , ok := kvList [i ].(string )
241
277
if ! ok {
242
- snippet := f .pretty (kvList [i ])
243
- if len (snippet ) > 16 {
244
- snippet = snippet [:16 ]
245
- }
246
- k = fmt .Sprintf ("<non-string-key: %s>" , snippet )
278
+ k = f .nonStringKey (kvList [i ])
247
279
kvList [i ] = k
248
280
}
249
281
v := kvList [i + 1 ]
@@ -253,7 +285,7 @@ func (f Formatter) flatten(buf *bytes.Buffer, kvList []interface{}, continuing b
253
285
buf .WriteByte (',' )
254
286
} else {
255
287
// In theory the format could be something we don't understand. In
256
- // practice, we control it, so it won't
288
+ // practice, we control it, so it won't be.
257
289
buf .WriteByte (' ' )
258
290
}
259
291
}
@@ -337,6 +369,26 @@ func (f Formatter) prettyWithFlags(value interface{}, flags uint32) string {
337
369
return `"` + strconv .FormatComplex (complex128 (v ), 'f' , - 1 , 64 ) + `"`
338
370
case complex128 :
339
371
return `"` + strconv .FormatComplex (v , 'f' , - 1 , 128 ) + `"`
372
+ case PseudoStruct :
373
+ buf := bytes .NewBuffer (make ([]byte , 0 , 1024 ))
374
+ v = f .sanitize (v )
375
+ if flags & flagRawStruct == 0 {
376
+ buf .WriteByte ('{' )
377
+ }
378
+ for i := 0 ; i < len (v ); i += 2 {
379
+ if i > 0 {
380
+ buf .WriteByte (',' )
381
+ }
382
+ buf .WriteByte ('"' )
383
+ buf .WriteString (v [i ].(string ))
384
+ buf .WriteByte ('"' )
385
+ buf .WriteByte (':' )
386
+ buf .WriteString (f .pretty (v [i + 1 ]))
387
+ }
388
+ if flags & flagRawStruct == 0 {
389
+ buf .WriteByte ('}' )
390
+ }
391
+ return buf .String ()
340
392
}
341
393
342
394
buf := bytes .NewBuffer (make ([]byte , 0 , 256 ))
@@ -480,17 +532,27 @@ func isEmpty(v reflect.Value) bool {
480
532
return false
481
533
}
482
534
483
- type callerID struct {
535
+ // Caller represents the original call site for a log line, after considering
536
+ // logr.Logger.WithCallDepth and logr.Logger.WithCallStackHelper. The File and
537
+ // Line fields will always be provided, while the Func field is optional.
538
+ // Users can set the render hook fields in Options to examine logged key-value
539
+ // pairs, one of which will be {"caller", Caller} if the Options.LogCaller
540
+ // field is enabled for the given MessageClass.
541
+ type Caller struct {
542
+ // File is the basename of the file for this call site.
484
543
File string `json:"file"`
485
- Line int `json:"line"`
544
+ // Line is the line number in the file for this call site.
545
+ Line int `json:"line"`
546
+ // Func is the function name for this call site, or empty if
547
+ // Options.LogCallerFunc is not enabled.
486
548
Func string `json:"function,omitempty"`
487
549
}
488
550
489
- func (f Formatter ) caller () callerID {
551
+ func (f Formatter ) caller () Caller {
490
552
// +1 for this frame, +1 for Info/Error.
491
553
pc , file , line , ok := runtime .Caller (f .depth + 2 )
492
554
if ! ok {
493
- return callerID {"<unknown>" , 0 , "" }
555
+ return Caller {"<unknown>" , 0 , "" }
494
556
}
495
557
fn := ""
496
558
if f .opts .LogCallerFunc {
@@ -499,7 +561,40 @@ func (f Formatter) caller() callerID {
499
561
}
500
562
}
501
563
502
- return callerID {filepath .Base (file ), line , fn }
564
+ return Caller {filepath .Base (file ), line , fn }
565
+ }
566
+
567
+ const noValue = "<no-value>"
568
+
569
+ func (f Formatter ) nonStringKey (v interface {}) string {
570
+ return fmt .Sprintf ("<non-string-key: %s>" , f .snippet (v ))
571
+ }
572
+
573
+ // snippet produces a short snippet string of an arbitrary value.
574
+ func (f Formatter ) snippet (v interface {}) string {
575
+ const snipLen = 16
576
+
577
+ snip := f .pretty (v )
578
+ if len (snip ) > snipLen {
579
+ snip = snip [:snipLen ]
580
+ }
581
+ return snip
582
+ }
583
+
584
+ // sanitize ensures that a list of key-value pairs has a value for every key
585
+ // (adding a value if needed) and that each key is a string (substituting a key
586
+ // if needed).
587
+ func (f Formatter ) sanitize (kvList []interface {}) []interface {} {
588
+ if len (kvList )% 2 != 0 {
589
+ kvList = append (kvList , noValue )
590
+ }
591
+ for i := 0 ; i < len (kvList ); i += 2 {
592
+ _ , ok := kvList [i ].(string )
593
+ if ! ok {
594
+ kvList [i ] = f .nonStringKey (kvList [i ])
595
+ }
596
+ }
597
+ return kvList
503
598
}
504
599
505
600
// Init configures this Formatter from runtime info, such as the call depth
@@ -520,8 +615,8 @@ func (f Formatter) GetDepth() int {
520
615
return f .depth
521
616
}
522
617
523
- // FormatInfo flattens an Info log message into strings.
524
- // The prefix will be empty when no names were set, or when the output is
618
+ // FormatInfo renders an Info log message into strings. The prefix will be
619
+ // empty when no names were set (via AddNames) , or when the output is
525
620
// configured for JSON.
526
621
func (f Formatter ) FormatInfo (level int , msg string , kvList []interface {}) (prefix , argsStr string ) {
527
622
args := make ([]interface {}, 0 , 64 ) // using a constant here impacts perf
@@ -540,8 +635,8 @@ func (f Formatter) FormatInfo(level int, msg string, kvList []interface{}) (pref
540
635
return prefix , f .render (args , kvList )
541
636
}
542
637
543
- // FormatError flattens an Error log message into strings.
544
- // The prefix will be empty when no names were set, or when the output is
638
+ // FormatError renders an Error log message into strings. The prefix will be
639
+ // empty when no names were set (via AddNames) , or when the output is
545
640
// configured for JSON.
546
641
func (f Formatter ) FormatError (err error , msg string , kvList []interface {}) (prefix , argsStr string ) {
547
642
args := make ([]interface {}, 0 , 64 ) // using a constant here impacts perf
@@ -578,11 +673,16 @@ func (f *Formatter) AddName(name string) {
578
673
// AddValues adds key-value pairs to the set of saved values to be logged with
579
674
// each log line.
580
675
func (f * Formatter ) AddValues (kvList []interface {}) {
581
- // Pre-render values, so we don't have to do it on each Info/Error call.
582
- buf := bytes .NewBuffer (make ([]byte , 0 , 1024 ))
583
676
// Three slice args forces a copy.
584
677
n := len (f .values )
585
- f .values = f .flatten (buf , append (f .values [:n :n ], kvList ... ), false )
678
+ vals := append (f .values [:n :n ], kvList ... )
679
+ if hook := f .opts .RenderValuesHook ; hook != nil {
680
+ vals = hook (f .sanitize (vals ))
681
+ }
682
+
683
+ // Pre-render values, so we don't have to do it on each Info/Error call.
684
+ buf := bytes .NewBuffer (make ([]byte , 0 , 1024 ))
685
+ f .values = f .flatten (buf , vals , false )
586
686
f .valuesStr = buf .String ()
587
687
}
588
688
0 commit comments