///|
/// Append one plain text line, prefixed with the current ISO timestamp:
/// `2026-10-03T08:15:30.123Z `. Failures raise so the caller stays
/// in charge of best-effort handling.
pub fn plain(path : String, text : String) -> Unit raise @fsx.FileError {
@fsx.append_file(
path,
@utf8.encode(@dateku.iso8601(@dateku.now_ms()) + " " + text + "\n"),
)
}
///|
/// Append one JSONL management event:
/// `{"ts":"...","event":"...","":...}`. Keys are quoted by the
/// library; values must arrive as already-encoded JSON fragments — pass
/// `@fndash.jstr(text)` for strings and raw digits for numbers, so a field
/// the caller omits simply does not appear on the wire.
pub fn event(
path : String,
event : String,
fields : Array[(String, String)],
) -> Unit raise @fsx.FileError {
let all : Array[(String, String)] = [
("ts", @fndash.jstr(@dateku.iso8601(@dateku.now_ms()))),
("event", @fndash.jstr(event)),
]
for pair in fields {
let (key, value) = pair
all.push((key, value))
}
@fsx.append_file(path, @utf8.encode(@fndash.jobj(all) + "\n"))
}
///|
/// Rotate `path` once it exceeds `max_bytes`: copytruncate for logs held
/// open by app processes (they keep writing to the old inode), rename for
/// logs written by the caller itself. Returns true when a rotation ran.
/// Raises FileError when a needed rotation failed; a missing file is not
/// an error (nothing to rotate).
pub fn maybe_rotate(
path : String,
max_bytes : Int64,
copytruncate? : Bool = false,
) -> Bool raise @fsx.FileError {
match @fsx.file_size(path) {
Some(size) if size > max_bytes => {
if copytruncate {
@fsx.copytruncate(path)
} else {
@fsx.rotate(path)
}
true
}
_ => false
}
}