From 761b1553aa2316a41e102371ab3643dddc56608a Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Mon, 15 May 2017 20:08:02 +0200 Subject: [PATCH] [log] allow filtering with fields --- log/filter.go | 59 ++++++++++++++++++++++++++++++++++++++++++---- log/filter_test.go | 30 ++++++++++++++++++----- 2 files changed, 78 insertions(+), 11 deletions(-) diff --git a/log/filter.go b/log/filter.go index f410d2228..9633d88a8 100644 --- a/log/filter.go +++ b/log/filter.go @@ -8,7 +8,8 @@ import "fmt" // Error helper methods are squelched. func NewFilter(next Logger, options ...Option) Logger { l := &filter{ - next: next, + next: next, + allowedKeyvals: make(map[keyval]level), } for _, option := range options { option(l) @@ -34,9 +35,15 @@ func NewFilterByLevel(next Logger, lvl string) (Logger, error) { } type filter struct { - next Logger - allowed level - errNotAllowed error + next Logger + allowed level + allowedKeyvals map[keyval]level + errNotAllowed error +} + +type keyval struct { + key interface{} + value interface{} } func (l *filter) Info(msg string, keyvals ...interface{}) error { @@ -63,8 +70,30 @@ func (l *filter) Error(msg string, keyvals ...interface{}) error { return l.next.Error(msg, keyvals...) } +// With implements Logger by constructing a new filter with a keyvals appended +// to the logger. +// +// If custom level was set for a keyval pair using one of the +// Allow*With methods, it is used as the logger's level. +// +// Examples: +// logger = log.NewFilter(logger, log.AllowError(), log.AllowInfoWith("module", "crypto")) +// logger.With("module", "crypto").Info("Hello") # produces "I... Hello module=crypto" +// +// logger = log.NewFilter(logger, log.AllowError(), log.AllowInfoWith("module", "crypto"), log.AllowNoneWith("user", "Sam")) +// logger.With("module", "crypto", "user", "Sam").Info("Hello") # returns nil +// +// logger = log.NewFilter(logger, log.AllowError(), log.AllowInfoWith("module", "crypto"), log.AllowNoneWith("user", "Sam")) +// logger.With("user", "Sam").With("module", "crypto").Info("Hello") # produces "I... Hello module=crypto user=Sam" func (l *filter) With(keyvals ...interface{}) Logger { - return &filter{next: l.next.With(keyvals...), allowed: l.allowed, errNotAllowed: l.errNotAllowed} + for i := len(keyvals) - 2; i >= 0; i -= 2 { + for kv, allowed := range l.allowedKeyvals { + if keyvals[i] == kv.key && keyvals[i+1] == kv.value { + return &filter{next: l.next.With(keyvals...), allowed: allowed, errNotAllowed: l.errNotAllowed, allowedKeyvals: l.allowedKeyvals} + } + } + } + return &filter{next: l.next.With(keyvals...), allowed: l.allowed, errNotAllowed: l.errNotAllowed, allowedKeyvals: l.allowedKeyvals} } // Option sets a parameter for the filter. @@ -107,6 +136,26 @@ func ErrNotAllowed(err error) Option { return func(l *filter) { l.errNotAllowed = err } } +// AllowDebugWith allows error, info and debug level log events to pass for a specific key value pair. +func AllowDebugWith(key interface{}, value interface{}) Option { + return func(l *filter) { l.allowedKeyvals[keyval{key, value}] = levelError | levelInfo | levelDebug } +} + +// AllowInfoWith allows error and info level log events to pass for a specific key value pair. +func AllowInfoWith(key interface{}, value interface{}) Option { + return func(l *filter) { l.allowedKeyvals[keyval{key, value}] = levelError | levelInfo } +} + +// AllowErrorWith allows only error level log events to pass for a specific key value pair. +func AllowErrorWith(key interface{}, value interface{}) Option { + return func(l *filter) { l.allowedKeyvals[keyval{key, value}] = levelError } +} + +// AllowNoneWith allows no leveled log events to pass for a specific key value pair. +func AllowNoneWith(key interface{}, value interface{}) Option { + return func(l *filter) { l.allowedKeyvals[keyval{key, value}] = 0 } +} + type level byte const ( diff --git a/log/filter_test.go b/log/filter_test.go index edde86249..4665db3df 100644 --- a/log/filter_test.go +++ b/log/filter_test.go @@ -108,13 +108,31 @@ func TestLevelContext(t *testing.T) { } } -func TestNewFilterByLevel(t *testing.T) { +func TestVariousAllowWith(t *testing.T) { + var buf bytes.Buffer + var logger log.Logger - logger = log.NewNopLogger() - if _, err := log.NewFilterByLevel(logger, "info"); err != nil { - t.Fatal(err) + logger = log.NewTMJSONLogger(&buf) + + logger1 := log.NewFilter(logger, log.AllowError(), log.AllowInfoWith("context", "value")) + logger1.With("context", "value").Info("foo", "bar", "baz") + if want, have := `{"_msg":"foo","bar":"baz","context":"value","level":"info"}`, strings.TrimSpace(buf.String()); want != have { + t.Errorf("\nwant '%s'\nhave '%s'", want, have) } - if _, err := log.NewFilterByLevel(logger, "other"); err == nil { - t.Fatal(err) + + buf.Reset() + + logger2 := log.NewFilter(logger, log.AllowError(), log.AllowInfoWith("context", "value"), log.AllowNoneWith("user", "Sam")) + logger2.With("context", "value", "user", "Sam").Info("foo", "bar", "baz") + if want, have := ``, strings.TrimSpace(buf.String()); want != have { + t.Errorf("\nwant '%s'\nhave '%s'", want, have) + } + + buf.Reset() + + logger3 := log.NewFilter(logger, log.AllowError(), log.AllowInfoWith("context", "value"), log.AllowNoneWith("user", "Sam")) + logger3.With("user", "Sam").With("context", "value").Info("foo", "bar", "baz") + if want, have := `{"_msg":"foo","bar":"baz","context":"value","level":"info","user":"Sam"}`, strings.TrimSpace(buf.String()); want != have { + t.Errorf("\nwant '%s'\nhave '%s'", want, have) } }