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