detached b182a80

fix(app): always resolve log file path and support http.Flusher in logging middleware

Ensure file logging path is always resolved so admin API log streaming
works regardless of config flag. Add Flush() to logging ResponseWriter
so SSE streams flush correctly through the middleware chain.
commit
b182a800a72f78241f7b2d00ddcec53610cd8806
author
brice · · 2026-02-01 18:11 UTC
committer
brice · · 2026-02-02 18:05 UTC
parent
f6a0e5d
signature
unsigned
2 files · +28 -23
M internal/adapters/in/http/middleware/logging.go +7 -0
@@ -50,6 +50,13 @@ func (rw *ResponseWriter) BytesWritten() int {
5050 return rw.bytes
5151 }
5252
53+// Flush implements http.Flusher by delegating to the underlying ResponseWriter.
54+func (rw *ResponseWriter) Flush() {
55+ if f, ok := rw.ResponseWriter.(http.Flusher); ok {
56+ f.Flush()
57+ }
58+}
59+
5360 // RequestLogger is a middleware that logs HTTP requests using zerowrap.
5461 // It also attaches the logger to the request context for downstream handlers.
5562 // The trustedNets parameter controls which proxy headers are trusted for IP extraction.
@@ -50,6 +50,13 @@ func (rw *ResponseWriter) BytesWritten() int {
50 return rw.bytes50 return rw.bytes
51 }51 }
52 52
53+// Flush implements http.Flusher by delegating to the underlying ResponseWriter.
54+func (rw *ResponseWriter) Flush() {
55+ if f, ok := rw.ResponseWriter.(http.Flusher); ok {
56+ f.Flush()
57+ }
58+}
59+
53 // RequestLogger is a middleware that logs HTTP requests using zerowrap.60 // RequestLogger is a middleware that logs HTTP requests using zerowrap.
54 // It also attaches the logger to the request context for downstream handlers.61 // It also attaches the logger to the request context for downstream handlers.
55 // The trustedNets parameter controls which proxy headers are trusted for IP extraction.62 // The trustedNets parameter controls which proxy headers are trusted for IP extraction.
M internal/app/run.go +21 -23
@@ -218,28 +218,29 @@ func initLogger(cfg Config) (zerowrap.Logger, func(), error) {
218218 Format: cfg.Logging.Format,
219219 }
220220
221- if cfg.Logging.File.Enabled {
222- logPath := cfg.Logging.File.Path
223- if logPath == "" {
224- // Default to {data_dir}/logs/gordon.log
225- logPath = filepath.Join(cfg.Server.DataDir, "logs", "gordon.log")
226- }
227-
228- log, cleanup, err := zerowrap.NewWithFile(logConfig, zerowrap.FileConfig{
229- Enabled: true,
230- Path: logPath,
231- MaxSize: cfg.Logging.File.MaxSize,
232- MaxBackups: cfg.Logging.File.MaxBackups,
233- MaxAge: cfg.Logging.File.MaxAge,
234- Compress: true,
235- })
236- if err != nil {
237- return zerowrap.Default(), nil, fmt.Errorf("failed to create logger with file: %w", err)
221+ // Always enable file logging so admin API can read process logs
222+ // (Config flag is respected for backward-compatibility)
223+ logPath := cfg.Logging.File.Path
224+ if logPath == "" {
225+ dataDir := cfg.Server.DataDir
226+ if dataDir == "" {
227+ dataDir = DefaultDataDir()
238228 }
239- return log, cleanup, nil
229+ logPath = filepath.Join(dataDir, "logs", "gordon.log")
240230 }
241231
242- return zerowrap.New(logConfig), nil, nil
232+ log, cleanup, err := zerowrap.NewWithFile(logConfig, zerowrap.FileConfig{
233+ Enabled: cfg.Logging.File.Enabled,
234+ Path: logPath,
235+ MaxSize: cfg.Logging.File.MaxSize,
236+ MaxBackups: cfg.Logging.File.MaxBackups,
237+ MaxAge: cfg.Logging.File.MaxAge,
238+ Compress: true,
239+ })
240+ if err != nil {
241+ return zerowrap.Default(), nil, fmt.Errorf("failed to create logger with file: %w", err)
242+ }
243+ return log, cleanup, nil
243244 }
244245
245246 // createServices creates all the application services.
@@ -383,10 +384,7 @@ func resolveLogFilePath(cfg Config) string {
383384 if cfg.Logging.File.Path != "" {
384385 return cfg.Logging.File.Path
385386 }
386- if cfg.Logging.File.Enabled {
387- return filepath.Join(cfg.Server.DataDir, "logs", "gordon.log")
388- }
389- return ""
387+ return filepath.Join(cfg.Server.DataDir, "logs", "gordon.log")
390388 }
391389
392390 // createOutputAdapters creates the Docker runtime and event bus.
@@ -218,28 +218,29 @@ func initLogger(cfg Config) (zerowrap.Logger, func(), error) {
218 Format: cfg.Logging.Format,218 Format: cfg.Logging.Format,
219 }219 }
220 220
221- if cfg.Logging.File.Enabled {221+ // Always enable file logging so admin API can read process logs
222- logPath := cfg.Logging.File.Path222+ // (Config flag is respected for backward-compatibility)
223- if logPath == "" {223+ logPath := cfg.Logging.File.Path
224- // Default to {data_dir}/logs/gordon.log224+ if logPath == "" {
225- logPath = filepath.Join(cfg.Server.DataDir, "logs", "gordon.log")225+ dataDir := cfg.Server.DataDir
226- }226+ if dataDir == "" {
227-227+ dataDir = DefaultDataDir()
228- log, cleanup, err := zerowrap.NewWithFile(logConfig, zerowrap.FileConfig{
229- Enabled: true,
230- Path: logPath,
231- MaxSize: cfg.Logging.File.MaxSize,
232- MaxBackups: cfg.Logging.File.MaxBackups,
233- MaxAge: cfg.Logging.File.MaxAge,
234- Compress: true,
235- })
236- if err != nil {
237- return zerowrap.Default(), nil, fmt.Errorf("failed to create logger with file: %w", err)
238 }228 }
239- return log, cleanup, nil229+ logPath = filepath.Join(dataDir, "logs", "gordon.log")
240 }230 }
241 231
242- return zerowrap.New(logConfig), nil, nil232+ log, cleanup, err := zerowrap.NewWithFile(logConfig, zerowrap.FileConfig{
233+ Enabled: cfg.Logging.File.Enabled,
234+ Path: logPath,
235+ MaxSize: cfg.Logging.File.MaxSize,
236+ MaxBackups: cfg.Logging.File.MaxBackups,
237+ MaxAge: cfg.Logging.File.MaxAge,
238+ Compress: true,
239+ })
240+ if err != nil {
241+ return zerowrap.Default(), nil, fmt.Errorf("failed to create logger with file: %w", err)
242+ }
243+ return log, cleanup, nil
243 }244 }
244 245
245 // createServices creates all the application services.246 // createServices creates all the application services.
@@ -383,10 +384,7 @@ func resolveLogFilePath(cfg Config) string {
383 if cfg.Logging.File.Path != "" {384 if cfg.Logging.File.Path != "" {
384 return cfg.Logging.File.Path385 return cfg.Logging.File.Path
385 }386 }
386- if cfg.Logging.File.Enabled {387+ return filepath.Join(cfg.Server.DataDir, "logs", "gordon.log")
387- return filepath.Join(cfg.Server.DataDir, "logs", "gordon.log")
388- }
389- return ""
390 }388 }
391 389
392 // createOutputAdapters creates the Docker runtime and event bus.390 // createOutputAdapters creates the Docker runtime and event bus.

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 7

Tags 106

↑↓ moveenter openesc close