Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(log): wire logger filtering #15601

Merged
merged 20 commits into from
Mar 30, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion log/level.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@ import (
"github.com/rs/zerolog"
)

const defaultLogLevelKey = "*"

// FilterFunc is a function that returns true if the log level is filtered for the given key
// When the filter returns true, the log entry is discarded.
type FilterFunc func(key, level string) bool

const defaultLogLevelKey = "*"
// NoFilterFunc is a filter that does not filter any log level
var NoFilterFunc = func(key, level string) bool { return false }

// ParseLogLevel parses complex log level
// A comma-separated list of module:level pairs with an optional *:level pair
Expand Down
38 changes: 6 additions & 32 deletions log/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,15 @@ func NewLogger(dst io.Writer) Logger {
return zeroLogWrapper{&logger}
}

// NewLoggerWithKV is shorthand for NewLogger(dst).With(key, value).
func NewLoggerWithKV(dst io.Writer, key, value string) Logger {
return NewLogger(dst).With(key, value)
// NewLoggerWithFilter returns a new logger that filters out all key/value pairs that do not match the filter.
func NewLoggerWithFilter(dst io.Writer, filter FilterFunc) Logger {
output := zerolog.ConsoleWriter{Out: dst, TimeFormat: time.Kitchen}
logger := zerolog.New(NewFilterWriter(output, filter)).With().Timestamp().Logger()
return zeroLogWrapper{&logger}
}

// NewCustomLogger returns a new logger with the given zerolog logger.
// NOTE: For creating a custom logger with a filter, use the NewFilterWriter function as wrapper around the output.
func NewCustomLogger(logger zerolog.Logger) Logger {
return zeroLogWrapper{&logger}
}
Expand Down Expand Up @@ -101,35 +104,6 @@ func (l zeroLogWrapper) Impl() interface{} {
return l.Logger
}

// FilterKeys returns a new logger that filters out all key/value pairs that do not match the filter.
// This functions assumes that the logger is a zerolog.Logger, which is the case for the logger returned by log.NewLogger().
// NOTE: filtering has a performance impact on the logger.
func FilterKeys(logger Logger, filter FilterFunc) Logger {
zl, ok := logger.Impl().(*zerolog.Logger)
if !ok {
panic("logger is not a zerolog.Logger")
}

filteredLogger := zl.Hook(zerolog.HookFunc(func(e *zerolog.Event, lvl zerolog.Level, _ string) {
// TODO(@julienrbrt) wait for https://github.com/rs/zerolog/pull/527 to be merged
// keys, err := e.GetKeys()
// if err != nil {
// panic(err)
// }

keys := []string{}

for _, key := range keys {
if filter(key, lvl.String()) {
e.Discard()
break
}
}
}))

return NewCustomLogger(filteredLogger)
}

// nopLogger is a Logger that does nothing when called.
// See the "specialized nop logger" benchmark and compare with the "zerolog nop logger" benchmark.
// The custom implementation is about 3x faster.
Expand Down
43 changes: 43 additions & 0 deletions log/writer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package log

import (
"encoding/json"
"fmt"
"io"
)

func NewFilterWriter(parent io.Writer, filter FilterFunc) io.Writer {
return &FilterWriter{parent, filter}
}

// FilterWriter is a writer that filters out all key/value pairs that do not match the filter.
type FilterWriter struct {
parent io.Writer
filter FilterFunc
}

func (fw *FilterWriter) Write(p []byte) (n int, err error) {
if fw.filter == nil {
return fw.parent.Write(p)
}

event := make(map[string]interface{})
if err := json.Unmarshal(p, &event); err != nil {
return 0, fmt.Errorf("failed to unmarshal event: %w", err)
}

level, ok := event["level"].(string)
if !ok {
return 0, fmt.Errorf("failed to get level from event")
}

// only filter module keys
module, ok := event[ModuleKey].(string)
julienrbrt marked this conversation as resolved.
Show resolved Hide resolved
julienrbrt marked this conversation as resolved.
Show resolved Hide resolved
if ok {
if fw.filter(module, level) {
return len(p), nil
}
}

return fw.parent.Write(p)
}
27 changes: 27 additions & 0 deletions log/writer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package log_test

import (
"bytes"
"strings"
"testing"

"cosmossdk.io/log"
"gotest.tools/v3/assert"
)

func TestFilteredWriter(t *testing.T) {
checkBuf := new(bytes.Buffer)
julienrbrt marked this conversation as resolved.
Show resolved Hide resolved

level := "consensus:debug,mempool:debug,*:error"
filter, err := log.ParseLogLevel(level)
assert.NilError(t, err)

logger := log.NewLoggerWithFilter(checkBuf, filter)
logger.Debug("this log line should be displayed", log.ModuleKey, "consensus")
assert.Check(t, strings.Contains(checkBuf.String(), "this log line should be displayed"))
checkBuf.Reset()

logger.Debug("this log line should be filtered", log.ModuleKey, "server")
assert.Check(t, !strings.Contains(checkBuf.String(), "this log line should be filtered"))
julienrbrt marked this conversation as resolved.
Show resolved Hide resolved
checkBuf.Reset()
}