logger: per-process log files under /var/log/<boot-stamp>/

The logger service drains the tagged ring every 250 ms and demultiplexes
it into one file per process on the flash volume:

    /mnt/usb/var/log/2026-07-21T150434Z/system/services/fat.log
    [     0.214] mounted FAT (fat32, 128992 clusters, partition lba 0)

The boot stamp is the wall-clock anchor from klog_status (FAT-safe, no
colons); the kernel's records go to kernel.log; each line carries the
record's monotonic timestamp and level. Storage is best-effort and
late — the ring buffers a whole boot until the mount appears, then the
first drain writes the backlog, storage-stack records included. Lost
records surface as '-- N records lost --' from sequence gaps. Files
close (= fat's device cache flush) after a 2 s quiet period, bounding
data-at-risk without per-record flush thrash. The logger announces
itself once — a periodic status line would feed the stream it drains.

init spawns the logger last, so the reverse-order shutdown stops it
first and its final drain runs over a live storage chain; log-flush and
init's own DANOS.LOG shutdown flush retire (superseded). runtime.fs
makePath treats components at or above a mount point as router names —
create is best-effort per prefix, the final verdict is exists(path).
This commit is contained in:
Daniel Samson
2026-07-21 16:05:46 +01:00
parent 30d6ea622a
commit 127ea2dad9
7 changed files with 325 additions and 118 deletions
+7 -41
View File
@@ -22,11 +22,6 @@ const runtime = @import("runtime");
const power = runtime.power_protocol;
const build_options = @import("build_options");
/// Where the kernel boot log is persisted on the USB FAT volume — an 8.3 name at
/// the mount root (see system/services/log-flush). init writes it at shutdown;
/// the log-flush one-shot writes it once at boot.
const log_path = "/mnt/usb/DANOS.LOG";
/// The system services init brings up at boot, in order, by binary path. This is
/// init's policy — the microkernel keeps such choices in user space, not the
/// kernel. Drivers are absent on purpose: the device manager owns those. (A
@@ -39,6 +34,10 @@ const boot_services = [_][]const u8{
"/system/services/fat",
"/system/services/display",
"/system/services/display-demo",
// Last: at shutdown children stop in reverse order, so the logger goes down
// FIRST — its final drain still has the fat server (and the whole storage
// chain) alive underneath it.
"/system/services/logger",
};
/// The live process id of each boot service (0 = not running), indexed by its position
@@ -86,14 +85,6 @@ pub fn main() void {
if (runtime.system.spawnSupervised(service, &.{}, supervision_endpoint)) |id| child_ids[i] = id;
}
// Once the storage stack is up, a one-shot copies the boot log to the USB
// volume (/mnt/usb/DANOS.LOG) so it can be read on another machine — the only
// way to see it on a headless/real board with no host capturing serial. Fire
// and forget: it polls for the mount itself, and is deliberately NOT one of
// init's supervised children (a transient one-shot must not be stopped-and-
// waited-for during shutdown).
_ = runtime.system.spawn("/system/services/log-flush");
// Subscribe to power events (retry: the power service registers well after
// init starts). Best-effort — without it, a `terminate` signal still
// triggers the same shutdown path.
@@ -178,30 +169,6 @@ fn subscribePower() void {
_ = runtime.ipc.callCap(h, std.mem.asBytes(&request), &reply, supervision_endpoint) catch {};
}
/// Copy the whole kernel log to /mnt/usb/DANOS.LOG (the same file log-flush
/// writes at boot), so a poweroff captures the fullest log. Best-effort: if the
/// USB volume is not mounted, the open fails and it does nothing. Must run while
/// the storage services are still alive (see shutDown).
fn flushKernelLog() void {
// Truncate on open so this fuller flush replaces the boot-time one cleanly.
var file = runtime.fs.open(log_path, .{ .create = true, .truncate = true }) orelse return; // no USB volume
defer file.close();
var chunk: [4096]u8 = undefined;
// Interim (until the logger service in M-E): the stream is now framed
// records, so the file holds raw frames rather than plain text. Start at
// the ring's tail — offset 0 is gone once the ring has wrapped.
var offset: u64 = if (runtime.system.klogStatus()) |s| s.tail else 0;
const start = offset;
while (true) {
const got = runtime.system.klogRead(offset, &chunk) orelse break; // cursor lost
if (got == 0) break; // caught up
if (file.writeAll(chunk[0..got]) == null) break; // storage went away
offset += got;
}
var line: [96]u8 = undefined;
_ = runtime.system.write(std.fmt.bufPrint(&line, "/system/services/init: flushed log to {s} ({d} bytes)\n", .{ log_path, offset - start }) catch "");
}
/// The stop sequence: persist the log while storage is still up, then terminate
/// each child in reverse spawn order (vfs last — other services may flush through
/// it), waiting up to a deadline for each to exit before killing it, then ask the
@@ -209,10 +176,9 @@ fn flushKernelLog() void {
fn shutDown() void {
shutting_down = true; // the stop loop below kills children — those deaths aren't crashes
_ = runtime.system.write("/system/services/init: shutting down\n");
// Persist the fullest log to the USB volume BEFORE tearing anything down: the
// reverse-order stop loop below kills the fat server first, so /mnt/usb must be
// written while it is still mounted.
flushKernelLog();
// Log persistence is the logger service's job: it is the LAST boot service,
// so the reverse-order stop below terminates it first and its final drain
// runs while the whole storage chain is still alive.
var i = boot_services.len;
while (i > 0) {
i -= 1;