Repository: hashicorp/go-hclog
Pull request #64 (2020-05-03 03:54:12)

Provide a way to filter out log messages easily
When dealing with a package that logs too much, it can be desired to filter those log messages out. The only way to do that today is to implement a whole logger interface to hook into the proper point to filter them.

This instead adds a simple mechanism in intlogger to give the go/no-go on each log message before it's logged.

jefferai: I like it generally and the code looks fine, but I'd call it simply Filter instead of FilterOut.

Maybe the included implementation could be something like MessageFilter.
evanphx: A initially called it `Filter` but worried folks would think it allowed you to change the log message. Totally fine changing it back. Good call on MessageFilter as the struct type too.

Patch:
diff --git a/exclude.go b/exclude.go
new file mode 100644
index 0000000..cfd4307
--- /dev/null
+++ b/exclude.go
@@ -0,0 +1,71 @@
+package hclog
+
+import (
+	"regexp"
+	"strings"
+)
+
+// ExcludeByMessage provides a simple way to build a list of log messages that
+// can be queried and matched. This is meant to be used with the Exclude
+// option on Options to suppress log messages. This does not hold any mutexs
+// within itself, so normal usage would be to Add entries at setup and none after
+// Exclude is going to be called. Exclude is called with a mutex held within
+// the Logger, so that doesn't need to use a mutex. Example usage:
+//
+//	f := new(ExcludeByMessage)
+//	f.Add("Noisy log message text")
+//	appLogger.Exclude = f.Exclude
+type ExcludeByMessage struct {
+	messages map[string]struct{}
+}
+
+// Add a message to be filtered. Do not call this after Exclude is to be called
+// due to concurrency issues.
+func (f *ExcludeByMessage) Add(msg string) {
+	if f.messages == nil {
+		f.messages = make(map[string]struct{})
+	}
+
+	f.messages[msg] = struct{}{}
+}
+
+// Return true if the given message should be included
+func (f *ExcludeByMessage) Exclude(level Level, msg string, args ...interface{}) bool {
+	_, ok := f.messages[msg]
+	return ok
+}
+
+// ExcludeByPrefix is a simple type to match a message string that has a common prefix.
+type ExcludeByPrefix string
+
+// Matches an message that starts with the prefix.
+func (p ExcludeByPrefix) Exclude(level Level, msg string, args ...interface{}) bool {
+	return strings.HasPrefix(msg, string(p))
+}
+
+// ExcludeByRegexp takes a regexp and uses it to match a log message string. If it matches
+// the log entry is excluded.
+type ExcludeByRegexp struct {
+	Regexp *regexp.Regexp
+}
+
+// Exclude the log message if the message string matches the regexp
+func (e ExcludeByRegexp) Exclude(level Level, msg string, args ...interface{}) bool {
+	return e.Regexp.MatchString(msg)
+}
+
+// ExcludeFuncs is a slice of functions that will called to see if a log entry
+// should be filtered or not. It stops calling functions once at least one returns
+// true.
+type ExcludeFuncs []func(level Level, msg string, args ...interface{}) bool
+
+// Calls each function until one of them returns true
+func (ff ExcludeFuncs) Exclude(level Level, msg string, args ...interface{}) bool {
+	for _, f := range ff {
+		if f(level, msg, args...) {
+			return true
+		}
+	}
+
+	return false
+}
diff --git a/intlogger.go b/intlogger.go
index 1e5c843..034d13c 100644
--- a/intlogger.go
+++ b/intlogger.go
@@ -65,6 +65,8 @@ type intLogger struct {
 	level  *int32
 
 	implied []interface{}
+
+	exclude func(level Level, msg string, args ...interface{}) bool
 }
 
 // New returns a configured logger.
@@ -106,6 +108,7 @@ func newLogger(opts *LoggerOptions) *intLogger {
 		mutex:      mutex,
 		writer:     newWriter(output, opts.Color),
 		level:      new(int32),
+		exclude:    opts.Exclude,
 	}
 
 	l.setColorization(opts)
@@ -131,6 +134,10 @@ func (l *intLogger) log(name string, level Level, msg string, args ...interface{
 	l.mutex.Lock()
 	defer l.mutex.Unlock()
 
+	if l.exclude != nil && l.exclude(level, msg, args...) {
+		return
+	}
+
 	if l.json {
 		l.logJSON(t, name, level, msg, args...)
 	} else {
diff --git a/logger.go b/logger.go
index 66f4b7d..273b493 100644
--- a/logger.go
+++ b/logger.go
@@ -221,6 +221,12 @@ type LoggerOptions struct {
 	// Color the output. On Windows, colored logs are only avaiable for io.Writers that
 	// are concretely instances of *os.File.
 	Color ColorOption
+
+	// A function which is called with the log information and if it returns true the value
+	// should not be logged.
+	// This is useful when interacting with a system that you wish to suppress the log
+	// message for (because it's too noisy, etc)
+	Exclude func(level Level, msg string, args ...interface{}) bool
 }
 
 // InterceptLogger describes the interface for using a logger


Test patch:
diff --git a/exclude_test.go b/exclude_test.go
new file mode 100644
index 0000000..0f07999
--- /dev/null
+++ b/exclude_test.go
@@ -0,0 +1,55 @@
+package hclog
+
+import (
+	"regexp"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestExclude(t *testing.T) {
+	t.Run("excludes by message", func(t *testing.T) {
+		var em ExcludeByMessage
+		em.Add("foo")
+		em.Add("bar")
+
+		assert.True(t, em.Exclude(Info, "foo"))
+		assert.True(t, em.Exclude(Info, "bar"))
+		assert.False(t, em.Exclude(Info, "qux"))
+		assert.False(t, em.Exclude(Info, "foo qux"))
+		assert.False(t, em.Exclude(Info, "qux bar"))
+	})
+
+	t.Run("excludes by prefix", func(t *testing.T) {
+		ebp := ExcludeByPrefix("foo: ")
+
+		assert.True(t, ebp.Exclude(Info, "foo: rocks"))
+		assert.False(t, ebp.Exclude(Info, "foo"))
+		assert.False(t, ebp.Exclude(Info, "qux foo: bar"))
+	})
+
+	t.Run("exclude by regexp", func(t *testing.T) {
+		ebr := &ExcludeByRegexp{
+			Regexp: regexp.MustCompile("(foo|bar)"),
+		}
+
+		assert.True(t, ebr.Exclude(Info, "foo"))
+		assert.True(t, ebr.Exclude(Info, "bar"))
+		assert.True(t, ebr.Exclude(Info, "foo qux"))
+		assert.True(t, ebr.Exclude(Info, "qux bar"))
+		assert.False(t, ebr.Exclude(Info, "qux"))
+	})
+
+	t.Run("excludes many funcs", func(t *testing.T) {
+		ef := ExcludeFuncs{
+			ExcludeByPrefix("foo: ").Exclude,
+			ExcludeByPrefix("bar: ").Exclude,
+		}
+
+		assert.True(t, ef.Exclude(Info, "foo: rocks"))
+		assert.True(t, ef.Exclude(Info, "bar: rocks"))
+		assert.False(t, ef.Exclude(Info, "foo"))
+		assert.False(t, ef.Exclude(Info, "qux foo: bar"))
+
+	})
+}