feat: verbose logging during upgrades (#24720)

Co-authored-by: Alex | Interchain Labs <alex@interchainlabs.io>
This commit is contained in:
Aaron Craelius
2025-05-13 20:48:30 +00:00
committed by GitHub
co-authored by Alex | Interchain Labs
parent 3d777ad46a
commit be955efe25
27 changed files with 364 additions and 34 deletions
+2
View File
@@ -22,6 +22,8 @@ Each entry must include the Github issue reference in the following format:
## [Unreleased]
* [#24720](https://github.com/cosmos/cosmos-sdk/pull/24720) add `VerboseModeLogger` extension interface and `VerboseLevel` configuration option for increasing log verbosity during sensitive operations such as upgrades.
## [v1.5.1](https://github.com/cosmos/cosmos-sdk/releases/tag/log/v1.5.1) - 2025-03-07
* [#23928](https://github.com/cosmos/cosmos-sdk/pull/23928) Bump sonic json library to [v1.3.1](https://github.com/bytedance/sonic/releases/tag/v1.13.1) for Go 1.24 compatibility.
+129
View File
@@ -1,8 +1,12 @@
package log_test
import (
"bytes"
"fmt"
"testing"
"github.com/rs/zerolog"
"cosmossdk.io/log"
)
@@ -88,3 +92,128 @@ func TestParseLogLevel(t *testing.T) {
t.Errorf("expected filter to return true for state:debug")
}
}
func TestVerboseMode(t *testing.T) {
logMessages := []struct {
level zerolog.Level
module string
message string
}{
{
zerolog.InfoLevel,
"foo",
"msg 1",
},
{
zerolog.WarnLevel,
"foo",
"msg 2",
},
{
zerolog.ErrorLevel,
"bar",
"msg 3",
},
{
zerolog.DebugLevel,
"foo",
"msg 4",
},
}
tt := []struct {
name string
level zerolog.Level
verboseLevel zerolog.Level
filter string
expected string
}{
{
name: "verbose mode simple case",
level: zerolog.WarnLevel,
verboseLevel: zerolog.DebugLevel,
expected: `* WRN msg 2 module=foo
* ERR msg 3 module=bar
* ERR Start Verbose Mode
* INF msg 1 module=foo
* WRN msg 2 module=foo
* ERR msg 3 module=bar
* DBG msg 4 module=foo
`,
},
{
name: "verbose mode with filter",
level: zerolog.WarnLevel,
verboseLevel: zerolog.InfoLevel,
filter: "foo:error",
expected: `* ERR msg 3 module=bar
* ERR Start Verbose Mode
* INF msg 1 module=foo
* WRN msg 2 module=foo
* ERR msg 3 module=bar
`,
},
{
name: "no verbose mode",
level: zerolog.WarnLevel,
verboseLevel: zerolog.NoLevel,
expected: `* WRN msg 2 module=foo
* ERR msg 3 module=bar
* ERR Start Verbose Mode
* WRN msg 2 module=foo
* ERR msg 3 module=bar
`,
},
{
name: "no verbose mode with filter",
level: zerolog.WarnLevel,
verboseLevel: zerolog.NoLevel,
filter: "foo:error",
expected: `* ERR msg 3 module=bar
* ERR Start Verbose Mode
* ERR msg 3 module=bar
`,
},
}
for i, tc := range tt {
t.Run(fmt.Sprintf("%d: %s", i, tc.name), func(t *testing.T) {
out := new(bytes.Buffer)
opts := []log.Option{
log.LevelOption(tc.level),
log.VerboseLevelOption(tc.verboseLevel),
log.ColorOption(false),
log.TimeFormatOption("*"), // disable non-deterministic time format
}
if tc.filter != "" {
filter, err := log.ParseLogLevel(tc.filter)
if err != nil {
t.Fatalf("failed to parse log level: %v", err)
}
opts = append(opts, log.FilterOption(filter))
}
logger := log.NewLogger(out, opts...)
writeMsgs := func() {
for _, msg := range logMessages {
switch msg.level {
case zerolog.InfoLevel:
logger.Info(msg.message, log.ModuleKey, msg.module)
case zerolog.WarnLevel:
logger.Warn(msg.message, log.ModuleKey, msg.module)
case zerolog.DebugLevel:
logger.Debug(msg.message, log.ModuleKey, msg.module)
case zerolog.ErrorLevel:
logger.Error(msg.message, log.ModuleKey, msg.module)
default:
t.Fatalf("unexpected level: %v", msg.level)
}
}
}
writeMsgs()
logger.Error("Start Verbose Mode")
logger.(log.VerboseModeLogger).SetVerboseMode(true)
writeMsgs()
if tc.expected != out.String() {
t.Fatalf("expected:\n%s\ngot:\n%s", tc.expected, out.String())
}
})
}
}
+54 -9
View File
@@ -62,6 +62,15 @@ type Logger interface {
Impl() any
}
// VerboseModeLogger is an extension interface of Logger which allows verbosity to be configured.
type VerboseModeLogger interface {
Logger
// SetVerboseMode configures whether the logger enters verbose mode or not for
// special operations where increased observability of log messages is desired
// (such as chain upgrades).
SetVerboseMode(bool)
}
// WithJSONMarshal configures zerolog global json encoding.
func WithJSONMarshal(marshaler func(v any) ([]byte, error)) {
zerolog.InterfaceMarshalFunc = func(i any) ([]byte, error) {
@@ -80,6 +89,11 @@ func WithJSONMarshal(marshaler func(v any) ([]byte, error)) {
type zeroLogWrapper struct {
*zerolog.Logger
regularLevel zerolog.Level
verboseLevel zerolog.Level
// this field is used to disable filtering during verbose logging
// and will only be non-nil when we have a filterWriter
filterWriter *filterWriter
}
// NewLogger returns a new logger that writes to the given destination.
@@ -105,8 +119,13 @@ func NewLogger(dst io.Writer, options ...Option) Logger {
}
}
var fltWtr *filterWriter
if logCfg.Filter != nil {
output = NewFilterWriter(output, logCfg.Filter)
fltWtr = &filterWriter{
parent: output,
filter: logCfg.Filter,
}
output = fltWtr
}
logger := zerolog.New(output)
@@ -123,18 +142,25 @@ func NewLogger(dst io.Writer, options ...Option) Logger {
logger = logger.With().Timestamp().Logger()
}
if logCfg.Level != zerolog.NoLevel {
logger = logger.Level(logCfg.Level)
}
logger = logger.Level(logCfg.Level)
logger = logger.Hook(logCfg.Hooks...)
return zeroLogWrapper{&logger}
return zeroLogWrapper{
Logger: &logger,
regularLevel: logCfg.Level,
verboseLevel: logCfg.VerboseLevel,
filterWriter: fltWtr,
}
}
// NewCustomLogger returns a new logger with the given zerolog logger.
func NewCustomLogger(logger zerolog.Logger) Logger {
return zeroLogWrapper{&logger}
return zeroLogWrapper{
Logger: &logger,
regularLevel: logger.GetLevel(),
verboseLevel: zerolog.NoLevel,
filterWriter: nil,
}
}
// Info takes a message and a set of key/value pairs and logs with level INFO.
@@ -164,13 +190,15 @@ func (l zeroLogWrapper) Debug(msg string, keyVals ...interface{}) {
// With returns a new wrapped logger with additional context provided by a set.
func (l zeroLogWrapper) With(keyVals ...interface{}) Logger {
logger := l.Logger.With().Fields(keyVals).Logger()
return zeroLogWrapper{&logger}
l.Logger = &logger
return l
}
// WithContext returns a new wrapped logger with additional context provided by a set.
func (l zeroLogWrapper) WithContext(keyVals ...interface{}) any {
logger := l.Logger.With().Fields(keyVals).Logger()
return zeroLogWrapper{&logger}
l.Logger = &logger
return l
}
// Impl returns the underlying zerolog logger.
@@ -179,6 +207,23 @@ func (l zeroLogWrapper) Impl() interface{} {
return l.Logger
}
// SetVerboseMode implements VerboseModeLogger interface.
func (l zeroLogWrapper) SetVerboseMode(enable bool) {
if enable && l.verboseLevel != zerolog.NoLevel {
*l.Logger = l.Level(l.verboseLevel)
if l.filterWriter != nil {
l.filterWriter.disableFilter = true
}
} else {
*l.Logger = l.Level(l.regularLevel)
if l.filterWriter != nil {
l.filterWriter.disableFilter = false
}
}
}
var _ VerboseModeLogger = zeroLogWrapper{}
// NewNopLogger returns a new logger that does nothing.
func NewNopLogger() Logger {
// The custom nopLogger is about 3x faster than a zeroLogWrapper with zerolog.Nop().
+19 -2
View File
@@ -8,7 +8,7 @@ import (
// defaultConfig has all the options disabled, except Color and TimeFormat
var defaultConfig = Config{
Level: zerolog.NoLevel,
Level: zerolog.TraceLevel, // this is the default level that zerolog initializes new Logger's with
Filter: nil,
OutputJSON: false,
Color: true,
@@ -19,7 +19,16 @@ var defaultConfig = Config{
// Config defines configuration for the logger.
type Config struct {
Level zerolog.Level
// Level is the default logging level.
Level zerolog.Level
// VerboseLevel is the logging level to use when verbose mode is enabled.
// If there is a filter enabled, it will be disabled when verbose mode is enabled
// and all log messages will be emitted at the VerboseLevel.
// If this is set to NoLevel, then no changes to the logging level or filter will be made
// when verbose mode is enabled.
VerboseLevel zerolog.Level
// Filter is the filter function to use that allows for filtering by key and level.
// When verbose mode is enabled, the filter will be disabled unless VerboseLevel is set to NoLevel.
Filter FilterFunc
OutputJSON bool
Color bool
@@ -45,6 +54,14 @@ func LevelOption(level zerolog.Level) Option {
}
}
// VerboseLevelOption sets the verbose level for the Logger.
// When verbose mode is enabled, the logger will be switched to this level.
func VerboseLevelOption(level zerolog.Level) Option {
return func(cfg *Config) {
cfg.VerboseLevel = level
}
}
// OutputJSONOption sets the output of the logger to JSON.
// By default, the logger outputs to a human-readable format.
func OutputJSONOption() Option {
+46
View File
@@ -0,0 +1,46 @@
package log
import (
"bytes"
"testing"
"github.com/rs/zerolog"
)
// this test ensures that when the With and WithContext methods are called,
// that the log wrapper is properly copied with all of its associated options
// otherwise, verbose mode will fail
func TestLoggerWith(t *testing.T) {
logger := zerolog.New(&bytes.Buffer{})
regularLevel := zerolog.WarnLevel
verboseLevel := zerolog.InfoLevel
filterWriter := &filterWriter{}
wrapper := zeroLogWrapper{
Logger: &logger,
regularLevel: regularLevel,
verboseLevel: verboseLevel,
filterWriter: filterWriter,
}
wrapper2 := wrapper.With("x", "y").(zeroLogWrapper)
if wrapper2.filterWriter != filterWriter {
t.Fatalf("expected filterWriter to be copied, but it was not")
}
if wrapper2.regularLevel != regularLevel {
t.Fatalf("expected regularLevel to be copied, but it was not")
}
if wrapper2.verboseLevel != verboseLevel {
t.Fatalf("expected verboseLevel to be copied, but it was not")
}
wrapper3 := wrapper.WithContext("a", "b").(zeroLogWrapper)
if wrapper3.filterWriter != filterWriter {
t.Fatalf("expected filterWriter to be copied, but it was not")
}
if wrapper3.regularLevel != regularLevel {
t.Fatalf("expected regularLevel to be copied, but it was not")
}
if wrapper3.verboseLevel != verboseLevel {
t.Fatalf("expected verboseLevel to be copied, but it was not")
}
}
+5 -4
View File
@@ -11,16 +11,17 @@ import (
// If the filter is nil, the writer will pass all events through.
// The filter function is called with the module and level of the event.
func NewFilterWriter(parent io.Writer, filter FilterFunc) io.Writer {
return &filterWriter{parent, filter}
return &filterWriter{parent: parent, filter: filter}
}
type filterWriter struct {
parent io.Writer
filter FilterFunc
parent io.Writer
filter FilterFunc
disableFilter bool
}
func (fw *filterWriter) Write(p []byte) (n int, err error) {
if fw.filter == nil {
if fw.filter == nil || fw.disableFilter {
return fw.parent.Write(p)
}