Documentation
¶
Overview ¶
Package logrotate implements size- and age-bounded rotation for the append-only JSONL logs ctm writes under ~/.config/ctm/logs/.
Design choices:
- Rotated files use a unix-nanosecond suffix plus ".gz": e.g. "<path>.1745000000000000000.gz". This yields stable chronological ordering without cascading renames and makes age-based pruning a mtime lookup rather than a filename parse.
- Rotation runs synchronously in the hook path. At the default 50 MiB threshold, gzip is well under 1s on a modern CPU and is triggered at most once per threshold crossing, not per line.
- A sibling ".rotate.lock" coordinates concurrent writers so only one rotates at a time; a late caller finds the file already small and no-ops.
- The active file's mode is preserved; rotated .gz siblings inherit the same mode. Callers that want 0600 on the active file will automatically get 0600 on .gz siblings too.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func MaybeRotate ¶
MaybeRotate rotates path if it exceeds p.MaxSize. It renames the file to "<path>.<unix-nano>", gzips the result in place, and truncates the original to a fresh empty file. After rotation it runs Prune so the caller does not have to.
Contract:
- Missing file → no-op (nothing to rotate).
- p.MaxSize == 0 → no-op (size-based rotation disabled; caller may still invoke Prune separately for age-only retention).
- Concurrent calls serialize on a sibling "<path>.rotate.lock"; the second caller re-stats post-lock and no-ops if the file is back under the threshold.
Writers that keep an open fd to path across a rotation continue appending to the rotated file (same inode). We accept that small trailing-edge data loss (a handful of lines landing in the just- rotated file instead of the fresh active) as the cost of not coordinating fds across processes; the data is still on disk, just in the .gz rather than the active log.
func Open ¶
func Open(path string) (io.ReadCloser, error)
Open returns a reader for a log source. If path ends in ".gz" the reader transparently decompresses; otherwise it's a plain file reader. The returned ReadCloser owns both the file and the gzip reader (if any); a single Close cleans up both.
func Prune ¶
Prune removes rotated siblings of path that violate the retention caps in p: older than MaxAge, or beyond MaxFiles (oldest discarded first). The active log file itself is never touched by Prune.
Zero-valued caps disable the corresponding check.
func Sources ¶
Sources returns every readable log source belonging to path, in chronological order (oldest first). Rotated .gz siblings come first; the active path is last. Entries that cannot be stat()ed are dropped.
Missing active file is not an error — Sources only returns rotated siblings in that case.
Types ¶
type Policy ¶
type Policy struct {
MaxSize int64 // rotate when the active file exceeds this (bytes)
MaxAge time.Duration // prune rotated siblings older than this
MaxFiles int // cap rotated siblings (keep the newest N)
}
Policy governs rotation and retention. Zero-valued fields disable the corresponding check (e.g. MaxSize=0 disables size-based rotation).
func DefaultPolicy ¶
func DefaultPolicy() Policy
DefaultPolicy returns the ctm default: rotate at 50 MiB, keep at most 30 days of history or 10 files, whichever is smaller.