repos / zmx

session persistence for terminal processes
git clone https://github.com/neurosnap/zmx.git

zmx / src
Eric Bower  ·  2025-12-05

log.zig

 1const std = @import("std");
 2
 3pub const LogSystem = struct {
 4    file: ?std.fs.File = null,
 5    mutex: std.Thread.Mutex = .{},
 6    current_size: u64 = 0,
 7    max_size: u64 = 5 * 1024 * 1024, // 5MB
 8    path: []const u8 = "",
 9    alloc: std.mem.Allocator = undefined,
10
11    pub fn init(self: *LogSystem, alloc: std.mem.Allocator, path: []const u8) !void {
12        self.alloc = alloc;
13        self.path = try alloc.dupe(u8, path);
14
15        const file = std.fs.openFileAbsolute(path, .{ .mode = .read_write }) catch |err| switch (err) {
16            error.FileNotFound => try std.fs.createFileAbsolute(path, .{ .read = true, .mode = 0o640 }),
17            else => return err,
18        };
19
20        const end_pos = try file.getEndPos();
21        try file.seekTo(end_pos);
22        self.current_size = end_pos;
23        self.file = file;
24    }
25
26    pub fn deinit(self: *LogSystem) void {
27        if (self.file) |f| f.close();
28        if (self.path.len > 0) self.alloc.free(self.path);
29    }
30
31    pub fn log(self: *LogSystem, comptime level: std.log.Level, comptime scope: @Type(.enum_literal), comptime format: []const u8, args: anytype) void {
32        self.mutex.lock();
33        defer self.mutex.unlock();
34
35        if (self.file == null) {
36            std.log.defaultLog(level, scope, format, args);
37            return;
38        }
39
40        if (self.current_size >= self.max_size) {
41            self.rotate() catch |err| {
42                std.debug.print("Log rotation failed: {s}\n", .{@errorName(err)});
43            };
44        }
45
46        const now = std.time.milliTimestamp();
47        const prefix = "[{d}] [{s}] ({s}): ";
48        const scope_name = @tagName(scope);
49        const level_name = level.asText();
50
51        const prefix_args = .{
52            now,
53            level_name,
54            scope_name,
55        };
56
57        if (self.file) |f| {
58            const prefix_len = std.fmt.count(prefix, prefix_args);
59            const msg_len = std.fmt.count(format, args);
60            const newline_len = 1;
61            const total_len = prefix_len + msg_len + newline_len;
62            self.current_size += total_len;
63
64            var buf: [4096]u8 = undefined;
65            var w = f.writerStreaming(&buf);
66            w.interface.print(prefix ++ format ++ "\n", prefix_args ++ args) catch {};
67            w.interface.flush() catch {};
68        }
69    }
70
71    fn rotate(self: *LogSystem) !void {
72        if (self.file) |f| {
73            f.close();
74            self.file = null;
75        }
76
77        const old_path = try std.fmt.allocPrint(self.alloc, "{s}.old", .{self.path});
78        defer self.alloc.free(old_path);
79
80        std.fs.renameAbsolute(self.path, old_path) catch |err| switch (err) {
81            error.FileNotFound => {},
82            else => return err,
83        };
84
85        self.file = try std.fs.createFileAbsolute(self.path, .{ .truncate = true, .read = true, .mode = 0o640 });
86        self.current_size = 0;
87    }
88};