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  }