Documentation
¶
Overview ¶
Package liblog provides a date-organised, size-bounded log directory: the writer a long-lived host points its structured logs at when nobody is watching a terminal.
It exists because an append-only log file is a slow leak. `contenox serve` is meant to run for days, and a process whose only log destination grows without bound eventually fills the disk it shares with the database — a failure that arrives long after the change that caused it, on the machine least likely to be watched.
Files are named `<name>-<YYYY-MM-DD>.log`, and a day that outgrows its size bound continues in `<name>-<YYYY-MM-DD>.2.log`, `.3.log`, and so on. Asking "what did this host do on Tuesday?" is then a question about filenames rather than about how many times the file happened to roll.
Why there is no background goroutine ¶
Every decision this package makes happens on the write path, under the lock the writer needs anyway:
- the day check and the size check are an integer compare and a cached date-string compare, far cheaper than the file write they precede;
- the expensive part — scanning the directory to retire old files — runs only when a new file is actually created, which is at most once per day plus once per size part.
So retention is event-driven rather than swept on a timer: no goroutine to supervise, no second mutex, and nothing to leak if the host is killed. A counter ("organise every N appends") would be cheaper still, but it cannot be right at a day boundary — an idle host that logs once after midnight would file that line under yesterday until the counter happened to trip.
A Writer is safe for concurrent use, because slog handlers write from whichever goroutine logged.
Index ¶
- Constants
- func FormatSize(n int64) string
- func ParseSize(s string) (int64, error)
- type Config
- type Writer
- func (w *Writer) Close() error
- func (w *Writer) Dir() string
- func (w *Writer) MaxAge() time.Duration
- func (w *Writer) MaxBytes() int64
- func (w *Writer) MaxFiles() int
- func (w *Writer) Path() string
- func (w *Writer) Reconfigure(maxBytes int64, maxFiles int, maxAge time.Duration)
- func (w *Writer) Size() int64
- func (w *Writer) Write(p []byte) (int, error)
Constants ¶
const DefaultMaxAge = 14 * 24 * time.Hour
DefaultMaxAge retires a log by date regardless of how few files exist, so a quiet host does not keep last quarter's logs simply because it never wrote enough to hit the file count.
const DefaultMaxBytes int64 = 10 << 20 // 10 MiB
DefaultMaxBytes is a single day-part's size ceiling when a caller expresses no preference: large enough that a normal day of INFO-level operation fits in one part, small enough to open in an editor.
const DefaultMaxFiles = 14
DefaultMaxFiles is how many log files survive by default, counted across every date and part. At the default size that is a bounded ~140 MiB.
const Unlimited = -1
Unlimited disables a retention bound.
Variables ¶
This section is empty.
Functions ¶
func FormatSize ¶
FormatSize renders a byte count the way ParseSize would accept it back, so a status screen and the config value that set it agree.
func ParseSize ¶
ParseSize reads a human-written size — "10MB", "512kb", "1 GiB", "2048" — into bytes. A bare number is bytes.
KB and KiB both mean 1024, and so on down the ladder. That is technically wrong about SI, and deliberate: the value is a disk budget typed by an operator who means "ten megabytes of log", and resolving KB to 1000 here would make `log-max-size 10MB` quietly produce files 5% smaller than the number they typed, for a distinction nobody was drawing.
Types ¶
type Config ¶
type Config struct {
// Dir is the directory log files live in. Created if absent.
Dir string
// Name is the base name: "serve" yields serve-2026-08-15.log.
Name string
// MaxBytes bounds one part. Non-positive means [DefaultMaxBytes].
MaxBytes int64
// MaxFiles bounds how many files are retained across all dates.
// Non-positive means [DefaultMaxFiles]; use [Unlimited] for no bound.
MaxFiles int
// MaxAge retires files older than this by their date stamp. Zero means
// [DefaultMaxAge]; negative means no age bound.
MaxAge time.Duration
// Now is the clock, injectable so tests can cross a midnight boundary
// without waiting for one. Nil means [time.Now].
Now func() time.Time
}
Config describes a log directory. The zero value is not usable; every field except Now has a documented fallback applied by Open.
type Writer ¶
type Writer struct {
// contains filtered or unexported fields
}
Writer is an io.WriteCloser over a date-organised log directory.
func Open ¶
Open prepares the log directory and opens today's current part for appending. A restart continues the latest existing part rather than starting a new one, so a host that is restarted repeatedly does not shard its day into a file per launch.
func (*Writer) Dir ¶
Dir, MaxBytes, MaxFiles and MaxAge report the settings in force, so a status screen can state them rather than restating defaults and risking a lie.
func (*Writer) Path ¶
Path reports the file currently being written, so a status screen can name the exact file rather than a pattern.
func (*Writer) Reconfigure ¶
Reconfigure updates the retention settings of a live log.
It exists because a host must be able to log before it can read its own configuration: the log opens on defaults during boot, and the stored settings are applied once the database is readable. Only bounds change — the directory and name are fixed at Open, so no in-flight write can be redirected out from under a reader.
func (*Writer) Write ¶
Write appends p, starting a new file first when the date has changed or when p would carry the current part past its ceiling.
A single write larger than the ceiling is still written whole: a log line is the unit of meaning here, and splitting one across two files to honour a size bound would corrupt the record to protect a number.