From 9bf407d1a88a812a924da710ad30ae1ef39c12c5 Mon Sep 17 00:00:00 2001 From: Julien Pivotto Date: Sat, 18 Jul 2020 22:35:01 +0200 Subject: [PATCH 1/2] logger: add dynamic logger Add the required code to make logger level dynamic in prometheus. Signed-off-by: Julien Pivotto --- promlog/log.go | 67 ++++++++++++++++++++++++++++++ promlog/log_test.go | 99 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 166 insertions(+) diff --git a/promlog/log.go b/promlog/log.go index 5062e280..1c9c14af 100644 --- a/promlog/log.go +++ b/promlog/log.go @@ -18,6 +18,7 @@ package promlog import ( "os" + "sync" "time" "github.com/go-kit/kit/log" @@ -42,6 +43,23 @@ type AllowedLevel struct { o level.Option } +func (l *AllowedLevel) UnmarshalYAML(unmarshal func(interface{}) error) error { + var s string + type plain string + if err := unmarshal((*plain)(&s)); err != nil { + return err + } + if s == "" { + return nil + } + lo := &AllowedLevel{} + if err := lo.Set(s); err != nil { + return err + } + *l = *lo + return nil +} + func (l *AllowedLevel) String() string { return l.s } @@ -106,3 +124,52 @@ func New(config *Config) log.Logger { l = log.With(l, "ts", timestampFormat, "caller", log.DefaultCaller) return l } + +// NewDynamic returns a new leveled logger. Each logged line will be annotated +// with a timestamp. The output always goes to stderr. Some properties can be +// changed, like the level. +func NewDynamic(config *Config) *logger { + var l log.Logger + if config.Format != nil && config.Format.s == "json" { + l = log.NewJSONLogger(log.NewSyncWriter(os.Stderr)) + } else { + l = log.NewLogfmtLogger(log.NewSyncWriter(os.Stderr)) + } + l = log.With(l, "ts", timestampFormat, "caller", log.DefaultCaller) + + lo := &logger{ + base: l, + leveled: l, + } + if config.Level != nil { + lo.SetLevel(config.Level) + } + return lo +} + +type logger struct { + base log.Logger + leveled log.Logger + currentLevel *AllowedLevel + mtx sync.Mutex +} + +// l implements logger.Log +func (l *logger) Log(keyvals ...interface{}) error { + l.mtx.Lock() + defer l.mtx.Unlock() + return l.leveled.Log(keyvals...) +} + +// SetLevel allows changing the level +func (l *logger) SetLevel(lvl *AllowedLevel) { + l.mtx.Lock() + defer l.mtx.Unlock() + if lvl != nil { + if l.currentLevel != nil && l.currentLevel.s != lvl.s { + _ = l.base.Log("msg", "Log level changed", "prev", l.currentLevel, "current", lvl) + } + l.currentLevel = lvl + } + l.leveled = level.NewFilter(l.base, lvl.o) +} diff --git a/promlog/log_test.go b/promlog/log_test.go index ff51fd30..898393f0 100644 --- a/promlog/log_test.go +++ b/promlog/log_test.go @@ -14,7 +14,11 @@ package promlog import ( + "fmt" "testing" + + "github.com/go-kit/kit/log/level" + "gopkg.in/yaml.v2" ) // Make sure creating and using a logger with an empty configuration doesn't @@ -26,3 +30,98 @@ func TestDefaultConfig(t *testing.T) { t.Fatal(err) } } + +func TestUnmarshallLevel(t *testing.T) { + l := &AllowedLevel{} + err := yaml.Unmarshal([]byte(`debug`), l) + if err != nil { + t.Error(err) + } + if l.s != "debug" { + t.Errorf("expected %s, got %s", "debug", l.s) + } +} + +func TestUnmarshallEmptyLevel(t *testing.T) { + l := &AllowedLevel{} + err := yaml.Unmarshal([]byte(``), l) + if err != nil { + t.Error(err) + } + if l.s != "" { + t.Errorf("expected empty level, got %s", l.s) + } +} + +func TestUnmarshallBadLevel(t *testing.T) { + l := &AllowedLevel{} + err := yaml.Unmarshal([]byte(`debugg`), l) + if err == nil { + t.Error("expected error") + } + expErr := `unrecognized log level "debugg"` + if err.Error() != expErr { + t.Errorf("expected error %s, got %s", expErr, err.Error()) + } + if l.s != "" { + t.Errorf("expected empty level, got %s", l.s) + } +} + +type recordKeyvalLogger struct { + count int +} + +func (r *recordKeyvalLogger) Log(keyvals ...interface{}) error { + for _, v := range keyvals { + if fmt.Sprintf("%v", v) == "Log level changed" { + return nil + } + } + r.count++ + return nil +} + +func TestDynamic(t *testing.T) { + logger := NewDynamic(&Config{}) + + debugLevel := &AllowedLevel{} + if err := debugLevel.Set("debug"); err != nil { + t.Fatal(err) + } + infoLevel := &AllowedLevel{} + if err := infoLevel.Set("info"); err != nil { + t.Fatal(err) + } + + recorder := &recordKeyvalLogger{} + logger.base = recorder + logger.SetLevel(debugLevel) + if err := level.Debug(logger).Log("hello", "world"); err != nil { + t.Fatal(err) + } + if recorder.count != 1 { + t.Fatal("log not found") + } + + recorder.count = 0 + logger.SetLevel(infoLevel) + if err := level.Debug(logger).Log("hello", "world"); err != nil { + t.Fatal(err) + } + if recorder.count != 0 { + t.Fatal("log found") + } + if err := level.Info(logger).Log("hello", "world"); err != nil { + t.Fatal(err) + } + if recorder.count != 1 { + t.Fatal("log not found") + } + if err := level.Debug(logger).Log("hello", "world"); err != nil { + t.Fatal(err) + } + if recorder.count != 1 { + t.Fatal("extra log found") + } +} From e97ece87dc020c1f87abbeb0c422a0ae5460ae29 Mon Sep 17 00:00:00 2001 From: Julien Pivotto Date: Mon, 10 May 2021 22:33:10 +0200 Subject: [PATCH 2/2] Fix comments Signed-off-by: Julien Pivotto --- promlog/log.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/promlog/log.go b/promlog/log.go index 1c9c14af..aa306b8e 100644 --- a/promlog/log.go +++ b/promlog/log.go @@ -154,14 +154,14 @@ type logger struct { mtx sync.Mutex } -// l implements logger.Log +// Log implements logger.Log. func (l *logger) Log(keyvals ...interface{}) error { l.mtx.Lock() defer l.mtx.Unlock() return l.leveled.Log(keyvals...) } -// SetLevel allows changing the level +// SetLevel changes the log level. func (l *logger) SetLevel(lvl *AllowedLevel) { l.mtx.Lock() defer l.mtx.Unlock()