Repository: prometheus/common
Pull request #296 (2021-05-04 15:33:20)

Add dynamic logging capabilities


Patch:
diff --git a/promlog/log.go b/promlog/log.go
index 5062e28..aa306b8 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
+}
+
+// Log implements logger.Log.
+func (l *logger) Log(keyvals ...interface{}) error {
+	l.mtx.Lock()
+	defer l.mtx.Unlock()
+	return l.leveled.Log(keyvals...)
+}
+
+// SetLevel changes the log 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)
+}


Test patch:
diff --git a/promlog/log_test.go b/promlog/log_test.go
index ff51fd3..898393f 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")
+	}
+}