From d13f7e900f3138a5746f05b43ce2ca32ff379d92 Mon Sep 17 00:00:00 2001 From: Ken Hibino Date: Thu, 7 May 2020 21:28:06 -0700 Subject: [PATCH] Allow setting minimum log level for logger --- internal/log/log.go | 105 ++++++++++++++++++++++++++++++++++++++- internal/log/log_test.go | 51 +++++++++++++++++++ server.go | 32 +++++++++++- 3 files changed, 185 insertions(+), 3 deletions(-) diff --git a/internal/log/log.go b/internal/log/log.go index 7fdd5bf..d191e6b 100644 --- a/internal/log/log.go +++ b/internal/log/log.go @@ -10,6 +10,7 @@ import ( "io" stdlog "log" "os" + "sync" ) // Base supports logging with various log levels. @@ -78,16 +79,105 @@ func newBase(out io.Writer) *baseLogger { } // NewLogger creates and returns a new instance of Logger. +// Log level is set to DebugLevel by default. func NewLogger(base Base) *Logger { if base == nil { base = newBase(os.Stderr) } - return &Logger{base} + return &Logger{base: base, level: DebugLevel} } // Logger logs message to io.Writer with various log levels. type Logger struct { - Base + base Base + + mu sync.Mutex + // Minimum log level for this logger. + // Message with level lower than this level won't be outputted. + level Level +} + +// Level represents a log level. +type Level int32 + +const ( + // DebugLevel is the lowest level of logging. + // Debug logs are intended for debugging and development purposes. + DebugLevel Level = iota + + // InfoLevel is used for general informational log messages. + InfoLevel + + // WarnLevel is used for undesired but relatively expected events, + // which may indicate a problem. + WarnLevel + + // ErrorLevel is used for undesired and unexpected events that + // the program can recover from. + ErrorLevel + + // FatalLevel is used for undesired and unexpected events that + // the program cannot recover from. + FatalLevel +) + +func (l Level) String() string { + switch l { + case DebugLevel: + return "debug" + case InfoLevel: + return "info" + case WarnLevel: + return "warning" + case ErrorLevel: + return "error" + case FatalLevel: + return "fatal" + default: + return "unknown" + } +} + +// canLogAt reports whether logger can log at level v. +func (l *Logger) canLogAt(v Level) bool { + l.mu.Lock() + defer l.mu.Unlock() + return v >= l.level +} + +func (l *Logger) Debug(args ...interface{}) { + if !l.canLogAt(DebugLevel) { + return + } + l.base.Debug(args...) +} + +func (l *Logger) Info(args ...interface{}) { + if !l.canLogAt(InfoLevel) { + return + } + l.base.Info(args...) +} + +func (l *Logger) Warn(args ...interface{}) { + if !l.canLogAt(WarnLevel) { + return + } + l.base.Warn(args...) +} + +func (l *Logger) Error(args ...interface{}) { + if !l.canLogAt(WarnLevel) { + return + } + l.base.Error(args...) +} + +func (l *Logger) Fatal(args ...interface{}) { + if !l.canLogAt(WarnLevel) { + return + } + l.base.Fatal(args...) } func (l *Logger) Debugf(format string, args ...interface{}) { @@ -109,3 +199,14 @@ func (l *Logger) Errorf(format string, args ...interface{}) { func (l *Logger) Fatalf(format string, args ...interface{}) { l.Fatal(fmt.Sprintf(format, args...)) } + +// SetLevel sets the logger level. +// It panics if v is less than DebugLevel or greater than FatalLevel. +func (l *Logger) SetLevel(v Level) { + l.mu.Lock() + defer l.mu.Unlock() + if v < DebugLevel || v > FatalLevel { + panic("log: invalid log level") + } + l.level = v +} diff --git a/internal/log/log_test.go b/internal/log/log_test.go index 2285ae3..925cfce 100644 --- a/internal/log/log_test.go +++ b/internal/log/log_test.go @@ -283,3 +283,54 @@ func TestLoggerErrorf(t *testing.T) { } } } + +func TestLoggerWithMinLevels(t *testing.T) { + tests := []struct { + level Level + op string + }{ + // with level one above + {InfoLevel, "Debug"}, + {InfoLevel, "Debugf"}, + {WarnLevel, "Info"}, + {WarnLevel, "Infof"}, + {ErrorLevel, "Warn"}, + {ErrorLevel, "Warnf"}, + {FatalLevel, "Error"}, + {FatalLevel, "Errorf"}, + // with skip level + {WarnLevel, "Debug"}, + {ErrorLevel, "Infof"}, + } + + for _, tc := range tests { + var buf bytes.Buffer + logger := NewLogger(newBase(&buf)) + logger.SetLevel(tc.level) + + switch tc.op { + case "Debug": + logger.Debug("hello") + case "Debugf": + logger.Debugf("hello, %s", "world") + case "Info": + logger.Info("hello") + case "Infof": + logger.Infof("hello, %s", "world") + case "Warn": + logger.Warn("hello") + case "Warnf": + logger.Warnf("hello, %s", "world") + case "Error": + logger.Error("hello") + case "Errorf": + logger.Errorf("hello, %s", "world") + default: + t.Fatalf("unexpected op: %q", tc.op) + } + + if buf.String() != "" { + t.Errorf("logger.%s outputted log message when level is set to %v", tc.op, tc.level) + } + } +} diff --git a/server.go b/server.go index 86ff04f..5580606 100644 --- a/server.go +++ b/server.go @@ -112,6 +112,11 @@ type Config struct { // If unset, default logger is used. Logger Logger + // LogLevel specifies the minimum log level to enable. + // + // If unset, DebugLevel is used by default. + LogLevel LogLevel + // ShutdownTimeout specifies the duration to wait to let workers finish their tasks // before forcing them to abort when stopping the server. // @@ -152,6 +157,30 @@ type Logger interface { Fatal(args ...interface{}) } +// LogLevel represents logging level. +type LogLevel int32 + +const ( + // DebugLevel is the lowest level of logging. + // Debug logs are intended for debugging and development purposes. + DebugLevel LogLevel = iota + + // InfoLevel is used for general informational log messages. + InfoLevel + + // WarnLevel is used for undesired but relatively expected events, + // which may indicate a problem. + WarnLevel + + // ErrorLevel is used for undesired and unexpected events that + // the program can recover from. + ErrorLevel + + // FatalLevel is used for undesired and unexpected events that + // the program cannot recover from. + FatalLevel +) + // Formula taken from https://github.com/mperham/sidekiq. func defaultDelayFunc(n int, e error, t *Task) time.Duration { r := rand.New(rand.NewSource(time.Now().UnixNano())) @@ -189,6 +218,8 @@ func NewServer(r RedisConnOpt, cfg Config) *Server { if shutdownTimeout == 0 { shutdownTimeout = defaultShutdownTimeout } + logger := log.NewLogger(cfg.Logger) + logger.SetLevel(log.Level(cfg.LogLevel)) host, err := os.Hostname() if err != nil { @@ -197,7 +228,6 @@ func NewServer(r RedisConnOpt, cfg Config) *Server { pid := os.Getpid() rdb := rdb.NewRDB(createRedisClient(r)) - logger := log.NewLogger(cfg.Logger) ss := base.NewServerState(host, pid, n, queues, cfg.StrictPriority) syncCh := make(chan *syncRequest) cancels := base.NewCancelations()