diff --git a/cmd/root.go b/cmd/root.go index 75a24be..49568b8 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,6 +1,7 @@ package cmd import ( + "errors" "math" "net/http" "os" @@ -78,23 +79,8 @@ func Execute() { func PreRun(cmd *cobra.Command, _ []string) { f := cmd.PersistentFlags() flags.ProcessFlagAliases(f) - - if enabled, _ := f.GetBool("no-color"); enabled { - log.SetFormatter(&log.TextFormatter{ - DisableColors: true, - }) - } else { - // enable logrus built-in support for https://bixense.com/clicolors/ - log.SetFormatter(&log.TextFormatter{ - EnvironmentOverrideColors: true, - }) - } - - rawLogLevel, _ := f.GetString(`log-level`) - if logLevel, err := log.ParseLevel(rawLogLevel); err != nil { - log.Fatalf("Invalid log level: %s", err.Error()) - } else { - log.SetLevel(logLevel) + if err := flags.SetupLogging(f); err != nil { + log.Fatalf("Failed to initialize logging: %s", err.Error()) } scheduleSpec, _ = f.GetString("schedule") @@ -201,7 +187,7 @@ func Run(c *cobra.Command, names []string) { httpAPI.RegisterHandler(metricsHandler.Path, metricsHandler.Handle) } - if err := httpAPI.Start(enableUpdateAPI && !unblockHTTPAPI); err != nil && err != http.ErrServerClosed { + if err := httpAPI.Start(enableUpdateAPI && !unblockHTTPAPI); err != nil && !errors.Is(err, http.ErrServerClosed) { log.Error("failed to start API", err) } diff --git a/docs/arguments.md b/docs/arguments.md index 8e5a8b6..7563b70 100644 --- a/docs/arguments.md +++ b/docs/arguments.md @@ -134,6 +134,17 @@ Environment Variable: WATCHTOWER_LOG_LEVEL Default: info ``` +## Logging format + +Sets what logging format to use for console output. + +```text + Argument: --log-format, -l +Environment Variable: WATCHTOWER_LOG_FORMAT + Possible values: Auto, LogFmt, Pretty or JSON + Default: Auto +``` + ## ANSI colors Disable ANSI color escape codes in log output. @@ -407,6 +418,7 @@ Environment Variable: WATCHTOWER_WARN_ON_HEAD_FAILURE Possible values: always, auto, never Default: auto ``` + ## Programatic Output (porcelain) Writes the session results to STDOUT using a stable, machine-readable format (indicated by the argument VERSION). diff --git a/internal/flags/flags.go b/internal/flags/flags.go index 5ae6a77..c0db557 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -85,6 +85,12 @@ func RegisterSystemFlags(rootCmd *cobra.Command) { envBool("WATCHTOWER_LABEL_ENABLE"), "Watch containers where the com.centurylinklabs.watchtower.enable label is true") + flags.StringP( + "log-format", + "l", + viper.GetString("WATCHTOWER_LOG_FORMAT"), + "Sets what logging format to use for console output. Possible values: Auto, LogFmt, Pretty, JSON") + flags.BoolP( "debug", "d", @@ -409,6 +415,7 @@ func SetDefaults() { viper.SetDefault("WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG", "") viper.SetDefault("WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER", "watchtower") viper.SetDefault("WATCHTOWER_LOG_LEVEL", "info") + viper.SetDefault("WATCHTOWER_LOG_FORMAT", "auto") } // EnvConfig translates the command-line options into environment variables @@ -611,6 +618,46 @@ func ProcessFlagAliases(flags *pflag.FlagSet) { } +// SetupLogging reads only the flags that is needed to set up logging and applies them to the global logger +func SetupLogging(f *pflag.FlagSet) error { + logFormat, _ := f.GetString(`log-format`) + noColor, _ := f.GetBool("no-color") + + switch strings.ToLower(logFormat) { + case "auto": + // This will either use the "pretty" or "logfmt" format, based on whether the standard out is connected to a TTY + log.SetFormatter(&log.TextFormatter{ + DisableColors: noColor, + // enable logrus built-in support for https://bixense.com/clicolors/ + EnvironmentOverrideColors: true, + }) + case "json": + log.SetFormatter(&log.JSONFormatter{}) + case "logfmt": + log.SetFormatter(&log.TextFormatter{ + DisableColors: true, + FullTimestamp: true, + }) + case "pretty": + log.SetFormatter(&log.TextFormatter{ + // "Pretty" format combined with `--no-color` will only change the timestamp to the time since start + ForceColors: !noColor, + FullTimestamp: false, + }) + default: + return fmt.Errorf("invalid log format: %s", logFormat) + } + + rawLogLevel, _ := f.GetString(`log-level`) + if logLevel, err := log.ParseLevel(rawLogLevel); err != nil { + return fmt.Errorf("invalid log level: %e", err) + } else { + log.SetLevel(logLevel) + } + + return nil +} + func flagIsEnabled(flags *pflag.FlagSet, name string) bool { value, err := flags.GetBool(name) if err != nil { diff --git a/internal/flags/flags_test.go b/internal/flags/flags_test.go index 394b2c7..2856456 100644 --- a/internal/flags/flags_test.go +++ b/internal/flags/flags_test.go @@ -177,6 +177,57 @@ func TestProcessFlagAliasesLogLevelFromEnvironment(t *testing.T) { assert.Equal(t, `debug`, logLevel) } +func TestLogFormatFlag(t *testing.T) { + cmd := new(cobra.Command) + + SetDefaults() + RegisterDockerFlags(cmd) + RegisterSystemFlags(cmd) + + // Ensure the default value is Auto + require.NoError(t, cmd.ParseFlags([]string{})) + require.NoError(t, SetupLogging(cmd.Flags())) + assert.IsType(t, &logrus.TextFormatter{}, logrus.StandardLogger().Formatter) + + // Test JSON format + require.NoError(t, cmd.ParseFlags([]string{`--log-format`, `JSON`})) + require.NoError(t, SetupLogging(cmd.Flags())) + assert.IsType(t, &logrus.JSONFormatter{}, logrus.StandardLogger().Formatter) + + // Test Pretty format + require.NoError(t, cmd.ParseFlags([]string{`--log-format`, `pretty`})) + require.NoError(t, SetupLogging(cmd.Flags())) + assert.IsType(t, &logrus.TextFormatter{}, logrus.StandardLogger().Formatter) + textFormatter, ok := (logrus.StandardLogger().Formatter).(*logrus.TextFormatter) + assert.True(t, ok) + assert.True(t, textFormatter.ForceColors) + assert.False(t, textFormatter.FullTimestamp) + + // Test LogFmt format + require.NoError(t, cmd.ParseFlags([]string{`--log-format`, `logfmt`})) + require.NoError(t, SetupLogging(cmd.Flags())) + textFormatter, ok = (logrus.StandardLogger().Formatter).(*logrus.TextFormatter) + assert.True(t, ok) + assert.True(t, textFormatter.DisableColors) + assert.True(t, textFormatter.FullTimestamp) + + // Test invalid format + require.NoError(t, cmd.ParseFlags([]string{`--log-format`, `cowsay`})) + require.Error(t, SetupLogging(cmd.Flags())) +} + +func TestLogLevelFlag(t *testing.T) { + cmd := new(cobra.Command) + + SetDefaults() + RegisterDockerFlags(cmd) + RegisterSystemFlags(cmd) + + // Test invalid format + require.NoError(t, cmd.ParseFlags([]string{`--log-level`, `gossip`})) + require.Error(t, SetupLogging(cmd.Flags())) +} + func TestProcessFlagAliasesSchedAndInterval(t *testing.T) { logrus.StandardLogger().ExitFunc = func(_ int) { panic(`FATAL`) } cmd := new(cobra.Command)