package observability import ( "bytes" "encoding/json" "errors" "fmt" "io" "os" "path/filepath" "strconv" "sync" "time" ) // RotatingFile is an append-only daemon log with bounded numbered archives. // It is deliberately a single-process writer: deployment supplies one daemon // process per configured path, and a second writer must use a distinct file. // Archive .1 is newest; .maxFiles is oldest. type RotatingFile struct { mu sync.Mutex path string maxBytes uint64 maxFiles uint32 file *os.File size uint64 } // OpenRotatingFile opens path for append, creating its parent directories with // conservative permissions. A blank path disables file logging and returns a // no-op closer. Both rotation controls must be zero (unbounded) or positive. func OpenRotatingFile(path string, maxBytes uint64, maxFiles uint32) (io.WriteCloser, error) { if path == "" { return nopWriteCloser{Writer: io.Discard}, nil } if (maxBytes == 0) != (maxFiles == 0) { return nil, errors.New("log rotation byte/file limits must both be zero or positive") } if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { return nil, fmt.Errorf("create log directory: %w", err) } result := &RotatingFile{path: path, maxBytes: maxBytes, maxFiles: maxFiles} if err := result.open(); err != nil { return nil, err } return result, nil } func (file *RotatingFile) open() error { opened, err := os.OpenFile(file.path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o600) if err != nil { return fmt.Errorf("open log file: %w", err) } info, err := opened.Stat() if err != nil { _ = opened.Close() return fmt.Errorf("stat log file: %w", err) } file.file = opened file.size = uint64(info.Size()) return nil } func (file *RotatingFile) Write(data []byte) (int, error) { file.mu.Lock() defer file.mu.Unlock() if file.file == nil { return 0, os.ErrClosed } if file.maxBytes != 0 && file.size != 0 && (file.size >= file.maxBytes || uint64(len(data)) > file.maxBytes-file.size) { if err := file.rotate(); err != nil { return 0, err } } written, err := file.file.Write(data) file.size += uint64(written) return written, err } func (file *RotatingFile) Close() error { file.mu.Lock() defer file.mu.Unlock() if file.file == nil { return nil } err := file.file.Close() file.file = nil return err } func (file *RotatingFile) rotate() error { if err := file.file.Close(); err != nil { return fmt.Errorf("close log before rotation: %w", err) } file.file = nil oldest := file.archivePath(file.maxFiles) if err := os.Remove(oldest); err != nil && !errors.Is(err, os.ErrNotExist) { return fmt.Errorf("remove oldest log archive: %w", err) } for index := file.maxFiles; index > 1; index-- { from := file.archivePath(index - 1) to := file.archivePath(index) if err := os.Rename(from, to); err != nil && !errors.Is(err, os.ErrNotExist) { return fmt.Errorf("rotate log archive: %w", err) } } if err := os.Rename(file.path, file.archivePath(1)); err != nil && !errors.Is(err, os.ErrNotExist) { return fmt.Errorf("seal current log: %w", err) } return file.open() } func (file *RotatingFile) archivePath(index uint32) string { return file.path + "." + strconv.FormatUint(uint64(index), 10) } type nopWriteCloser struct{ io.Writer } func (nopWriteCloser) Close() error { return nil } // FormatLog makes file records either newline-delimited JSON or plain text. // It is intentionally applied only to daemon diagnostics, never command // output, stdin, environment values, or other payload-bearing data. func FormatLog(destination io.Writer, format string) io.Writer { if destination == nil || format != "json" { return destination } return &jsonLogWriter{destination: destination} } type jsonLogWriter struct { mu sync.Mutex destination io.Writer } func (writer *jsonLogWriter) Write(data []byte) (int, error) { writer.mu.Lock() defer writer.mu.Unlock() for _, line := range bytes.Split(data, []byte{'\n'}) { if len(line) == 0 { continue } record, err := json.Marshal(struct { Time string `json:"time"` Level string `json:"level"` Message string `json:"message"` }{Time: time.Now().UTC().Format(time.RFC3339Nano), Level: "info", Message: string(line)}) if err != nil { return 0, fmt.Errorf("encode structured log: %w", err) } if _, err := writer.destination.Write(append(record, '\n')); err != nil { return 0, err } } return len(data), nil }