repos / zmx

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

zmx / src
Adrian  ·  2026-04-13

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