diff --git a/logutils/geth_adapter.go b/logutils/geth_adapter.go new file mode 100644 index 000000000..ab6b860fd --- /dev/null +++ b/logutils/geth_adapter.go @@ -0,0 +1,51 @@ +package logutils + +import ( + "fmt" + "strings" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + + "github.com/ethereum/go-ethereum/log" +) + +// gethAdapter returns a log.Handler interface that forwards logs to a zap.Logger. +// Logs are forwarded raw as if geth were printing them. +func gethAdapter(logger *zap.Logger) log.Handler { + return log.FuncHandler(func(r *log.Record) error { + level, err := lvlFromString(r.Lvl.String()) + if err != nil { + return err + } + // Skip trace logs to not clutter the output + if level == traceLevel { + return nil + } + serializedLog := string(log.TerminalFormat(false).Format(r)) + logger.Check(level, fmt.Sprintf("'%s'", strings.TrimSuffix(serializedLog, "\n"))).Write() + return nil + }) +} + +const traceLevel = zapcore.DebugLevel - 1 + +// lvlFromString returns the appropriate zapcore.Level from a string. +func lvlFromString(lvlString string) (zapcore.Level, error) { + switch strings.ToLower(lvlString) { + case "trace", "trce": + return traceLevel, nil // zap does not have a trace level, use custom + case "debug", "dbug": + return zapcore.DebugLevel, nil + case "info": + return zapcore.InfoLevel, nil + case "warn": + return zapcore.WarnLevel, nil + case "error", "eror": + return zapcore.ErrorLevel, nil + case "crit": + return zapcore.DPanicLevel, nil // zap does not have a crit level, using DPanicLevel as closest + default: + return zapcore.InvalidLevel, fmt.Errorf("unknown level: %v", lvlString) + } +} diff --git a/logutils/geth_adapter_test.go b/logutils/geth_adapter_test.go new file mode 100644 index 000000000..af9b5f535 --- /dev/null +++ b/logutils/geth_adapter_test.go @@ -0,0 +1,43 @@ +package logutils + +import ( + "bytes" + "testing" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + + "github.com/stretchr/testify/require" + + "github.com/ethereum/go-ethereum/log" +) + +func TestGethAdapter(t *testing.T) { + level := zap.NewAtomicLevelAt(zap.InfoLevel) + buffer := bytes.NewBuffer(nil) + + core := zapcore.NewCore( + zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()), + zapcore.AddSync(buffer), + level, + ) + logger := zap.New(core) + + log.Root().SetHandler(gethAdapter(logger)) + + log.Debug("should not be printed, as it's below the log level") + require.Empty(t, buffer.String()) + + buffer.Reset() + log.Info("should be printed") + require.Regexp(t, `INFO\s+'INFO\s*\[.*\]\s*should be printed '`, buffer.String()) + + buffer.Reset() + level.SetLevel(zap.DebugLevel) + log.Debug("should be printed with context", "value1", 12345, "value2", "string") + require.Regexp(t, `DEBUG\s+'DEBUG\s*\[.*\]\s*should be printed with context\s+value1=12345\s+value2=string'`, buffer.String()) + + buffer.Reset() + log.Trace("should be skipped") + require.Empty(t, buffer.String()) +}