detached 3de22f5

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() {
7070 TimeFormat: "15:04:05",
7171 },
7272 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
7677 },
7778 )
7879 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) {
5555 logDir := filepath.Join(home, ".local", "state", "dumber", "logs")
5656
5757 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
6162 })
6263 if err != nil {
6364 // 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 logging64 // Fallback to no-file logging
M internal/logging/logger.go +26 -18
@@ -18,9 +18,10 @@ type Config struct {
1818
1919 // FileConfig holds file logging configuration
2020 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)
2425 }
2526
2627 // DefaultConfig returns sensible defaults
@@ -54,7 +55,7 @@ func New(cfg Config) zerolog.Logger {
5455 Logger()
5556 }
5657
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.
5859 // The session file uses JSON format for easy parsing by the CLI logs command.
5960 // LogDir must exist before calling this function (handled by config.EnsureDirectories).
6061 // 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)
6263 var writers []io.Writer
6364 var cleanup func() = func() {}
6465
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+ }
7982 }
8083
8184 // Session file output (JSON format for parsing)
@@ -95,6 +98,11 @@ func NewWithFile(cfg Config, fileCfg FileConfig) (zerolog.Logger, func(), error)
9598 cleanup = func() { file.Close() }
9699 }
97100
101+ // Fallback to discard if no writers configured
102+ if len(writers) == 0 {
103+ writers = append(writers, io.Discard)
104+ }
105+
98106 multi := zerolog.MultiLevelWriter(writers...)
99107 ctx := zerolog.New(multi).
100108 Level(cfg.Level).
@@ -18,9 +18,10 @@ type Config struct {
18 18
19 // FileConfig holds file logging configuration19 // FileConfig holds file logging configuration
20 type FileConfig struct {20 type FileConfig struct {
21- Enabled bool21+ Enabled bool
22- LogDir string22+ LogDir string
23- SessionID string23+ SessionID string
24+ WriteToStderr bool // if false, logs go to file only (not stderr)
24 }25 }
25 26
26 // DefaultConfig returns sensible defaults27 // 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.Writer63 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).

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close