From 66ae1cd14a0b8247cc0e8bc9e34b624e6812b6b5 Mon Sep 17 00:00:00 2001 From: John-Alan Simmons Date: Mon, 25 Jul 2022 22:26:28 -0400 Subject: [PATCH] feat: Add named config overrides (#659) Supports a new format for configuring loggers that targets specific named loggers. It uses a comma-seperated-value (CSV) list of keyvalues. eg: --loglevel error,defra.cli=debug will set the global level for all loggers to error and then the defra.cli level to debug specifically. It supports an unbounded number of named logger values. Additionally, introduces a new cli flag --logger which uses the same format, but for all fields of a specific logger, instead of individual fields of many loggers. eg: --logger defra.cli,level=debug,output=stdout,format=json will set the level to debug, output to stdout, and format to json for the logger named defra.cli. Co-authored-by: Andrew Sisley --- cli/cli.go | 167 +++++++++++++++++++++++ cli/init.go | 13 +- cli/root.go | 43 +++--- cli/start.go | 13 +- config/config.go | 102 ++++++++++---- config/config_test.go | 8 +- logging/config.go | 19 +-- logging/logging_test.go | 16 +-- tests/integration/cli/log_config_test.go | 109 +++++++++++++++ 9 files changed, 403 insertions(+), 87 deletions(-) create mode 100644 tests/integration/cli/log_config_test.go diff --git a/cli/cli.go b/cli/cli.go index 8d8325fa76..50c5df932c 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -20,10 +20,12 @@ import ( "encoding/json" "fmt" "os" + "strconv" "strings" "github.com/sourcenetwork/defradb/config" "github.com/sourcenetwork/defradb/logging" + "github.com/spf13/cobra" ) const badgerDatastoreName = "badger" @@ -82,3 +84,168 @@ func hasGraphQLErrors(buf []byte) (bool, error) { return false, nil } } + +// parseAndConfigLog parses and then configures the given config.Config logging subconfig. +// we use log.Fatal instead of returning an error because we can't gurantee +// atomic updates, its either everything is properly set, or we Fatal() +func parseAndConfigLog(ctx context.Context, cfg *config.LoggingConfig, cmd *cobra.Command) error { + // handle --loglevels ,=,... + err := parseAndConfigLogStringParam(ctx, cfg, cfg.Level, func(l *config.LoggingConfig, v string) { + l.Level = v + }) + if err != nil { + return err + } + + // handle --logger ,=,... + loggerKVs, err := cmd.Flags().GetString("logger") + if err != nil { + return fmt.Errorf("can't get logger flag: %w", err) + } + + if loggerKVs != "" { + if err := parseAndConfigLogAllParams(ctx, cfg, loggerKVs); err != nil { + return err + } + } + loggingConfig, err := cfg.ToLoggerConfig() + if err != nil { + return fmt.Errorf("could not get logging config: %w", err) + } + logging.SetConfig(loggingConfig) + + return nil +} + +func parseAndConfigLogAllParams(ctx context.Context, cfg *config.LoggingConfig, kvs string) error { + if kvs == "" { + return nil //nothing todo + } + + // check if a CSV is provided + parsed := strings.Split(kvs, ",") + if len(parsed) <= 1 { + log.Fatal(ctx, "invalid --logger format, must be a csv") + } + name := parsed[0] + + // verify KV format (,=,...) + // skip the first as that will be set above + for _, kv := range parsed[1:] { + parsedKV := strings.Split(kv, "=") + if len(parsedKV) != 2 { + return fmt.Errorf("level was not provided as = pair: %s", kv) + } + + logcfg, err := cfg.GetOrCreateNamedLogger(name) + if err != nil { + return fmt.Errorf("could not get named logger config: %w", err) + } + + // handle field + switch param := strings.ToLower(parsedKV[0]); param { + case "level": // string + logcfg.Level = parsedKV[1] + case "format": // string + logcfg.Format = parsedKV[1] + case "output": // string + logcfg.OutputPath = parsedKV[1] + case "stacktrace": // bool + boolValue, err := strconv.ParseBool(parsedKV[1]) + if err != nil { + return fmt.Errorf("couldn't parse kv bool: %w", err) + } + logcfg.Stacktrace = boolValue + case "nocolor": // bool + boolValue, err := strconv.ParseBool(parsedKV[1]) + if err != nil { + return fmt.Errorf("couldn't parse kv bool: %w", err) + } + logcfg.NoColor = boolValue + default: + return fmt.Errorf("unknown parameter for logger: %s", param) + } + } + return nil +} + +func parseAndConfigLogStringParam( + ctx context.Context, + cfg *config.LoggingConfig, + kvs string, + paramSetterFn logParamSetterStringFn) error { + if kvs == "" { + return nil //nothing todo + } + + // check if a CSV is provided + // if its not a CSV, then just do the regular binding to the config + parsed := strings.Split(kvs, ",") + paramSetterFn(cfg, parsed[0]) + if len(parsed) == 1 { + return nil //nothing more todo + } + + // verify KV format (,=,...) + // skip the first as that will be set above + for _, kv := range parsed[1:] { + parsedKV := strings.Split(kv, "=") + if len(parsedKV) != 2 { + return fmt.Errorf("level was not provided as = pair: %s", kv) + } + + logcfg, err := cfg.GetOrCreateNamedLogger(parsedKV[0]) + if err != nil { + return fmt.Errorf("could not get named logger config: %w", err) + } + + paramSetterFn(&logcfg.LoggingConfig, parsedKV[1]) + } + return nil +} + +type logParamSetterStringFn func(*config.LoggingConfig, string) + +// +// LEAVE FOR NOW - IMPLEMENTING SOON - PLEASE IGNORE FOR NOW +// +// func parseAndConfigLogBoolParam( +// ctx context.Context, cfg *config.LoggingConfig, kvs string, paramFn logParamSetterBoolFn) { +// if kvs == "" { +// return //nothing todo +// } + +// // check if a CSV is provided +// // if its not a CSV, then just do the regular binding to the config +// parsed := strings.Split(kvs, ",") +// boolValue, err := strconv.ParseBool(parsed[0]) +// if err != nil { +// log.FatalE(ctx, "couldn't parse kv bool", err) +// } +// paramFn(cfg, boolValue) +// if len(parsed) == 1 { +// return //nothing more todo +// } + +// // verify KV format (,=,...) +// // skip the first as that will be set above +// for _, kv := range parsed[1:] { +// parsedKV := strings.Split(kv, "=") +// if len(parsedKV) != 2 { +// log.Fatal(ctx, "field was not provided as = pair", logging.NewKV("pair", kv)) +// } + +// logcfg, err := cfg.GetOrCreateNamedLogger(parsedKV[0]) +// if err != nil { +// log.FatalE(ctx, "could not get named logger config", err) +// } + +// boolValue, err := strconv.ParseBool(parsedKV[1]) +// if err != nil { +// log.FatalE(ctx, "couldn't parse kv bool", err) +// } +// paramFn(&logcfg.LoggingConfig, boolValue) +// } +// } + +// type logParamSetterBoolFn func(*config.LoggingConfig, bool) diff --git a/cli/init.go b/cli/init.go index 31225a0892..ce19565f2f 100644 --- a/cli/init.go +++ b/cli/init.go @@ -15,7 +15,6 @@ import ( "os" "github.com/sourcenetwork/defradb/config" - "github.com/sourcenetwork/defradb/logging" "github.com/spf13/cobra" ) @@ -34,17 +33,15 @@ var initCmd = &cobra.Command{ Short: "Initialize DefraDB's root directory and configuration file", Long: "Initialize a directory for configuration and data at the given path.", // Load a default configuration, considering env. variables and CLI flags. - PersistentPreRunE: func(_ *cobra.Command, _ []string) error { + PersistentPreRunE: func(cmd *cobra.Command, _ []string) error { err := cfg.LoadWithoutRootDir() if err != nil { return fmt.Errorf("failed to load configuration: %w", err) } - loggingConfig, err := cfg.GetLoggingConfig() - if err != nil { - return fmt.Errorf("failed to load logging configuration: %w", err) - } - logging.SetConfig(loggingConfig) - return nil + + // parse loglevel overrides. + // binding the flags / EnvVars to the struct + return parseAndConfigLog(cmd.Context(), cfg.Log, cmd) }, RunE: func(cmd *cobra.Command, args []string) error { rootDirPath := "" diff --git a/cli/root.go b/cli/root.go index 6893152d02..46ec8b19f0 100644 --- a/cli/root.go +++ b/cli/root.go @@ -15,7 +15,6 @@ import ( "fmt" "github.com/sourcenetwork/defradb/config" - "github.com/sourcenetwork/defradb/logging" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -40,27 +39,32 @@ See https://docs.source.network/BSLv0.2.txt for more information. if err != nil { return fmt.Errorf("failed to get root dir: %w", err) } + defaultConfig := false if exists { err := cfg.Load(rootDir) if err != nil { return fmt.Errorf("failed to load config: %w", err) } - loggingConfig, err := cfg.GetLoggingConfig() - if err != nil { - return fmt.Errorf("failed to get logging config: %w", err) - } - logging.SetConfig(loggingConfig) - log.Debug(cmd.Context(), fmt.Sprintf("Configuration loaded from DefraDB directory %v", rootDir)) } else { err := cfg.LoadWithoutRootDir() if err != nil { return fmt.Errorf("failed to load config: %w", err) } - loggingConfig, err := cfg.GetLoggingConfig() - if err != nil { - return fmt.Errorf("failed to get logging config: %w", err) - } - logging.SetConfig(loggingConfig) + defaultConfig = true + } + + // parse loglevel overrides + // we use `cfg.Logging.Level` as an argument since the viper.Bind already handles + // binding the flags / EnvVars to the struct + if err := parseAndConfigLog(cmd.Context(), cfg.Log, cmd); err != nil { + return err + } + + if defaultConfig { + log.Info(cmd.Context(), "Using default configuration") + } else { + log.Info(cmd.Context(), fmt.Sprintf("Configuration loaded from DefraDB directory %v", rootDir)) + } return nil }, @@ -76,16 +80,21 @@ func init() { "loglevel", cfg.Log.Level, "Log level to use. Options are debug, info, error, fatal", ) - err := viper.BindPFlag("log.level", rootCmd.PersistentFlags().Lookup("loglevel")) + err := viper.BindPFlag("logging.level", rootCmd.PersistentFlags().Lookup("loglevel")) if err != nil { - log.FatalE(context.Background(), "Could not bind log.loglevel", err) + log.FatalE(context.Background(), "Could not bind logging.loglevel", err) } + rootCmd.PersistentFlags().String( + "logger", "", + "Named logger parameter override. usage: --logger ,level=,output=,...", + ) + rootCmd.PersistentFlags().String( "logoutput", cfg.Log.OutputPath, "Log output path", ) - err = viper.BindPFlag("log.outputpath", rootCmd.PersistentFlags().Lookup("logoutput")) + err = viper.BindPFlag("logging.outputpath", rootCmd.PersistentFlags().Lookup("logoutput")) if err != nil { log.FatalE(context.Background(), "Could not bind log.outputpath", err) } @@ -94,7 +103,7 @@ func init() { "logformat", cfg.Log.Format, "Log format to use. Options are csv, json", ) - err = viper.BindPFlag("log.format", rootCmd.PersistentFlags().Lookup("logformat")) + err = viper.BindPFlag("logging.format", rootCmd.PersistentFlags().Lookup("logformat")) if err != nil { log.FatalE(context.Background(), "Could not bind log.format", err) } @@ -103,7 +112,7 @@ func init() { "logtrace", cfg.Log.Stacktrace, "Include stacktrace in error and fatal logs", ) - err = viper.BindPFlag("log.stacktrace", rootCmd.PersistentFlags().Lookup("logtrace")) + err = viper.BindPFlag("logging.stacktrace", rootCmd.PersistentFlags().Lookup("logtrace")) if err != nil { log.FatalE(context.Background(), "Could not bind log.stacktrace", err) } diff --git a/cli/start.go b/cli/start.go index 696b5be826..4ede925796 100644 --- a/cli/start.go +++ b/cli/start.go @@ -62,16 +62,17 @@ var startCmd = &cobra.Command{ if err != nil { return fmt.Errorf("failed to load config: %w", err) } - loggingConfig, err := cfg.GetLoggingConfig() - if err != nil { - return fmt.Errorf("failed to get logging config: %w", err) + + // parse loglevel overrides + if err := parseAndConfigLog(cmd.Context(), cfg.Log, cmd); err != nil { + return err } - logging.SetConfig(loggingConfig) log.Info(cmd.Context(), fmt.Sprintf("Configuration loaded from DefraDB directory %v", rootDir)) return nil }, - RunE: func(cmd *cobra.Command, _ []string) error { - log.Info(cmd.Context(), "Starting DefraDB service...") + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + log.Info(ctx, "Starting DefraDB service...") // setup signal handlers signalCh := make(chan os.Signal, 1) diff --git a/config/config.go b/config/config.go index 46038dae11..611a9a9a10 100644 --- a/config/config.go +++ b/config/config.go @@ -77,7 +77,7 @@ type Config struct { Datastore *DatastoreConfig API *APIConfig Net *NetConfig - Log *LogConfig + Log *LoggingConfig } // Load Config and handles parameters from config file, environment variables. @@ -334,27 +334,34 @@ func (cfg *Config) NodeConfig() node.NodeOpt { } // LogConfig configures output and logger. -type LogConfig struct { - Level string - Stacktrace bool - Format string - OutputPath string // logging actually supports multiple output paths, but here only one is supported - Caller bool - NoColor bool +type LoggingConfig struct { + Level string + Stacktrace bool + Format string + OutputPath string // logging actually supports multiple output paths, but here only one is supported + Caller bool + NoColor bool + NamedOverrides map[string]*NamedLoggingConfig } -func defaultLogConfig() *LogConfig { - return &LogConfig{ - Level: logLevelInfo, - Stacktrace: false, - Format: "csv", - OutputPath: "stderr", - Caller: false, - NoColor: false, +type NamedLoggingConfig struct { + LoggingConfig + Name string +} + +func defaultLogConfig() *LoggingConfig { + return &LoggingConfig{ + Level: logLevelInfo, + Stacktrace: false, + Format: "csv", + OutputPath: "stderr", + Caller: false, + NoColor: false, + NamedOverrides: make(map[string]*NamedLoggingConfig), } } -func (logcfg *LogConfig) validate() error { +func (logcfg *LoggingConfig) validate() error { switch logcfg.Level { case logLevelDebug, logLevelInfo, logLevelError, logLevelFatal: default: @@ -363,10 +370,9 @@ func (logcfg *LogConfig) validate() error { return nil } -// GetLoggingConfig provides logging-specific configuration, from top-level Config. -func (cfg *Config) GetLoggingConfig() (logging.Config, error) { +func (logcfg LoggingConfig) ToLoggerConfig() (logging.Config, error) { var loglvl logging.LogLevel - switch cfg.Log.Level { + switch logcfg.Level { case logLevelDebug: loglvl = logging.Debug case logLevelInfo: @@ -376,27 +382,65 @@ func (cfg *Config) GetLoggingConfig() (logging.Config, error) { case logLevelFatal: loglvl = logging.Fatal default: - return logging.Config{}, fmt.Errorf("invalid log level: %s", cfg.Log.Level) + return logging.Config{}, fmt.Errorf("invalid log level: %s", logcfg.Level) } var encfmt logging.EncoderFormat - switch cfg.Log.Format { + switch logcfg.Format { case "json": encfmt = logging.JSON case "csv": encfmt = logging.CSV default: - return logging.Config{}, fmt.Errorf("invalid log format: %s", cfg.Log.Format) + return logging.Config{}, fmt.Errorf("invalid log format: %s", logcfg.Format) + } + // handle named overrides + overrides := make(map[string]logging.Config) + for name, cfg := range logcfg.NamedOverrides { + c, err := cfg.ToLoggerConfig() + if err != nil { + return logging.Config{}, fmt.Errorf("couldn't convert override config: %w", err) + } + overrides[name] = c } return logging.Config{ - Level: logging.NewLogLevelOption(loglvl), - EnableStackTrace: logging.NewEnableStackTraceOption(cfg.Log.Stacktrace), - EnableCaller: logging.NewEnableCallerOption(cfg.Log.Caller), - DisableColor: logging.NewDisableColorOption(cfg.Log.NoColor), - EncoderFormat: logging.NewEncoderFormatOption(encfmt), - OutputPaths: []string{cfg.Log.OutputPath}, + Level: logging.NewLogLevelOption(loglvl), + EnableStackTrace: logging.NewEnableStackTraceOption(logcfg.Stacktrace), + DisableColor: logging.NewDisableColorOption(logcfg.NoColor), + EncoderFormat: logging.NewEncoderFormatOption(encfmt), + OutputPaths: []string{logcfg.OutputPath}, + OverridesByLoggerName: overrides, }, nil } +// this is a copy that doesn't deep copy the NamedOverrides map +// copy is handled by runtime "pass-by-value" +func (logcfg LoggingConfig) copy() LoggingConfig { + logcfg.NamedOverrides = make(map[string]*NamedLoggingConfig) + return logcfg +} + +func (logcfg *LoggingConfig) GetOrCreateNamedLogger(name string) (*NamedLoggingConfig, error) { + if name == "" { + return nil, fmt.Errorf("provided name can't be empty for named config") + } + if namedCfg, exists := logcfg.NamedOverrides[name]; exists { + return namedCfg, nil + } + // create default and save to overrides + namedCfg := &NamedLoggingConfig{ + Name: name, + LoggingConfig: logcfg.copy(), + } + logcfg.NamedOverrides[name] = namedCfg + + return namedCfg, nil +} + +// GetLoggingConfig provides logging-specific configuration, from top-level Config. +func (cfg *Config) GetLoggingConfig() (logging.Config, error) { + return cfg.Log.ToLoggerConfig() +} + // ToJSON serializes the config to a JSON string. func (c *Config) ToJSON() ([]byte, error) { jsonbytes, err := json.Marshal(c) diff --git a/config/config_test.go b/config/config_test.go index 4f8cf1d654..6bf9af65d0 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -34,8 +34,8 @@ var envVarsDifferentThanDefault = map[string]string{ "DEFRA_NET_RPCTIMEOUT": "90s", "DEFRA_NET_PUBSUB": "false", "DEFRA_NET_RELAY": "false", - "DEFRA_LOG_LEVEL": "info", - "DEFRA_LOG_STACKTRACE": "false", + "DEFRA_LOG_LEVEL": "error", + "DEFRA_LOG_STACKTRACE": "true", "DEFRA_LOG_FORMAT": "json", } @@ -147,8 +147,8 @@ func TestEnvVariablesAllConsidered(t *testing.T) { assert.Equal(t, "90s", cfg.Net.RPCTimeout) assert.Equal(t, false, cfg.Net.PubSubEnabled) assert.Equal(t, false, cfg.Net.RelayEnabled) - assert.Equal(t, "info", cfg.Log.Level) - assert.Equal(t, false, cfg.Log.Stacktrace) + assert.Equal(t, "error", cfg.Log.Level) + assert.Equal(t, true, cfg.Log.Stacktrace) assert.Equal(t, "json", cfg.Log.Format) } diff --git a/logging/config.go b/logging/config.go index 884179566b..38ec9942b2 100644 --- a/logging/config.go +++ b/logging/config.go @@ -105,18 +105,7 @@ type Config struct { EnableCaller EnableCallerOption DisableColor DisableColorOption OutputPaths []string - OverridesByLoggerName map[string]OverrideConfig - - pipe io.Writer // this is used for testing purposes only -} - -type OverrideConfig struct { - Level LogLevelOption - EncoderFormat EncoderFormatOption - EnableStackTrace EnableStackTraceOption - EnableCaller EnableCallerOption - DisableColor DisableColorOption - OutputPaths []string + OverridesByLoggerName map[string]Config pipe io.Writer // this is used for testing purposes only } @@ -160,9 +149,9 @@ func (c Config) forLogger(name string) Config { } func (c Config) copy() Config { - overridesByLoggerName := make(map[string]OverrideConfig, len(c.OverridesByLoggerName)) + overridesByLoggerName := make(map[string]Config, len(c.OverridesByLoggerName)) for k, o := range c.OverridesByLoggerName { - overridesByLoggerName[k] = OverrideConfig{ + overridesByLoggerName[k] = Config{ Level: o.Level, EnableStackTrace: o.EnableStackTrace, EncoderFormat: o.EncoderFormat, @@ -220,7 +209,7 @@ func (oldConfig Config) with(newConfigOptions Config) Config { for k, o := range newConfigOptions.OverridesByLoggerName { // We fully overwrite overrides to allow for ease of // reset/removal (can provide empty to return to default) - newConfig.OverridesByLoggerName[k] = OverrideConfig{ + newConfig.OverridesByLoggerName[k] = Config{ Level: o.Level, EnableStackTrace: o.EnableStackTrace, EnableCaller: o.EnableCaller, diff --git a/logging/logging_test.go b/logging/logging_test.go index 2fa8819025..e5710c9478 100644 --- a/logging/logging_test.go +++ b/logging/logging_test.go @@ -637,7 +637,7 @@ func TestLogDoesNotWriteMessagesToLogGivenOverrideForAnotherLoggerReducingLogLev ctx := context.Background() logger, logPath := getLogger(t, func(c *Config) { c.Level = NewLogLevelOption(Fatal) - c.OverridesByLoggerName = map[string]OverrideConfig{ + c.OverridesByLoggerName = map[string]Config{ "not this logger": {Level: NewLogLevelOption(Info)}, } }) @@ -660,7 +660,7 @@ func TestLogWritesMessagesToLogGivenOverrideForLoggerReducingLogLevel(t *testing ctx := context.Background() logger, logPath := getLogger(t, func(c *Config) { c.Level = NewLogLevelOption(Fatal) - c.OverridesByLoggerName = map[string]OverrideConfig{ + c.OverridesByLoggerName = map[string]Config{ "TestLogName": {Level: NewLogLevelOption(Info)}, } }) @@ -691,7 +691,7 @@ func TestLogWritesMessagesToLogGivenOverrideForLoggerRaisingLogLevel(t *testing. ctx := context.Background() logger, logPath := getLogger(t, func(c *Config) { c.Level = NewLogLevelOption(Info) - c.OverridesByLoggerName = map[string]OverrideConfig{ + c.OverridesByLoggerName = map[string]Config{ "not this logger": {Level: NewLogLevelOption(Fatal)}, } }) @@ -722,7 +722,7 @@ func TestLogDoesNotWriteMessagesToLogGivenOverrideForLoggerRaisingLogLevel(t *te ctx := context.Background() logger, logPath := getLogger(t, func(c *Config) { c.Level = NewLogLevelOption(Info) - c.OverridesByLoggerName = map[string]OverrideConfig{ + c.OverridesByLoggerName = map[string]Config{ "TestLogName": {Level: NewLogLevelOption(Fatal)}, } }) @@ -747,7 +747,7 @@ func TestLogDoesNotWriteMessagesToLogGivenOverrideUpdatedForAnotherLoggerReducin c.Level = NewLogLevelOption(Fatal) }) SetConfig(Config{ - OverridesByLoggerName: map[string]OverrideConfig{ + OverridesByLoggerName: map[string]Config{ "not this logger": {Level: NewLogLevelOption(Info)}, }, }) @@ -772,7 +772,7 @@ func TestLogWritesMessagesToLogGivenOverrideUpdatedForLoggerReducingLogLevel(t * c.Level = NewLogLevelOption(Fatal) }) SetConfig(Config{ - OverridesByLoggerName: map[string]OverrideConfig{ + OverridesByLoggerName: map[string]Config{ "TestLogName": {Level: NewLogLevelOption(Info)}, }, }) @@ -805,7 +805,7 @@ func TestLogWritesMessagesToLogGivenOverrideUpdatedForAnotherLoggerRaisingLogLev c.Level = NewLogLevelOption(Info) }) SetConfig(Config{ - OverridesByLoggerName: map[string]OverrideConfig{ + OverridesByLoggerName: map[string]Config{ "not this logger": {Level: NewLogLevelOption(Fatal)}, }, }) @@ -838,7 +838,7 @@ func TestLogDoesNotWriteMessagesToLogGivenOverrideUpdatedForLoggerRaisingLogLeve c.Level = NewLogLevelOption(Info) }) SetConfig(Config{ - OverridesByLoggerName: map[string]OverrideConfig{ + OverridesByLoggerName: map[string]Config{ "TestLogName": {Level: NewLogLevelOption(Fatal)}, }, }) diff --git a/tests/integration/cli/log_config_test.go b/tests/integration/cli/log_config_test.go new file mode 100644 index 0000000000..62d955ab89 --- /dev/null +++ b/tests/integration/cli/log_config_test.go @@ -0,0 +1,109 @@ +// Copyright 2022 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package cli + +import ( + "bufio" + "bytes" + "context" + "fmt" + "io" + "os" + "testing" + + "github.com/sourcenetwork/defradb/cli" + "github.com/sourcenetwork/defradb/logging" + "github.com/stretchr/testify/assert" +) + +const ( + testLogger1 = "testLogger1" + testLogger2 = "testLogger2" + testLogger3 = "testLogger3" +) + +var ( + log1 = logging.MustNewLogger(testLogger1) + log2 = logging.MustNewLogger(testLogger2) + log3 = logging.MustNewLogger(testLogger3) +) + +func TestCLILogsToStderrGivenNamedLogLevel(t *testing.T) { + ctx := context.Background() + logLines := captureLogLines( + t, + func() { + // set the log levels + // general: error + // testLogger1: debug + // testLogger2: info + os.Args = append(os.Args, "--loglevel") + os.Args = append(os.Args, fmt.Sprintf("%s,%s=debug,%s=info", "error", testLogger1, testLogger2)) + }, + func() { + log1.Error(ctx, "error") + log1.Debug(ctx, "debug") + log2.Info(ctx, "info") + log3.Debug(ctx, "info") + }, + ) + + assert.Len(t, logLines, 3) +} + +func captureLogLines(t *testing.T, setup func(), predicate func()) []string { + r, w, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + stderr := os.Stderr + os.Stderr = w + defer func() { + os.Stderr = stderr + }() + + directory := t.TempDir() + + // Set the default logger output path to a file in the temp dir + // so that production logs don't polute and confuse the tests + // os.Args = append(os.Args, "--logoutput", directory+"/log.txt") + os.Args = append(os.Args, "init", directory) + + setup() + cli.Execute() + predicate() + log1.Flush() + log2.Flush() + log3.Flush() + + w.Close() + var buf bytes.Buffer + io.Copy(&buf, r) + logLines, err := parseLines(&buf) + if err != nil { + t.Fatal(err) + } + + return logLines +} + +func parseLines(r io.Reader) ([]string, error) { + fileScanner := bufio.NewScanner(r) + + fileScanner.Split(bufio.ScanLines) + + logLines := []string{} + for fileScanner.Scan() { + logLines = append(logLines, fileScanner.Text()) + } + + return logLines, nil +}