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 { | |||
| 50 | 50 | 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 | 60 | // RequestLogger is a middleware that logs HTTP requests using zerowrap. | |
| 54 | 61 | // It also attaches the logger to the request context for downstream handlers. | |
| 55 | 62 | // The trustedNets parameter controls which proxy headers are trusted for IP extraction. | |
| @@ -50,6 +50,13 @@ func (rw *ResponseWriter) BytesWritten() int { | |||||
| 50 | return rw.bytes | 50 | 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) { | |||
| 218 | 218 | Format: cfg.Logging.Format, | |
| 219 | 219 | } | |
| 220 | 220 | ||
| 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() | |
| 238 | 228 | } | |
| 239 | - | return log, cleanup, nil | |
| 229 | + | logPath = filepath.Join(dataDir, "logs", "gordon.log") | |
| 240 | 230 | } | |
| 241 | 231 | ||
| 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 | |
| 243 | 244 | } | |
| 244 | 245 | ||
| 245 | 246 | // createServices creates all the application services. | |
| @@ -383,10 +384,7 @@ func resolveLogFilePath(cfg Config) string { | |||
| 383 | 384 | if cfg.Logging.File.Path != "" { | |
| 384 | 385 | return cfg.Logging.File.Path | |
| 385 | 386 | } | |
| 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") | |
| 390 | 388 | } | |
| 391 | 389 | ||
| 392 | 390 | // 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.Path | 222 | + | // (Config flag is respected for backward-compatibility) |
| 223 | - | if logPath == "" { | 223 | + | logPath := cfg.Logging.File.Path |
| 224 | - | // Default to {data_dir}/logs/gordon.log | 224 | + | 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, nil | 229 | + | logPath = filepath.Join(dataDir, "logs", "gordon.log") |
| 240 | } | 230 | } | ||
| 241 | 231 | ||||
| 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 | |||
| 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.Path | 385 | 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. | ||