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 }