funcr.go

  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}