I’m trying to create my own facade to the std.log.* functions that will treat a struct argument as a set of key-value pairs to be formatted as structured logging. In short, I want to transform the first snippet into the second snippet below:
const qux = 42;
mylog.debug("hello", .{.foo = "bar", .baz = qux});
const qux = 42;
std.log.debug("hello foo={s}, baz={d}\n", .{"bar, qux});
I thought I had this figured out, but now I’m getting an error about alignment.
/nix/store/gppcnnfah4li6smqvysxv8axyc2zbb4n-zig-0.15.2/lib/zig/std/meta.zig:942:35: error: use of undefined value here causes illegal behavior
.alignment = @alignOf(T),
^
/nix/store/gppcnnfah4li6smqvysxv8axyc2zbb4n-zig-0.15.2/lib/zig/std/meta.zig:929:29: note: called at comptime here
return CreateUniqueTuple(types.len, types[0..types.len].*);
~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/log.zig:130:26: note: called at comptime here
return std.meta.Tuple(&types);
~~~~~~~~~~~~~~^~~~~~~~
src/log.zig:211:56: note: called at comptime here
return .{ .fmt = &fmt_buf, .tuple_t = TupleType(args_type_info) };
~~~~~~~~~^~~~~~~~~~~~~~~~
src/log.zig:230:54: note: called at comptime here
comptime var fmt_info = structuredLogFormatString(format, @TypeOf(args));
Below is the bulk of the comptime code that does this transformation. It’s kind of my first time writing comptime code so feel free to point out where things could be cleaner/simpler.
Here’s the general strategy:
- Determine whether the format arguments are a struct or tuple struct
- For a struct:
- Loop over the struct fields to collect their types and the length of their portion of the transformed format strings
- Create a buffer for the format string
- Create a tuple with the correct types, initially filled with
undefined - Loop over the struct fields again to fill out the buffer of the format string now that we have one.
- For a tuple struct:
- Similar, but simpler since we don’t need to format as key-value pairs, we just want to prefix with a timestamp (which the above also does).
fn structuredLogFormatString(comptime format: []const u8, comptime args_type: type) struct { fmt: []const u8, tuple_t: type } {
const args_type_info = @typeInfo(args_type);
if (args_type_info != .@"struct") {
@compileError("expected tuple or struct argument, found " ++ @typeName(args_type));
}
const fields_info = args_type_info.@"struct".fields;
const max_format_args = @typeInfo(std.fmt.ArgSetType).int.bits;
if (fields_info.len > max_format_args) {
@compileError("32 arguments max are supported per format call");
}
const ts_fragment = "{d}ms: ";
comptime var fmt_buf_len: usize = 0;
fmt_buf_len += ts_fragment.len;
fmt_buf_len += format.len;
const struct_info = args_type_info.@"struct";
if (!struct_info.is_tuple) {
// Convert to tuple form
comptime var types: [struct_info.fields.len + 1]type = undefined;
types[0] = i64; // timestamp
inline for (1.., struct_info.fields) |i, field| {
fmt_buf_len += 1 + field.name.len;
switch (@typeInfo(field.type)) {
.int => {
fmt_buf_len += "={d}".len;
},
.pointer => |ptr_t| {
if (isString(ptr_t)) {
fmt_buf_len += "=\"{s}\"".len;
} else {
fmt_buf_len += "={any}".len;
}
},
else => {
fmt_buf_len += "={any}".len;
},
}
types[i] = field.type;
}
fmt_buf_len += 1; // newline
comptime var fmt_buf: [fmt_buf_len]u8 = undefined;
comptime var cursor: usize = 0;
@memcpy(fmt_buf[cursor..(cursor + ts_fragment.len)], ts_fragment);
cursor += ts_fragment.len;
@memcpy(fmt_buf[cursor..(cursor + format.len)], format);
inline for (1.., struct_info.fields) |i, field| {
fmt_buf[cursor] = ' ';
cursor += 1;
const name = field.name;
@memcpy(fmt_buf[cursor..(cursor + name.len)], name);
cursor += name.len;
switch (@typeInfo(field.type)) {
.int => {
@memcpy(fmt_buf[cursor..(cursor + 4)], "={d}");
cursor += 4;
},
.pointer => |ptr_t| {
if (isString(ptr_t)) {
@memcpy(fmt_buf[cursor..(cursor + 6)], "=\"{s}\"");
cursor += 6;
} else {
@memcpy(fmt_buf[cursor..(cursor + 6)], "={any}");
cursor += 6;
}
},
else => {
@memcpy(fmt_buf[cursor..(cursor + 6)], "={any}");
cursor += 6;
},
}
types[i] = field.type;
fmt_buf[fmt_buf_len - 1] = '\n';
}
return .{ .fmt = &fmt_buf, .tuple_t = TupleType(args_type_info) };
} else {
comptime var types: [struct_info.fields.len + 1]type = undefined;
types[0] = i64; // timestamp
inline for (1.., struct_info.fields) |i, field| {
types[i] = field.type;
}
fmt_buf_len += 1; // newline
var fmt_buf: [fmt_buf_len]u8 = undefined;
var cursor: usize = 0;
@memcpy(fmt_buf[cursor..(cursor + ts_fragment.len)], ts_fragment);
cursor += ts_fragment.len;
@memcpy(fmt_buf[cursor..(cursor + format.len)], format);
fmt_buf[fmt_buf_len - 1] = '\n';
return .{ .fmt = &fmt_buf, .tuple_t = TupleType(args_type_info) };
}
}
fn TupleType(comptime arg_type: std.builtin.Type) type {
std.debug.assert(arg_type == .@"struct");
const s = arg_type.@"struct";
const len = s.fields.len + 1; // +1 for timestamp type
var types: [len]type = undefined;
inline for (1.., s.fields) |i, field| {
types[i] = field.type;
}
return std.meta.Tuple(&types);
}
fn structured(comptime level: std.log.Level, comptime format: []const u8, args: anytype) void {
comptime var fmt_info = structuredLogFormatString(format, @TypeOf(args));
comptime var fmt_str = fmt_info.fmt;
comptime var tuple_t = fmt_info.tuple_t;
const ts = std.time.milliTimestamp();
var values: fmt_info.tuple_t = undefined;
values[0] = ts;
inline for (1.., @typeInfo(@TypeOf(args)).@"struct".fields) |i, field| {
values[i] = @field(args, field.name);
}
switch (level) {
.debug => std.log.debug(fmt_str, args),
.info => std.log.info(fmt_str, args),
.warn => std.log.warn(fmt_str, args),
.err => std.log.err(fmt_str, args),
}
}
pub fn debug(comptime format: []const u8, args: anytype) void {
structured(.debug, format, args);
}