11const std = @import ("std" );
2+ const builtin = @import ("builtin" );
23
34const 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
59var mutex : std.Thread.Mutex = .{};
610
711const 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+
232298const testing = std .testing ;
233- const TestLogger = LogT (std .ArrayListUnmanaged (u8 ).Writer );
299+ const TestLogger = LogT (std .ArrayListUnmanaged (u8 ).Writer , false );
234300
235301test "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}
0 commit comments