github.com/decred/dcrlnd@v0.7.6/build/log.go (about)

     1  package build
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"strings"
     7  
     8  	"github.com/decred/slog"
     9  )
    10  
    11  // LogType is an indicating the type of logging specified by the build flag.
    12  type LogType byte
    13  
    14  const (
    15  	// LogTypeNone indicates no logging.
    16  	LogTypeNone LogType = iota
    17  
    18  	// LogTypeStdOut all logging is written directly to stdout.
    19  	LogTypeStdOut
    20  
    21  	// LogTypeDefault logs to both stdout and a given io.PipeWriter.
    22  	LogTypeDefault
    23  )
    24  
    25  // String returns a human readable identifier for the logging type.
    26  func (t LogType) String() string {
    27  	switch t {
    28  	case LogTypeNone:
    29  		return "none"
    30  	case LogTypeStdOut:
    31  		return "stdout"
    32  	case LogTypeDefault:
    33  		return "default"
    34  	default:
    35  		return "unknown"
    36  	}
    37  }
    38  
    39  // LogWriter is a stub type whose behavior can be changed using the build flags
    40  // "stdlog" and "nolog". The default behavior is to write to both stdout and the
    41  // RotatorPipe. Passing "stdlog" will cause it only to write to stdout, and
    42  // "nolog" implements Write as a no-op.
    43  type LogWriter struct {
    44  	// RotatorPipe is the write-end pipe for writing to the log rotator.  It
    45  	// is written to by the Write method of the LogWriter type. This only
    46  	// needs to be set if neither the stdlog or nolog builds are set.
    47  	RotatorPipe *io.PipeWriter
    48  }
    49  
    50  // NewSubLogger constructs a new subsystem log from the current LogWriter
    51  // implementation. This is primarily intended for use with stdlog, as the actual
    52  // writer is shared amongst all instantiations.
    53  func NewSubLogger(subsystem string,
    54  	genSubLogger func(string) slog.Logger) slog.Logger {
    55  
    56  	switch Deployment {
    57  
    58  	// For production builds, generate a new subsystem logger from the
    59  	// primary log backend. If no function is provided, logging will be
    60  	// disabled.
    61  	case Production:
    62  		if genSubLogger != nil {
    63  			return genSubLogger(subsystem)
    64  		}
    65  
    66  	// For development builds, we must handle two distinct types of logging:
    67  	// unit tests and running the live daemon, e.g. for integration testing.
    68  	case Development:
    69  		switch LoggingType {
    70  
    71  		// Default logging is used when running the standalone daemon.
    72  		// We'll use the optional sublogger constructor to mimic the
    73  		// production behavior.
    74  		case LogTypeDefault:
    75  			if genSubLogger != nil {
    76  				return genSubLogger(subsystem)
    77  			}
    78  
    79  		// Logging to stdout is used in unit tests. It is not important
    80  		// that they share the same backend, since all output is written
    81  		// to std out.
    82  		case LogTypeStdOut:
    83  			backend := slog.NewBackend(&LogWriter{})
    84  			logger := backend.Logger(subsystem)
    85  
    86  			// Set the logging level of the stdout logger to use the
    87  			// configured logging level specified by build flags.
    88  			level, _ := slog.LevelFromString(LogLevel)
    89  			logger.SetLevel(level)
    90  
    91  			return logger
    92  		}
    93  	}
    94  
    95  	// For any other configurations, we'll disable logging.
    96  	return slog.Disabled
    97  }
    98  
    99  // SubLoggers is a type that holds a map of subsystem loggers keyed by their
   100  // subsystem name.
   101  type SubLoggers map[string]slog.Logger
   102  
   103  // LeveledSubLogger provides the ability to retrieve the subsystem loggers of
   104  // a logger and set their log levels individually or all at once.
   105  type LeveledSubLogger interface {
   106  	// SubLoggers returns the map of all registered subsystem loggers.
   107  	SubLoggers() SubLoggers
   108  
   109  	// SupportedSubsystems returns a slice of strings containing the names
   110  	// of the supported subsystems. Should ideally correspond to the keys
   111  	// of the subsystem logger map and be sorted.
   112  	SupportedSubsystems() []string
   113  
   114  	// SetLogLevel assigns an individual subsystem logger a new log level.
   115  	SetLogLevel(subsystemID string, logLevel string)
   116  
   117  	// SetLogLevels assigns all subsystem loggers the same new log level.
   118  	SetLogLevels(logLevel string)
   119  }
   120  
   121  // ParseAndSetDebugLevels attempts to parse the specified debug level and set
   122  // the levels accordingly on the given logger. An appropriate error is returned
   123  // if anything is invalid.
   124  func ParseAndSetDebugLevels(level string, logger LeveledSubLogger) error {
   125  	// Split at the delimiter.
   126  	levels := strings.Split(level, ",")
   127  	if len(levels) == 0 {
   128  		return fmt.Errorf("invalid log level: %v", level)
   129  	}
   130  
   131  	// If the first entry has no =, treat is as the log level for all
   132  	// subsystems.
   133  	globalLevel := levels[0]
   134  	if !strings.Contains(globalLevel, "=") {
   135  		// Validate debug log level.
   136  		if !validLogLevel(globalLevel) {
   137  			str := "the specified debug level [%v] is invalid"
   138  			return fmt.Errorf(str, globalLevel)
   139  		}
   140  
   141  		// Change the logging level for all subsystems.
   142  		logger.SetLogLevels(globalLevel)
   143  
   144  		// The rest will target specific subsystems.
   145  		levels = levels[1:]
   146  	}
   147  
   148  	// Go through the subsystem/level pairs while detecting issues and
   149  	// update the log levels accordingly.
   150  	for _, logLevelPair := range levels {
   151  		if !strings.Contains(logLevelPair, "=") {
   152  			str := "the specified debug level contains an " +
   153  				"invalid subsystem/level pair [%v]"
   154  			return fmt.Errorf(str, logLevelPair)
   155  		}
   156  
   157  		// Extract the specified subsystem and log level.
   158  		fields := strings.Split(logLevelPair, "=")
   159  		if len(fields) != 2 {
   160  			str := "the specified debug level has an invalid " +
   161  				"format [%v] -- use format subsystem1=level1," +
   162  				"subsystem2=level2"
   163  			return fmt.Errorf(str, logLevelPair)
   164  		}
   165  		subsysID, logLevel := fields[0], fields[1]
   166  		subLoggers := logger.SubLoggers()
   167  
   168  		// Validate subsystem.
   169  		if _, exists := subLoggers[subsysID]; !exists {
   170  			str := "the specified subsystem [%v] is invalid -- " +
   171  				"supported subsystems are %v"
   172  			return fmt.Errorf(
   173  				str, subsysID, logger.SupportedSubsystems(),
   174  			)
   175  		}
   176  
   177  		// Validate log level.
   178  		if !validLogLevel(logLevel) {
   179  			str := "the specified debug level [%v] is invalid"
   180  			return fmt.Errorf(str, logLevel)
   181  		}
   182  
   183  		logger.SetLogLevel(subsysID, logLevel)
   184  	}
   185  
   186  	return nil
   187  }
   188  
   189  // validLogLevel returns whether or not logLevel is a valid debug log level.
   190  func validLogLevel(logLevel string) bool {
   191  	switch logLevel {
   192  	case "trace":
   193  		fallthrough
   194  	case "debug":
   195  		fallthrough
   196  	case "info":
   197  		fallthrough
   198  	case "warn":
   199  		fallthrough
   200  	case "error":
   201  		fallthrough
   202  	case "critical":
   203  		fallthrough
   204  	case "off":
   205  		return true
   206  	}
   207  	return false
   208  }