From 7b08a4e23a5f15e2faa87377346ec37d5d5db127 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Fri, 6 Mar 2026 22:31:27 -0500 Subject: [PATCH 1/4] feat: add log.file, log.format, log.level config file options CLI flags still take precedence. log.level accepts "debug", "info", or "warn". Useful for always writing debug logs to a file without passing flags on every invocation. Co-Authored-By: Claude Sonnet 4.6 --- cmd/root.go | 16 ++++++++++++++++ config/config.go | 15 ++++++++++++--- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 25b94d5..b8d5387 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -56,6 +56,22 @@ func LoadConfig(cmd *cobra.Command) error { applyBoolFlag(cmd, "update-only", &Cfg.Sync.UpdateOnly) applyStringFlag(cmd, "cache-dir", &Cfg.Sync.CacheDir) + // Seed log settings from config file; CLI flags take precedence. + if Cfg.Log.Level != "" && !debug && !verbose { + switch strings.ToLower(Cfg.Log.Level) { + case "debug": + debug = true + case "info": + verbose = true + } + } + if Cfg.Log.File != "" && !cmd.Flags().Changed("log-file") { + logFile = Cfg.Log.File + } + if Cfg.Log.Format != "" && !cmd.Flags().Changed("log-format") { + logFormat = Cfg.Log.Format + } + // Configure log level var level logrus.Level switch { diff --git a/config/config.go b/config/config.go index 92d7ba5..1ffa9b6 100644 --- a/config/config.go +++ b/config/config.go @@ -10,10 +10,19 @@ import ( // Config holds all configuration for axm2snipe. type Config struct { - ABM ABMConfig `yaml:"abm"` + ABM ABMConfig `yaml:"abm"` SnipeIT SnipeITConfig `yaml:"snipe_it"` - Sync SyncConfig `yaml:"sync"` - Slack SlackConfig `yaml:"slack"` + Sync SyncConfig `yaml:"sync"` + Slack SlackConfig `yaml:"slack"` + Log LogConfig `yaml:"log"` +} + +// LogConfig holds logging settings that can be set in the config file. +// CLI flags (--log-file, --log-format, -v, -d) take precedence. +type LogConfig struct { + File string `yaml:"file"` // path to log file (appended to stderr) + Format string `yaml:"format"` // "text" (default) or "json" + Level string `yaml:"level"` // "warn" (default), "info", or "debug" } // SlackConfig holds optional Slack webhook notification settings. From bf01eec941de2f1be168b2dff6f4f5384d631142 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Fri, 6 Mar 2026 22:36:03 -0500 Subject: [PATCH 2/4] fix: use cmd.Flags().Changed() for log level precedence; warn on unknown level Co-Authored-By: Claude Sonnet 4.6 --- cmd/root.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cmd/root.go b/cmd/root.go index b8d5387..9ced6ea 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -57,12 +57,16 @@ func LoadConfig(cmd *cobra.Command) error { applyStringFlag(cmd, "cache-dir", &Cfg.Sync.CacheDir) // Seed log settings from config file; CLI flags take precedence. - if Cfg.Log.Level != "" && !debug && !verbose { + if Cfg.Log.Level != "" && !cmd.Flags().Changed("debug") && !cmd.Flags().Changed("verbose") { switch strings.ToLower(Cfg.Log.Level) { case "debug": debug = true case "info": verbose = true + case "warn", "warning": + // default, no action needed + default: + log.Warnf("Unknown log.level %q in config, using default (warn)", Cfg.Log.Level) } } if Cfg.Log.File != "" && !cmd.Flags().Changed("log-file") { From 6813fb8a4477136251bd2442661d563ddcd4e01e Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Fri, 6 Mar 2026 22:41:44 -0500 Subject: [PATCH 3/4] refactor: use effective* locals instead of mutating flag-backed globals Seed log settings from config into local effectiveDebug/effectiveVerbose/ effectiveLogFile/effectiveLogFormat variables rather than mutating the cobra flag-backed globals, so CLI flag state remains unaltered. Co-Authored-By: Claude Sonnet 4.6 --- cmd/root.go | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 9ced6ea..9646a36 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -56,13 +56,19 @@ func LoadConfig(cmd *cobra.Command) error { applyBoolFlag(cmd, "update-only", &Cfg.Sync.UpdateOnly) applyStringFlag(cmd, "cache-dir", &Cfg.Sync.CacheDir) - // Seed log settings from config file; CLI flags take precedence. + // Compute effective log settings: config file provides defaults, + // explicit CLI flags take precedence. Do not mutate the flag-backed globals. + effectiveDebug := debug + effectiveVerbose := verbose + effectiveLogFile := logFile + effectiveLogFormat := logFormat + if Cfg.Log.Level != "" && !cmd.Flags().Changed("debug") && !cmd.Flags().Changed("verbose") { switch strings.ToLower(Cfg.Log.Level) { case "debug": - debug = true + effectiveDebug = true case "info": - verbose = true + effectiveVerbose = true case "warn", "warning": // default, no action needed default: @@ -70,18 +76,18 @@ func LoadConfig(cmd *cobra.Command) error { } } if Cfg.Log.File != "" && !cmd.Flags().Changed("log-file") { - logFile = Cfg.Log.File + effectiveLogFile = Cfg.Log.File } if Cfg.Log.Format != "" && !cmd.Flags().Changed("log-format") { - logFormat = Cfg.Log.Format + effectiveLogFormat = Cfg.Log.Format } // Configure log level var level logrus.Level switch { - case debug: + case effectiveDebug: level = logrus.DebugLevel - case verbose: + case effectiveVerbose: level = logrus.InfoLevel default: level = logrus.WarnLevel @@ -90,13 +96,13 @@ func LoadConfig(cmd *cobra.Command) error { // Configure formatter var formatter logrus.Formatter - switch strings.ToLower(logFormat) { + switch strings.ToLower(effectiveLogFormat) { case "json": formatter = &logrus.JSONFormatter{} case "text", "": formatter = &logrus.TextFormatter{FullTimestamp: true} default: - return fmt.Errorf("invalid --log-format %q: must be 'text' or 'json'", logFormat) + return fmt.Errorf("invalid --log-format %q: must be 'text' or 'json'", effectiveLogFormat) } setAllLogFormatters(formatter) @@ -106,8 +112,8 @@ func LoadConfig(cmd *cobra.Command) error { _ = logFileFD.Close() logFileFD = nil } - if logFile != "" { - f, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600) + if effectiveLogFile != "" { + f, err := os.OpenFile(effectiveLogFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600) if err != nil { return fmt.Errorf("opening log file: %w", err) } From 9425e3cca04bff9c85cfb06b78c6f41e3f5f82db Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Fri, 6 Mar 2026 23:58:21 -0500 Subject: [PATCH 4/4] fix: defer unknown log level warning until after formatter/output configured Also make the invalid log format error message source-agnostic ("invalid log format" rather than "invalid --log-format") since the value may come from config rather than a CLI flag. Co-Authored-By: Claude Sonnet 4.6 --- cmd/root.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 9646a36..4553f5d 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -63,6 +63,7 @@ func LoadConfig(cmd *cobra.Command) error { effectiveLogFile := logFile effectiveLogFormat := logFormat + var unknownLogLevelMsg string if Cfg.Log.Level != "" && !cmd.Flags().Changed("debug") && !cmd.Flags().Changed("verbose") { switch strings.ToLower(Cfg.Log.Level) { case "debug": @@ -72,7 +73,7 @@ func LoadConfig(cmd *cobra.Command) error { case "warn", "warning": // default, no action needed default: - log.Warnf("Unknown log.level %q in config, using default (warn)", Cfg.Log.Level) + unknownLogLevelMsg = fmt.Sprintf("Unknown log.level %q in config, using default (warn)", Cfg.Log.Level) } } if Cfg.Log.File != "" && !cmd.Flags().Changed("log-file") { @@ -102,7 +103,7 @@ func LoadConfig(cmd *cobra.Command) error { case "text", "": formatter = &logrus.TextFormatter{FullTimestamp: true} default: - return fmt.Errorf("invalid --log-format %q: must be 'text' or 'json'", effectiveLogFormat) + return fmt.Errorf("invalid log format %q: must be 'text' or 'json'", effectiveLogFormat) } setAllLogFormatters(formatter) @@ -121,6 +122,11 @@ func LoadConfig(cmd *cobra.Command) error { setAllLogOutputs(io.MultiWriter(os.Stderr, f)) } + // Emit deferred warning after format/output are configured. + if unknownLogLevelMsg != "" { + log.Warn(unknownLogLevelMsg) + } + return nil }