enhanced logger: move-safety, add fatal log level, refine buffering and error handling
This commit is contained in:
@@ -9,6 +9,7 @@ pub const Level = enum {
|
|||||||
info,
|
info,
|
||||||
warn,
|
warn,
|
||||||
@"error",
|
@"error",
|
||||||
|
fatal,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Configuration passed to `Logger.init`.
|
/// Configuration passed to `Logger.init`.
|
||||||
@@ -25,78 +26,102 @@ pub const Level = enum {
|
|||||||
/// or a log file). The logger takes no ownership of it beyond the
|
/// or a log file). The logger takes no ownership of it beyond the
|
||||||
/// lifetime of the wrapping `std.Io.File.Writer`.
|
/// lifetime of the wrapping `std.Io.File.Writer`.
|
||||||
pub const InitOptions = struct {
|
pub const InitOptions = struct {
|
||||||
level: Level = .info,
|
pub const ErrorCallback = Logger.ErrorCallback;
|
||||||
buffer: usize = 1024,
|
|
||||||
file: std.Io.File,
|
level: Level = .info,
|
||||||
io: std.Io,
|
buffer: usize = 1024,
|
||||||
|
on_error: ErrorCallback = null,
|
||||||
|
file: std.Io.File,
|
||||||
|
io: std.Io,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A minimal, thread-safe, buffered logger.
|
/// Move‑safe, thread‑safe, buffered file logger.
|
||||||
///
|
///
|
||||||
/// Writes are buffered and only flushed automatically on `warn`,
|
/// All mutable writer state is stored on the heap behind a stable pointer.
|
||||||
/// `@"error"`, or `deinit`.
|
/// The `Logger` struct itself can be copied or moved freely.
|
||||||
|
///
|
||||||
|
/// `level` sets the minimum severity that will be written; messages
|
||||||
|
/// below this level are discarded.
|
||||||
|
///
|
||||||
|
/// Writes are buffered and only flushed automatically on `warn`, `@"error"`,
|
||||||
|
/// when calling `deinit` or due to drain-on-overflow of the write buffer.
|
||||||
/// `debug` and `info` messages may sit in the buffer until it fills,
|
/// `debug` and `info` messages may sit in the buffer until it fills,
|
||||||
/// a higher-severity message is logged, `flush` or `deinit` is called.
|
/// a higher-severity message is logged, `flush` or `deinit` is called.
|
||||||
///
|
///
|
||||||
/// Must be initialized with `init` before use and cleaned up with
|
/// Unless an `ErrorCallback` is specified all errors in the logging functions will be swallowed.
|
||||||
/// `deinit`. Not copyable once initialized, as `mutex` and `writer`
|
|
||||||
/// hold state tied to the original instance.
|
|
||||||
pub const Logger = struct {
|
pub const Logger = struct {
|
||||||
level: Level = .info,
|
pub const ErrorCallback = ?*const fn (err: anyerror) void;
|
||||||
buffer: []u8 = undefined,
|
|
||||||
writer: std.Io.File.Writer = undefined,
|
level: Level = .info,
|
||||||
mutex: std.Io.Mutex = undefined,
|
on_error: ErrorCallback = null,
|
||||||
alloc: std.mem.Allocator = undefined,
|
|
||||||
io: std.Io = undefined,
|
io: std.Io = undefined,
|
||||||
|
state: *WriterState = undefined,
|
||||||
|
mutex: std.Io.Mutex = undefined,
|
||||||
|
allocator: std.mem.Allocator = undefined,
|
||||||
|
|
||||||
|
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
|
/// Heap-allocated home for the write buffer and the `File.Writer`
|
||||||
|
/// built on top of it. `File.Writer` holds a pointer into
|
||||||
|
/// `buffer`, so both must live at a stable address that survives
|
||||||
|
/// the `Logger` value itself being copied or moved.
|
||||||
|
const WriterState = struct {
|
||||||
|
buffer: []u8,
|
||||||
|
writer: std.Io.File.Writer,
|
||||||
|
};
|
||||||
|
|
||||||
/// Creates and returns an initialized `Logger`.
|
/// Creates and returns an initialized `Logger`.
|
||||||
///
|
///
|
||||||
/// Allocates `options.buffer` bytes from `allocator` for internal
|
/// Allocates `options.buffer` bytes from `allocator` for internal
|
||||||
/// write buffering; ownership of this allocation belongs to the
|
/// write buffering, along with a `WriterState` to hold that buffer
|
||||||
/// returned `Logger` and is freed in `deinit`.
|
/// and its `File.Writer` at a stable heap address. Ownership of
|
||||||
|
/// both allocations belongs to the returned `Logger` and are freed
|
||||||
|
/// in `deinit`.
|
||||||
///
|
///
|
||||||
/// `allocator` is stored on the result and reused for cleanup, so
|
/// `allocator` is stored on the result and reused for cleanup, so
|
||||||
/// it must remain valid for the logger's lifetime.
|
/// it must remain valid for the logger's lifetime.
|
||||||
///
|
///
|
||||||
/// Safe to move or copy the returned value freely before first
|
/// Returns an error if either allocation fails.
|
||||||
/// use, since the write buffer lives on the heap rather than
|
|
||||||
/// inside the `Logger` struct itself. Once a method has been
|
|
||||||
/// called on it, the logger must stay at a fixed address (e.g.
|
|
||||||
/// behind a pointer or in a `var` that isn't reassigned by value)
|
|
||||||
/// for the remainder of its life.
|
|
||||||
///
|
|
||||||
/// Returns an error if the buffer allocation fails.
|
|
||||||
pub fn init(allocator: std.mem.Allocator, options: InitOptions) !Self
|
pub fn init(allocator: std.mem.Allocator, options: InitOptions) !Self
|
||||||
{
|
{
|
||||||
const buffer = try allocator.alloc(u8, options.buffer);
|
const buffer = try allocator.alloc(u8, options.buffer);
|
||||||
|
errdefer allocator.free(buffer);
|
||||||
|
|
||||||
return Self{
|
const state = try allocator.create(WriterState);
|
||||||
.level = options.level,
|
state.* = .{
|
||||||
.buffer = buffer,
|
.buffer = buffer,
|
||||||
.writer = options.file.writer(options.io, buffer),
|
.writer = options.file.writer(options.io, buffer),
|
||||||
.mutex = .init,
|
};
|
||||||
.alloc = allocator,
|
|
||||||
.io = options.io,
|
return Self{
|
||||||
|
.level = options.level,
|
||||||
|
.on_error = options.on_error,
|
||||||
|
.io = options.io,
|
||||||
|
.state = state,
|
||||||
|
.mutex = .init,
|
||||||
|
.allocator = allocator,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Flushes any buffered output and releases the logger's buffer.
|
/// Flushes any buffered output and releases the logger's buffer
|
||||||
|
/// and `WriterState`.
|
||||||
///
|
///
|
||||||
/// Safe to call once initialization via `init` has succeeded.
|
/// Safe to call once initialization via `init` has succeeded.
|
||||||
/// Flush errors are silently ignored, since there is no caller
|
/// Flush errors are silently ignored, since there is no caller
|
||||||
/// left to meaningfully report them to at teardown time.
|
/// left to meaningfully report them to at teardown time.
|
||||||
///
|
///
|
||||||
/// Must not be called more than once, as the buffer is freed
|
/// Must not be called more than once, as the underlying
|
||||||
/// unconditionally.
|
/// allocations are freed unconditionally.
|
||||||
pub fn deinit(logger: *Self) void
|
pub fn deinit(logger: *Self) void
|
||||||
{
|
{
|
||||||
logger.mutex.lock(logger.io) catch {};
|
logger.mutex.lock(logger.io) catch {};
|
||||||
logger.writer.interface.flush() catch {};
|
logger.state.writer.interface.flush() catch {};
|
||||||
logger.mutex.unlock(logger.io);
|
logger.mutex.unlock(logger.io);
|
||||||
|
|
||||||
logger.alloc.free(logger.buffer);
|
logger.allocator.free(logger.state.buffer);
|
||||||
|
logger.allocator.destroy(logger.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -121,19 +146,23 @@ pub const Logger = struct {
|
|||||||
try logger.mutex.lock(logger.io);
|
try logger.mutex.lock(logger.io);
|
||||||
defer logger.mutex.unlock(logger.io);
|
defer logger.mutex.unlock(logger.io);
|
||||||
|
|
||||||
try logger.writer.interface.print(fmt, args);
|
try logger.state.writer.interface.print(fmt, args);
|
||||||
|
try logger.state.writer.interface.writeByte('\n');
|
||||||
|
|
||||||
if (level == .warn or level == .@"error") {
|
switch (level)
|
||||||
try logger.writer.interface.flush();
|
{
|
||||||
|
.warn, .@"error" => { try logger.state.writer.interface.flush(); },
|
||||||
|
.fatal => { std.debug.panic(fmt, args); },
|
||||||
|
else => return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|
||||||
pub fn debug(logger: *Self, comptime fmt: []const u8, args: anytype) void { logger.log(fmt, args, .debug) catch {}; }
|
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 {}; }
|
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 {}; }
|
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 {}; }
|
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); }
|
||||||
|
|
||||||
/// Forces any buffered output to be written to the underlying
|
/// Forces any buffered output to be written to the underlying
|
||||||
/// writer immediately.
|
/// writer immediately.
|
||||||
@@ -144,7 +173,7 @@ pub const Logger = struct {
|
|||||||
try logger.mutex.lock(logger.io);
|
try logger.mutex.lock(logger.io);
|
||||||
defer logger.mutex.unlock(logger.io);
|
defer logger.mutex.unlock(logger.io);
|
||||||
|
|
||||||
try logger.writer.interface.flush();
|
try logger.state.writer.interface.flush();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -178,9 +207,9 @@ test "filtering, buffering and automatic flushing"
|
|||||||
const dir = try std.Io.Dir.openDirAbsolute(io, path, .{});
|
const dir = try std.Io.Dir.openDirAbsolute(io, path, .{});
|
||||||
defer
|
defer
|
||||||
{
|
{
|
||||||
dir.deleteFile(io, fname) catch unreachable;
|
dir.deleteFile(io, fname) catch {};
|
||||||
dir.close(io);
|
dir.close(io);
|
||||||
std.Io.Dir.deleteDirAbsolute(io, path) catch unreachable;
|
std.Io.Dir.deleteDirAbsolute(io, path) catch {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const file = try dir.createFile(io, fname, .{ .lock = .exclusive });
|
const file = try dir.createFile(io, fname, .{ .lock = .exclusive });
|
||||||
@@ -206,10 +235,10 @@ 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", contents);
|
try std.testing.expectEqualStrings("info=1\n", contents);
|
||||||
}
|
}
|
||||||
try file.setLength(io, 0);
|
try file.setLength(io, 0);
|
||||||
try log.writer.seekTo(0);
|
try log.state.writer.seekTo(0);
|
||||||
@memset(read_buf[0..], 0);
|
@memset(read_buf[0..], 0);
|
||||||
|
|
||||||
// The file should include 'warn=1' as warnings and errors automatically trigger a flush to disk.
|
// The file should include 'warn=1' as warnings and errors automatically trigger a flush to disk.
|
||||||
@@ -217,10 +246,10 @@ 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", contents);
|
try std.testing.expectEqualStrings("warn=1\n", contents);
|
||||||
}
|
}
|
||||||
try file.setLength(io, 0);
|
try file.setLength(io, 0);
|
||||||
try log.writer.seekTo(0);
|
try log.state.writer.seekTo(0);
|
||||||
@memset(read_buf[0..], 0);
|
@memset(read_buf[0..], 0);
|
||||||
|
|
||||||
// The file should include 'info=x...' as it's bigger than the allocated 256 buffer, forcing it to automatically drain on overflow.
|
// The file should include 'info=x...' as it's bigger than the allocated 256 buffer, forcing it to automatically drain on overflow.
|
||||||
@@ -228,9 +257,10 @@ 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!
|
||||||
try std.testing.expectEqualStrings("info=" ++ ([_]u8{'x'} ** 300), contents);
|
try std.testing.expectEqualStrings("info=" ++ ([_]u8{'x'} ** 300), contents);
|
||||||
}
|
}
|
||||||
try file.setLength(io, 0);
|
try file.setLength(io, 0);
|
||||||
try log.writer.seekTo(0);
|
try log.state.writer.seekTo(0);
|
||||||
@memset(read_buf[0..], 0);
|
@memset(read_buf[0..], 0);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user