fix(logging): prevent CLI log leakage by disabling stderr output in CLI mode
- commit
- 3de22f57f72d6ac87024e5f6a52c49b0126dcb4c
- author
- bnema · · 2025-12-18 20:24 UTC
- parent
- 645dfb3
- signature
- unsigned
3 files · +34 -24
M cmd/dumber/main.go +4 -3
| @@ -70,9 +70,10 @@ func runGUI() { | |||
| 70 | 70 | TimeFormat: "15:04:05", | |
| 71 | 71 | }, | |
| 72 | 72 | logging.FileConfig{ | |
| 73 | - | Enabled: cfg.Logging.EnableFileLog, | |
| 74 | - | LogDir: cfg.Logging.LogDir, | |
| 75 | - | SessionID: sessionID, | |
| 73 | + | Enabled: cfg.Logging.EnableFileLog, | |
| 74 | + | LogDir: cfg.Logging.LogDir, | |
| 75 | + | SessionID: sessionID, | |
| 76 | + | WriteToStderr: true, // GUI: keep stderr output for debugging | |
| 76 | 77 | }, | |
| 77 | 78 | ) | |
| 78 | 79 | if err != nil { | |
| @@ -70,9 +70,10 @@ func runGUI() { | |||||
| 70 | TimeFormat: "15:04:05", | 70 | TimeFormat: "15:04:05", | ||
| 71 | }, | 71 | }, | ||
| 72 | logging.FileConfig{ | 72 | logging.FileConfig{ | ||
| 73 | - | Enabled: cfg.Logging.EnableFileLog, | 73 | + | Enabled: cfg.Logging.EnableFileLog, |
| 74 | - | LogDir: cfg.Logging.LogDir, | 74 | + | LogDir: cfg.Logging.LogDir, |
| 75 | - | SessionID: sessionID, | 75 | + | SessionID: sessionID, |
| 76 | + | WriteToStderr: true, // GUI: keep stderr output for debugging | |||
| 76 | }, | 77 | }, | ||
| 77 | ) | 78 | ) | ||
| 78 | if err != nil { | 79 | if err != nil { | ||
M internal/cli/app.go +4 -3
| @@ -55,9 +55,10 @@ func NewApp() (*App, error) { | |||
| 55 | 55 | logDir := filepath.Join(home, ".local", "state", "dumber", "logs") | |
| 56 | 56 | ||
| 57 | 57 | logger, logCleanup, err := logging.NewWithFile(logCfg, logging.FileConfig{ | |
| 58 | - | Enabled: true, | |
| 59 | - | LogDir: logDir, | |
| 60 | - | SessionID: sessionID, | |
| 58 | + | Enabled: true, | |
| 59 | + | LogDir: logDir, | |
| 60 | + | SessionID: sessionID, | |
| 61 | + | WriteToStderr: false, // CLI: log to file only, keep terminal clean | |
| 61 | 62 | }) | |
| 62 | 63 | if err != nil { | |
| 63 | 64 | // Fallback to no-file logging | |
| @@ -55,9 +55,10 @@ func NewApp() (*App, error) { | |||||
| 55 | logDir := filepath.Join(home, ".local", "state", "dumber", "logs") | 55 | logDir := filepath.Join(home, ".local", "state", "dumber", "logs") | ||
| 56 | 56 | ||||
| 57 | logger, logCleanup, err := logging.NewWithFile(logCfg, logging.FileConfig{ | 57 | logger, logCleanup, err := logging.NewWithFile(logCfg, logging.FileConfig{ | ||
| 58 | - | Enabled: true, | 58 | + | Enabled: true, |
| 59 | - | LogDir: logDir, | 59 | + | LogDir: logDir, |
| 60 | - | SessionID: sessionID, | 60 | + | SessionID: sessionID, |
| 61 | + | WriteToStderr: false, // CLI: log to file only, keep terminal clean | |||
| 61 | }) | 62 | }) | ||
| 62 | if err != nil { | 63 | if err != nil { | ||
| 63 | // Fallback to no-file logging | 64 | // Fallback to no-file logging | ||
M internal/logging/logger.go +26 -18
| @@ -18,9 +18,10 @@ type Config struct { | |||
| 18 | 18 | ||
| 19 | 19 | // FileConfig holds file logging configuration | |
| 20 | 20 | type FileConfig struct { | |
| 21 | - | Enabled bool | |
| 22 | - | LogDir string | |
| 23 | - | SessionID string | |
| 21 | + | Enabled bool | |
| 22 | + | LogDir string | |
| 23 | + | SessionID string | |
| 24 | + | WriteToStderr bool // if false, logs go to file only (not stderr) | |
| 24 | 25 | } | |
| 25 | 26 | ||
| 26 | 27 | // DefaultConfig returns sensible defaults | |
| @@ -54,7 +55,7 @@ func New(cfg Config) zerolog.Logger { | |||
| 54 | 55 | Logger() | |
| 55 | 56 | } | |
| 56 | 57 | ||
| 57 | - | // NewWithFile creates a logger that writes to both stderr and a session log file. | |
| 58 | + | // NewWithFile creates a logger that writes to stderr and/or a session log file. | |
| 58 | 59 | // The session file uses JSON format for easy parsing by the CLI logs command. | |
| 59 | 60 | // LogDir must exist before calling this function (handled by config.EnsureDirectories). | |
| 60 | 61 | // Returns the logger and a cleanup function to close the file. | |
| @@ -62,20 +63,22 @@ func NewWithFile(cfg Config, fileCfg FileConfig) (zerolog.Logger, func(), error) | |||
| 62 | 63 | var writers []io.Writer | |
| 63 | 64 | var cleanup func() = func() {} | |
| 64 | 65 | ||
| 65 | - | // Console output (stderr) | |
| 66 | - | switch cfg.Format { | |
| 67 | - | case "console", "text", "": | |
| 68 | - | writers = append(writers, zerolog.ConsoleWriter{ | |
| 69 | - | Out: os.Stderr, | |
| 70 | - | TimeFormat: cfg.TimeFormat, | |
| 71 | - | }) | |
| 72 | - | case "json": | |
| 73 | - | writers = append(writers, os.Stderr) | |
| 74 | - | default: | |
| 75 | - | writers = append(writers, zerolog.ConsoleWriter{ | |
| 76 | - | Out: os.Stderr, | |
| 77 | - | TimeFormat: cfg.TimeFormat, | |
| 78 | - | }) | |
| 66 | + | // Stderr output (only if enabled) | |
| 67 | + | if fileCfg.WriteToStderr { | |
| 68 | + | switch cfg.Format { | |
| 69 | + | case "console", "text", "": | |
| 70 | + | writers = append(writers, zerolog.ConsoleWriter{ | |
| 71 | + | Out: os.Stderr, | |
| 72 | + | TimeFormat: cfg.TimeFormat, | |
| 73 | + | }) | |
| 74 | + | case "json": | |
| 75 | + | writers = append(writers, os.Stderr) | |
| 76 | + | default: | |
| 77 | + | writers = append(writers, zerolog.ConsoleWriter{ | |
| 78 | + | Out: os.Stderr, | |
| 79 | + | TimeFormat: cfg.TimeFormat, | |
| 80 | + | }) | |
| 81 | + | } | |
| 79 | 82 | } | |
| 80 | 83 | ||
| 81 | 84 | // Session file output (JSON format for parsing) | |
| @@ -95,6 +98,11 @@ func NewWithFile(cfg Config, fileCfg FileConfig) (zerolog.Logger, func(), error) | |||
| 95 | 98 | cleanup = func() { file.Close() } | |
| 96 | 99 | } | |
| 97 | 100 | ||
| 101 | + | // Fallback to discard if no writers configured | |
| 102 | + | if len(writers) == 0 { | |
| 103 | + | writers = append(writers, io.Discard) | |
| 104 | + | } | |
| 105 | + | ||
| 98 | 106 | multi := zerolog.MultiLevelWriter(writers...) | |
| 99 | 107 | ctx := zerolog.New(multi). | |
| 100 | 108 | Level(cfg.Level). | |
| @@ -18,9 +18,10 @@ type Config struct { | |||||
| 18 | 18 | ||||
| 19 | // FileConfig holds file logging configuration | 19 | // FileConfig holds file logging configuration | ||
| 20 | type FileConfig struct { | 20 | type FileConfig struct { | ||
| 21 | - | Enabled bool | 21 | + | Enabled bool |
| 22 | - | LogDir string | 22 | + | LogDir string |
| 23 | - | SessionID string | 23 | + | SessionID string |
| 24 | + | WriteToStderr bool // if false, logs go to file only (not stderr) | |||
| 24 | } | 25 | } | ||
| 25 | 26 | ||||
| 26 | // DefaultConfig returns sensible defaults | 27 | // DefaultConfig returns sensible defaults | ||
| @@ -54,7 +55,7 @@ func New(cfg Config) zerolog.Logger { | |||||
| 54 | Logger() | 55 | Logger() | ||
| 55 | } | 56 | } | ||
| 56 | 57 | ||||
| 57 | - | // NewWithFile creates a logger that writes to both stderr and a session log file. | 58 | + | // NewWithFile creates a logger that writes to stderr and/or a session log file. |
| 58 | // The session file uses JSON format for easy parsing by the CLI logs command. | 59 | // The session file uses JSON format for easy parsing by the CLI logs command. | ||
| 59 | // LogDir must exist before calling this function (handled by config.EnsureDirectories). | 60 | // LogDir must exist before calling this function (handled by config.EnsureDirectories). | ||
| 60 | // Returns the logger and a cleanup function to close the file. | 61 | // Returns the logger and a cleanup function to close the file. | ||
| @@ -62,20 +63,22 @@ func NewWithFile(cfg Config, fileCfg FileConfig) (zerolog.Logger, func(), error) | |||||
| 62 | var writers []io.Writer | 63 | var writers []io.Writer | ||
| 63 | var cleanup func() = func() {} | 64 | var cleanup func() = func() {} | ||
| 64 | 65 | ||||
| 65 | - | // Console output (stderr) | 66 | + | // Stderr output (only if enabled) |
| 66 | - | switch cfg.Format { | 67 | + | if fileCfg.WriteToStderr { |
| 67 | - | case "console", "text", "": | 68 | + | switch cfg.Format { |
| 68 | - | writers = append(writers, zerolog.ConsoleWriter{ | 69 | + | case "console", "text", "": |
| 69 | - | Out: os.Stderr, | 70 | + | writers = append(writers, zerolog.ConsoleWriter{ |
| 70 | - | TimeFormat: cfg.TimeFormat, | 71 | + | Out: os.Stderr, |
| 71 | - | }) | 72 | + | TimeFormat: cfg.TimeFormat, |
| 72 | - | case "json": | 73 | + | }) |
| 73 | - | writers = append(writers, os.Stderr) | 74 | + | case "json": |
| 74 | - | default: | 75 | + | writers = append(writers, os.Stderr) |
| 75 | - | writers = append(writers, zerolog.ConsoleWriter{ | 76 | + | default: |
| 76 | - | Out: os.Stderr, | 77 | + | writers = append(writers, zerolog.ConsoleWriter{ |
| 77 | - | TimeFormat: cfg.TimeFormat, | 78 | + | Out: os.Stderr, |
| 78 | - | }) | 79 | + | TimeFormat: cfg.TimeFormat, |
| 80 | + | }) | |||
| 81 | + | } | |||
| 79 | } | 82 | } | ||
| 80 | 83 | ||||
| 81 | // Session file output (JSON format for parsing) | 84 | // Session file output (JSON format for parsing) | ||
| @@ -95,6 +98,11 @@ func NewWithFile(cfg Config, fileCfg FileConfig) (zerolog.Logger, func(), error) | |||||
| 95 | cleanup = func() { file.Close() } | 98 | cleanup = func() { file.Close() } | ||
| 96 | } | 99 | } | ||
| 97 | 100 | ||||
| 101 | + | // Fallback to discard if no writers configured | |||
| 102 | + | if len(writers) == 0 { | |||
| 103 | + | writers = append(writers, io.Discard) | |||
| 104 | + | } | |||
| 105 | + | ||||
| 98 | multi := zerolog.MultiLevelWriter(writers...) | 106 | multi := zerolog.MultiLevelWriter(writers...) | ||
| 99 | ctx := zerolog.New(multi). | 107 | ctx := zerolog.New(multi). | ||
| 100 | Level(cfg.Level). | 108 | Level(cfg.Level). | ||