main zmx / src / log.zig
Eric Bower  ·  2026-08-02
  1const std = @import("std");
  2const cross = @import("cross.zig");
  3
  4pub var log_system = LogSystem{};
  5
  6pub fn zmxLogFn(
  7    comptime level: std.log.Level,
  8    comptime scope: anytype,
  9    comptime format: []const u8,
 10    args: anytype,
 11) void {
 12    log_system.log(level, scope, format, args) catch {};
 13}
 14
 15pub const LogSystem = struct {
 16    file: ?std.Io.File = null,
 17    mutex: std.Io.Mutex = .init,
 18    current_size: u64 = 0,
 19    max_size: u64 = 2 * 1024 * 1024, // 2MB
 20    path: []const u8 = "",
 21    io: std.Io = undefined,
 22    mode: std.Io.File.Permissions = std.Io.File.Permissions.fromMode(0o640),
 23
 24    pub fn init(self: *LogSystem, io: std.Io, path: []const u8, mode: std.Io.File.Permissions) !void {
 25        self.io = io;
 26        self.path = path;
 27        self.mode = mode;
 28
 29        const file = std.Io.Dir.openFileAbsolute(self.io, path, .{ .mode = .read_write }) catch |err| switch (err) {
 30            error.FileNotFound => try std.Io.Dir.createFileAbsolute(
 31                self.io,
 32                path,
 33                .{ .read = true, .permissions = self.mode },
 34            ),
 35            else => return err,
 36        };
 37
 38        // Use lseek(SEEK_END) instead of length() + seekTo() to avoid a
 39        // TOCTOU race: after fork() the parent may still write to the log
 40        // between our length() check and seekTo(), causing us to overwrite
 41        // recent parent entries. lseek(fd, 0, SEEK_END) is atomic — it
 42        // always positions at the true end of file at seek time.
 43        const new_pos = cross.c.lseek(file.handle, 0, cross.c.SEEK_END);
 44        if (new_pos == -1) {
 45            std.Io.File.close(file, self.io);
 46            return error.SeekFailed;
 47        }
 48        self.current_size = @as(u64, @intCast(new_pos));
 49        self.file = file;
 50    }
 51
 52    pub fn deinit(self: *LogSystem) void {
 53        if (self.file) |f| std.Io.File.close(f, self.io);
 54    }
 55
 56    pub fn log(
 57        self: *LogSystem,
 58        comptime level: std.log.Level,
 59        comptime scope: anytype,
 60        comptime format: []const u8,
 61        args: anytype,
 62    ) !void {
 63        try self.mutex.lock(self.io);
 64        defer self.mutex.unlock(self.io);
 65
 66        if (self.file == null) {
 67            std.log.defaultLog(level, scope, format, args);
 68            return;
 69        }
 70
 71        if (self.current_size >= self.max_size) {
 72            self.wipe() catch |err| {
 73                std.debug.print("Log wipe failed: {s}\n", .{@errorName(err)});
 74            };
 75        }
 76
 77        const now: std.Io.Timestamp = .now(self.io, .real);
 78        const prefix = "[{d}] [{s}] ({s}): ";
 79        const scope_name = @tagName(scope);
 80        const level_name = level.asText();
 81
 82        const prefix_args = .{
 83            now.toSeconds(),
 84            level_name,
 85            scope_name,
 86        };
 87
 88        if (self.file) |f| {
 89            const prefix_len = std.fmt.count(prefix, prefix_args);
 90            const msg_len = std.fmt.count(format, args);
 91            const newline_len = 1;
 92            const total_len = prefix_len + msg_len + newline_len;
 93            self.current_size += total_len;
 94
 95            var buf: [4096]u8 = undefined;
 96            var w = f.writerStreaming(self.io, &buf);
 97            std.Io.Writer.print(&w.interface, prefix ++ format ++ "\n", prefix_args ++ args) catch {};
 98            w.interface.flush() catch {};
 99        }
100    }
101
102    fn wipe(self: *LogSystem) !void {
103        if (self.file) |f| {
104            std.Io.File.close(f, self.io);
105            self.file = null;
106        }
107
108        self.file = try std.Io.Dir.createFileAbsolute(
109            self.io,
110            self.path,
111            .{
112                .truncate = true,
113                .read = true,
114                .permissions = self.mode,
115            },
116        );
117        self.current_size = 0;
118    }
119};