github.com/CycloneDX/sbom-utility@v0.16.0/log/log.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 /* 3 * Licensed to the Apache Software Foundation (ASF) under one or more 4 * contributor license agreements. See the NOTICE file distributed with 5 * this work for additional information regarding copyright ownership. 6 * The ASF licenses this file to You under the Apache License, Version 2.0 7 * (the "License"); you may not use this file except in compliance with 8 * the License. You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, software 13 * distributed under the License is distributed on an "AS IS" BASIS, 14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 * See the License for the specific language governing permissions and 16 * limitations under the License. 17 */ 18 19 package log 20 21 import ( 22 "bufio" 23 "bytes" 24 "errors" 25 "fmt" 26 "io" 27 "os" 28 "reflect" 29 "runtime" 30 "runtime/debug" 31 "strings" 32 "time" 33 34 "github.com/fatih/color" 35 ) 36 37 type Level int 38 39 // Skip 2 on call stack 40 // i.e., skip public (Caller) method (e.g., "Trace()" and internal 41 // "dumpInterface()" function 42 const ( 43 STACK_SKIP int = 2 44 MAX_INDENT uint = 8 45 ) 46 47 // WARNING: some functional logic may assume incremental ordering of levels 48 const ( 49 ERROR Level = iota // 0 - Always output errors (stop execution) 50 WARNING // 1 - Always output warnings (continue executing) 51 INFO // 2 - General processing information (processing milestones) 52 TRACE // 3 - In addition to INFO, output functional info. (signature, parameter) 53 DEBUG // 4 - In addition to TRACE, output internal logic and intra-functional data 54 ) 55 56 // Assure default ENTER and EXIT default tags have same fixed-length chars. 57 // for better output alignment 58 const ( 59 DEFAULT_ENTER_TAG = "ENTER" 60 DEFAULT_EXIT_TAG = "EXIT " 61 ) 62 63 // TODO: Allow colorization to be a configurable option. 64 // on (default): for human-readable targets (e.g., console); 65 // off: for (remote) logging targets (file, network) stream 66 // See colors here: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors 67 var LevelNames = map[Level]string{ 68 DEBUG: color.GreenString("DEBUG"), 69 TRACE: color.CyanString("TRACE"), 70 INFO: color.WhiteString("INFO"), 71 WARNING: color.HiYellowString("WARN"), 72 ERROR: color.HiRedString("ERROR"), 73 } 74 75 var DEFAULT_LEVEL = INFO 76 var DEFAULT_INDENT_RUNE = []rune("") 77 var DEFAULT_INCREMENT_RUNE = []rune("") 78 79 // TODO: Support Unwrap() interface (i.e., %w) on all formatted output commands; 80 // however, it is a necessity for error-type output (e.g., Errorf(), Warningf()) 81 // TODO: allow timestamps to be turned on/off regardless of defaults 82 // TODO: allow colors to be set for each constituent part of the (TRACE) output 83 // TODO: allow multiple tags (with diff. colors) that can be enabled/disabled from the calling code 84 type MiniLogger struct { 85 logLevel Level 86 indentEnabled bool 87 indentRunes []rune 88 spacesIncrement []rune 89 tagEnter string 90 tagExit string 91 tagColor *color.Color 92 quietMode bool 93 outputFile io.Writer 94 outputWriter *bufio.Writer 95 maxStrLength int 96 } 97 98 func NewDefaultLogger() *MiniLogger { 99 logger := &MiniLogger{ 100 logLevel: DEFAULT_LEVEL, 101 indentEnabled: false, 102 indentRunes: DEFAULT_INDENT_RUNE, 103 spacesIncrement: DEFAULT_INCREMENT_RUNE, 104 tagEnter: DEFAULT_ENTER_TAG, 105 tagExit: DEFAULT_EXIT_TAG, 106 tagColor: color.New(color.FgMagenta), 107 outputFile: os.Stdout, 108 maxStrLength: 64, 109 } 110 111 // TODO: Use this instead of fmt.Print() variant functions 112 logger.outputWriter = bufio.NewWriter(logger.outputFile) 113 114 return logger 115 } 116 117 func NewLogger(level Level) *MiniLogger { 118 newLogger := NewDefaultLogger() 119 newLogger.SetLevel(level) 120 121 return newLogger 122 } 123 124 func (log *MiniLogger) EnableIndent(enable bool) { 125 log.indentEnabled = enable 126 } 127 128 func (log *MiniLogger) SetLevel(level Level) { 129 log.logLevel = level 130 } 131 132 func (log *MiniLogger) GetLevel() Level { 133 return log.logLevel 134 } 135 136 func (log *MiniLogger) SetQuietMode(on bool) { 137 log.quietMode = on 138 } 139 140 func (log *MiniLogger) QuietModeOn() bool { 141 return log.quietMode 142 } 143 144 func (log *MiniLogger) GetLevelName() string { 145 return LevelNames[log.logLevel] 146 } 147 148 // Helper method to check for and set typical log-related flags 149 // NOTE: Assumes these do not collide with existing flags set by importing application 150 // NOTE: "go test" utilizes the Go "flags" package and allows 151 // test packages to declare additional command line arguments 152 // which can be used to set log/trace levels (e.g., `--args --trace). 153 // The values for these variables are only avail. after init() processing is completed. 154 // See: https://go.dev/doc/go1.13#testing 155 // "Testing flags are now registered in the new Init function, which is invoked by the 156 // generated main function for the test. As a result, testing flags are now only registered 157 // when running a test binary, and packages that call flag.Parse during package initialization 158 // may cause tests to fail." 159 func (log *MiniLogger) InitLogLevelAndModeFromFlags() Level { 160 161 // NOTE: Uncomment to debug avail. args. during init. 162 // log.DumpArgs() 163 164 // Check for log-related flags (anywhere) and apply to logger 165 // as early as possible (before customary Cobra flag formalization) 166 // NOTE: the last log-level flag found, in order of appearance "wins" 167 // NOTE: Always use the `--args` flag of `go test` as this will assure non-conflict 168 // with built-in flags. 169 // NOTE: flags MUST be defined within the "test" package or `go test` will error 170 // e.g., var TestLogLevelError = flag.Bool("error", false, "") 171 for _, arg := range os.Args[1:] { 172 switch { 173 case arg == "-q" || arg == "-quiet" || arg == "--quiet" || arg == "quiet": 174 log.SetQuietMode(true) 175 case arg == "-t" || arg == "-trace" || arg == "--trace" || arg == "trace": 176 log.SetLevel(TRACE) 177 case arg == "-d" || arg == "-debug" || arg == "--debug" || arg == "debug": 178 log.SetLevel(DEBUG) 179 case arg == "--indent": 180 log.EnableIndent(true) 181 } 182 } 183 184 return log.GetLevel() 185 } 186 187 func (log *MiniLogger) Flush() (err error) { 188 if log.outputWriter != nil { 189 err = log.outputWriter.Flush() 190 } 191 return 192 } 193 194 func (log MiniLogger) Trace(value interface{}) { 195 log.dumpInterface(TRACE, "", value, STACK_SKIP) 196 } 197 198 func (log MiniLogger) Tracef(format string, value ...interface{}) { 199 message := fmt.Sprintf(format, value...) 200 log.dumpInterface(TRACE, "", message, STACK_SKIP) 201 } 202 203 func (log MiniLogger) Debug(value interface{}) { 204 log.dumpInterface(DEBUG, "", value, STACK_SKIP) 205 } 206 207 func (log MiniLogger) Debugf(format string, value ...interface{}) { 208 message := fmt.Sprintf(format, value...) 209 log.dumpInterface(DEBUG, "", message, STACK_SKIP) 210 } 211 212 func (log MiniLogger) Info(value interface{}) { 213 log.dumpInterface(INFO, "", value, STACK_SKIP) 214 } 215 216 func (log MiniLogger) Infof(format string, value ...interface{}) { 217 message := fmt.Sprintf(format, value...) 218 log.dumpInterface(INFO, "", message, STACK_SKIP) 219 } 220 221 func (log MiniLogger) Warning(value interface{}) { 222 log.dumpInterface(WARNING, "", value, STACK_SKIP) 223 } 224 225 func (log MiniLogger) Warningf(format string, value ...interface{}) { 226 message := fmt.Sprintf(format, value...) 227 log.dumpInterface(WARNING, "", message, STACK_SKIP) 228 } 229 230 // TODO: use fmt.fError in some manner and/or os.Stderr 231 func (log MiniLogger) Error(value interface{}) { 232 log.dumpInterface(ERROR, "", value, STACK_SKIP) 233 } 234 235 func (log MiniLogger) Errorf(format string, value ...interface{}) error { 236 err := fmt.Errorf(format, value...) 237 log.dumpInterface(ERROR, "", err, STACK_SKIP) 238 return err 239 } 240 241 // Specialized function entry/exit trace 242 // Note: can pass in "args[]" or params as needed to have a single logging line 243 func (log *MiniLogger) Enter(values ...interface{}) { 244 245 if log.logLevel >= TRACE { 246 sb := bytes.NewBufferString("") 247 if len(values) > 0 { 248 sb.WriteByte('(') 249 for index, value := range values { 250 sb.WriteString(fmt.Sprintf("(%T):%+v", value, value)) 251 if (index + 1) < len(values) { 252 sb.WriteString(", ") 253 } 254 255 } 256 sb.WriteByte(')') 257 } 258 log.dumpInterface(TRACE, log.tagColor.Sprintf(log.tagEnter), sb.String(), STACK_SKIP) 259 260 if log.indentEnabled { 261 // increase stack indent 262 log.indentRunes = append(log.indentRunes, ' ', ' ') 263 } 264 } 265 } 266 267 // exit and print returned values (typed) 268 // Note: can function "returns" as needed to have a single logging line 269 func (log *MiniLogger) Exit(values ...interface{}) { 270 271 if log.logLevel >= TRACE { 272 sb := bytes.NewBufferString("") 273 if len(values) > 0 { 274 sb.WriteByte('(') 275 for index, value := range values { 276 sb.WriteString(fmt.Sprintf("(%T): %+v", value, value)) 277 if (index + 1) < len(values) { 278 sb.WriteString(", ") 279 } 280 } 281 sb.WriteByte(')') 282 } 283 284 if log.indentEnabled { 285 // decrease stack indent 286 if length := len(log.indentRunes) - 2; length >= 0 { 287 log.indentRunes = log.indentRunes[:len(log.indentRunes)-2] 288 } 289 } 290 291 log.dumpInterface(TRACE, log.tagColor.Sprintf(log.tagExit), sb.String(), STACK_SKIP) 292 } 293 } 294 295 // Note: currently, "dump" methods output directly to stdout (stderr) 296 // Note: we comment out any "self-logging" or 'debug" for performance for release builds 297 // compose log output using a "byte buffer" for performance 298 func (log MiniLogger) dumpInterface(lvl Level, tag string, value interface{}, skip int) { 299 300 // Check for quiet mode enabled; 301 // if so, suppress any logging that is not an error 302 // Note: Quiet mode even means NO error output... that is, caller MUST 303 // use application return code value to detect an error condition 304 //fmt.Printf("Quiet mode: %t", log.quietMode) 305 if log.quietMode { 306 return 307 } 308 309 sb := bytes.NewBufferString("") 310 311 // indent based upon current callstack (as incremented/decremented via Enter/Exit funcs.) 312 if log.indentEnabled { 313 sb.WriteString(string(log.indentRunes)) 314 } 315 316 // Only (prepare to) output if intended log level is less than 317 // the current globally set log level 318 if lvl <= log.logLevel { 319 // retrieve all the info we might need 320 pc, fn, line, ok := runtime.Caller(skip) 321 322 // TODO: Provide means to order component output; 323 // for example, to add Timestamp component first (on each line) before Level 324 if ok { 325 // Setup "string builder" and initialize with log-level prefix 326 sb.WriteString(fmt.Sprintf("[%s] ", LevelNames[lvl])) 327 328 // Append UTC timestamp if level is TRACE or DEBUG 329 if log.logLevel == TRACE || log.logLevel == DEBUG { 330 // Append (optional) tag 331 if tag != "" { 332 sb.WriteString(fmt.Sprintf("[%s] ", tag)) 333 } 334 335 // UTC time shows fractions of a second 336 // TODO: add setting to show milli or micro seconds supported by "time" package 337 tmp := time.Now().UTC().String() 338 // create a (left) slice of the timestamp omitting the " +0000 UTC" portion 339 //ts = fmt.Sprintf("[%s] ", tmp[:strings.Index(tmp, "+")-1]) 340 sb.WriteString(fmt.Sprintf("[%s] ", tmp[:strings.Index(tmp, "+")-1])) 341 } 342 343 // Append calling callstack/function information 344 // for log levels used for developer problem determination 345 if log.logLevel == TRACE || log.logLevel == DEBUG || log.logLevel == ERROR { 346 347 // Append basic filename, line number, function name 348 basicFile := fn[strings.LastIndex(fn, "/")+1:] 349 sb.WriteString(fmt.Sprintf("%s(%d) ", basicFile, line)) 350 351 // TODO: add logger flag to show full module paths (not just module.function)\ 352 function := runtime.FuncForPC(pc) 353 basicModFnName := function.Name()[strings.LastIndex(function.Name(), "/")+1:] 354 sb.WriteString(fmt.Sprintf("%s() ", basicModFnName)) 355 } 356 357 // Append (optional) value if supplied 358 // Note: callers SHOULD resolve to string when possible to avoid empty output from interfaces 359 if value != nil && value != "" { 360 sb.WriteString(fmt.Sprintf("%+v", value)) 361 } 362 363 // TODO: use a general output writer (set to stdout, stderr, or file stream) 364 fmt.Println(sb.String()) 365 } else { 366 os.Stderr.WriteString("Error: Unable to retrieve call stack. Exiting...") 367 os.Exit(-2) 368 } 369 } 370 } 371 372 func (log MiniLogger) DumpString(value string) { 373 fmt.Print(value) 374 } 375 376 func (log MiniLogger) DumpStruct(structName string, field interface{}) error { 377 378 sb := bytes.NewBufferString("") 379 formattedStruct, err := log.FormatStructE(field) 380 381 if err != nil { 382 return err 383 } 384 385 if structName != "" { 386 sb.WriteString(fmt.Sprintf("`%s` (%T) = %s", structName, reflect.TypeOf(field), formattedStruct)) 387 } else { 388 sb.WriteString(formattedStruct) 389 } 390 391 // TODO: print to output stream 392 fmt.Println(sb.String()) 393 394 return nil 395 } 396 397 func (log MiniLogger) DumpArgs() { 398 args := os.Args 399 for i, a := range args { 400 // TODO: print to output stream 401 fmt.Print(log.indentRunes) 402 fmt.Printf("os.Arg[%d]: `%v`\n", i, a) 403 } 404 } 405 406 func (log MiniLogger) DumpSeparator(sep byte, repeat int) (string, error) { 407 if repeat <= 80 { 408 sb := bytes.NewBufferString("") 409 for i := 0; i < repeat; i++ { 410 sb.WriteByte(sep) 411 } 412 fmt.Println(sb.String()) 413 return sb.String(), nil 414 } else { 415 return "", errors.New("invalid repeat length (>80)") 416 } 417 } 418 419 func (log *MiniLogger) DumpStackTrace() { 420 fmt.Println(string(debug.Stack())) 421 }