zig-lys/src/log/logging.zig
2024-12-07 04:29:20 +02:00

254 lines
7 KiB
Zig

const std = @import("std");
const cham = @import("chameleon");
const SmartString = @import("../util/utils.zig").SmartString;
const log = std.log;
pub const Level = log.Level;
pub const Scope = @Type(.EnumLiteral);
pub const Color = enum {
red,
green,
yellow,
blue,
magenta,
cyan,
white,
default,
};
pub const ScopeModifier = struct {
scope: SmartString,
color: ?Color = .default,
bright: bool = false,
rename: ?SmartString = null,
};
const Allocator = std.mem.Allocator;
const Globals = struct {
allocator: Allocator,
enableFileOutput: bool = false,
outputFile: ?std.fs.File = null,
additionalScopes: std.ArrayList(ScopeModifier),
fn initOrGetFile(self: *Globals) !std.fs.File {
if (self.outputFile) |file| {
return file;
} else {
return error.NoFileSet;
}
}
fn init(allocator: Allocator) !Globals {
return .{
.allocator = allocator,
.enableFileOutput = false,
.outputFile = null,
.additionalScopes = std.ArrayList(ScopeModifier).init(allocator),
};
}
fn deinit(self: *Globals) void {
if (self.outputFile) |file| {
file.close();
}
for (self.additionalScopes.items) |*modifier| {
if (modifier.*.rename) |*value| {
value.deinit();
}
modifier.*.scope.deinit();
}
self.additionalScopes.deinit();
self.* = undefined;
}
};
var core: ?Globals = null;
pub const config = struct {
pub fn enableFileOutput(value: bool) void {
if (core) |*globals| {
globals.enableFileOutput = value;
} else {
unreachable; // logging is not initialized
}
}
pub fn isFileOutput() bool {
if (core) |*globals| {
return globals.enableFileOutput;
} else {
unreachable; // logging is not initialized
}
}
pub fn setOutputFile(file: std.fs.File) !void {
if (core) |*globals| {
globals.outputFile = file;
} else {
unreachable; // logging is not initialized
}
}
pub fn getOutputFile() ?*const std.fs.File {
if (core) |*globals| {
return &globals.outputFile;
} else {
unreachable; // logging is not initialized
}
}
pub fn addScope(modifier: ScopeModifier) !void {
if (core) |*globals| {
try globals.additionalScopes.append(modifier);
} else {
unreachable; // logging is not initialized
}
}
};
pub fn init(allocator: Allocator) !void {
if (core) |_| {
return error.AlreadyInitialized;
}
core = try Globals.init(allocator);
}
pub fn deinit() void {
if (core) |*globals| {
globals.deinit();
}
}
/// If using this log function, you *must* call `init` before any logging occurs.
/// Otherwise, it will complain. A lot.
pub fn logFn(comptime level: Level, comptime scope: Scope, comptime format: []const u8, args: anytype) void {
nosuspend logFnImpl(level, scope, format, args) catch |err| {
std.debug.print("lys: error while logging: {s}\n", .{@errorName(err)});
};
}
fn get() *Globals {
if (core) |*globals| {
return globals;
}
unreachable; // logging is not initialized
}
fn logFnImpl(comptime level: Level, comptime scope: Scope, comptime format: []const u8, args: anytype) !void {
const globals = get();
var arena = std.heap.ArenaAllocator.init(globals.allocator);
var c = cham.initRuntime(.{
.allocator = arena.allocator(),
});
defer {
c.deinit();
arena.deinit();
}
const scopeText = scopeTextBlk: {
switch (scope) {
.default => break :scopeTextBlk "main",
.gpa => {
const gpa = "GPAlloc";
break :scopeTextBlk try c.redBright().fmt("{s}", .{gpa});
},
else => {
for (globals.additionalScopes.items) |modifier| {
if (std.mem.eql(u8, modifier.scope.data, @tagName(scope))) {
const text = blk: {
if (modifier.rename) |rename| {
break :blk rename.data;
} else {
break :blk @tagName(scope);
}
};
if (modifier.color) |color| {
switch (color) {
.default => break :scopeTextBlk text,
.blue => break :scopeTextBlk try c.blue().fmt("{s}", .{text}),
.green => break :scopeTextBlk try c.green().fmt("{s}", .{text}),
.red => break :scopeTextBlk try c.red().fmt("{s}", .{text}),
.white => break :scopeTextBlk try c.white().fmt("{s}", .{text}),
.yellow => break :scopeTextBlk try c.yellow().fmt("{s}", .{text}),
.magenta => break :scopeTextBlk try c.magenta().fmt("{s}", .{text}),
.cyan => break :scopeTextBlk try c.cyan().fmt("{s}", .{text}),
}
} else {
break :scopeTextBlk text;
}
}
} else {
break :scopeTextBlk @tagName(scope);
}
},
}
unreachable;
};
const levelText = switch (level) {
.debug => try c.gray().fmt("{s: >5}", .{"DEBUG"}),
.info => try c.white().fmt("{s: >5}", .{"INFO"}),
.warn => try c.yellow().fmt("{s: >5}", .{"WARN"}),
.err => try c.red().fmt("{s: >5}", .{"ERROR"}),
};
const prefix = try std.fmt.allocPrint(arena.allocator(), "[{s}] {s}:", .{
levelText,
scopeText,
});
const message = try std.fmt.allocPrint(arena.allocator(), format, args);
if (globals.enableFileOutput and globals.outputFile != null) {
var file = try globals.initOrGetFile();
var writer = file.writer().any();
nosuspend try writer.print("{s} {s}\n", .{
prefix,
message,
});
} else {
nosuspend std.debug.print("{s} {s}\n", .{
prefix,
message,
});
}
}
const t = std.testing;
test "logFn works" {
try init(t.allocator);
defer deinit();
try config.addScope(.{
.scope = SmartString.constant("someScope"),
.rename = SmartString.constant("some rename"),
.color = .green,
});
try config.addScope(.{
.scope = SmartString.constant("other"),
.rename = SmartString.constant("other rename"),
.color = .blue,
});
try logFnImpl(.err, .default, "hello world", .{});
try logFnImpl(.info, .someScope, "hello world", .{});
try logFnImpl(.warn, .other, "hello world", .{});
}