Skip to content

Commit 1dae3fd

Browse files
committed
add friendlier output in debug
1 parent 497388f commit 1dae3fd

File tree

2 files changed

+108
-42
lines changed

2 files changed

+108
-42
lines changed

src/log.zig

Lines changed: 96 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
const std = @import("std");
2+
const builtin = @import("builtin");
23

34
const Allocator = std.mem.Allocator;
45

6+
// synchronizes writes to the output
7+
// in debug mode, also synchronizes the timestamp counter for a more human-
8+
// readable time display
59
var mutex: std.Thread.Mutex = .{};
610

711
const LogLevel : Log.Level = blk: {
812
const root = @import("root");
913
break :blk if (@hasDecl(root, "LogLevel")) root.LogLevel else .info;
1014
};
1115

12-
pub const Log = LogT(std.fs.File);
16+
pub const Log = LogT(std.fs.File, builtin.mode == .Debug);
1317

1418
// Generic so that we can test it against an ArrayList
15-
fn LogT(comptime Out: type) type {
19+
fn LogT(comptime Out: type, comptime enhanced_readability: bool) type {
1620
return struct {
1721
out: Out,
1822
inject: ?[]const u8,
@@ -25,7 +29,7 @@ fn LogT(comptime Out: type) type {
2529
debug,
2630
info,
2731
warn,
28-
err,
32+
@"error",
2933
fatal,
3034
};
3135

@@ -55,51 +59,91 @@ fn LogT(comptime Out: type) type {
5559
return @intFromEnum(level) >= @intFromEnum(LogLevel);
5660
}
5761

58-
pub fn debug(self: *Self, comptime ctx: []const u8, data: anytype) void {
59-
self.log(.debug, ctx, data);
62+
pub fn debug(self: *Self, comptime msg: []const u8, data: anytype) void {
63+
self.log(.debug, msg, data);
6064
}
6165

62-
pub fn info(self: *Self, comptime ctx: []const u8, data: anytype) void {
63-
self.log(.info, ctx, data);
66+
pub fn info(self: *Self, comptime msg: []const u8, data: anytype) void {
67+
self.log(.info, msg, data);
6468
}
6569

66-
pub fn warn(self: *Self, comptime ctx: []const u8, data: anytype) void {
67-
self.log(.warn, ctx, data);
70+
pub fn warn(self: *Self, comptime msg: []const u8, data: anytype) void {
71+
self.log(.warn, msg, data);
6872
}
6973

70-
pub fn err(self: *Self, comptime ctx: []const u8, data: anytype) void {
71-
self.log(.err, ctx, data);
74+
pub fn err(self: *Self, comptime msg: []const u8, data: anytype) void {
75+
self.log(.@"error", msg, data);
7276
}
7377

74-
pub fn fatal(self: *Self, comptime ctx: []const u8, data: anytype) void {
75-
self.log(.fatal, ctx, data);
78+
pub fn fatal(self: *Self, comptime msg: []const u8, data: anytype) void {
79+
self.log(.fatal, msg, data);
7680
}
7781

78-
fn log(self: *Self, comptime level: Level, comptime ctx: []const u8, data: anytype) void {
82+
fn log(self: *Self, comptime level: Level, comptime msg: []const u8, data: anytype) void {
7983
if (comptime enabled(level) == false) {
8084
return;
8185
}
8286
defer self.buffer.clearRetainingCapacity();
83-
self._log(level, ctx, data) catch |e| {
84-
std.debug.print("log error: {} ({s} - {s})\n", .{ e, @tagName(level), ctx });
87+
self._log(level, msg, data) catch |e| {
88+
std.debug.print("log error: {} ({s} - {s})\n", .{ e, @tagName(level), msg });
8589
};
8690
}
8791

88-
fn _log(self: *Self, comptime level: Level, comptime ctx: []const u8, data: anytype) !void {
89-
const now = getTime();
92+
fn _log(self: *Self, comptime level: Level, comptime msg: []const u8, data: anytype) !void {
9093
const allocator = self.allocator;
9194

9295
// We use *AssumeCapacity here because we expect buffer to have
93-
// a reasonable default size. We expect time + level + ctx + inject
96+
// a reasonable default size. We expect time + level + msg + inject
9497
// to fit in the initial buffer;
9598
var buffer = &self.buffer;
96-
std.debug.assert(buffer.capacity >= 1024);
9799

98-
buffer.appendSliceAssumeCapacity("_time=");
99-
try std.fmt.format(buffer.writer(allocator), "{d}", .{now});
100+
comptime {
101+
if (msg.len > 512) {
102+
@compileError("log msg cannot be greater than 512 characters: '" ++ msg ++ "'");
103+
}
104+
for (msg) |b| {
105+
switch (b) {
106+
'A'...'Z', 'a'...'z', ' ', '0'...'9', '_', '-', '.', '{', '}' => {},
107+
else => @compileError("log msg contains an invalid character '" ++ msg ++ "'"),
108+
}
109+
}
110+
}
100111

101-
const level_and_ctx = " _level=" ++ @tagName(level) ++ " _ctx=" ++ ctx;
102-
buffer.appendSliceAssumeCapacity(level_and_ctx);
112+
std.debug.assert(buffer.capacity >= 1024);
113+
114+
if (comptime enhanced_readability) {
115+
// used when developing, and we prefer readability over having
116+
// the output in logfmt
117+
switch (level) {
118+
.warn => buffer.appendSliceAssumeCapacity("\x1b[33m"),
119+
.@"error" => buffer.appendSliceAssumeCapacity("\x1b[31m"),
120+
.fatal => buffer.appendSliceAssumeCapacity("\x1b[41m"),
121+
else => {},
122+
}
123+
124+
const level_and_msg = @tagName(level) ++ "\x1b[0m | " ++ msg ++ " | ";
125+
buffer.appendSliceAssumeCapacity(level_and_msg);
126+
const since_last_log = msSinceLastLog();
127+
128+
if (since_last_log > 1000) {
129+
buffer.appendSliceAssumeCapacity("\x1b[35m");
130+
}
131+
try std.fmt.format(buffer.writer(allocator), "{d}\x1b[0m |", .{since_last_log});
132+
133+
} else {
134+
buffer.appendSliceAssumeCapacity("_time=");
135+
try std.fmt.format(buffer.writer(allocator), "{d}", .{getTime()});
136+
137+
const level_and_msg = comptime blk: {
138+
// only wrap msg in quotes if it contains a space
139+
const lm = " _level=" ++ @tagName(level) ++ " _msg=";
140+
if (std.mem.indexOfScalar(u8, msg, ' ') == null) {
141+
break :blk lm ++ msg;
142+
}
143+
break :blk lm ++ "\"" ++ msg ++ "\"";
144+
};
145+
buffer.appendSliceAssumeCapacity(level_and_msg);
146+
}
103147

104148
if (self.inject) |inject| {
105149
buffer.appendAssumeCapacity(' ');
@@ -116,6 +160,12 @@ fn LogT(comptime Out: type) type {
116160
buffer.appendAssumeCapacity('=');
117161
try writeValue(allocator, buffer, @field(data, f.name));
118162
}
163+
164+
if (comptime enhanced_readability) {
165+
// reset any color
166+
try buffer.appendSlice(allocator, "\x1b[0m");
167+
}
168+
119169
try buffer.append(allocator, '\n');
120170

121171
mutex.lock();
@@ -229,8 +279,24 @@ fn getTime() i64 {
229279
return std.time.milliTimestamp();
230280
}
231281

282+
var last_log_for_debug: i64 = 0;
283+
fn msSinceLastLog() i64 {
284+
if (comptime builtin.mode != .Debug) {
285+
@compileError("Log's enhanced_readability is not safe to use in non-Debug mode");
286+
}
287+
const now = getTime();
288+
289+
mutex.lock();
290+
defer mutex.unlock();
291+
defer last_log_for_debug = now;
292+
if (last_log_for_debug == 0) {
293+
return 0;
294+
}
295+
return now - last_log_for_debug;
296+
}
297+
232298
const testing = std.testing;
233-
const TestLogger = LogT(std.ArrayListUnmanaged(u8).Writer);
299+
const TestLogger = LogT(std.ArrayListUnmanaged(u8).Writer, false);
234300

235301
test "log: data" {
236302
var buf: std.ArrayListUnmanaged(u8) = .{};
@@ -241,15 +307,15 @@ test "log: data" {
241307

242308
{
243309
log.err("nope", .{});
244-
try testing.expectEqualStrings("_time=1739795092929 _level=err _ctx=nope\n", buf.items);
310+
try testing.expectEqualStrings("_time=1739795092929 _level=error _msg=nope\n", buf.items);
245311
}
246312

247313
{
248314
buf.clearRetainingCapacity();
249315
const string = try testing.allocator.dupe(u8, "spice_must_flow");
250316
defer testing.allocator.free(string);
251317

252-
log.warn("a_ctx", .{
318+
log.warn("a msg", .{
253319
.cint = 5,
254320
.cfloat = 3.43,
255321
.int = @as(i16, -49),
@@ -265,7 +331,7 @@ test "log: data" {
265331
});
266332

267333
try testing.expectEqualStrings(
268-
"_time=1739795092929 _level=warn _ctx=a_ctx " ++
334+
"_time=1739795092929 _level=warn _msg=\"a msg\" " ++
269335
"cint=5 cfloat=3.43 int=-49 float=0.0003232 bt=true bf=false " ++
270336
"nn=33 n=null lit=over9000! slice=spice_must_flow " ++
271337
"err=Nope level=warn\n"
@@ -280,7 +346,7 @@ test "log: string escape" {
280346
var log = try TestLogger.initTo(testing.allocator, buf.writer(testing.allocator));
281347
defer log.deinit();
282348

283-
const prefix = "_time=1739795092929 _level=err _ctx=test ";
349+
const prefix = "_time=1739795092929 _level=error _msg=test ";
284350
{
285351
log.err("test", .{.string = "hello world"});
286352
try testing.expectEqualStrings(prefix ++ "string=\"hello world\"\n", buf.items);
@@ -308,5 +374,5 @@ test "log: with inject" {
308374

309375
log.inject = "conn_id=339494";
310376
log.fatal("hit", .{.over = 9000});
311-
try testing.expectEqualStrings("_time=1739795092929 _level=fatal _ctx=hit conn_id=339494 over=9000\n", buf.items);
377+
try testing.expectEqualStrings("_time=1739795092929 _level=fatal _msg=hit conn_id=339494 over=9000\n", buf.items);
312378
}

src/main.zig

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ pub const UserContext = apiweb.UserContext;
3232
pub const IO = @import("asyncio").Wrapper(jsruntime.Loop);
3333

3434
const Log = @import("log.zig").Log;
35-
pub const LogLevel = Log.Level.debug;
35+
pub const LogLevel = Log.Level.info;
3636

3737
pub const std_options = .{
3838
// Set the log level to info
@@ -131,31 +131,31 @@ const CliMode = union(CliModeTag) {
131131
if (std.mem.eql(u8, "--port", opt)) {
132132
if (args.next()) |arg| {
133133
_server.port = std.fmt.parseInt(u16, arg, 10) catch |err| {
134-
log.err("--port {any}\n", .{err});
134+
log.err("--port {any}", .{.err = err});
135135
return printUsageExit(execname, 1);
136136
};
137137
continue;
138138
} else {
139-
log.err("--port not provided\n", .{});
139+
log.err("--port not provided", .{});
140140
return printUsageExit(execname, 1);
141141
}
142142
}
143143
if (std.mem.eql(u8, "--timeout", opt)) {
144144
if (args.next()) |arg| {
145145
_server.timeout = std.fmt.parseInt(u8, arg, 10) catch |err| {
146-
log.err("--timeout {any}\n", .{err});
146+
log.err("--timeout {any}", .{err});
147147
return printUsageExit(execname, 1);
148148
};
149149
continue;
150150
} else {
151-
log.err("--timeout not provided\n", .{});
151+
log.err("--timeout not provided", .{});
152152
return printUsageExit(execname, 1);
153153
}
154154
}
155155

156156
// unknown option
157157
if (std.mem.startsWith(u8, opt, "--")) {
158-
log.err("unknown option\n", .{});
158+
log.err("unknown option", .{});
159159
return printUsageExit(execname, 1);
160160
}
161161

@@ -164,7 +164,7 @@ const CliMode = union(CliModeTag) {
164164

165165
// allow only one url
166166
if (_fetch.url.len != 0) {
167-
log.err("more than 1 url provided\n", .{});
167+
log.err("more than 1 url provided", .{});
168168
return printUsageExit(execname, 1);
169169
}
170170

@@ -235,7 +235,7 @@ pub fn main() !void {
235235
switch (cli_mode) {
236236
.server => |opts| {
237237
const address = std.net.Address.parseIp4(opts.host, opts.port) catch |err| {
238-
log.fatal("main_opts_address", .{
238+
log.fatal("invalid address", .{
239239
.host = opts.host,
240240
.port = opts.port,
241241
.err = err,
@@ -248,13 +248,13 @@ pub fn main() !void {
248248

249249
const timeout = std.time.ns_per_s * @as(u64, opts.timeout);
250250
server.run(alloc, address, timeout, &loop) catch |err| {
251-
log.fatal("main_opts_server", .{.err = err});
251+
log.fatal("server error", .{.err = err});
252252
return err;
253253
};
254254
},
255255

256256
.fetch => |opts| {
257-
log.debug("main_fetch", .{
257+
log.debug("fetch mode", .{
258258
.url = opts.url,
259259
.dump = opts.dump,
260260
});
@@ -279,14 +279,14 @@ pub fn main() !void {
279279

280280
_ = page.navigate(opts.url, null) catch |err| switch (err) {
281281
error.UnsupportedUriScheme, error.UriMissingHost => {
282-
log.fatal("main_fetch_invalid_url", .{
282+
log.fatal("fetch has invalid url", .{
283283
.url = opts.url,
284284
.err = err,
285285
});
286286
return printUsageExit(opts.execname, 1);
287287
},
288288
else => {
289-
log.fatal("main_fetch_error", .{
289+
log.fatal("fetch error", .{
290290
.url = opts.url,
291291
.err = err,
292292
});

0 commit comments

Comments
 (0)