Introduce logging module with a minimal, thread-safe logger implementation
This commit is contained in:
149
src/logging/log.zig
Normal file
149
src/logging/log.zig
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
/// Severity levels for log messages, ordered from least to most severe.
|
||||||
|
///
|
||||||
|
/// A `Logger`'s configured `level` acts as a filter: messages logged
|
||||||
|
/// at a lower severity than the logger's level are silently dropped.
|
||||||
|
pub const Level = enum {
|
||||||
|
debug,
|
||||||
|
info,
|
||||||
|
warn,
|
||||||
|
@"error",
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Configuration passed to `Logger.init`.
|
||||||
|
///
|
||||||
|
/// `level` sets the minimum severity that will be written; messages
|
||||||
|
/// below this level are discarded.
|
||||||
|
///
|
||||||
|
/// `buffer` is the size, in bytes, of the internal write buffer
|
||||||
|
/// allocated for `writer`. Larger buffers reduce the number of
|
||||||
|
/// underlying writes at the cost of more memory and higher latency
|
||||||
|
/// before data is flushed.
|
||||||
|
///
|
||||||
|
/// `writer` is the underlying file the logger writes to (e.g. stderr
|
||||||
|
/// or a log file). The logger takes no ownership of it beyond the
|
||||||
|
/// lifetime of the wrapping `std.Io.File.Writer`.
|
||||||
|
pub const InitOptions = struct {
|
||||||
|
level: Level = .info,
|
||||||
|
buffer: usize = 1024,
|
||||||
|
file: std.Io.File,
|
||||||
|
io: std.Io,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A minimal, thread-safe, buffered logger.
|
||||||
|
///
|
||||||
|
/// Writes are buffered and only flushed automatically on `warn`,
|
||||||
|
/// `@"error"`, or `deinit`.
|
||||||
|
/// `debug` and `info` messages may sit in the buffer until it fills,
|
||||||
|
/// a higher-severity message is logged, `flush` or `deinit` is called.
|
||||||
|
///
|
||||||
|
/// Must be initialized with `init` before use and cleaned up with
|
||||||
|
/// `deinit`. Not copyable once initialized, as `mutex` and `writer`
|
||||||
|
/// hold state tied to the original instance.
|
||||||
|
pub const Logger = struct {
|
||||||
|
level: Level = .info,
|
||||||
|
buffer: []u8 = undefined,
|
||||||
|
writer: std.Io.File.Writer = undefined,
|
||||||
|
mutex: std.Io.Mutex = undefined,
|
||||||
|
alloc: std.mem.Allocator = undefined,
|
||||||
|
io: std.Io = undefined,
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
/// Creates and returns an initialized `Logger`.
|
||||||
|
///
|
||||||
|
/// Allocates `options.buffer` bytes from `allocator` for internal
|
||||||
|
/// write buffering; ownership of this allocation belongs to the
|
||||||
|
/// returned `Logger` and is freed in `deinit`.
|
||||||
|
///
|
||||||
|
/// `allocator` is stored on the result and reused for cleanup, so
|
||||||
|
/// it must remain valid for the logger's lifetime.
|
||||||
|
///
|
||||||
|
/// Safe to move or copy the returned value freely before first
|
||||||
|
/// 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
|
||||||
|
{
|
||||||
|
const buffer = try allocator.alloc(u8, options.buffer);
|
||||||
|
|
||||||
|
return Self{
|
||||||
|
.level = options.level,
|
||||||
|
.buffer = buffer,
|
||||||
|
.writer = options.file.writer(options.io, buffer),
|
||||||
|
.mutex = .init,
|
||||||
|
.alloc = allocator,
|
||||||
|
.io = options.io,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Flushes any buffered output and releases the logger's buffer.
|
||||||
|
///
|
||||||
|
/// Safe to call once initialization via `init` has succeeded.
|
||||||
|
/// Flush errors are silently ignored, since there is no caller
|
||||||
|
/// left to meaningfully report them to at teardown time.
|
||||||
|
///
|
||||||
|
/// Must not be called more than once, as the buffer is freed
|
||||||
|
/// unconditionally.
|
||||||
|
pub fn deinit(logger: *Self) void
|
||||||
|
{
|
||||||
|
logger.mutex.lock(logger.io) catch {};
|
||||||
|
logger.writer.interface.flush() catch {};
|
||||||
|
logger.mutex.unlock(logger.io);
|
||||||
|
|
||||||
|
logger.alloc.free(logger.buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
/// Formats and writes a log message at the given `level`.
|
||||||
|
///
|
||||||
|
/// Messages below `logger.level` are dropped before anything is
|
||||||
|
/// written or locked. `warn` and `@"error"` messages are flushed
|
||||||
|
/// immediately so they reach the destination even if the process
|
||||||
|
/// crashes shortly after; `debug` and `info` messages are left
|
||||||
|
/// buffered for efficiency.
|
||||||
|
///
|
||||||
|
/// No buffer size/overflow checks as the writer automatically
|
||||||
|
/// drains on overflow.
|
||||||
|
///
|
||||||
|
/// Returns an error if formatting or writing to the underlying
|
||||||
|
/// writer fails.
|
||||||
|
fn log(logger: *Self, comptime fmt: []const u8, args: anytype, level: Level) !void
|
||||||
|
{
|
||||||
|
if (@intFromEnum(level) < @intFromEnum(logger.level)) return;
|
||||||
|
|
||||||
|
try logger.mutex.lock(logger.io);
|
||||||
|
defer logger.mutex.unlock(logger.io);
|
||||||
|
|
||||||
|
try logger.writer.interface.print(fmt, args);
|
||||||
|
|
||||||
|
if (level == .warn or level == .@"error") {
|
||||||
|
try logger.writer.interface.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
pub fn debug(logger: *Self, comptime fmt: []const u8, args: anytype) void { logger.log(fmt, args, .debug) catch {}; }
|
||||||
|
pub fn info(logger: *Self, comptime fmt: []const u8, args: anytype) void { logger.log(fmt, args, .info) catch {}; }
|
||||||
|
pub fn warn(logger: *Self, comptime fmt: []const u8, args: anytype) void { logger.log(fmt, args, .warn) catch {}; }
|
||||||
|
pub fn @"error"(logger: *Self, comptime fmt: []const u8, args: anytype) void { logger.log(fmt, args, .@"error") catch {}; }
|
||||||
|
|
||||||
|
/// Forces any buffered output to be written to the underlying
|
||||||
|
/// writer immediately.
|
||||||
|
///
|
||||||
|
/// Returns an error if the underlying writer fails to flush.
|
||||||
|
pub fn flush(logger: *Self) !void
|
||||||
|
{
|
||||||
|
try logger.mutex.lock(logger.io);
|
||||||
|
defer logger.mutex.unlock(logger.io);
|
||||||
|
|
||||||
|
try logger.writer.interface.flush();
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -6,6 +6,7 @@ pub const deps = struct {
|
|||||||
pub const config = @import("config/config.zig");
|
pub const config = @import("config/config.zig");
|
||||||
pub const memory = @import("memory/util.zig");
|
pub const memory = @import("memory/util.zig");
|
||||||
pub const signals = @import("signals/models.zig");
|
pub const signals = @import("signals/models.zig");
|
||||||
|
pub const logging = @import("logging/log.zig");
|
||||||
|
|
||||||
test {
|
test {
|
||||||
_ = config;
|
_ = config;
|
||||||
|
|||||||
Reference in New Issue
Block a user