made logger copy-safe, clarified shared state, updated comments
This commit is contained in:
@@ -40,15 +40,16 @@ pub const Level = enum {
|
||||
}
|
||||
};
|
||||
|
||||
/// Configuration passed to `Logger.init`.
|
||||
/// Configuration used to create an initial `Logger` handle.
|
||||
///
|
||||
/// `level` sets the minimum severity that will be written; messages
|
||||
/// below this level are discarded.
|
||||
/// `level` sets the initial handle's minimum severity. A logger made
|
||||
/// with `clone` receives an independent copy of this setting and may
|
||||
/// change it without affecting other logger handles that share the
|
||||
/// same writer.
|
||||
///
|
||||
/// `buffer` is the size, in bytes, allocated for the internal write
|
||||
/// buffer. Larger buffers reduce the number of underlying
|
||||
/// writes at the cost of more memory and higher latency
|
||||
/// before data is flushed.
|
||||
/// `buffer` is the size, in bytes, allocated for the shared internal
|
||||
/// write buffer. Larger buffers can reduce underlying writes at the
|
||||
/// cost of memory use and latency before buffered messages are flushed.
|
||||
pub const InitOptions = struct {
|
||||
pub const ErrorCallback = Logger.ErrorCallback;
|
||||
|
||||
@@ -59,54 +60,72 @@ pub const InitOptions = struct {
|
||||
io: std.Io,
|
||||
};
|
||||
|
||||
/// Move‑safe, thread‑safe, buffered file logger.
|
||||
/// Thread-safe, move-safe, copy-safe (through `.clone`), buffered file logger
|
||||
/// with shared writer state.
|
||||
///
|
||||
/// All mutable writer state is stored on the heap behind a stable pointer.
|
||||
/// The `Logger` struct itself can be copied or moved freely.
|
||||
/// A `Logger` is an owning handle to shared writer state. Handles made
|
||||
/// with `clone` share one buffer, `File.Writer`, I/O context, and mutex,
|
||||
/// so writes from all clones are serialized and cannot interleave.
|
||||
///
|
||||
/// `level` sets the minimum severity that will be written; messages
|
||||
/// below this level are discarded.
|
||||
/// `level` and `on_error` belong to each individual logger handle.
|
||||
/// Consequently, clones may use different severity filters and error
|
||||
/// callbacks while writing to the same destination.
|
||||
///
|
||||
/// 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,
|
||||
/// a higher-severity message is logged, `flush` or `deinit` is called.
|
||||
/// Use `clone` to create another owning logger handle. Do not duplicate
|
||||
/// a `Logger` through assignment, aggregate initialization, or
|
||||
/// `@memcpy`; those operations do not retain the shared state. Every
|
||||
/// logger returned by `init` or `clone` must be passed to `deinit`
|
||||
/// exactly once.
|
||||
///
|
||||
/// Unless an `ErrorCallback` is specified all errors in the logging functions will be swallowed.
|
||||
/// Writes are buffered and automatically flushed for `warn`, `@"error"`,
|
||||
/// and `fatal` messages; they are also flushed when the final owning
|
||||
/// handle is deinitialized or when the writer drains due to buffer
|
||||
/// overflow. `debug` and `info` messages may remain buffered until then,
|
||||
/// or until `flush` is called.
|
||||
///
|
||||
/// Fatal logging is best-effort: the logger attempts to lock, write, and
|
||||
/// flush the fatal message, but ignores failures so logging failures can
|
||||
/// never prevent the subsequent panic.
|
||||
///
|
||||
/// Unless an `ErrorCallback` is specified, errors from the public
|
||||
/// logging methods are swallowed.
|
||||
pub const Logger = struct {
|
||||
pub const ErrorCallback = ?*const fn (err: anyerror) void;
|
||||
|
||||
level: Level = .info,
|
||||
on_error: ErrorCallback = null,
|
||||
|
||||
io: std.Io,
|
||||
state: *WriterState,
|
||||
allocator: std.mem.Allocator,
|
||||
|
||||
|
||||
const Self = @This();
|
||||
|
||||
/// Heap-allocated home for the write buffer, the `File.Writer`
|
||||
/// built on top of it, and the mutex guarding both. `File.Writer`
|
||||
/// holds a pointer into `buffer`, so all three must live at a
|
||||
/// stable address that survives the `Logger` value itself being
|
||||
/// copied or moved.
|
||||
/// Heap-allocated state shared by all `Logger` clones.
|
||||
///
|
||||
/// This stores the buffer, writer, I/O context, and mutex together at
|
||||
/// a stable address. `File.Writer` retains a pointer into `buffer`, so
|
||||
/// neither may move for as long as any owning logger handle remains.
|
||||
///
|
||||
/// `refs` counts owning handles created by `init` and `clone`. The last
|
||||
/// handle released by `deinit` flushes the writer and frees this state.
|
||||
const WriterState = struct {
|
||||
buffer: []u8,
|
||||
mutex: std.Io.Mutex,
|
||||
writer: std.Io.File.Writer,
|
||||
refs: std.atomic.Value(usize) = .init(1),
|
||||
|
||||
io: std.Io,
|
||||
allocator: std.mem.Allocator,
|
||||
buffer: []u8,
|
||||
mutex: std.Io.Mutex,
|
||||
writer: std.Io.File.Writer,
|
||||
};
|
||||
|
||||
/// Creates and returns an initialized `Logger`.
|
||||
/// Creates an initialized owning logger handle.
|
||||
///
|
||||
/// Allocates `options.buffer` bytes from `allocator` for internal
|
||||
/// write buffering, along with a `WriterState` to hold that buffer
|
||||
/// and its `File.Writer` at a stable heap address. Ownership of
|
||||
/// both allocations belongs to the returned `Logger` and are freed
|
||||
/// in `deinit`.
|
||||
/// Allocates `options.buffer` bytes for the shared write buffer and a
|
||||
/// `WriterState` containing the buffer's writer, I/O context, mutex,
|
||||
/// allocator, and initial ownership reference.
|
||||
///
|
||||
/// `allocator` is stored on the result and reused for cleanup, so
|
||||
/// it must remain valid for the logger's lifetime.
|
||||
/// The returned logger owns one reference to the shared state. It must
|
||||
/// be passed to `deinit` exactly once, unless ownership is explicitly
|
||||
/// transferred to another part of the program.
|
||||
///
|
||||
/// Returns an error if either allocation fails.
|
||||
pub fn init(allocator: std.mem.Allocator, options: InitOptions) !Self
|
||||
@@ -116,42 +135,57 @@ pub const Logger = struct {
|
||||
|
||||
const state = try allocator.create(WriterState);
|
||||
state.* = .{
|
||||
.buffer = buffer,
|
||||
.mutex = .init,
|
||||
.writer = options.file.writer(options.io, buffer),
|
||||
.allocator = allocator,
|
||||
.io = options.io,
|
||||
.buffer = buffer,
|
||||
.mutex = .init,
|
||||
.writer = options.file.writer(options.io, buffer),
|
||||
};
|
||||
|
||||
return Self{
|
||||
.level = options.level,
|
||||
.on_error = options.on_error,
|
||||
|
||||
.io = options.io,
|
||||
.state = state,
|
||||
.allocator = allocator,
|
||||
};
|
||||
}
|
||||
|
||||
/// Flushes any buffered output and releases the logger's buffer
|
||||
/// and `WriterState`.
|
||||
/// Releases this logger handle's ownership of the shared writer state.
|
||||
///
|
||||
/// Safe to call once initialization via `init` has succeeded.
|
||||
/// Flush and lock errors are reported via `on_error` if set,
|
||||
/// but otherwise ignored, since there is no caller left to
|
||||
/// meaningfully return them to at teardown time.
|
||||
/// If other logger handles created with `clone` remain, this only
|
||||
/// decrements the shared reference count. The final owning handle
|
||||
/// flushes buffered output and frees the shared buffer and `WriterState`.
|
||||
///
|
||||
/// Must not be called more than once, as the underlying
|
||||
/// allocations are freed unconditionally.
|
||||
/// Flush and lock errors during final cleanup are reported through this
|
||||
/// handle's `on_error` callback when one is configured; otherwise they
|
||||
/// are ignored.
|
||||
///
|
||||
/// Each logger returned by `init` or `clone` must be deinitialized
|
||||
/// exactly once.
|
||||
pub fn deinit(logger: *Self) void
|
||||
{
|
||||
if (logger.state.mutex.lock(logger.io)) |_|
|
||||
const state = logger.state;
|
||||
|
||||
// Another owning logger remains responsible for the shared state.
|
||||
if (state.refs.fetchSub(1, .acq_rel) != 1) return;
|
||||
|
||||
if (state.mutex.lock(state.io)) |_|
|
||||
{
|
||||
logger.state.writer.interface.flush() catch |e| if (logger.on_error) |h| h(e);
|
||||
logger.state.mutex.unlock(logger.io);
|
||||
state.writer.interface.flush() catch |e| if (logger.on_error) |h| h(e);
|
||||
state.mutex.unlock(state.io);
|
||||
}
|
||||
else |e| { if (logger.on_error) |h| h(e); }
|
||||
|
||||
logger.allocator.free(logger.state.buffer);
|
||||
logger.allocator.destroy(logger.state);
|
||||
state.allocator.free(state.buffer);
|
||||
state.allocator.destroy(state);
|
||||
}
|
||||
|
||||
/// Returns another owning logger handle that shares the same buffered
|
||||
/// writer and mutex, while retaining this handle's current level and
|
||||
/// error callback by value.
|
||||
pub fn clone(logger: *const Self) Self
|
||||
{
|
||||
_ = logger.state.refs.fetchAdd(1, .monotonic);
|
||||
return logger.*;
|
||||
}
|
||||
|
||||
//
|
||||
@@ -175,8 +209,8 @@ pub const Logger = struct {
|
||||
|
||||
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.mutex.lock(logger.state.io);
|
||||
defer logger.state.mutex.unlock(logger.state.io);
|
||||
|
||||
try logger.state.writer.interface.print(
|
||||
"{s} " ++ fmt,
|
||||
@@ -201,7 +235,7 @@ pub const Logger = struct {
|
||||
{
|
||||
@branchHint(.cold);
|
||||
|
||||
logger.state.mutex.lock(logger.io) catch {};
|
||||
logger.state.mutex.lock(logger.state.io) catch {};
|
||||
|
||||
logger.state.writer.interface.print(
|
||||
"{s} " ++ fmt,
|
||||
@@ -228,8 +262,8 @@ pub const Logger = struct {
|
||||
/// Returns an error if the underlying writer fails to flush.
|
||||
pub fn flush(logger: *Self) !void
|
||||
{
|
||||
try logger.state.mutex.lock(logger.io);
|
||||
defer logger.state.mutex.unlock(logger.io);
|
||||
try logger.state.mutex.lock(logger.state.io);
|
||||
defer logger.state.mutex.unlock(logger.state.io);
|
||||
|
||||
try logger.state.writer.interface.flush();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user