1/*
2Copyright 2021 The logr Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8 http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17// Package funcr implements formatting of structured log messages and
18// optionally captures the call site and timestamp.
19//
20// The simplest way to use it is via its implementation of a
21// github.com/go-logr/logr.LogSink with output through an arbitrary
22// "write" function. See New and NewJSON for details.
23//
24// # Custom LogSinks
25//
26// For users who need more control, a funcr.Formatter can be embedded inside
27// your own custom LogSink implementation. This is useful when the LogSink
28// needs to implement additional methods, for example.
29//
30// # Formatting
31//
32// This will respect logr.Marshaler, fmt.Stringer, and error interfaces for
33// values which are being logged. When rendering a struct, funcr will use Go's
34// standard JSON tags (all except "string").
35package funcr
36
37import (
38 "bytes"
39 "encoding"
40 "encoding/json"
41 "fmt"
42 "path/filepath"
43 "reflect"
44 "runtime"
45 "strconv"
46 "strings"
47 "time"
48
49 "github.com/go-logr/logr"
50)
51
52// New returns a logr.Logger which is implemented by an arbitrary function.
53func New(fn func(prefix, args string), opts Options) logr.Logger {
54 return logr.New(newSink(fn, NewFormatter(opts)))
55}
56
57// NewJSON returns a logr.Logger which is implemented by an arbitrary function
58// and produces JSON output.
59func NewJSON(fn func(obj string), opts Options) logr.Logger {
60 fnWrapper := func(_, obj string) {
61 fn(obj)
62 }
63 return logr.New(newSink(fnWrapper, NewFormatterJSON(opts)))
64}
65
66// Underlier exposes access to the underlying logging function. Since
67// callers only have a logr.Logger, they have to know which
68// implementation is in use, so this interface is less of an
69// abstraction and more of a way to test type conversion.
70type Underlier interface {
71 GetUnderlying() func(prefix, args string)
72}
73
74func newSink(fn func(prefix, args string), formatter Formatter) logr.LogSink {
75 l := &fnlogger{
76 Formatter: formatter,
77 write: fn,
78 }
79 // For skipping fnlogger.Info and fnlogger.Error.
80 l.Formatter.AddCallDepth(1)
81 return l
82}
83
84// Options carries parameters which influence the way logs are generated.
85type Options struct {
86 // LogCaller tells funcr to add a "caller" key to some or all log lines.
87 // This has some overhead, so some users might not want it.
88 LogCaller MessageClass
89
90 // LogCallerFunc tells funcr to also log the calling function name. This
91 // has no effect if caller logging is not enabled (see Options.LogCaller).
92 LogCallerFunc bool
93
94 // LogTimestamp tells funcr to add a "ts" key to log lines. This has some
95 // overhead, so some users might not want it.
96 LogTimestamp bool
97
98 // TimestampFormat tells funcr how to render timestamps when LogTimestamp
99 // is enabled. If not specified, a default format will be used. For more
100 // details, see docs for Go's time.Layout.
101 TimestampFormat string
102
103 // LogInfoLevel tells funcr what key to use to log the info level.
104 // If not specified, the info level will be logged as "level".
105 // If this is set to "", the info level will not be logged at all.
106 LogInfoLevel *string
107
108 // Verbosity tells funcr which V logs to produce. Higher values enable
109 // more logs. Info logs at or below this level will be written, while logs
110 // above this level will be discarded.
111 Verbosity int
112
113 // RenderBuiltinsHook allows users to mutate the list of key-value pairs
114 // while a log line is being rendered. The kvList argument follows logr
115 // conventions - each pair of slice elements is comprised of a string key
116 // and an arbitrary value (verified and sanitized before calling this
117 // hook). The value returned must follow the same conventions. This hook
118 // can be used to audit or modify logged data. For example, you might want
119 // to prefix all of funcr's built-in keys with some string. This hook is
120 // only called for built-in (provided by funcr itself) key-value pairs.
121 // Equivalent hooks are offered for key-value pairs saved via
122 // logr.Logger.WithValues or Formatter.AddValues (see RenderValuesHook) and
123 // for user-provided pairs (see RenderArgsHook).
124 RenderBuiltinsHook func(kvList []any) []any
125
126 // RenderValuesHook is the same as RenderBuiltinsHook, except that it is
127 // only called for key-value pairs saved via logr.Logger.WithValues. See
128 // RenderBuiltinsHook for more details.
129 RenderValuesHook func(kvList []any) []any
130
131 // RenderArgsHook is the same as RenderBuiltinsHook, except that it is only
132 // called for key-value pairs passed directly to Info and Error. See
133 // RenderBuiltinsHook for more details.
134 RenderArgsHook func(kvList []any) []any
135
136 // MaxLogDepth tells funcr how many levels of nested fields (e.g. a struct
137 // that contains a struct, etc.) it may log. Every time it finds a struct,
138 // slice, array, or map the depth is increased by one. When the maximum is
139 // reached, the value will be converted to a string indicating that the max
140 // depth has been exceeded. If this field is not specified, a default
141 // value will be used.
142 MaxLogDepth int
143}
144
145// MessageClass indicates which category or categories of messages to consider.
146type MessageClass int
147
148const (
149 // None ignores all message classes.
150 None MessageClass = iota
151 // All considers all message classes.
152 All
153 // Info only considers info messages.
154 Info
155 // Error only considers error messages.
156 Error
157)
158
159// fnlogger inherits some of its LogSink implementation from Formatter
160// and just needs to add some glue code.
161type fnlogger struct {
162 Formatter
163 write func(prefix, args string)
164}
165
166func (l fnlogger) WithName(name string) logr.LogSink {
167 l.Formatter.AddName(name)
168 return &l
169}
170
171func (l fnlogger) WithValues(kvList ...any) logr.LogSink {
172 l.Formatter.AddValues(kvList)
173 return &l
174}
175
176func (l fnlogger) WithCallDepth(depth int) logr.LogSink {
177 l.Formatter.AddCallDepth(depth)
178 return &l
179}
180
181func (l fnlogger) Info(level int, msg string, kvList ...any) {
182 prefix, args := l.FormatInfo(level, msg, kvList)
183 l.write(prefix, args)
184}
185
186func (l fnlogger) Error(err error, msg string, kvList ...any) {
187 prefix, args := l.FormatError(err, msg, kvList)
188 l.write(prefix, args)
189}
190
191func (l fnlogger) GetUnderlying() func(prefix, args string) {
192 return l.write
193}
194
195// Assert conformance to the interfaces.
196var _ logr.LogSink = &fnlogger{}
197var _ logr.CallDepthLogSink = &fnlogger{}
198var _ Underlier = &fnlogger{}
199
200// NewFormatter constructs a Formatter which emits a JSON-like key=value format.
201func NewFormatter(opts Options) Formatter {
202 return newFormatter(opts, outputKeyValue)
203}
204
205// NewFormatterJSON constructs a Formatter which emits strict JSON.
206func NewFormatterJSON(opts Options) Formatter {
207 return newFormatter(opts, outputJSON)
208}
209
210// Defaults for Options.
211const defaultTimestampFormat = "2006-01-02 15:04:05.000000"
212const defaultMaxLogDepth = 16
213
214func newFormatter(opts Options, outfmt outputFormat) Formatter {
215 if opts.TimestampFormat == "" {
216 opts.TimestampFormat = defaultTimestampFormat
217 }
218 if opts.MaxLogDepth == 0 {
219 opts.MaxLogDepth = defaultMaxLogDepth
220 }
221 if opts.LogInfoLevel == nil {
222 opts.LogInfoLevel = new(string)
223 *opts.LogInfoLevel = "level"
224 }
225 f := Formatter{
226 outputFormat: outfmt,
227 prefix: "",
228 values: nil,
229 depth: 0,
230 opts: &opts,
231 }
232 return f
233}
234
235// Formatter is an opaque struct which can be embedded in a LogSink
236// implementation. It should be constructed with NewFormatter. Some of
237// its methods directly implement logr.LogSink.
238type Formatter struct {
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
247}
248
249// outputFormat indicates which outputFormat to use.
250type outputFormat int
251
252const (
253 // outputKeyValue emits a JSON-like key=value format, but not strict JSON.
254 outputKeyValue outputFormat = iota
255 // outputJSON emits strict JSON.
256 outputJSON
257)
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.
261type groupDef struct {
262 name string
263 values string
264}
265
266// PseudoStruct is a list of key-value pairs that gets logged as a struct.
267type PseudoStruct []any
268
269// render produces a log line, ready to use.
270func (f Formatter) render(builtins, args []any) string {
271 // Empirically bytes.Buffer is faster than strings.Builder for this.
272 buf := bytes.NewBuffer(make([]byte, 0, 1024))
273
274 if f.outputFormat == outputJSON {
275 buf.WriteByte('{') // for the whole record
276 }
277
278 // Render builtins
279 vals := builtins
280 if hook := f.opts.RenderBuiltinsHook; hook != nil {
281 vals = hook(f.sanitize(vals))
282 }
283 f.flatten(buf, vals, false) // keys are ours, no need to escape
284 continuing := len(builtins) > 0
285
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))
293 }
294 f.flatten(buf, vals, true) // escape user-provided keys
295
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
306 }
307 bodyStr = f.renderGroup(grp.name, grp.values, bodyStr)
308 }
309
310 if bodyStr != "" {
311 if continuing {
312 buf.WriteByte(f.comma())
313 }
314 buf.WriteString(bodyStr)
315 }
316
317 if f.outputFormat == outputJSON {
318 buf.WriteByte('}') // for the whole record
319 }
320
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.
330func (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
339 }
340
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('}')
356 }
357
358 return buf.String()
359}
360
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.
364//
365// This function returns a potentially modified version of kvList, which
366// ensures that there is a value for every key (adding a value if needed) and
367// that each key is a string (substituting a key if needed).
368func (f Formatter) flatten(buf *bytes.Buffer, kvList []any, escapeKeys bool) []any {
369 // This logic overlaps with sanitize() but saves one type-cast per key,
370 // which can be measurable.
371 if len(kvList)%2 != 0 {
372 kvList = append(kvList, noValue)
373 }
374 copied := false
375 for i := 0; i < len(kvList); i += 2 {
376 k, ok := kvList[i].(string)
377 if !ok {
378 if !copied {
379 newList := make([]any, len(kvList))
380 copy(newList, kvList)
381 kvList = newList
382 copied = true
383 }
384 k = f.nonStringKey(kvList[i])
385 kvList[i] = k
386 }
387 v := kvList[i+1]
388
389 if i > 0 {
390 if f.outputFormat == outputJSON {
391 buf.WriteByte(f.comma())
392 } else {
393 // In theory the format could be something we don't understand. In
394 // practice, we control it, so it won't be.
395 buf.WriteByte(' ')
396 }
397 }
398
399 buf.WriteString(f.quoted(k, escapeKeys))
400 buf.WriteByte(f.colon())
401 buf.WriteString(f.pretty(v))
402 }
403 return kvList
404}
405
406func (f Formatter) quoted(str string, escape bool) string {
407 if escape {
408 return prettyString(str)
409 }
410 // this is faster
411 return `"` + str + `"`
412}
413
414func (f Formatter) comma() byte {
415 if f.outputFormat == outputJSON {
416 return ','
417 }
418 return ' '
419}
420
421func (f Formatter) colon() byte {
422 if f.outputFormat == outputJSON {
423 return ':'
424 }
425 return '='
426}
427
428func (f Formatter) pretty(value any) string {
429 return f.prettyWithFlags(value, 0, 0)
430}
431
432const (
433 flagRawStruct = 0x1 // do not print braces on structs
434)
435
436// TODO: This is not fast. Most of the overhead goes here.
437func (f Formatter) prettyWithFlags(value any, flags uint32, depth int) string {
438 if depth > f.opts.MaxLogDepth {
439 return `"<max-log-depth-exceeded>"`
440 }
441
442 // Handle types that take full control of logging.
443 if v, ok := value.(logr.Marshaler); ok {
444 // Replace the value with what the type wants to get logged.
445 // That then gets handled below via reflection.
446 value = invokeMarshaler(v)
447 }
448
449 // Handle types that want to format themselves.
450 switch v := value.(type) {
451 case fmt.Stringer:
452 value = invokeStringer(v)
453 case error:
454 value = invokeError(v)
455 }
456
457 // Handling the most common types without reflect is a small perf win.
458 switch v := value.(type) {
459 case bool:
460 return strconv.FormatBool(v)
461 case string:
462 return prettyString(v)
463 case int:
464 return strconv.FormatInt(int64(v), 10)
465 case int8:
466 return strconv.FormatInt(int64(v), 10)
467 case int16:
468 return strconv.FormatInt(int64(v), 10)
469 case int32:
470 return strconv.FormatInt(int64(v), 10)
471 case int64:
472 return strconv.FormatInt(int64(v), 10)
473 case uint:
474 return strconv.FormatUint(uint64(v), 10)
475 case uint8:
476 return strconv.FormatUint(uint64(v), 10)
477 case uint16:
478 return strconv.FormatUint(uint64(v), 10)
479 case uint32:
480 return strconv.FormatUint(uint64(v), 10)
481 case uint64:
482 return strconv.FormatUint(v, 10)
483 case uintptr:
484 return strconv.FormatUint(uint64(v), 10)
485 case float32:
486 return strconv.FormatFloat(float64(v), 'f', -1, 32)
487 case float64:
488 return strconv.FormatFloat(v, 'f', -1, 64)
489 case complex64:
490 return `"` + strconv.FormatComplex(complex128(v), 'f', -1, 64) + `"`
491 case complex128:
492 return `"` + strconv.FormatComplex(v, 'f', -1, 128) + `"`
493 case PseudoStruct:
494 buf := bytes.NewBuffer(make([]byte, 0, 1024))
495 v = f.sanitize(v)
496 if flags&flagRawStruct == 0 {
497 buf.WriteByte('{')
498 }
499 for i := 0; i < len(v); i += 2 {
500 if i > 0 {
501 buf.WriteByte(f.comma())
502 }
503 k, _ := v[i].(string) // sanitize() above means no need to check success
504 // arbitrary keys might need escaping
505 buf.WriteString(prettyString(k))
506 buf.WriteByte(f.colon())
507 buf.WriteString(f.prettyWithFlags(v[i+1], 0, depth+1))
508 }
509 if flags&flagRawStruct == 0 {
510 buf.WriteByte('}')
511 }
512 return buf.String()
513 }
514
515 buf := bytes.NewBuffer(make([]byte, 0, 256))
516 t := reflect.TypeOf(value)
517 if t == nil {
518 return "null"
519 }
520 v := reflect.ValueOf(value)
521 switch t.Kind() {
522 case reflect.Bool:
523 return strconv.FormatBool(v.Bool())
524 case reflect.String:
525 return prettyString(v.String())
526 case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
527 return strconv.FormatInt(int64(v.Int()), 10)
528 case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
529 return strconv.FormatUint(uint64(v.Uint()), 10)
530 case reflect.Float32:
531 return strconv.FormatFloat(float64(v.Float()), 'f', -1, 32)
532 case reflect.Float64:
533 return strconv.FormatFloat(v.Float(), 'f', -1, 64)
534 case reflect.Complex64:
535 return `"` + strconv.FormatComplex(complex128(v.Complex()), 'f', -1, 64) + `"`
536 case reflect.Complex128:
537 return `"` + strconv.FormatComplex(v.Complex(), 'f', -1, 128) + `"`
538 case reflect.Struct:
539 if flags&flagRawStruct == 0 {
540 buf.WriteByte('{')
541 }
542 printComma := false // testing i>0 is not enough because of JSON omitted fields
543 for i := 0; i < t.NumField(); i++ {
544 fld := t.Field(i)
545 if fld.PkgPath != "" {
546 // reflect says this field is only defined for non-exported fields.
547 continue
548 }
549 if !v.Field(i).CanInterface() {
550 // reflect isn't clear exactly what this means, but we can't use it.
551 continue
552 }
553 name := ""
554 omitempty := false
555 if tag, found := fld.Tag.Lookup("json"); found {
556 if tag == "-" {
557 continue
558 }
559 if comma := strings.Index(tag, ","); comma != -1 {
560 if n := tag[:comma]; n != "" {
561 name = n
562 }
563 rest := tag[comma:]
564 if strings.Contains(rest, ",omitempty,") || strings.HasSuffix(rest, ",omitempty") {
565 omitempty = true
566 }
567 } else {
568 name = tag
569 }
570 }
571 if omitempty && isEmpty(v.Field(i)) {
572 continue
573 }
574 if printComma {
575 buf.WriteByte(f.comma())
576 }
577 printComma = true // if we got here, we are rendering a field
578 if fld.Anonymous && fld.Type.Kind() == reflect.Struct && name == "" {
579 buf.WriteString(f.prettyWithFlags(v.Field(i).Interface(), flags|flagRawStruct, depth+1))
580 continue
581 }
582 if name == "" {
583 name = fld.Name
584 }
585 // field names can't contain characters which need escaping
586 buf.WriteString(f.quoted(name, false))
587 buf.WriteByte(f.colon())
588 buf.WriteString(f.prettyWithFlags(v.Field(i).Interface(), 0, depth+1))
589 }
590 if flags&flagRawStruct == 0 {
591 buf.WriteByte('}')
592 }
593 return buf.String()
594 case reflect.Slice, reflect.Array:
595 // If this is outputing as JSON make sure this isn't really a json.RawMessage.
596 // If so just emit "as-is" and don't pretty it as that will just print
597 // it as [X,Y,Z,...] which isn't terribly useful vs the string form you really want.
598 if f.outputFormat == outputJSON {
599 if rm, ok := value.(json.RawMessage); ok {
600 // If it's empty make sure we emit an empty value as the array style would below.
601 if len(rm) > 0 {
602 buf.Write(rm)
603 } else {
604 buf.WriteString("null")
605 }
606 return buf.String()
607 }
608 }
609 buf.WriteByte('[')
610 for i := 0; i < v.Len(); i++ {
611 if i > 0 {
612 buf.WriteByte(f.comma())
613 }
614 e := v.Index(i)
615 buf.WriteString(f.prettyWithFlags(e.Interface(), 0, depth+1))
616 }
617 buf.WriteByte(']')
618 return buf.String()
619 case reflect.Map:
620 buf.WriteByte('{')
621 // This does not sort the map keys, for best perf.
622 it := v.MapRange()
623 i := 0
624 for it.Next() {
625 if i > 0 {
626 buf.WriteByte(f.comma())
627 }
628 // If a map key supports TextMarshaler, use it.
629 keystr := ""
630 if m, ok := it.Key().Interface().(encoding.TextMarshaler); ok {
631 txt, err := m.MarshalText()
632 if err != nil {
633 keystr = fmt.Sprintf("<error-MarshalText: %s>", err.Error())
634 } else {
635 keystr = string(txt)
636 }
637 keystr = prettyString(keystr)
638 } else {
639 // prettyWithFlags will produce already-escaped values
640 keystr = f.prettyWithFlags(it.Key().Interface(), 0, depth+1)
641 if t.Key().Kind() != reflect.String {
642 // JSON only does string keys. Unlike Go's standard JSON, we'll
643 // convert just about anything to a string.
644 keystr = prettyString(keystr)
645 }
646 }
647 buf.WriteString(keystr)
648 buf.WriteByte(f.colon())
649 buf.WriteString(f.prettyWithFlags(it.Value().Interface(), 0, depth+1))
650 i++
651 }
652 buf.WriteByte('}')
653 return buf.String()
654 case reflect.Ptr, reflect.Interface:
655 if v.IsNil() {
656 return "null"
657 }
658 return f.prettyWithFlags(v.Elem().Interface(), 0, depth)
659 }
660 return fmt.Sprintf(`"<unhandled-%s>"`, t.Kind().String())
661}
662
663func prettyString(s string) string {
664 // Avoid escaping (which does allocations) if we can.
665 if needsEscape(s) {
666 return strconv.Quote(s)
667 }
668 b := bytes.NewBuffer(make([]byte, 0, 1024))
669 b.WriteByte('"')
670 b.WriteString(s)
671 b.WriteByte('"')
672 return b.String()
673}
674
675// needsEscape determines whether the input string needs to be escaped or not,
676// without doing any allocations.
677func needsEscape(s string) bool {
678 for _, r := range s {
679 if !strconv.IsPrint(r) || r == '\\' || r == '"' {
680 return true
681 }
682 }
683 return false
684}
685
686func isEmpty(v reflect.Value) bool {
687 switch v.Kind() {
688 case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
689 return v.Len() == 0
690 case reflect.Bool:
691 return !v.Bool()
692 case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
693 return v.Int() == 0
694 case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
695 return v.Uint() == 0
696 case reflect.Float32, reflect.Float64:
697 return v.Float() == 0
698 case reflect.Complex64, reflect.Complex128:
699 return v.Complex() == 0
700 case reflect.Interface, reflect.Ptr:
701 return v.IsNil()
702 }
703 return false
704}
705
706func invokeMarshaler(m logr.Marshaler) (ret any) {
707 defer func() {
708 if r := recover(); r != nil {
709 ret = fmt.Sprintf("<panic: %s>", r)
710 }
711 }()
712 return m.MarshalLog()
713}
714
715func invokeStringer(s fmt.Stringer) (ret string) {
716 defer func() {
717 if r := recover(); r != nil {
718 ret = fmt.Sprintf("<panic: %s>", r)
719 }
720 }()
721 return s.String()
722}
723
724func invokeError(e error) (ret string) {
725 defer func() {
726 if r := recover(); r != nil {
727 ret = fmt.Sprintf("<panic: %s>", r)
728 }
729 }()
730 return e.Error()
731}
732
733// Caller represents the original call site for a log line, after considering
734// logr.Logger.WithCallDepth and logr.Logger.WithCallStackHelper. The File and
735// Line fields will always be provided, while the Func field is optional.
736// Users can set the render hook fields in Options to examine logged key-value
737// pairs, one of which will be {"caller", Caller} if the Options.LogCaller
738// field is enabled for the given MessageClass.
739type Caller struct {
740 // File is the basename of the file for this call site.
741 File string `json:"file"`
742 // Line is the line number in the file for this call site.
743 Line int `json:"line"`
744 // Func is the function name for this call site, or empty if
745 // Options.LogCallerFunc is not enabled.
746 Func string `json:"function,omitempty"`
747}
748
749func (f Formatter) caller() Caller {
750 // +1 for this frame, +1 for Info/Error.
751 pc, file, line, ok := runtime.Caller(f.depth + 2)
752 if !ok {
753 return Caller{"<unknown>", 0, ""}
754 }
755 fn := ""
756 if f.opts.LogCallerFunc {
757 if fp := runtime.FuncForPC(pc); fp != nil {
758 fn = fp.Name()
759 }
760 }
761
762 return Caller{filepath.Base(file), line, fn}
763}
764
765const noValue = "<no-value>"
766
767func (f Formatter) nonStringKey(v any) string {
768 return fmt.Sprintf("<non-string-key: %s>", f.snippet(v))
769}
770
771// snippet produces a short snippet string of an arbitrary value.
772func (f Formatter) snippet(v any) string {
773 const snipLen = 16
774
775 snip := f.pretty(v)
776 if len(snip) > snipLen {
777 snip = snip[:snipLen]
778 }
779 return snip
780}
781
782// sanitize ensures that a list of key-value pairs has a value for every key
783// (adding a value if needed) and that each key is a string (substituting a key
784// if needed).
785func (f Formatter) sanitize(kvList []any) []any {
786 if len(kvList)%2 != 0 {
787 kvList = append(kvList, noValue)
788 }
789 for i := 0; i < len(kvList); i += 2 {
790 _, ok := kvList[i].(string)
791 if !ok {
792 kvList[i] = f.nonStringKey(kvList[i])
793 }
794 }
795 return kvList
796}
797
798// startGroup opens a new group scope (basically a sub-struct), which locks all
799// the current saved values and starts them anew. This is needed to satisfy
800// slog.
801func (f *Formatter) startGroup(name string) {
802 // Unnamed groups are just inlined.
803 if name == "" {
804 return
805 }
806
807 n := len(f.groups)
808 f.groups = append(f.groups[:n:n], groupDef{f.groupName, f.valuesStr})
809
810 // Start collecting new values.
811 f.groupName = name
812 f.valuesStr = ""
813 f.values = nil
814}
815
816// Init configures this Formatter from runtime info, such as the call depth
817// imposed by logr itself.
818// Note that this receiver is a pointer, so depth can be saved.
819func (f *Formatter) Init(info logr.RuntimeInfo) {
820 f.depth += info.CallDepth
821}
822
823// Enabled checks whether an info message at the given level should be logged.
824func (f Formatter) Enabled(level int) bool {
825 return level <= f.opts.Verbosity
826}
827
828// GetDepth returns the current depth of this Formatter. This is useful for
829// implementations which do their own caller attribution.
830func (f Formatter) GetDepth() int {
831 return f.depth
832}
833
834// FormatInfo renders an Info log message into strings. The prefix will be
835// empty when no names were set (via AddNames), or when the output is
836// configured for JSON.
837func (f Formatter) FormatInfo(level int, msg string, kvList []any) (prefix, argsStr string) {
838 args := make([]any, 0, 64) // using a constant here impacts perf
839 prefix = f.prefix
840 if f.outputFormat == outputJSON {
841 args = append(args, "logger", prefix)
842 prefix = ""
843 }
844 if f.opts.LogTimestamp {
845 args = append(args, "ts", time.Now().Format(f.opts.TimestampFormat))
846 }
847 if policy := f.opts.LogCaller; policy == All || policy == Info {
848 args = append(args, "caller", f.caller())
849 }
850 if key := *f.opts.LogInfoLevel; key != "" {
851 args = append(args, key, level)
852 }
853 args = append(args, "msg", msg)
854 return prefix, f.render(args, kvList)
855}
856
857// FormatError renders an Error log message into strings. The prefix will be
858// empty when no names were set (via AddNames), or when the output is
859// configured for JSON.
860func (f Formatter) FormatError(err error, msg string, kvList []any) (prefix, argsStr string) {
861 args := make([]any, 0, 64) // using a constant here impacts perf
862 prefix = f.prefix
863 if f.outputFormat == outputJSON {
864 args = append(args, "logger", prefix)
865 prefix = ""
866 }
867 if f.opts.LogTimestamp {
868 args = append(args, "ts", time.Now().Format(f.opts.TimestampFormat))
869 }
870 if policy := f.opts.LogCaller; policy == All || policy == Error {
871 args = append(args, "caller", f.caller())
872 }
873 args = append(args, "msg", msg)
874 var loggableErr any
875 if err != nil {
876 loggableErr = err.Error()
877 }
878 args = append(args, "error", loggableErr)
879 return prefix, f.render(args, kvList)
880}
881
882// AddName appends the specified name. funcr uses '/' characters to separate
883// name elements. Callers should not pass '/' in the provided name string, but
884// this library does not actually enforce that.
885func (f *Formatter) AddName(name string) {
886 if len(f.prefix) > 0 {
887 f.prefix += "/"
888 }
889 f.prefix += name
890}
891
892// AddValues adds key-value pairs to the set of saved values to be logged with
893// each log line.
894func (f *Formatter) AddValues(kvList []any) {
895 // Three slice args forces a copy.
896 n := len(f.values)
897 f.values = append(f.values[:n:n], kvList...)
898
899 vals := f.values
900 if hook := f.opts.RenderValuesHook; hook != nil {
901 vals = hook(f.sanitize(vals))
902 }
903
904 // Pre-render values, so we don't have to do it on each Info/Error call.
905 buf := bytes.NewBuffer(make([]byte, 0, 1024))
906 f.flatten(buf, vals, true) // escape user-provided keys
907 f.valuesStr = buf.String()
908}
909
910// AddCallDepth increases the number of stack-frames to skip when attributing
911// the log line to a file and line.
912func (f *Formatter) AddCallDepth(depth int) {
913 f.depth += depth
914}