github.com/banzaicloud/operator-tools@v0.28.10/pkg/logger/logsink.go (about) 1 // Copyright © 2020 Banzai Cloud 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package logger 16 17 import ( 18 "fmt" 19 "io" 20 "os" 21 "strings" 22 "sync" 23 "time" 24 25 "emperror.dev/errors" 26 "github.com/briandowns/spinner" 27 "github.com/fatih/color" 28 "github.com/go-logr/logr" 29 "github.com/spf13/cast" 30 terminal "github.com/wayneashleyberry/terminal-dimensions" 31 ) 32 33 const ( 34 defaultDisableColor = false 35 ) 36 37 type SpinnerLogSink struct { 38 names []string 39 values []interface{} 40 out io.Writer 41 err io.Writer 42 43 grouppable bool 44 truncate bool 45 showTime bool 46 grouped bool 47 disableColor bool 48 checkMark rune 49 errorMark rune 50 separatorCharacter rune 51 timeFormat string 52 53 colors Colors 54 55 spinner *spinner.Spinner 56 57 mux sync.Mutex 58 } 59 60 func NewSpinnerLogSink(options ...Option) *SpinnerLogSink { 61 l := &SpinnerLogSink{ 62 names: []string{}, 63 out: os.Stderr, 64 err: os.Stderr, 65 66 checkMark: '✓', 67 errorMark: '✗', 68 separatorCharacter: '❯', 69 colors: Colors{ 70 Info: color.FgGreen, 71 Error: color.FgRed, 72 Key: color.FgHiGreen, 73 }, 74 disableColor: defaultDisableColor, 75 76 mux: sync.Mutex{}, 77 } 78 79 for _, opt := range options { 80 opt(l) 81 } 82 83 return l 84 } 85 86 func (log *SpinnerLogSink) Init(_ logr.RuntimeInfo) {} 87 88 // Info implements logr.LogSink interface 89 func (log *SpinnerLogSink) Info(level int, msg string, keysAndValues ...interface{}) { 90 91 colorPrinter := log.getColorPrinter(log.colors.Info) 92 93 if !log.Enabled(level) { 94 return 95 } 96 allVal := append(keysAndValues, log.values...) 97 if len(allVal) > 0 { 98 msg = fmt.Sprintf("%s %s", msg, log.joinAndSeparatePairs(allVal)) 99 } 100 101 names := log.printNames() 102 if names != "" { 103 msg = fmt.Sprintf("%s %c %s", names, log.separatorCharacter, msg) 104 } 105 106 if log.timeFormat != "" && log.showTime { 107 msg = fmt.Sprintf("[%s] %s", time.Now().Format(log.timeFormat), msg) 108 } 109 110 if log.truncate { 111 w, _ := terminal.Width() 112 // vscode debug window returns 0 width (without an error) 113 if w > 3 { 114 w -= 3 // reduced by 3 (spinner char, space, leeway) 115 msg = log.truncateString(msg, int(w)) 116 } 117 } 118 119 if log.spinner == nil { 120 log.initSpinner() 121 } 122 123 log.mux.Lock() 124 if log.spinner != nil { 125 log.spinner.Writer = log.out 126 log.spinner.Suffix = " " + msg // Append text after the spinner 127 log.spinner.FinalMSG = colorPrinter.Sprintf("%c", log.checkMark) + log.spinner.Suffix + "\n" 128 } 129 log.mux.Unlock() 130 131 if log.spinner != nil && !log.grouped { 132 log.stopSpinner() 133 } 134 } 135 136 // Enabled implements logr.LogSink interface 137 func (log *SpinnerLogSink) Enabled(level int) bool { 138 return GlobalLogLevel >= level 139 } 140 141 // Error implements logr.LogSink interface 142 func (log *SpinnerLogSink) Error(e error, msg string, keysAndValues ...interface{}) { 143 allVal := append(keysAndValues, log.values...) 144 145 colorPrinter := log.getColorPrinter(log.colors.Error) 146 147 if msg != "" { 148 msg = colorPrinter.Sprintf("%s: %s", msg, log.getDetailedErr(e)) 149 } else { 150 msg = colorPrinter.Sprintf("%s", log.getDetailedErr(e)) 151 } 152 if len(allVal) > 0 { 153 msg = fmt.Sprintf("%s %s", msg, log.joinAndSeparatePairs(allVal)) 154 } 155 156 names := log.printNames() 157 if names != "" { 158 msg = fmt.Sprintf("%s %c %s", names, log.separatorCharacter, msg) 159 } 160 161 if log.timeFormat != "" && log.showTime { 162 msg = fmt.Sprintf("[%s] %s", time.Now().Format(log.timeFormat), msg) 163 } 164 165 if log.spinner == nil { 166 log.initSpinner() 167 } else { 168 log.spinner.Restart() 169 } 170 171 log.spinner.Writer = log.err 172 log.spinner.Suffix = " " + msg // Append text after the spinner 173 log.spinner.FinalMSG = colorPrinter.Sprintf("%c", log.errorMark) + log.spinner.Suffix + "\n" 174 175 log.stopSpinner() 176 } 177 178 // WithName implements logr.LogSink interface 179 func (log *SpinnerLogSink) WithName(name string) logr.LogSink { 180 l := log.copyLogger() 181 l.names = append(l.names, name) 182 183 return l 184 } 185 186 // WithValues implements logr.LogSink interface 187 func (log *SpinnerLogSink) WithValues(keysAndValues ...interface{}) logr.LogSink { 188 l := log.copyLogger() 189 l.values = append(l.values, keysAndValues...) 190 191 return l 192 } 193 194 func (log *SpinnerLogSink) SetOptions(options ...Option) { 195 for _, opt := range options { 196 opt(log) 197 } 198 } 199 200 func (log *SpinnerLogSink) ShowTime(f bool) *SpinnerLogSink { 201 l := log.copyLogger() 202 l.showTime = f 203 204 return l 205 } 206 207 func (log *SpinnerLogSink) Grouped(state bool) { 208 if !log.grouppable { 209 return 210 } 211 212 if state && log.spinner == nil { 213 log.initSpinner() 214 } else if !state && log.spinner != nil { 215 log.stopSpinner() 216 } 217 218 log.grouped = state 219 } 220 221 func (log *SpinnerLogSink) GetValues() []interface{} { 222 return log.values 223 } 224 225 func (log *SpinnerLogSink) AddValues(keyAndValues []interface{}) { 226 log.values = append(log.values, keyAndValues...) 227 } 228 229 func (log *SpinnerLogSink) AddName(name string) { 230 log.names = append(log.names, name) 231 } 232 233 func (log *SpinnerLogSink) GetGrouppable() bool { 234 return log.grouppable 235 } 236 237 func (log *SpinnerLogSink) SetGrouppable(state bool) { 238 log.grouppable = state 239 } 240 241 func (log *SpinnerLogSink) GetShowTime() bool { 242 return log.showTime 243 } 244 245 func (log *SpinnerLogSink) SetShowTime(time bool) { 246 log.showTime = time 247 } 248 249 func (log *SpinnerLogSink) SetDisableColor(disableColor bool) { 250 log.disableColor = disableColor 251 } 252 253 func (log *SpinnerLogSink) GetSpinner() *spinner.Spinner { 254 return log.spinner 255 } 256 257 func (log *SpinnerLogSink) StopSpinner() { 258 log.stopSpinner() 259 } 260 261 func (log *SpinnerLogSink) InitSpinner() { 262 log.initSpinner() 263 } 264 265 func (log *SpinnerLogSink) Copy() *SpinnerLogSink { 266 return log.copyLogger() 267 } 268 269 func (log *SpinnerLogSink) printNames() string { 270 return strings.Join(log.names, "/") 271 } 272 273 func (log *SpinnerLogSink) initSpinner() { 274 log.mux.Lock() 275 defer log.mux.Unlock() 276 log.spinner = spinner.New( 277 spinner.CharSets[21], 278 100*time.Millisecond, 279 spinner.WithHiddenCursor(false), 280 spinner.WithWriter(log.out), 281 ) 282 _ = log.spinner.Color("green") 283 log.spinner.Start() 284 } 285 286 func (log *SpinnerLogSink) stopSpinner() { 287 log.mux.Lock() 288 defer log.mux.Unlock() 289 if log.spinner != nil { 290 log.spinner.Stop() 291 log.spinner = nil 292 } 293 } 294 295 func (log *SpinnerLogSink) copyLogger() *SpinnerLogSink { 296 names := make([]string, len(log.names)) 297 copy(names, log.names) 298 299 values := make([]interface{}, len(log.values)) 300 copy(values, log.values) 301 302 return &SpinnerLogSink{ 303 names: log.names, 304 values: log.values, 305 out: log.out, 306 err: log.err, 307 grouppable: log.grouppable, 308 truncate: log.truncate, 309 timeFormat: log.timeFormat, 310 showTime: log.showTime, 311 checkMark: log.checkMark, 312 errorMark: log.errorMark, 313 separatorCharacter: log.separatorCharacter, 314 colors: log.colors, 315 disableColor: log.disableColor, 316 317 grouped: log.grouped, 318 spinner: log.spinner, 319 320 mux: sync.Mutex{}, 321 } 322 } 323 324 func (*SpinnerLogSink) truncateString(str string, num int) string { 325 bnoden := str 326 if len(str) > num { 327 if num > 3 { //nolint:gomnd 328 num -= 3 329 } 330 bnoden = str[0:num] + "..." 331 } 332 return bnoden 333 } 334 335 func (log *SpinnerLogSink) joinAndSeparatePairs(values []interface{}) string { 336 joined := "" 337 c := log.colors.Key 338 for i, v := range values { 339 s, err := cast.ToStringE(v) 340 if err != nil { 341 s = fmt.Sprintf("%v", v) 342 } 343 344 colorPrinter := log.getColorPrinter(c) 345 joined += colorPrinter.Sprint(s) 346 347 if i%2 == 0 { 348 c = 0 349 joined += "=" 350 } else { 351 c = log.colors.Key 352 if i < len(values)-1 { 353 joined += ", " 354 } 355 } 356 } 357 return joined 358 } 359 360 func (log *SpinnerLogSink) getDetailedErr(err error) string { 361 if err == nil { 362 return "" 363 } 364 365 details := errors.GetDetails(err) 366 if len(details) == 0 { 367 return err.Error() 368 } 369 return fmt.Sprintf("%s (%s)", err.Error(), log.joinAndSeparatePairs(details)) 370 } 371 372 func (log *SpinnerLogSink) getColorPrinter(attribute color.Attribute) *color.Color { 373 colorPrinter := color.New(attribute) 374 if log.disableColor { 375 colorPrinter.DisableColor() 376 } 377 378 return colorPrinter 379 }