enhanced logger: add fixed-width [LEVEL] prefixes, introduce fatalLog for .fatal messages, improve alignment and error handling, made fatal noreturn

This commit is contained in:
2026-08-08 00:01:39 +02:00
parent 963f1611fb
commit f1b07147b1

View File

@@ -10,6 +10,34 @@ pub const Level = enum {
warn, warn,
@"error", @"error",
fatal, 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`. /// Configuration passed to `Logger.init`.
@@ -148,26 +176,54 @@ pub const Logger = struct {
{ {
if (@intFromEnum(level) < @intFromEnum(logger.level)) return; if (@intFromEnum(level) < @intFromEnum(logger.level)) return;
if (level == .fatal) return logger.fatalLog(fmt, args);
try logger.state.mutex.lock(logger.io); try logger.state.mutex.lock(logger.io);
defer logger.state.mutex.unlock(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'); try logger.state.writer.interface.writeByte('\n');
switch (level) switch (level)
{ {
.warn, .@"error" => { try logger.state.writer.interface.flush(); }, .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 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 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 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 @"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 /// Forces any buffered output to be written to the underlying
/// writer immediately. /// writer immediately.
@@ -240,7 +296,7 @@ test "filtering, buffering and automatic flushing"
{ {
const contents = try dir.readFile(io, fname, &read_buf); 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 file.setLength(io, 0);
try log.state.writer.seekTo(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); 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 file.setLength(io, 0);
try log.state.writer.seekTo(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); 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! // 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 file.setLength(io, 0);
try log.state.writer.seekTo(0); try log.state.writer.seekTo(0);