package build import ( "fmt" "sort" "strings" "sync" "github.com/btcsuite/btclog/v2" ) // SubLogCreator can be used to create a new logger for a particular subsystem. type SubLogCreator interface { // Logger returns a new logger for a particular subsytem. Logger(subsystemTag string) btclog.Logger } // SubLoggerManager manages a set of subsystem loggers. Level updates will be // applied to all the loggers managed by the manager. type SubLoggerManager struct { genLogger SubLogCreator loggers SubLoggers mu sync.Mutex } // A compile time check to ensure SubLoggerManager implements the // LeveledSubLogger interface. var _ LeveledSubLogger = (*SubLoggerManager)(nil) // NewSubLoggerManager constructs a new SubLoggerManager. func NewSubLoggerManager(handlers ...btclog.Handler) *SubLoggerManager { return &SubLoggerManager{ loggers: make(SubLoggers), genLogger: newSubLogGenerator( newHandlerSet(btclog.LevelInfo, handlers...), ), } } // GenSubLogger creates a new sub-logger and adds it to the set managed by the // SubLoggerManager. A shutdown callback function is provided to be able to shut // down in case of a critical error. func (r *SubLoggerManager) GenSubLogger(subsystem string, shutdown func()) btclog.Logger { // Create a new logger with the given subsystem tag. logger := r.genLogger.Logger(subsystem) // Wrap the new logger in a Shutdown logger so that the shutdown // call back is called if a critical log is ever written via this new // logger. l := NewShutdownLogger(logger, shutdown) r.RegisterSubLogger(subsystem, l) return l } // RegisterSubLogger registers the given logger under the given subsystem name. func (r *SubLoggerManager) RegisterSubLogger(subsystem string, logger btclog.Logger) { // Add the new logger to the set of loggers managed by the manager. r.mu.Lock() r.loggers[subsystem] = logger r.mu.Unlock() } // SubLoggers returns all currently registered subsystem loggers for this log // writer. // // NOTE: This is part of the LeveledSubLogger interface. func (r *SubLoggerManager) SubLoggers() SubLoggers { r.mu.Lock() defer r.mu.Unlock() return r.loggers } // SupportedSubsystems returns a sorted string slice of all keys in the // subsystems map, corresponding to the names of the subsystems. // // NOTE: This is part of the LeveledSubLogger interface. func (r *SubLoggerManager) SupportedSubsystems() []string { r.mu.Lock() defer r.mu.Unlock() // Convert the subsystemLoggers map keys to a string slice. subsystems := make([]string, 0, len(r.loggers)) for subsysID := range r.loggers { subsystems = append(subsystems, subsysID) } // Sort the subsystems for stable display. sort.Strings(subsystems) return subsystems } // SetLogLevel sets the logging level for provided subsystem. Invalid // subsystems are ignored. Uninitialized subsystems are dynamically created as // needed. // // NOTE: This is part of the LeveledSubLogger interface. func (r *SubLoggerManager) SetLogLevel(subsystemID string, logLevel string) { r.mu.Lock() defer r.mu.Unlock() r.setLogLevelUnsafe(subsystemID, logLevel) } // setLogLevelUnsafe sets the logging level for provided subsystem. Invalid // subsystems are ignored. Uninitialized subsystems are dynamically created as // needed. // // NOTE: the SubLoggerManager mutex must be held before calling this method. func (r *SubLoggerManager) setLogLevelUnsafe(subsystemID string, logLevel string) { // Ignore invalid subsystems. logger, ok := r.loggers[subsystemID] if !ok { return } // Defaults to info if the log level is invalid. level, _ := btclog.LevelFromString(logLevel) logger.SetLevel(level) } // SetLogLevels sets the log level for all subsystem loggers to the passed // level. It also dynamically creates the subsystem loggers as needed, so it // can be used to initialize the logging system. // // NOTE: This is part of the LeveledSubLogger interface. func (r *SubLoggerManager) SetLogLevels(logLevel string) { r.mu.Lock() defer r.mu.Unlock() // Configure all sub-systems with the new logging level. Dynamically // create loggers as needed. for subsystemID := range r.loggers { r.setLogLevelUnsafe(subsystemID, logLevel) } } // SubLoggers is a type that holds a map of subsystem loggers keyed by their // subsystem name. type SubLoggers map[string]btclog.Logger // LeveledSubLogger provides the ability to retrieve the subsystem loggers of // a logger and set their log levels individually or all at once. type LeveledSubLogger interface { // SubLoggers returns the map of all registered subsystem loggers. SubLoggers() SubLoggers // SupportedSubsystems returns a slice of strings containing the names // of the supported subsystems. Should ideally correspond to the keys // of the subsystem logger map and be sorted. SupportedSubsystems() []string // SetLogLevel assigns an individual subsystem logger a new log level. SetLogLevel(subsystemID string, logLevel string) // SetLogLevels assigns all subsystem loggers the same new log level. SetLogLevels(logLevel string) } // ParseAndSetDebugLevels attempts to parse the specified debug level and set // the levels accordingly on the given logger. An appropriate error is returned // if anything is invalid. func ParseAndSetDebugLevels(level string, logger LeveledSubLogger) error { // Split at the delimiter. levels := strings.Split(level, ",") if len(levels) == 0 { return fmt.Errorf("invalid log level: %v", level) } // If the first entry has no =, treat is as the log level for all // subsystems. globalLevel := levels[0] if !strings.Contains(globalLevel, "=") { // Validate debug log level. if !validLogLevel(globalLevel) { str := "the specified debug level [%v] is invalid" return fmt.Errorf(str, globalLevel) } // Change the logging level for all subsystems. logger.SetLogLevels(globalLevel) // The rest will target specific subsystems. levels = levels[1:] } // Go through the subsystem/level pairs while detecting issues and // update the log levels accordingly. for _, logLevelPair := range levels { if !strings.Contains(logLevelPair, "=") { str := "the specified debug level contains an " + "invalid subsystem/level pair [%v]" return fmt.Errorf(str, logLevelPair) } // Extract the specified subsystem and log level. fields := strings.Split(logLevelPair, "=") if len(fields) != 2 { str := "the specified debug level has an invalid " + "format [%v] -- use format subsystem1=level1," + "subsystem2=level2" return fmt.Errorf(str, logLevelPair) } subsysID, logLevel := fields[0], fields[1] subLoggers := logger.SubLoggers() // Validate subsystem. if _, exists := subLoggers[subsysID]; !exists { str := "the specified subsystem [%v] is invalid -- " + "supported subsystems are %v" return fmt.Errorf( str, subsysID, logger.SupportedSubsystems(), ) } // Validate log level. if !validLogLevel(logLevel) { str := "the specified debug level [%v] is invalid" return fmt.Errorf(str, logLevel) } logger.SetLogLevel(subsysID, logLevel) } return nil } // validLogLevel returns whether or not logLevel is a valid debug log level. func validLogLevel(logLevel string) bool { switch logLevel { case "trace": fallthrough case "debug": fallthrough case "info": fallthrough case "warn": fallthrough case "error": fallthrough case "critical": fallthrough case "off": return true } return false }