sigs.k8s.io/cluster-api-provider-azure@v1.14.3/internal/test/record/logger.go (about) 1 /* 2 Copyright 2020 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package record 18 19 import ( 20 "encoding/json" 21 "fmt" 22 "io" 23 "sync" 24 25 "github.com/go-logr/logr" 26 "github.com/google/uuid" 27 "github.com/pkg/errors" 28 ) 29 30 // LogEntry defines the information that can be used for composing a log line. 31 type LogEntry struct { 32 // Prefix of the log line, composed of the hierarchy of log.WithName values. 33 Prefix string 34 35 // LogFunc of the log entry, e.g. "Info", "Error" 36 LogFunc string 37 38 // Level of the LogEntry. 39 Level int 40 41 // Values of the log line, composed of the concatenation of log.WithValues and KeyValue pairs passed to log.Info. 42 Values []interface{} 43 } 44 45 // Option is a configuration option supplied to NewLogger. 46 type Option func(*Logger) 47 48 // WithThreshold implements a New Option that allows to set the threshold level for a new logger. 49 // The logger will write only log messages with a level/V(x) equal or higher to the threshold. 50 func WithThreshold(threshold *int) Option { 51 return func(c *Logger) { 52 c.threshold = threshold 53 } 54 } 55 56 // WithWriter configures the logger to write to the io.Writer. 57 func WithWriter(wr io.Writer) Option { 58 return func(c *Logger) { 59 c.writer = wr 60 } 61 } 62 63 // NewLogger returns a new instance of the clusterctl. 64 func NewLogger(options ...Option) *Logger { 65 l := &Logger{ 66 listeners: map[string]*Listener{}, 67 } 68 for _, o := range options { 69 o(l) 70 } 71 return l 72 } 73 74 var _ logr.LogSink = (*Logger)(nil) 75 76 type ( 77 // Logger defines a test-friendly logr.Logger. 78 Logger struct { 79 threshold *int 80 level int 81 prefix string 82 values []interface{} 83 listenerMu sync.Mutex 84 listeners map[string]*Listener 85 writer io.Writer 86 root *Logger 87 cloneMu sync.Mutex 88 info logr.RuntimeInfo 89 } 90 91 // Listener defines a listener for log entries. 92 Listener struct { 93 logger *Logger 94 entriesMu sync.RWMutex 95 entries []LogEntry 96 } 97 ) 98 99 // NewListener returns a new listener with the specified logger. 100 func NewListener(logger *Logger) *Listener { 101 return &Listener{ 102 logger: logger, 103 } 104 } 105 106 // Listen adds this listener to its logger. 107 func (li *Listener) Listen() func() { 108 return li.logger.addListener(li) 109 } 110 111 // GetEntries returns a copy of the list of log entries. 112 func (li *Listener) GetEntries() []LogEntry { 113 li.entriesMu.RLock() 114 defer li.entriesMu.RUnlock() 115 116 return copyLogEntries(li.entries) 117 } 118 119 func (li *Listener) addEntry(entry LogEntry) { 120 li.entriesMu.Lock() 121 defer li.entriesMu.Unlock() 122 123 li.entries = append(li.entries, entry) 124 } 125 126 var _ logr.LogSink = &Logger{} 127 128 func (l *Logger) addListener(listener *Listener) func() { 129 if l.root != nil { 130 return l.root.addListener(listener) 131 } 132 133 l.listenerMu.Lock() 134 defer l.listenerMu.Unlock() 135 136 id := uuid.New().String() 137 l.listeners[id] = listener 138 return func() { 139 l.removeListener(id) 140 } 141 } 142 143 func (l *Logger) removeListener(id string) { 144 l.listenerMu.Lock() 145 defer l.listenerMu.Unlock() 146 147 delete(l.listeners, id) 148 } 149 150 // Init initializes the logger from runtime information. 151 func (l *Logger) Init(info logr.RuntimeInfo) { 152 l.info = info 153 } 154 155 // Enabled is always enabled. 156 func (l *Logger) Enabled(v int) bool { 157 return true 158 } 159 160 // Info logs a non-error message with the given key/value pairs as context. 161 func (l *Logger) Info(level int, msg string, kvs ...interface{}) { 162 values := copySlice(l.values) 163 values = append(values, kvs...) 164 values = append(values, "msg", msg) 165 l.write("Info", values) 166 } 167 168 // Error logs an error message with the given key/value pairs as context. 169 func (l *Logger) Error(err error, msg string, kvs ...interface{}) { 170 values := copySlice(l.values) 171 values = append(values, kvs...) 172 values = append(values, "msg", msg, "error", err) 173 l.write("Error", values) 174 } 175 176 // V returns an Logger value for a specific verbosity level. 177 func (l *Logger) V(level int) logr.LogSink { 178 nl := l.clone() 179 nl.level = level 180 return nl 181 } 182 183 // WithName adds a new element to the logger's name. 184 func (l *Logger) WithName(name string) logr.LogSink { 185 nl := l.clone() 186 if len(l.prefix) > 0 { 187 nl.prefix = l.prefix + "/" 188 } 189 nl.prefix += name 190 return nl 191 } 192 193 // WithValues adds some key-value pairs of context to a logger. 194 func (l *Logger) WithValues(kvList ...interface{}) logr.LogSink { 195 nl := l.clone() 196 nl.values = append(nl.values, kvList...) 197 return nl 198 } 199 200 func (l *Logger) write(logFunc string, values []interface{}) { 201 entry := LogEntry{ 202 Prefix: l.prefix, 203 LogFunc: logFunc, 204 Level: l.level, 205 Values: copySlice(values), 206 } 207 f, err := flatten(entry) 208 if err != nil { 209 panic(err) 210 } 211 212 l.writeToListeners(entry) 213 214 if l.writer != nil { 215 str := fmt.Sprintf("%s\n", f) 216 if _, err = l.writer.Write([]byte(str)); err != nil { 217 panic(err) 218 } 219 return 220 } 221 222 fmt.Println(f) 223 } 224 225 func (l *Logger) writeToListeners(entry LogEntry) { 226 if l.root != nil { 227 l.root.writeToListeners(entry) 228 return 229 } 230 231 l.listenerMu.Lock() 232 defer l.listenerMu.Unlock() 233 234 for _, listener := range l.listeners { 235 listener.addEntry(entry) 236 } 237 } 238 239 func (l *Logger) clone() *Logger { 240 l.cloneMu.Lock() 241 defer l.cloneMu.Unlock() 242 243 root := l.root 244 if root == nil { 245 root = l 246 } 247 248 return &Logger{ 249 threshold: l.threshold, 250 level: l.level, 251 prefix: l.prefix, 252 values: copySlice(l.values), 253 writer: l.writer, 254 root: root, 255 } 256 } 257 258 func copyLogEntries(in []LogEntry) []LogEntry { 259 out := make([]LogEntry, len(in)) 260 copy(out, in) 261 return out 262 } 263 264 func copySlice(in []interface{}) []interface{} { 265 out := make([]interface{}, len(in)) 266 copy(out, in) 267 return out 268 } 269 270 // flatten returns a human readable/machine parsable text representing the LogEntry. 271 // Most notable difference with the klog implementation are: 272 // - The message is printed at the beginning of the line, without the Msg= variable name e.g. 273 // "Msg"="This is a message" --> This is a message 274 // - Variables name are not quoted, eg. 275 // This is a message "Var1"="value" --> This is a message Var1="value" 276 // - Variables are not sorted, thus allowing full control to the developer on the output. 277 func flatten(entry LogEntry) (string, error) { 278 var msgValue string 279 var errorValue error 280 if len(entry.Values)%2 == 1 { 281 return "", errors.New("log entry cannot have odd number off keyAndValues") 282 } 283 284 keys := make([]string, 0, len(entry.Values)/2) 285 values := make(map[string]interface{}, len(entry.Values)/2) 286 for i := 0; i < len(entry.Values); i += 2 { 287 k, ok := entry.Values[i].(string) 288 if !ok { 289 panic(fmt.Sprintf("key is not a string: %s", entry.Values[i])) 290 } 291 var v interface{} 292 if i+1 < len(entry.Values) { 293 v = entry.Values[i+1] 294 } 295 switch k { 296 case "msg": 297 msgValue, ok = v.(string) 298 if !ok { 299 panic(fmt.Sprintf("the msg value is not of type string: %s", v)) 300 } 301 case "error": 302 errorValue, ok = v.(error) 303 if !ok { 304 panic(fmt.Sprintf("the error value is not of type error: %s", v)) 305 } 306 default: 307 if _, ok := values[k]; !ok { 308 keys = append(keys, k) 309 } 310 values[k] = v 311 } 312 } 313 str := "" 314 if entry.Prefix != "" { 315 str += fmt.Sprintf("[%s] ", entry.Prefix) 316 } 317 str += msgValue 318 if errorValue != nil { 319 if msgValue != "" { 320 str += ": " 321 } 322 str += errorValue.Error() 323 } 324 for _, k := range keys { 325 prettyValue, err := pretty(values[k]) 326 if err != nil { 327 return "", err 328 } 329 str += fmt.Sprintf(" %s=%s", k, prettyValue) 330 } 331 return str, nil 332 } 333 334 func pretty(value interface{}) (string, error) { 335 jb, err := json.Marshal(value) 336 if err != nil { 337 return "", errors.Wrapf(err, "Failed to marshal %s", value) 338 } 339 return string(jb), nil 340 }