From 8a5f92df2f142906fc05a0104f6128e389758c4f Mon Sep 17 00:00:00 2001 From: "Quentin McGaw (desktop)" Date: Mon, 1 Nov 2021 14:31:45 +0000 Subject: [PATCH] Read log levels from flags --- cmd/gossamer/config.go | 150 +++++++++++++----------------- cmd/gossamer/config_test.go | 181 ++++++++++++++++++++++++++++++++++++ cmd/gossamer/flags.go | 51 +++++++++- 3 files changed, 298 insertions(+), 84 deletions(-) diff --git a/cmd/gossamer/config.go b/cmd/gossamer/config.go index 0f287244ce7..3d2021962f3 100644 --- a/cmd/gossamer/config.go +++ b/cmd/gossamer/config.go @@ -18,6 +18,7 @@ package main import ( "fmt" + "regexp" "strconv" "strings" "time" @@ -287,115 +288,98 @@ 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 - } +type getStringer interface { + String(key string) (value string) +} - if cfg.Global.LogLvl == "" { - cfg.Global.LogLvl = gssmr.DefaultLvl.String() +// 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(ctx getStringer, flagName, tomlValue string, defaultLevel log.Lvl) ( + level log.Lvl, err error) { + if flagValue := ctx.String(flagName); flagValue != "" { + return parseLogLevelString(flagValue) } - var err error - globalCfg.LogLvl, err = log.LvlFromString(cfg.Global.LogLvl) - if err != nil { - return err + if tomlValue == "" { + return defaultLevel, nil } - // 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 - } + return parseLogLevelString(tomlValue) +} - logCfg.CoreLvl = lvl - } +var regexDigits = regexp.MustCompile("^[0-9]+$") - if cfg.Log.SyncLvl == "" { - logCfg.SyncLvl = globalCfg.LogLvl - } else { - lvl, err := log.LvlFromString(cfg.Log.SyncLvl) - if err != nil { - return err +func parseLogLevelString(logLevelString string) (logLevel log.Lvl, err error) { + if regexDigits.MatchString(logLevelString) { + levelInt, err := strconv.Atoi(logLevelString) + if err != nil { // should never happen + return 0, fmt.Errorf("cannot parse log level digits: %w", err) } + logLevel = log.Lvl(levelInt) + return logLevel, nil + } - logCfg.SyncLvl = lvl + logLevel, err = log.LvlFromString(logLevelString) + if err != nil { + return 0, fmt.Errorf("cannot parse log level string: %w", err) } - if cfg.Log.NetworkLvl == "" { - logCfg.NetworkLvl = globalCfg.LogLvl - } else { - lvl, err := log.LvlFromString(cfg.Log.NetworkLvl) - if err != nil { - return err - } + return logLevel, nil +} - logCfg.NetworkLvl = lvl +func setLogConfig(ctx getStringer, cfg *ctoml.Config, globalCfg *dot.GlobalConfig, logCfg *dot.LogConfig) (err error) { + if cfg == nil { + cfg = new(ctoml.Config) } - if cfg.Log.RPCLvl == "" { - logCfg.RPCLvl = globalCfg.LogLvl - } else { - lvl, err := log.LvlFromString(cfg.Log.RPCLvl) - if err != nil { - return err - } - - logCfg.RPCLvl = lvl + globalCfg.LogLvl, err = getLogLevel(ctx, 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() - if cfg.Log.StateLvl == "" { - logCfg.StateLvl = globalCfg.LogLvl - } else { - lvl, err := log.LvlFromString(cfg.Log.StateLvl) - if err != nil { - return err - } + logCfg.CoreLvl, err = getLogLevel(ctx, LogCoreLevelFlag.Name, cfg.Log.CoreLvl, globalCfg.LogLvl) + if err != nil { + return fmt.Errorf("cannot get core log level: %w", err) + } - logCfg.StateLvl = lvl + logCfg.SyncLvl, err = getLogLevel(ctx, LogSyncLevelFlag.Name, cfg.Log.SyncLvl, globalCfg.LogLvl) + if err != nil { + return fmt.Errorf("cannot get sync log level: %w", err) } - if cfg.Log.RuntimeLvl == "" { - logCfg.RuntimeLvl = globalCfg.LogLvl - } else { - lvl, err := log.LvlFromString(cfg.Log.RuntimeLvl) - if err != nil { - return err - } + logCfg.NetworkLvl, err = getLogLevel(ctx, LogNetworkLevelFlag.Name, cfg.Log.NetworkLvl, globalCfg.LogLvl) + if err != nil { + return fmt.Errorf("cannot get network log level: %w", err) + } - logCfg.RuntimeLvl = lvl + logCfg.RPCLvl, err = getLogLevel(ctx, LogRPCLevelFlag.Name, cfg.Log.RPCLvl, globalCfg.LogLvl) + if err != nil { + return fmt.Errorf("cannot get RPC log level: %w", err) } - if cfg.Log.BlockProducerLvl == "" { - logCfg.BlockProducerLvl = globalCfg.LogLvl - } else { - lvl, err := log.LvlFromString(cfg.Log.BlockProducerLvl) - if err != nil { - return err - } + logCfg.StateLvl, err = getLogLevel(ctx, LogStateLevelFlag.Name, cfg.Log.StateLvl, globalCfg.LogLvl) + if err != nil { + return fmt.Errorf("cannot get state log level: %w", err) + } - logCfg.BlockProducerLvl = lvl + logCfg.RuntimeLvl, err = getLogLevel(ctx, LogRuntimeLevelFlag.Name, cfg.Log.RuntimeLvl, globalCfg.LogLvl) + if err != nil { + return fmt.Errorf("cannot get runtime log level: %w", err) } - if cfg.Log.FinalityGadgetLvl == "" { - logCfg.FinalityGadgetLvl = globalCfg.LogLvl - } else { - lvl, err := log.LvlFromString(cfg.Log.FinalityGadgetLvl) - if err != nil { - return err - } + logCfg.BlockProducerLvl, err = getLogLevel(ctx, LogBlockProducerLevelFlag.Name, cfg.Log.BlockProducerLvl, globalCfg.LogLvl) + if err != nil { + return fmt.Errorf("cannot get block producer log level: %w", err) + } - logCfg.FinalityGadgetLvl = lvl + logCfg.FinalityGadgetLvl, err = getLogLevel(ctx, LogFinalityGadgetLevelFlag.Name, cfg.Log.FinalityGadgetLvl, globalCfg.LogLvl) + if err != nil { + return fmt.Errorf("cannot get finality gadget log level: %w", err) } logger.Debug("set log configuration", "--log", ctx.String(LogFlag.Name), "global", globalCfg.LogLvl) diff --git a/cmd/gossamer/config_test.go b/cmd/gossamer/config_test.go index a26cd9dc4e3..c60124c2a05 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" ) @@ -986,3 +989,181 @@ 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 { + ctx getStringer + flagName string + tomlValue string + defaultLevel log.Lvl + level log.Lvl + err error + }{ + "no value with default": { + ctx: newMockGetStringer(map[string]string{}), + defaultLevel: log.LvlError, + level: log.LvlError, + }, + "flag integer value": { + ctx: newMockGetStringer(map[string]string{"x": "1"}), + flagName: "x", + level: log.LvlError, + }, + "flag string value": { + ctx: newMockGetStringer(map[string]string{"x": "eror"}), + flagName: "x", + level: log.LvlError, + }, + "flag bad string value": { + ctx: newMockGetStringer(map[string]string{}), + flagName: "x", + err: errors.New("cannot parse log level string: Unknown level: garbage"), + }, + "toml integer value": { + ctx: newMockGetStringer(map[string]string{}), + tomlValue: "1", + level: log.LvlError, + }, + "toml string value": { + ctx: newMockGetStringer(map[string]string{}), + tomlValue: "eror", + level: log.LvlError, + }, + "toml bad string value": { + ctx: newMockGetStringer(map[string]string{}), + tomlValue: "garbage", + err: errors.New("cannot parse log level string: Unknown level: garbage"), + }, + "flag takes precedence": { + ctx: 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.ctx, 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_setLogConfig(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + ctx getStringer + 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 a8eb71666a3..c12157e24c1 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, + } + LogBlockProducerLevelFlag = cli.StringFlag{ + Name: "log-blockproducer", + Usage: "Block producer package log level. Supports levels crit (silent), eror, warn, info, dbug and trce (trace)", + Value: LogFlag.Value, + } + LogFinalityGadgetLevelFlag = cli.StringFlag{ + Name: "log-finalitygadget", + Usage: "Finality Gadget 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", @@ -329,6 +370,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, + LogBlockProducerLevelFlag, + LogFinalityGadgetLevelFlag, NameFlag, ChainFlag, ConfigFlag,