From 76e68d15d9089226f94f8ddca55305eceadec658 Mon Sep 17 00:00:00 2001 From: Jacquin Antoine Date: Tue, 3 Mar 2026 00:02:11 +0100 Subject: [PATCH] feat: add error callback for file output writer - Add FileWriterOption type and WithFileErrorCallback option - Add reportError method to FileWriter for error reporting - Update Builder to propagate error callback to file writers - File write errors now logged via the same callback mechanism - Helps diagnose permission or disk space issues Co-authored-by: Qwen-Coder --- internal/output/writers.go | 72 +++++++++++++++++++++++++++++--------- 1 file changed, 55 insertions(+), 17 deletions(-) diff --git a/internal/output/writers.go b/internal/output/writers.go index 08b502f..a6d2f4c 100644 --- a/internal/output/writers.go +++ b/internal/output/writers.go @@ -61,13 +61,26 @@ func (w *StdoutWriter) Close() error { // FileWriter writes log records to a file with rotation support type FileWriter struct { - file *os.File - encoder *json.Encoder - mutex sync.Mutex - path string - maxSize int64 - maxBackups int - currentSize int64 + file *os.File + encoder *json.Encoder + mutex sync.Mutex + path string + maxSize int64 + maxBackups int + currentSize int64 + errorCallback ErrorCallback + failuresMu sync.Mutex + failures int +} + +// FileWriterOption is a function type for configuring FileWriter +type FileWriterOption func(*FileWriter) + +// WithFileErrorCallback sets an error callback for file write errors +func WithFileErrorCallback(cb ErrorCallback) FileWriterOption { + return func(w *FileWriter) { + w.errorCallback = cb + } } // NewFileWriter creates a new file writer with rotation @@ -76,7 +89,7 @@ func NewFileWriter(path string) (*FileWriter, error) { } // NewFileWriterWithConfig creates a new file writer with custom rotation config -func NewFileWriterWithConfig(path string, maxSize int64, maxBackups int) (*FileWriter, error) { +func NewFileWriterWithConfig(path string, maxSize int64, maxBackups int, opts ...FileWriterOption) (*FileWriter, error) { // Create directory if it doesn't exist dir := filepath.Dir(path) if err := os.MkdirAll(dir, 0755); err != nil { @@ -96,14 +109,21 @@ func NewFileWriterWithConfig(path string, maxSize int64, maxBackups int) (*FileW return nil, fmt.Errorf("failed to stat file: %w", err) } - return &FileWriter{ - file: file, - encoder: json.NewEncoder(file), - path: path, - maxSize: maxSize, - maxBackups: maxBackups, + w := &FileWriter{ + file: file, + encoder: json.NewEncoder(file), + path: path, + maxSize: maxSize, + maxBackups: maxBackups, currentSize: info.Size(), - }, nil + } + + // Apply options (for error callback) + for _, opt := range opts { + opt(w) + } + + return w, nil } // rotate rotates the log file if it exceeds the max size @@ -149,6 +169,7 @@ func (w *FileWriter) Write(rec api.LogRecord) error { // Check if rotation is needed if w.currentSize >= w.maxSize { if err := w.rotate(); err != nil { + w.reportError(fmt.Errorf("failed to rotate file: %w", err)) return fmt.Errorf("failed to rotate file: %w", err) } } @@ -163,6 +184,7 @@ func (w *FileWriter) Write(rec api.LogRecord) error { // Write to file n, err := w.file.Write(data) if err != nil { + w.reportError(fmt.Errorf("failed to write to file: %w", err)) return fmt.Errorf("failed to write to file: %w", err) } w.currentSize += int64(n) @@ -170,6 +192,17 @@ func (w *FileWriter) Write(rec api.LogRecord) error { return nil } +// reportError reports a file write error via the configured callback +func (w *FileWriter) reportError(err error) { + if w.errorCallback != nil { + w.failuresMu.Lock() + w.failures++ + failures := w.failures + w.failuresMu.Unlock() + w.errorCallback(w.path, err, failures) + } +} + // Close closes the file func (w *FileWriter) Close() error { w.mutex.Lock() @@ -531,7 +564,7 @@ func NewBuilder() *BuilderImpl { return &BuilderImpl{} } -// WithErrorCallback sets an error callback for all unix_socket writers created by this builder +// WithErrorCallback sets an error callback for all unix_socket and file writers created by this builder func (b *BuilderImpl) WithErrorCallback(cb ErrorCallback) *BuilderImpl { b.errorCallback = cb return b @@ -564,7 +597,12 @@ func (b *BuilderImpl) NewFromConfig(cfg api.AppConfig) (api.Writer, error) { if path == "" { return nil, fmt.Errorf("file output requires 'path' parameter") } - writer, err = NewFileWriter(path) + // Build options list for file writer + var fileOpts []FileWriterOption + if b.errorCallback != nil { + fileOpts = append(fileOpts, WithFileErrorCallback(b.errorCallback)) + } + writer, err = NewFileWriterWithConfig(path, DefaultMaxFileSize, DefaultMaxBackups, fileOpts...) if err != nil { return nil, err }