decred.org/dcrdex@v1.0.5/dex/logging.go (about)

     1  // This code is available on the terms of the project LICENSE.md file,
     2  // also available online at https://blueoakcouncil.org/license/1.0.0.
     3  
     4  package dex
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  	"path/filepath"
    11  	"strings"
    12  	"sync"
    13  	"time"
    14  
    15  	"github.com/decred/slog"
    16  	"github.com/jrick/logrotate/rotator"
    17  )
    18  
    19  // Disabled is a Logger that will never output anything.
    20  var Disabled Logger = &logger{
    21  	Logger:  slog.Disabled,
    22  	level:   LevelOff,
    23  	backend: slog.NewBackend(io.Discard),
    24  }
    25  
    26  // Level constants.
    27  const (
    28  	LevelTrace    = slog.LevelTrace
    29  	LevelDebug    = slog.LevelDebug
    30  	LevelInfo     = slog.LevelInfo
    31  	LevelWarn     = slog.LevelWarn
    32  	LevelError    = slog.LevelError
    33  	LevelCritical = slog.LevelCritical
    34  	LevelOff      = slog.LevelOff
    35  
    36  	DefaultLogLevel = LevelInfo
    37  )
    38  
    39  // Logger is a logger. Many dcrdex types will take a logger as an argument.
    40  type Logger interface {
    41  	slog.Logger
    42  	SubLogger(name string) Logger
    43  	FileLogger(r *rotator.Rotator) Logger
    44  	Meter(callerID string, delay time.Duration) Logger
    45  }
    46  
    47  // LoggerMaker allows creation of new log subsystems with predefined levels.
    48  type LoggerMaker struct {
    49  	*slog.Backend
    50  	DefaultLevel slog.Level
    51  	Levels       map[string]slog.Level
    52  }
    53  
    54  // logger contains the slog.Logger and fields needed to spawn subloggers. It
    55  // satisfies the Logger interface.
    56  type logger struct {
    57  	slog.Logger
    58  	name    string
    59  	level   slog.Level
    60  	levels  map[string]slog.Level
    61  	backend *slog.Backend
    62  
    63  	meterMtx sync.Mutex
    64  	meters   map[string]time.Time
    65  }
    66  
    67  // SubLogger creates a new Logger for the subsystem with the given name. If name
    68  // exists in the levels map, use that level, otherwise the parent's log level is
    69  // used.
    70  func (lggr *logger) SubLogger(name string) Logger {
    71  	return lggr.newLoggerWithBackend(lggr.backend, name)
    72  }
    73  
    74  // FileLogger creates a logger that logs to a file rotator. Subloggers will also
    75  // log to the file only.
    76  func (lggr *logger) FileLogger(r *rotator.Rotator) Logger {
    77  	return lggr.newLoggerWithBackend(slog.NewBackend(r), "F")
    78  }
    79  
    80  func (lggr *logger) newLoggerWithBackend(backend *slog.Backend, name string) *logger {
    81  	level := lggr.level
    82  	// If name is in the levels map, use that level.
    83  	if lvl, ok := lggr.levels[name]; ok {
    84  		level = lvl
    85  	}
    86  
    87  	combinedName := fmt.Sprintf("%s[%s]", lggr.name, name)
    88  	newLggr := backend.Logger(combinedName)
    89  	newLggr.SetLevel(level)
    90  	return &logger{
    91  		Logger:  newLggr,
    92  		name:    combinedName,
    93  		level:   level,
    94  		levels:  lggr.levels,
    95  		backend: backend,
    96  	}
    97  }
    98  
    99  // Meter enforces a time delay on logging. The first call to a metered logger
   100  // always logs. Subsequent calls for the same callerID are ignored until the
   101  // delay is surpassed.
   102  func (log *logger) Meter(callerID string, delay time.Duration) Logger {
   103  	log.meterMtx.Lock()
   104  	defer log.meterMtx.Unlock()
   105  	if log.meters == nil {
   106  		log.meters = make(map[string]time.Time)
   107  	}
   108  	if lastLog, exists := log.meters[callerID]; exists && time.Since(lastLog) < delay {
   109  		return Disabled
   110  	}
   111  	log.meters[callerID] = time.Now()
   112  	return log
   113  }
   114  
   115  // LogRotator creates a file logger that rotates up to 8 files of 32 MiB each.
   116  func LogRotator(dir, name string) (*rotator.Rotator, error) {
   117  	const maxLogRolls = 8
   118  	if err := os.MkdirAll(dir, 0744); err != nil {
   119  		return nil, fmt.Errorf("error creating log directory: %w", err)
   120  	}
   121  
   122  	logFilename := filepath.Join(dir, name)
   123  	return rotator.New(logFilename, 32*1024, false, maxLogRolls)
   124  }
   125  
   126  func inUTC() slog.BackendOption {
   127  	return slog.WithFlags(slog.LUTC)
   128  }
   129  
   130  // NewLogger creates a new Logger with the given name, log level, and io.Writer.
   131  func NewLogger(name string, lvl slog.Level, writer io.Writer, utc ...bool) Logger {
   132  	var opts []slog.BackendOption
   133  	if len(utc) > 0 && utc[0] {
   134  		opts = append(opts, inUTC())
   135  	}
   136  	backend := slog.NewBackend(writer, opts...)
   137  	lggr := backend.Logger(name)
   138  	lggr.SetLevel(lvl)
   139  	return &logger{
   140  		Logger:  lggr,
   141  		name:    name,
   142  		level:   lvl,
   143  		levels:  make(map[string]slog.Level),
   144  		backend: backend,
   145  	}
   146  }
   147  
   148  // StdOutLogger creates a Logger with the provided name with lvl as the log
   149  // level and prints to standard out.
   150  func StdOutLogger(name string, lvl slog.Level, utc ...bool) Logger {
   151  	var opts []slog.BackendOption
   152  	if len(utc) > 0 && utc[0] {
   153  		opts = append(opts, inUTC())
   154  	}
   155  	backend := slog.NewBackend(os.Stdout, opts...)
   156  	lggr := backend.Logger(name)
   157  	lggr.SetLevel(lvl)
   158  	return &logger{
   159  		Logger:  lggr,
   160  		name:    name,
   161  		level:   lvl,
   162  		levels:  make(map[string]slog.Level),
   163  		backend: backend,
   164  	}
   165  }
   166  
   167  // NewLoggerMaker creates a new LoggerMaker from the provided io.Writer and
   168  // debug level string. See SetLevels for details on the debug level string.
   169  func NewLoggerMaker(writer io.Writer, debugLevel string, utc ...bool) (*LoggerMaker, error) {
   170  	var opts []slog.BackendOption
   171  	if len(utc) > 0 && utc[0] {
   172  		opts = append(opts, inUTC())
   173  	}
   174  	lm := &LoggerMaker{
   175  		Backend:      slog.NewBackend(writer, opts...),
   176  		Levels:       make(map[string]slog.Level),
   177  		DefaultLevel: DefaultLogLevel,
   178  	}
   179  
   180  	err := lm.SetLevels(debugLevel)
   181  	if err != nil {
   182  		return nil, err
   183  	}
   184  
   185  	return lm, nil
   186  }
   187  
   188  // SetLevelsFromMap sets all logs for certain subsystems with the same name to
   189  // the corresponding log level in the map.
   190  func (lm *LoggerMaker) SetLevelsFromMap(lvls map[string]slog.Level) {
   191  	for name, lvl := range lvls {
   192  		lm.Levels[name] = lvl
   193  	}
   194  }
   195  
   196  // SetLevels either set the DefaultLevel or resets the Levels map for future
   197  // subsystems created with the LoggerMaker.
   198  //
   199  // The debugLevel string can specify a single verbosity for the entire system:
   200  // "trace", "debug", "info", "warn", "error", "critical", "off". The Levels map
   201  // is not modified with this syntax.
   202  //
   203  // Or the verbosity can be specified for individual subsystems, separating
   204  // subsystems by commas and assigning each specifically. Such a debugLevel
   205  // string might look like `CORE=debug,SWAP=trace`. The DefaultLevel is not
   206  // modified with this syntax.
   207  func (lm *LoggerMaker) SetLevels(debugLevel string) error {
   208  	// When the specified string doesn't have any delimiters, treat it as
   209  	// the log level for all subsystems.
   210  	if !strings.Contains(debugLevel, ",") && !strings.Contains(debugLevel, "=") {
   211  		// Validate debug log level.
   212  		lvl, ok := slog.LevelFromString(debugLevel)
   213  		if !ok {
   214  			str := "The specified debug level [%v] is invalid"
   215  			return fmt.Errorf(str, debugLevel)
   216  		}
   217  		lm.DefaultLevel = lvl
   218  		return nil
   219  	}
   220  
   221  	// Split the specified string into subsystem/level pairs while detecting
   222  	// issues and update the log levels accordingly.
   223  	levelPairs := strings.Split(debugLevel, ",")
   224  	lm.Levels = make(map[string]slog.Level, len(levelPairs))
   225  	for _, logLevelPair := range levelPairs {
   226  		if !strings.Contains(logLevelPair, "=") {
   227  			// Mixed defaults and key=val pairs are not permitted.
   228  			str := "The specified debug level contains an invalid " +
   229  				"subsystem/level pair [%v]"
   230  			return fmt.Errorf(str, logLevelPair)
   231  		}
   232  
   233  		// Extract the specified subsystem and log level.
   234  		fields := strings.Split(logLevelPair, "=")
   235  		subsysID, logLevel := fields[0], fields[1]
   236  
   237  		// Validate log level.
   238  		lvl, ok := slog.LevelFromString(logLevel)
   239  		if !ok {
   240  			str := "The specified debug level [%v] is invalid"
   241  			return fmt.Errorf(str, logLevel)
   242  		}
   243  		lm.Levels[subsysID] = lvl
   244  	}
   245  	return nil
   246  }
   247  
   248  // Level returns the log level for the named subsystem. If a level is not
   249  // configured for this subsystem, the LoggerMaker's DefaultLevel is returned.
   250  func (lm *LoggerMaker) Level(name string) slog.Level {
   251  	level, found := lm.Levels[name]
   252  	if found {
   253  		return level
   254  	}
   255  	return lm.DefaultLevel
   256  }
   257  
   258  // NewLogger creates a new Logger for the subsystem with the given name. If a
   259  // log level is specified, it is used for the Logger. Otherwise the DefaultLevel
   260  // is used.
   261  func (lm *LoggerMaker) NewLogger(name string, level ...slog.Level) Logger {
   262  	lvl := lm.DefaultLevel
   263  	if len(level) > 0 {
   264  		lvl = level[0]
   265  	}
   266  	lggr := lm.Backend.Logger(name)
   267  	lggr.SetLevel(lvl)
   268  	return &logger{
   269  		Logger:  lggr,
   270  		name:    name,
   271  		level:   lvl,
   272  		levels:  lm.Levels,
   273  		backend: lm.Backend,
   274  	}
   275  }
   276  
   277  // Logger creates a logger with the provided name, using the log level for that
   278  // name if it was set, otherwise the default log level. This differs from
   279  // NewLogger, which does not look in the Level map for the name.
   280  func (lm *LoggerMaker) Logger(name string) Logger {
   281  	lggr := lm.Backend.Logger(name)
   282  	lvl := lm.bestLevel(name)
   283  	lggr.SetLevel(lvl)
   284  	return &logger{
   285  		Logger:  lggr,
   286  		name:    name,
   287  		level:   lvl,
   288  		levels:  lm.Levels,
   289  		backend: lm.Backend,
   290  	}
   291  }
   292  
   293  // bestLevel takes a hierarchical list of logger names, least important to most
   294  // important, and returns the best log level found in the Levels map, else the
   295  // default.
   296  func (lm *LoggerMaker) bestLevel(lvls ...string) slog.Level {
   297  	lvl := lm.DefaultLevel
   298  	for _, l := range lvls {
   299  		lev, found := lm.Levels[l]
   300  		if found {
   301  			lvl = lev
   302  		}
   303  	}
   304  	return lvl
   305  }