diff --git a/cmd/gossamer/config.go b/cmd/gossamer/config.go index 1d083972f7..054d2a4f9e 100644 --- a/cmd/gossamer/config.go +++ b/cmd/gossamer/config.go @@ -17,6 +17,7 @@ package main import ( + "errors" "fmt" "strconv" "strings" @@ -287,118 +288,131 @@ func createExportConfig(ctx *cli.Context) (*dot.Config, error) { return cfg, nil } -func setLogConfig(ctx *cli.Context, cfg *ctoml.Config, globalCfg *dot.GlobalConfig, logCfg *dot.LogConfig) error { - if cfg == nil { - cfg = new(ctoml.Config) - } - - if lvlStr := ctx.String(LogFlag.Name); lvlStr != "" { - if lvlToInt, err := strconv.Atoi(lvlStr); err == nil { - lvlStr = log.Lvl(lvlToInt).String() - } - cfg.Global.LogLvl = lvlStr - } - - if cfg.Global.LogLvl == "" { - cfg.Global.LogLvl = gssmr.DefaultLvl.String() - } - - var err error - globalCfg.LogLvl, err = log.LvlFromString(cfg.Global.LogLvl) - if err != nil { - return err - } - - // check and set log levels for each pkg - if cfg.Log.CoreLvl == "" { - logCfg.CoreLvl = globalCfg.LogLvl - } else { - lvl, err := log.LvlFromString(cfg.Log.CoreLvl) - if err != nil { - return err - } - - logCfg.CoreLvl = lvl - } - - if cfg.Log.SyncLvl == "" { - logCfg.SyncLvl = globalCfg.LogLvl - } else { - lvl, err := log.LvlFromString(cfg.Log.SyncLvl) - if err != nil { - return err - } +type stringKVStore interface { + String(key string) (value string) +} - logCfg.SyncLvl = lvl +// getLogLevel obtains the log level in the following order: +// 1. Try to obtain it from the flag value corresponding to flagName. +// 2. Try to obtain it from the TOML value given, if step 1. failed. +// 3. Return the default value given if both previous steps failed. +// For steps 1 and 2, it tries to parse the level as an integer to convert it +// to a level, and also tries to parse it as a string. +func getLogLevel(flagsKVStore stringKVStore, flagName, tomlValue string, defaultLevel log.Lvl) ( + level log.Lvl, err error) { + if flagValue := flagsKVStore.String(flagName); flagValue != "" { + return parseLogLevelString(flagValue) } - if cfg.Log.NetworkLvl == "" { - logCfg.NetworkLvl = globalCfg.LogLvl - } else { - lvl, err := log.LvlFromString(cfg.Log.NetworkLvl) - if err != nil { - return err - } - - logCfg.NetworkLvl = lvl + if tomlValue == "" { + return defaultLevel, nil } - if cfg.Log.RPCLvl == "" { - logCfg.RPCLvl = globalCfg.LogLvl - } else { - lvl, err := log.LvlFromString(cfg.Log.RPCLvl) - if err != nil { - return err - } + return parseLogLevelString(tomlValue) +} - logCfg.RPCLvl = lvl - } +var ErrLogLevelIntegerOutOfRange = errors.New("log level integer can only be between 0 and 5 included") - if cfg.Log.StateLvl == "" { - logCfg.StateLvl = globalCfg.LogLvl - } else { - lvl, err := log.LvlFromString(cfg.Log.StateLvl) - if err != nil { - return err +func parseLogLevelString(logLevelString string) (logLevel log.Lvl, err error) { + levelInt, err := strconv.Atoi(logLevelString) + if err == nil { // level given as an integer + if levelInt < 0 || levelInt > 5 { + return 0, fmt.Errorf("%w: log level given: %d", ErrLogLevelIntegerOutOfRange, levelInt) } - - logCfg.StateLvl = lvl + logLevel = log.Lvl(levelInt) + return logLevel, nil } - if cfg.Log.RuntimeLvl == "" { - logCfg.RuntimeLvl = globalCfg.LogLvl - } else { - lvl, err := log.LvlFromString(cfg.Log.RuntimeLvl) - if err != nil { - return err - } - - logCfg.RuntimeLvl = lvl + logLevel, err = log.LvlFromString(logLevelString) + if err != nil { + return 0, fmt.Errorf("cannot parse log level string: %w", err) } - if cfg.Log.BlockProducerLvl == "" { - logCfg.BlockProducerLvl = globalCfg.LogLvl - } else { - lvl, err := log.LvlFromString(cfg.Log.BlockProducerLvl) - if err != nil { - return err - } + return logLevel, nil +} - logCfg.BlockProducerLvl = lvl +func setLogConfig(flagsKVStore stringKVStore, cfg *ctoml.Config, globalCfg *dot.GlobalConfig, logCfg *dot.LogConfig) (err error) { + if cfg == nil { + cfg = new(ctoml.Config) } - if cfg.Log.FinalityGadgetLvl == "" { - logCfg.FinalityGadgetLvl = globalCfg.LogLvl - } else { - lvl, err := log.LvlFromString(cfg.Log.FinalityGadgetLvl) + globalCfg.LogLvl, err = getLogLevel(flagsKVStore, LogFlag.Name, cfg.Global.LogLvl, gssmr.DefaultLvl) + if err != nil { + return fmt.Errorf("cannot get global log level: %w", err) + } + cfg.Global.LogLvl = globalCfg.LogLvl.String() + + levelsData := []struct { + name string + flagName string + tomlValue string + levelPtr *log.Lvl // pointer to value to modify + }{ + { + name: "core", + flagName: LogCoreLevelFlag.Name, + tomlValue: cfg.Log.CoreLvl, + levelPtr: &logCfg.CoreLvl, + }, + { + name: "sync", + flagName: LogSyncLevelFlag.Name, + tomlValue: cfg.Log.SyncLvl, + levelPtr: &logCfg.SyncLvl, + }, + { + name: "network", + flagName: LogNetworkLevelFlag.Name, + tomlValue: cfg.Log.NetworkLvl, + levelPtr: &logCfg.NetworkLvl, + }, + { + name: "RPC", + flagName: LogRPCLevelFlag.Name, + tomlValue: cfg.Log.RPCLvl, + levelPtr: &logCfg.RPCLvl, + }, + { + name: "state", + flagName: LogStateLevelFlag.Name, + tomlValue: cfg.Log.StateLvl, + levelPtr: &logCfg.StateLvl, + }, + { + name: "runtime", + flagName: LogRuntimeLevelFlag.Name, + tomlValue: cfg.Log.RuntimeLvl, + levelPtr: &logCfg.RuntimeLvl, + }, + { + name: "block producer", + flagName: LogBabeLevelFlag.Name, + tomlValue: cfg.Log.BlockProducerLvl, + levelPtr: &logCfg.BlockProducerLvl, + }, + { + name: "finality gadget", + flagName: LogGrandpaLevelFlag.Name, + tomlValue: cfg.Log.FinalityGadgetLvl, + levelPtr: &logCfg.FinalityGadgetLvl, + }, + { + name: "sync", + flagName: LogSyncLevelFlag.Name, + tomlValue: cfg.Log.SyncLvl, + levelPtr: &logCfg.SyncLvl, + }, + } + + for _, levelData := range levelsData { + level, err := getLogLevel(flagsKVStore, levelData.flagName, levelData.tomlValue, globalCfg.LogLvl) if err != nil { - return err + return fmt.Errorf("cannot get %s log level: %w", levelData.name, err) } - - logCfg.FinalityGadgetLvl = lvl + *levelData.levelPtr = level } - logger.Debug("set log configuration", "--log", ctx.String(LogFlag.Name), "global", globalCfg.LogLvl) + logger.Debug("set log configuration", "--log", flagsKVStore.String(LogFlag.Name), "global", globalCfg.LogLvl) return nil } diff --git a/cmd/gossamer/config_test.go b/cmd/gossamer/config_test.go index 64ef0ee06c..c7a8b23799 100644 --- a/cmd/gossamer/config_test.go +++ b/cmd/gossamer/config_test.go @@ -17,6 +17,7 @@ package main import ( + "errors" "io/ioutil" "testing" "time" @@ -24,12 +25,14 @@ import ( "github.com/ChainSafe/gossamer/chain/dev" "github.com/ChainSafe/gossamer/chain/gssmr" "github.com/ChainSafe/gossamer/dot" + ctoml "github.com/ChainSafe/gossamer/dot/config/toml" "github.com/ChainSafe/gossamer/dot/state" "github.com/ChainSafe/gossamer/dot/types" "github.com/ChainSafe/gossamer/lib/genesis" "github.com/ChainSafe/gossamer/lib/utils" log "github.com/ChainSafe/log15" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/urfave/cli" ) @@ -1065,3 +1068,227 @@ func TestGlobalNodeNamePriorityOrder(t *testing.T) { require.NotEqual(t, cfg.Global.Name, createdCfg.Global.Name) }) } + +type mockGetStringer struct { + kv map[string]string +} + +func (m *mockGetStringer) String(key string) (value string) { + return m.kv[key] +} + +func newMockGetStringer(keyValue map[string]string) *mockGetStringer { + kv := make(map[string]string, len(keyValue)) + for k, v := range keyValue { + kv[k] = v + } + return &mockGetStringer{kv: kv} +} + +func Test_getLogLevel(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + flagsKVStore stringKVStore + flagName string + tomlValue string + defaultLevel log.Lvl + level log.Lvl + err error + }{ + "no value with default": { + flagsKVStore: newMockGetStringer(map[string]string{}), + defaultLevel: log.LvlError, + level: log.LvlError, + }, + "flag integer value": { + flagsKVStore: newMockGetStringer(map[string]string{"x": "1"}), + flagName: "x", + level: log.LvlError, + }, + "flag string value": { + flagsKVStore: newMockGetStringer(map[string]string{"x": "eror"}), + flagName: "x", + level: log.LvlError, + }, + "flag bad string value": { + flagsKVStore: newMockGetStringer(map[string]string{"x": "garbage"}), + flagName: "x", + err: errors.New("cannot parse log level string: Unknown level: garbage"), + }, + "toml integer value": { + flagsKVStore: newMockGetStringer(map[string]string{}), + tomlValue: "1", + level: log.LvlError, + }, + "toml string value": { + flagsKVStore: newMockGetStringer(map[string]string{}), + tomlValue: "eror", + level: log.LvlError, + }, + "toml bad string value": { + flagsKVStore: newMockGetStringer(map[string]string{}), + tomlValue: "garbage", + err: errors.New("cannot parse log level string: Unknown level: garbage"), + }, + "flag takes precedence": { + flagsKVStore: newMockGetStringer(map[string]string{"x": "eror"}), + flagName: "x", + tomlValue: "warn", + level: log.LvlError, + }, + } + + for name, testCase := range testCases { + testCase := testCase + t.Run(name, func(t *testing.T) { + t.Parallel() + + level, err := getLogLevel(testCase.flagsKVStore, testCase.flagName, + testCase.tomlValue, testCase.defaultLevel) + + if testCase.err != nil { + assert.EqualError(t, err, testCase.err.Error()) + } else { + assert.NoError(t, err) + } + assert.Equal(t, testCase.level, level) + }) + } +} + +func Test_parseLogLevelString(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + logLevelString string + logLevel log.Lvl + err error + }{ + "empty string": { + err: errors.New("cannot parse log level string: Unknown level: "), + }, + "valid integer": { + logLevelString: "1", + logLevel: log.LvlError, + }, + "minus one": { + logLevelString: "-1", + err: errors.New("log level integer can only be between 0 and 5 included: log level given: -1"), + }, + "over 5": { + logLevelString: "6", + err: errors.New("log level integer can only be between 0 and 5 included: log level given: 6"), + }, + "valid string": { + logLevelString: "error", + logLevel: log.LvlError, + }, + } + + for name, testCase := range testCases { + testCase := testCase + t.Run(name, func(t *testing.T) { + t.Parallel() + + logLevel, err := parseLogLevelString(testCase.logLevelString) + + if testCase.err != nil { + assert.EqualError(t, err, testCase.err.Error()) + } else { + assert.NoError(t, err) + } + assert.Equal(t, testCase.logLevel, logLevel) + }) + } +} + +func Test_setLogConfig(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + ctx stringKVStore + initialCfg ctoml.Config + initialGlobalCfg dot.GlobalConfig + initialLogCfg dot.LogConfig + expectedCfg ctoml.Config + expectedGlobalCfg dot.GlobalConfig + expectedLogCfg dot.LogConfig + err error + }{ + "no value": { + ctx: newMockGetStringer(map[string]string{}), + expectedCfg: ctoml.Config{ + Global: ctoml.GlobalConfig{ + LogLvl: log.LvlInfo.String(), + }, + }, + expectedGlobalCfg: dot.GlobalConfig{ + LogLvl: log.LvlInfo, + }, + expectedLogCfg: dot.LogConfig{ + CoreLvl: log.LvlInfo, + SyncLvl: log.LvlInfo, + NetworkLvl: log.LvlInfo, + RPCLvl: log.LvlInfo, + StateLvl: log.LvlInfo, + RuntimeLvl: log.LvlInfo, + BlockProducerLvl: log.LvlInfo, + FinalityGadgetLvl: log.LvlInfo, + }, + }, + "some values": { + ctx: newMockGetStringer(map[string]string{}), + initialCfg: ctoml.Config{ + Log: ctoml.LogConfig{ + CoreLvl: log.LvlError.String(), + SyncLvl: log.LvlDebug.String(), + StateLvl: log.LvlWarn.String(), + }, + }, + expectedCfg: ctoml.Config{ + Global: ctoml.GlobalConfig{ + LogLvl: log.LvlInfo.String(), + }, + Log: ctoml.LogConfig{ + CoreLvl: log.LvlError.String(), + SyncLvl: log.LvlDebug.String(), + StateLvl: log.LvlWarn.String(), + }, + }, + expectedGlobalCfg: dot.GlobalConfig{ + LogLvl: log.LvlInfo, + }, + expectedLogCfg: dot.LogConfig{ + CoreLvl: log.LvlError, + SyncLvl: log.LvlDebug, + NetworkLvl: log.LvlInfo, + RPCLvl: log.LvlInfo, + StateLvl: log.LvlWarn, + RuntimeLvl: log.LvlInfo, + BlockProducerLvl: log.LvlInfo, + FinalityGadgetLvl: log.LvlInfo, + }, + }, + } + + for name, testCase := range testCases { + testCase := testCase + t.Run(name, func(t *testing.T) { + t.Parallel() + + err := setLogConfig(testCase.ctx, &testCase.initialCfg, + &testCase.initialGlobalCfg, &testCase.initialLogCfg) + + if testCase.err != nil { + assert.EqualError(t, err, testCase.err.Error()) + } else { + assert.NoError(t, err) + } + + assert.Equal(t, testCase.expectedCfg, testCase.initialCfg) + assert.Equal(t, testCase.expectedGlobalCfg, testCase.initialGlobalCfg) + assert.Equal(t, testCase.expectedLogCfg, testCase.initialLogCfg) + }) + } +} diff --git a/cmd/gossamer/flags.go b/cmd/gossamer/flags.go index 93b2d6d472..5ae6a3118b 100644 --- a/cmd/gossamer/flags.go +++ b/cmd/gossamer/flags.go @@ -56,9 +56,50 @@ var ( // LogFlag cli service settings LogFlag = cli.StringFlag{ Name: "log", - Usage: "Supports levels crit (silent) to trce (trace)", + Usage: "Global log level. Supports levels crit (silent), eror, warn, info, dbug and trce (trace)", Value: log.LvlInfo.String(), } + LogCoreLevelFlag = cli.StringFlag{ + Name: "log-core", + Usage: "Core package log level. Supports levels crit (silent), eror, warn, info, dbug and trce (trace)", + Value: LogFlag.Value, + } + LogSyncLevelFlag = cli.StringFlag{ + Name: "log-sync", + Usage: "Sync package log level. Supports levels crit (silent), eror, warn, info, dbug and trce (trace)", + Value: LogFlag.Value, + } + LogNetworkLevelFlag = cli.StringFlag{ + Name: "log-network", + Usage: "Network package log level. Supports levels crit (silent), eror, warn, info, dbug and trce (trace)", + Value: LogFlag.Value, + } + LogRPCLevelFlag = cli.StringFlag{ + Name: "log-rpc", + Usage: "RPC package log level. Supports levels crit (silent), eror, warn, info, dbug and trce (trace)", + Value: LogFlag.Value, + } + LogStateLevelFlag = cli.StringFlag{ + Name: "log-state", + Usage: "State package log level. Supports levels crit (silent), eror, warn, info, dbug and trce (trace)", + Value: LogFlag.Value, + } + LogRuntimeLevelFlag = cli.StringFlag{ + Name: "log-runtime", + Usage: "Runtime package log level. Supports levels crit (silent), eror, warn, info, dbug and trce (trace)", + Value: LogFlag.Value, + } + LogBabeLevelFlag = cli.StringFlag{ + Name: "log-babe", + Usage: "BABE package log level. Supports levels crit (silent), eror, warn, info, dbug and trce (trace)", + Value: LogFlag.Value, + } + LogGrandpaLevelFlag = cli.StringFlag{ + Name: "log-grandpa", + Usage: "Grandpa package log level. Supports levels crit (silent), eror, warn, info, dbug and trce (trace)", + Value: LogFlag.Value, + } + // NameFlag node implementation name NameFlag = cli.StringFlag{ Name: "name", @@ -342,6 +383,14 @@ var ( // GlobalFlags are flags that are valid for use with the root command and all subcommands GlobalFlags = []cli.Flag{ LogFlag, + LogCoreLevelFlag, + LogSyncLevelFlag, + LogNetworkLevelFlag, + LogRPCLevelFlag, + LogStateLevelFlag, + LogRuntimeLevelFlag, + LogBabeLevelFlag, + LogGrandpaLevelFlag, NameFlag, ChainFlag, ConfigFlag,