From f1b07147b14ec8530a86f27d44fd33b0ed615f41 Mon Sep 17 00:00:00 2001 From: Overlord Date: Sat, 8 Aug 2026 00:01:39 +0200 Subject: [PATCH] enhanced logger: add fixed-width `[LEVEL]` prefixes, introduce `fatalLog` for `.fatal` messages, improve alignment and error handling, made fatal noreturn --- src/logging/log.zig | 80 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 68 insertions(+), 12 deletions(-) diff --git a/src/logging/log.zig b/src/logging/log.zig index ae47a33..ea3f1b3 100644 --- a/src/logging/log.zig +++ b/src/logging/log.zig @@ -10,6 +10,34 @@ pub const Level = enum { warn, @"error", fatal, + + /// Returns the fixed-width, uppercase label used as the log-line + /// prefix for `level` (e.g. `"INFO "` for `.info`), derived from + /// the enum's own field name via `@tagName`. + /// All labels are padded to the width of the longest level name, + /// so prefixes line up in a fixed-width terminal/file. + fn prefix(level: Level) []const u8 + { + return switch (level) + { + inline else => |l| comptime comptimeTag(@tagName(l)), + }; + } + + fn comptimeTag(comptime name: []const u8) []const u8 + { + const inner_width = 5; + const total_width = inner_width + 2; // account for "[" and "]" + + comptime var buf: [total_width]u8 = .{' '} ** total_width; + + buf[0] = '['; + inline for (name, 0..) |c, i| buf[i + 1] = std.ascii.toUpper(c); + buf[name.len + 1] = ']'; + + const result = buf; + return &result; + } }; /// Configuration passed to `Logger.init`. @@ -52,8 +80,8 @@ pub const InitOptions = struct { pub const Logger = struct { pub const ErrorCallback = ?*const fn (err: anyerror) void; - level: Level = .info, - on_error: ErrorCallback = null, + level: Level = .info, + on_error: ErrorCallback = null, io: std.Io, state: *WriterState, @@ -148,26 +176,54 @@ pub const Logger = struct { { if (@intFromEnum(level) < @intFromEnum(logger.level)) return; + if (level == .fatal) return logger.fatalLog(fmt, args); + try logger.state.mutex.lock(logger.io); defer logger.state.mutex.unlock(logger.io); - try logger.state.writer.interface.print(fmt, args); + try logger.state.writer.interface.print( + "{s} " ++ fmt, + .{Level.prefix(level)} ++ args + ); try logger.state.writer.interface.writeByte('\n'); switch (level) { .warn, .@"error" => { try logger.state.writer.interface.flush(); }, - .fatal => { std.debug.panic(fmt, args); }, - else => return + else => return, } } + /// Best-effort write-then-panic path for `.fatal` messages. + /// + /// Ignores lock/write/flush failures rather than propagating them, + /// a failure to persist the fatal message must never prevent the + /// panic itself. Marked cold since this is checked on every `log` + /// call but taken essentially never. + fn fatalLog(logger: *Self, comptime fmt: []const u8, args: anytype) noreturn + { + @branchHint(.cold); + + logger.state.mutex.lock(logger.io) catch {}; + + logger.state.writer.interface.print( + "{s} " ++ fmt, + .{Level.prefix(.fatal)} ++ args + ) catch {}; + + logger.state.writer.interface.writeByte('\n') catch {}; + logger.state.writer.interface.flush() catch {}; + + std.debug.panic(fmt, args); + } + // - pub fn debug(logger: *Self, comptime fmt: []const u8, args: anytype) void { logger.log(fmt, args, .debug) catch |e| if (logger.on_error) |h| h(e); } - pub fn info(logger: *Self, comptime fmt: []const u8, args: anytype) void { logger.log(fmt, args, .info) catch |e| if (logger.on_error) |h| h(e); } - pub fn warn(logger: *Self, comptime fmt: []const u8, args: anytype) void { logger.log(fmt, args, .warn) catch |e| if (logger.on_error) |h| h(e); } - pub fn @"error"(logger: *Self, comptime fmt: []const u8, args: anytype) void { logger.log(fmt, args, .@"error") catch |e| if (logger.on_error) |h| h(e); } + pub fn debug(logger: *Self, comptime fmt: []const u8, args: anytype) void { logger.log(fmt, args, .debug) catch |e| if (logger.on_error) |h| h(e); } + pub fn info(logger: *Self, comptime fmt: []const u8, args: anytype) void { logger.log(fmt, args, .info) catch |e| if (logger.on_error) |h| h(e); } + pub fn warn(logger: *Self, comptime fmt: []const u8, args: anytype) void { logger.log(fmt, args, .warn) catch |e| if (logger.on_error) |h| h(e); } + pub fn @"error"(logger: *Self, comptime fmt: []const u8, args: anytype) void { logger.log(fmt, args, .@"error") catch |e| if (logger.on_error) |h| h(e); } + pub fn fatal(logger: *Self, comptime fmt: []const u8, args: anytype) noreturn { logger.log(fmt, args, .fatal) catch unreachable; unreachable; } /// Forces any buffered output to be written to the underlying /// writer immediately. @@ -240,7 +296,7 @@ test "filtering, buffering and automatic flushing" { const contents = try dir.readFile(io, fname, &read_buf); - try std.testing.expectEqualStrings("info=1\n", contents); + try std.testing.expectEqualStrings("[INFO] info=1\n", contents); } try file.setLength(io, 0); try log.state.writer.seekTo(0); @@ -251,7 +307,7 @@ test "filtering, buffering and automatic flushing" { const contents = try dir.readFile(io, fname, &read_buf); - try std.testing.expectEqualStrings("warn=1\n", contents); + try std.testing.expectEqualStrings("[WARN] warn=1\n", contents); } try file.setLength(io, 0); try log.state.writer.seekTo(0); @@ -263,7 +319,7 @@ test "filtering, buffering and automatic flushing" { const contents = try dir.readFile(io, fname, &read_buf); // Note: This doesn't expect a newline, as it only drains the actual overflow, not the newline written into the buffer afterwards! - try std.testing.expectEqualStrings("info=" ++ ([_]u8{'x'} ** 300), contents); + try std.testing.expectEqualStrings("[INFO] info=" ++ ([_]u8{'x'} ** 300), contents); } try file.setLength(io, 0); try log.state.writer.seekTo(0);