1// Package klogr implements github.com/go-logr/logr.Logger in terms of
2// k8s.io/klog.
3package klogr
4
5import (
6	"bytes"
7	"encoding/json"
8	"fmt"
9	"runtime"
10	"sort"
11	"strings"
12
13	"github.com/go-logr/logr"
14	"k8s.io/klog/v2"
15)
16
17// Option is a functional option that reconfigures the logger created with New.
18type Option func(*klogger)
19
20// Format defines how log output is produced.
21type Format string
22
23const (
24	// FormatSerialize tells klogr to turn key/value pairs into text itself
25	// before invoking klog.
26	FormatSerialize Format = "Serialize"
27
28	// FormatKlog tells klogr to pass all text messages and key/value pairs
29	// directly to klog. Klog itself then serializes in a human-readable
30	// format and optionally passes on to a structure logging backend.
31	FormatKlog Format = "Klog"
32)
33
34// WithFormat selects the output format.
35func WithFormat(format Format) Option {
36	return func(l *klogger) {
37		l.format = format
38	}
39}
40
41// New returns a logr.Logger which serializes output itself
42// and writes it via klog.
43func New() logr.Logger {
44	return NewWithOptions(WithFormat(FormatSerialize))
45}
46
47// NewWithOptions returns a logr.Logger which serializes as determined
48// by the WithFormat option and writes via klog. The default is
49// FormatKlog.
50func NewWithOptions(options ...Option) logr.Logger {
51	l := klogger{
52		level:  0,
53		prefix: "",
54		values: nil,
55		format: FormatKlog,
56	}
57	for _, option := range options {
58		option(&l)
59	}
60	return l
61}
62
63type klogger struct {
64	level     int
65	callDepth int
66	prefix    string
67	values    []interface{}
68	format    Format
69}
70
71func (l klogger) clone() klogger {
72	return klogger{
73		level:  l.level,
74		prefix: l.prefix,
75		values: copySlice(l.values),
76		format: l.format,
77	}
78}
79
80func copySlice(in []interface{}) []interface{} {
81	out := make([]interface{}, len(in))
82	copy(out, in)
83	return out
84}
85
86// Magic string for intermediate frames that we should ignore.
87const autogeneratedFrameName = "<autogenerated>"
88
89// Discover how many frames we need to climb to find the caller. This approach
90// was suggested by Ian Lance Taylor of the Go team, so it *should* be safe
91// enough (famous last words).
92//
93// It is needed because binding the specific klogger functions to the
94// logr interface creates one additional call frame that neither we nor
95// our caller know about.
96func framesToCaller() int {
97	// 1 is the immediate caller.  3 should be too many.
98	for i := 1; i < 3; i++ {
99		_, file, _, _ := runtime.Caller(i + 1) // +1 for this function's frame
100		if file != autogeneratedFrameName {
101			return i
102		}
103	}
104	return 1 // something went wrong, this is safe
105}
106
107// trimDuplicates will deduplicates elements provided in multiple KV tuple
108// slices, whilst maintaining the distinction between where the items are
109// contained.
110func trimDuplicates(kvLists ...[]interface{}) [][]interface{} {
111	// maintain a map of all seen keys
112	seenKeys := map[interface{}]struct{}{}
113	// build the same number of output slices as inputs
114	outs := make([][]interface{}, len(kvLists))
115	// iterate over the input slices backwards, as 'later' kv specifications
116	// of the same key will take precedence over earlier ones
117	for i := len(kvLists) - 1; i >= 0; i-- {
118		// initialise this output slice
119		outs[i] = []interface{}{}
120		// obtain a reference to the kvList we are processing
121		kvList := kvLists[i]
122
123		// start iterating at len(kvList) - 2 (i.e. the 2nd last item) for
124		// slices that have an even number of elements.
125		// We add (len(kvList) % 2) here to handle the case where there is an
126		// odd number of elements in a kvList.
127		// If there is an odd number, then the last element in the slice will
128		// have the value 'null'.
129		for i2 := len(kvList) - 2 + (len(kvList) % 2); i2 >= 0; i2 -= 2 {
130			k := kvList[i2]
131			// if we have already seen this key, do not include it again
132			if _, ok := seenKeys[k]; ok {
133				continue
134			}
135			// make a note that we've observed a new key
136			seenKeys[k] = struct{}{}
137			// attempt to obtain the value of the key
138			var v interface{}
139			// i2+1 should only ever be out of bounds if we handling the first
140			// iteration over a slice with an odd number of elements
141			if i2+1 < len(kvList) {
142				v = kvList[i2+1]
143			}
144			// add this KV tuple to the *start* of the output list to maintain
145			// the original order as we are iterating over the slice backwards
146			outs[i] = append([]interface{}{k, v}, outs[i]...)
147		}
148	}
149	return outs
150}
151
152func flatten(kvList ...interface{}) string {
153	keys := make([]string, 0, len(kvList))
154	vals := make(map[string]interface{}, len(kvList))
155	for i := 0; i < len(kvList); i += 2 {
156		k, ok := kvList[i].(string)
157		if !ok {
158			panic(fmt.Sprintf("key is not a string: %s", pretty(kvList[i])))
159		}
160		var v interface{}
161		if i+1 < len(kvList) {
162			v = kvList[i+1]
163		}
164		keys = append(keys, k)
165		vals[k] = v
166	}
167	sort.Strings(keys)
168	buf := bytes.Buffer{}
169	for i, k := range keys {
170		v := vals[k]
171		if i > 0 {
172			buf.WriteRune(' ')
173		}
174		buf.WriteString(pretty(k))
175		buf.WriteString("=")
176		buf.WriteString(pretty(v))
177	}
178	return buf.String()
179}
180
181func pretty(value interface{}) string {
182	if err, ok := value.(error); ok {
183		if _, ok := value.(json.Marshaler); !ok {
184			value = err.Error()
185		}
186	}
187	buffer := &bytes.Buffer{}
188	encoder := json.NewEncoder(buffer)
189	encoder.SetEscapeHTML(false)
190	encoder.Encode(value)
191	return strings.TrimSpace(string(buffer.Bytes()))
192}
193
194func (l klogger) Info(msg string, kvList ...interface{}) {
195	if l.Enabled() {
196		switch l.format {
197		case FormatSerialize:
198			msgStr := flatten("msg", msg)
199			trimmed := trimDuplicates(l.values, kvList)
200			fixedStr := flatten(trimmed[0]...)
201			userStr := flatten(trimmed[1]...)
202			klog.InfoDepth(framesToCaller()+l.callDepth, l.prefix, " ", msgStr, " ", fixedStr, " ", userStr)
203		case FormatKlog:
204			trimmed := trimDuplicates(l.values, kvList)
205			if l.prefix != "" {
206				msg = l.prefix + ": " + msg
207			}
208			klog.InfoSDepth(framesToCaller()+l.callDepth, msg, append(trimmed[0], trimmed[1]...)...)
209		}
210	}
211}
212
213func (l klogger) Enabled() bool {
214	return bool(klog.V(klog.Level(l.level)).Enabled())
215}
216
217func (l klogger) Error(err error, msg string, kvList ...interface{}) {
218	msgStr := flatten("msg", msg)
219	var loggableErr interface{}
220	if err != nil {
221		loggableErr = err.Error()
222	}
223	switch l.format {
224	case FormatSerialize:
225		errStr := flatten("error", loggableErr)
226		trimmed := trimDuplicates(l.values, kvList)
227		fixedStr := flatten(trimmed[0]...)
228		userStr := flatten(trimmed[1]...)
229		klog.ErrorDepth(framesToCaller()+l.callDepth, l.prefix, " ", msgStr, " ", errStr, " ", fixedStr, " ", userStr)
230	case FormatKlog:
231		trimmed := trimDuplicates(l.values, kvList)
232		if l.prefix != "" {
233			msg = l.prefix + ": " + msg
234		}
235		klog.ErrorSDepth(framesToCaller()+l.callDepth, err, msg, append(trimmed[0], trimmed[1]...)...)
236	}
237}
238
239func (l klogger) V(level int) logr.Logger {
240	new := l.clone()
241	new.level = level
242	return new
243}
244
245// WithName returns a new logr.Logger with the specified name appended.  klogr
246// uses '/' characters to separate name elements.  Callers should not pass '/'
247// in the provided name string, but this library does not actually enforce that.
248func (l klogger) WithName(name string) logr.Logger {
249	new := l.clone()
250	if len(l.prefix) > 0 {
251		new.prefix = l.prefix + "/"
252	}
253	new.prefix += name
254	return new
255}
256
257func (l klogger) WithValues(kvList ...interface{}) logr.Logger {
258	new := l.clone()
259	new.values = append(new.values, kvList...)
260	return new
261}
262
263func (l klogger) WithCallDepth(depth int) logr.Logger {
264	new := l.clone()
265	new.callDepth += depth
266	return new
267}
268
269var _ logr.Logger = klogger{}
270var _ logr.CallDepthLogger = klogger{}
271