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};