const std = @import("std"); const assert = std.debug.assert; const Level = std.log.Level; const unscoped = scoped(""); pub const fatal = unscoped.fatal; pub const err = unscoped.err; pub const warn = unscoped.warn; pub const info = unscoped.info; pub const debug = unscoped.debug; pub fn scoped(comptime name: []const u8) type { const prefix = if (name.len == 0) "" else name ++ ": "; return struct { pub fn fatal(comptime msg: []const u8, details: anytype) noreturn { @branchHint(.cold); log_structured("FAT", msg, details); std.process.exit(1); } pub fn err(comptime msg: []const u8, details: anytype) void { @branchHint(.cold); log(.err, prefix ++ msg, details); } pub inline fn warn(comptime msg: []const u8, details: anytype) void { log(.warn, prefix ++ msg, details); } pub inline fn info(comptime msg: []const u8, details: anytype) void { log(.info, prefix ++ msg, details); } pub inline fn debug(comptime msg: []const u8, details: anytype) void { log(.debug, prefix ++ msg, details); } }; } inline fn log( comptime level: Level, comptime msg: []const u8, details: anytype, ) void { if (comptime std.log.defaultLogEnabled(level)) { const level_text = switch (level) { .err => "ERR", .warn => "WRN", .info => "INF", .debug => "DBG", }; log_structured(level_text, msg, details); } } fn log_structured( comptime level: []const u8, comptime msg: []const u8, details: anytype, ) void { comptime { if (std.mem.indexOfScalar(u8, msg, '"') != null) { @compileError("double quotation marks not allowed in log message"); } } // "a formatted message" {key=val, ...} const stderr = std.io.getStdErr().writer(); nosuspend { const message = level ++ " \"" ++ msg ++ "\""; if (@typeInfo(@TypeOf(details)).@"struct".fields.len > 0) { stderr.print(message ++ " {}\n", .{structured(details)}) catch return; } else { stderr.print(message ++ "\n", .{}) catch return; } } } fn Structured(comptime T: type) type { comptime { assert(@typeInfo(T) == .@"struct"); assert(@typeInfo(T).@"struct".fields.len > 0); for (@typeInfo(T).@"struct".fields) |field| { for (field.name) |ch| { switch (ch) { 'a'...'z', 'A'...'Z', '0'...'9', '_' => {}, else => @compileError("invalid key '" ++ field.name ++ "' in log details"), } } } } return struct { val: T, inline fn init(val: T) @This() { return .{ .val = val }; } pub inline fn format(self: @This(), comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void { _ = fmt; _ = options; try format_structured_struct(self.val, writer); } }; } inline fn structured(value: anytype) Structured(@TypeOf(value)) { return Structured(@TypeOf(value)).init(value); } inline fn format_structured_any(value: anytype, writer: anytype) !void { const T = @TypeOf(value); switch (@typeInfo(T)) { .void => { try writer.writeAll("void"); }, .bool => { try writer.writeAll(if (value) "true" else "false"); }, .int => { try std.fmt.formatIntValue(value, "d", .{}, writer); }, .float => { try std.fmt.formatFloatValue(value, "e", .{}, writer); }, .pointer => |pointer| { if (pointer.size != .slice) @compileError("cannot log pointers (only slices allowed)"); if (pointer.child == u8) { try format_structured_string(value, writer); } else { try format_structured_sequence(value, writer); } }, .array => |array| { if (array.child == u8) { try format_structured_string(value, writer); } else { try format_structured_sequence(value, writer); } }, .@"struct" => { try format_structured_struct(value, writer); }, .null => { try writer.writeAll("null"); }, .optional => { if (value) |v| { try format_structured_any(v, writer); } else { try writer.writeAll("null"); } }, .error_set => { try writer.writeAll(@errorName(value)); }, .@"enum" => { try format_structured_enum(value, writer); }, .@"union" => { try format_structured_union(value, writer); }, else => @compileError("cannot log type " ++ @typeName(T)), } } fn format_structured_string(value: []const u8, writer: anytype) !void { try writer.writeAll("\""); try writer.writeAll(value); try writer.writeAll("\""); } fn format_structured_sequence(value: anytype, writer: anytype) !void { if (value.len == 0) { try writer.writeAll("[]"); } else { try writer.writeAll("["); try format_structured_any(value[0], writer); for (value[1..]) |v| { try writer.writeAll(", "); try format_structured_any(v, writer); } try writer.writeAll("]"); } } fn format_structured_struct(value: anytype, writer: anytype) !void { const struct_info = @typeInfo(@TypeOf(value)).@"struct"; if (struct_info.fields.len == 0) { try writer.writeAll("{}"); } else { try writer.writeAll("{ " ++ struct_info.fields[0].name ++ "="); try format_structured_any(@field(value, struct_info.fields[0].name), writer); inline for (struct_info.fields[1..]) |field| { try writer.writeAll(", " ++ field.name ++ "="); try format_structured_any(@field(value, field.name), writer); } try writer.writeAll(" }"); } } inline fn format_structured_enum(value: anytype, writer: anytype) !void { const E = @TypeOf(value); if (std.meta.hasMethod(E, "format")) { try value.format("", .{}, writer); } else if (@typeInfo(E).Enum.is_exhaustive) { try writer.writeAll(@tagName(value)); } else { try std.fmt.formatIntValue(@intFromEnum(value), "d", .{}, writer); } } inline fn format_structured_union(value: anytype, writer: anytype) !void { const U = @TypeOf(value); if (std.meta.hasMethod(U, "format")) { try value.format("", .{}, writer); } else { @compileError("cannot log union " ++ @typeName(U)); } }