mirror of
				https://github.com/lightpanda-io/browser.git
				synced 2025-10-29 15:13:28 +00:00 
			
		
		
		
	Try stateless logger (to save memory)
This commit is contained in:
		| @@ -22,7 +22,8 @@ const builtin = @import("builtin"); | ||||
| const JsObject = @import("../env.zig").Env.JsObject; | ||||
| const SessionState = @import("../env.zig").SessionState; | ||||
|  | ||||
| const log = if (builtin.is_test) &test_capture else @import("../../log.zig"); | ||||
| // const log = if (builtin.is_test) &test_capture else @import("../../log.zig"); | ||||
| const log = @import("../../log.zig"); | ||||
|  | ||||
| pub const Console = struct { | ||||
|     // TODO: configurable writer | ||||
| @@ -150,137 +151,137 @@ fn timestamp() u32 { | ||||
|     return @intCast(ts.sec); | ||||
| } | ||||
|  | ||||
| var test_capture = TestCapture{}; | ||||
| const testing = @import("../../testing.zig"); | ||||
| test "Browser.Console" { | ||||
|     defer testing.reset(); | ||||
| // var test_capture = TestCapture{}; | ||||
| // const testing = @import("../../testing.zig"); | ||||
| // test "Browser.Console" { | ||||
| //     defer testing.reset(); | ||||
|  | ||||
|     var runner = try testing.jsRunner(testing.tracking_allocator, .{}); | ||||
|     defer runner.deinit(); | ||||
| //     var runner = try testing.jsRunner(testing.tracking_allocator, .{}); | ||||
| //     defer runner.deinit(); | ||||
|  | ||||
|     { | ||||
|         try runner.testCases(&.{ | ||||
|             .{ "console.log('a')", "undefined" }, | ||||
|             .{ "console.warn('hello world', 23, true, new Object())", "undefined" }, | ||||
|         }, .{}); | ||||
| //     { | ||||
| //         try runner.testCases(&.{ | ||||
| //             .{ "console.log('a')", "undefined" }, | ||||
| //             .{ "console.warn('hello world', 23, true, new Object())", "undefined" }, | ||||
| //         }, .{}); | ||||
|  | ||||
|         const captured = test_capture.captured.items; | ||||
|         try testing.expectEqual("[info] args=a", captured[0]); | ||||
|         try testing.expectEqual("[warn] args=hello world 23 true [object Object]", captured[1]); | ||||
|     } | ||||
| //         const captured = test_capture.captured.items; | ||||
| //         try testing.expectEqual("[info] args=a", captured[0]); | ||||
| //         try testing.expectEqual("[warn] args=hello world 23 true [object Object]", captured[1]); | ||||
| //     } | ||||
|  | ||||
|     { | ||||
|         test_capture.reset(); | ||||
|         try runner.testCases(&.{ | ||||
|             .{ "console.countReset()", "undefined" }, | ||||
|             .{ "console.count()", "undefined" }, | ||||
|             .{ "console.count('teg')", "undefined" }, | ||||
|             .{ "console.count('teg')", "undefined" }, | ||||
|             .{ "console.count('teg')", "undefined" }, | ||||
|             .{ "console.count()", "undefined" }, | ||||
|             .{ "console.countReset('teg')", "undefined" }, | ||||
|             .{ "console.countReset()", "undefined" }, | ||||
|             .{ "console.count()", "undefined" }, | ||||
|         }, .{}); | ||||
| //     { | ||||
| //         test_capture.reset(); | ||||
| //         try runner.testCases(&.{ | ||||
| //             .{ "console.countReset()", "undefined" }, | ||||
| //             .{ "console.count()", "undefined" }, | ||||
| //             .{ "console.count('teg')", "undefined" }, | ||||
| //             .{ "console.count('teg')", "undefined" }, | ||||
| //             .{ "console.count('teg')", "undefined" }, | ||||
| //             .{ "console.count()", "undefined" }, | ||||
| //             .{ "console.countReset('teg')", "undefined" }, | ||||
| //             .{ "console.countReset()", "undefined" }, | ||||
| //             .{ "console.count()", "undefined" }, | ||||
| //         }, .{}); | ||||
|  | ||||
|         const captured = test_capture.captured.items; | ||||
|         try testing.expectEqual("[invalid counter] label=default", captured[0]); | ||||
|         try testing.expectEqual("[count] label=default count=1", captured[1]); | ||||
|         try testing.expectEqual("[count] label=teg count=1", captured[2]); | ||||
|         try testing.expectEqual("[count] label=teg count=2", captured[3]); | ||||
|         try testing.expectEqual("[count] label=teg count=3", captured[4]); | ||||
|         try testing.expectEqual("[count] label=default count=2", captured[5]); | ||||
|         try testing.expectEqual("[count reset] label=teg count=3", captured[6]); | ||||
|         try testing.expectEqual("[count reset] label=default count=2", captured[7]); | ||||
|         try testing.expectEqual("[count] label=default count=1", captured[8]); | ||||
|     } | ||||
| //         const captured = test_capture.captured.items; | ||||
| //         try testing.expectEqual("[invalid counter] label=default", captured[0]); | ||||
| //         try testing.expectEqual("[count] label=default count=1", captured[1]); | ||||
| //         try testing.expectEqual("[count] label=teg count=1", captured[2]); | ||||
| //         try testing.expectEqual("[count] label=teg count=2", captured[3]); | ||||
| //         try testing.expectEqual("[count] label=teg count=3", captured[4]); | ||||
| //         try testing.expectEqual("[count] label=default count=2", captured[5]); | ||||
| //         try testing.expectEqual("[count reset] label=teg count=3", captured[6]); | ||||
| //         try testing.expectEqual("[count reset] label=default count=2", captured[7]); | ||||
| //         try testing.expectEqual("[count] label=default count=1", captured[8]); | ||||
| //     } | ||||
|  | ||||
|     { | ||||
|         test_capture.reset(); | ||||
|         try runner.testCases(&.{ | ||||
|             .{ "console.assert(true)", "undefined" }, | ||||
|             .{ "console.assert('a', 2, 3, 4)", "undefined" }, | ||||
|             .{ "console.assert('')", "undefined" }, | ||||
|             .{ "console.assert('', 'x', true)", "undefined" }, | ||||
|             .{ "console.assert(false, 'x')", "undefined" }, | ||||
|         }, .{}); | ||||
| //     { | ||||
| //         test_capture.reset(); | ||||
| //         try runner.testCases(&.{ | ||||
| //             .{ "console.assert(true)", "undefined" }, | ||||
| //             .{ "console.assert('a', 2, 3, 4)", "undefined" }, | ||||
| //             .{ "console.assert('')", "undefined" }, | ||||
| //             .{ "console.assert('', 'x', true)", "undefined" }, | ||||
| //             .{ "console.assert(false, 'x')", "undefined" }, | ||||
| //         }, .{}); | ||||
|  | ||||
|         const captured = test_capture.captured.items; | ||||
|         try testing.expectEqual("[assertion failed] values=", captured[0]); | ||||
|         try testing.expectEqual("[assertion failed] values=x true", captured[1]); | ||||
|         try testing.expectEqual("[assertion failed] values=x", captured[2]); | ||||
|     } | ||||
| } | ||||
| const TestCapture = struct { | ||||
|     captured: std.ArrayListUnmanaged([]const u8) = .{}, | ||||
| //         const captured = test_capture.captured.items; | ||||
| //         try testing.expectEqual("[assertion failed] values=", captured[0]); | ||||
| //         try testing.expectEqual("[assertion failed] values=x true", captured[1]); | ||||
| //         try testing.expectEqual("[assertion failed] values=x", captured[2]); | ||||
| //     } | ||||
| // } | ||||
| // const TestCapture = struct { | ||||
| //     captured: std.ArrayListUnmanaged([]const u8) = .{}, | ||||
|  | ||||
|     fn reset(self: *TestCapture) void { | ||||
|         self.captured = .{}; | ||||
|     } | ||||
| //     fn reset(self: *TestCapture) void { | ||||
| //         self.captured = .{}; | ||||
| //     } | ||||
|  | ||||
|     fn debug( | ||||
|         self: *TestCapture, | ||||
|         comptime scope: @Type(.enum_literal), | ||||
|         comptime msg: []const u8, | ||||
|         args: anytype, | ||||
|     ) void { | ||||
|         self.capture(scope, msg, args); | ||||
|     } | ||||
| //     fn debug( | ||||
| //         self: *TestCapture, | ||||
| //         comptime scope: @Type(.enum_literal), | ||||
| //         comptime msg: []const u8, | ||||
| //         args: anytype, | ||||
| //     ) void { | ||||
| //         self.capture(scope, msg, args); | ||||
| //     } | ||||
|  | ||||
|     fn info( | ||||
|         self: *TestCapture, | ||||
|         comptime scope: @Type(.enum_literal), | ||||
|         comptime msg: []const u8, | ||||
|         args: anytype, | ||||
|     ) void { | ||||
|         self.capture(scope, msg, args); | ||||
|     } | ||||
| //     fn info( | ||||
| //         self: *TestCapture, | ||||
| //         comptime scope: @Type(.enum_literal), | ||||
| //         comptime msg: []const u8, | ||||
| //         args: anytype, | ||||
| //     ) void { | ||||
| //         self.capture(scope, msg, args); | ||||
| //     } | ||||
|  | ||||
|     fn warn( | ||||
|         self: *TestCapture, | ||||
|         comptime scope: @Type(.enum_literal), | ||||
|         comptime msg: []const u8, | ||||
|         args: anytype, | ||||
|     ) void { | ||||
|         self.capture(scope, msg, args); | ||||
|     } | ||||
| //     fn warn( | ||||
| //         self: *TestCapture, | ||||
| //         comptime scope: @Type(.enum_literal), | ||||
| //         comptime msg: []const u8, | ||||
| //         args: anytype, | ||||
| //     ) void { | ||||
| //         self.capture(scope, msg, args); | ||||
| //     } | ||||
|  | ||||
|     fn err( | ||||
|         self: *TestCapture, | ||||
|         comptime scope: @Type(.enum_literal), | ||||
|         comptime msg: []const u8, | ||||
|         args: anytype, | ||||
|     ) void { | ||||
|         self.capture(scope, msg, args); | ||||
|     } | ||||
| //     fn err( | ||||
| //         self: *TestCapture, | ||||
| //         comptime scope: @Type(.enum_literal), | ||||
| //         comptime msg: []const u8, | ||||
| //         args: anytype, | ||||
| //     ) void { | ||||
| //         self.capture(scope, msg, args); | ||||
| //     } | ||||
|  | ||||
|     fn capture( | ||||
|         self: *TestCapture, | ||||
|         comptime scope: @Type(.enum_literal), | ||||
|         comptime msg: []const u8, | ||||
|         args: anytype, | ||||
|     ) void { | ||||
|         self._capture(scope, msg, args) catch unreachable; | ||||
|     } | ||||
| //     fn capture( | ||||
| //         self: *TestCapture, | ||||
| //         comptime scope: @Type(.enum_literal), | ||||
| //         comptime msg: []const u8, | ||||
| //         args: anytype, | ||||
| //     ) void { | ||||
| //         self._capture(scope, msg, args) catch unreachable; | ||||
| //     } | ||||
|  | ||||
|     fn _capture( | ||||
|         self: *TestCapture, | ||||
|         comptime scope: @Type(.enum_literal), | ||||
|         comptime msg: []const u8, | ||||
|         args: anytype, | ||||
|     ) !void { | ||||
|         std.debug.assert(scope == .console); | ||||
| //     fn _capture( | ||||
| //         self: *TestCapture, | ||||
| //         comptime scope: @Type(.enum_literal), | ||||
| //         comptime msg: []const u8, | ||||
| //         args: anytype, | ||||
| //     ) !void { | ||||
| //         std.debug.assert(scope == .console); | ||||
|  | ||||
|         const allocator = testing.arena_allocator; | ||||
|         var buf: std.ArrayListUnmanaged(u8) = .empty; | ||||
|         try buf.appendSlice(allocator, "[" ++ msg ++ "] "); | ||||
| //         const allocator = testing.arena_allocator; | ||||
| //         var buf: std.ArrayListUnmanaged(u8) = .empty; | ||||
| //         try buf.appendSlice(allocator, "[" ++ msg ++ "] "); | ||||
|  | ||||
|         inline for (@typeInfo(@TypeOf(args)).@"struct".fields) |f| { | ||||
|             try buf.appendSlice(allocator, f.name); | ||||
|             try buf.append(allocator, '='); | ||||
|             try @import("../../log.zig").writeValue(allocator, &buf, false, @field(args, f.name)); | ||||
|             try buf.append(allocator, ' '); | ||||
|         } | ||||
|         self.captured.append(testing.arena_allocator, std.mem.trimRight(u8, buf.items, " ")) catch unreachable; | ||||
|     } | ||||
| }; | ||||
| //         inline for (@typeInfo(@TypeOf(args)).@"struct".fields) |f| { | ||||
| //             try buf.appendSlice(allocator, f.name); | ||||
| //             try buf.append(allocator, '='); | ||||
| //             try @import("../../log.zig").writeValue(allocator, &buf, false, @field(args, f.name)); | ||||
| //             try buf.append(allocator, ' '); | ||||
| //         } | ||||
| //         self.captured.append(testing.arena_allocator, std.mem.trimRight(u8, buf.items, " ")) catch unreachable; | ||||
| //     } | ||||
| // }; | ||||
|   | ||||
							
								
								
									
										462
									
								
								src/log.zig
									
									
									
									
									
								
							
							
						
						
									
										462
									
								
								src/log.zig
									
									
									
									
									
								
							| @@ -1,3 +1,21 @@ | ||||
| // Copyright (C) 2023-2024  Lightpanda (Selecy SAS) | ||||
| // | ||||
| // Francis Bouvier <francis@lightpanda.io> | ||||
| // Pierre Tachoire <pierre@lightpanda.io> | ||||
| // | ||||
| // This program is free software: you can redistribute it and/or modify | ||||
| // it under the terms of the GNU Affero General Public License as | ||||
| // published by the Free Software Foundation, either version 3 of the | ||||
| // License, or (at your option) any later version. | ||||
| // | ||||
| // This program is distributed in the hope that it will be useful, | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| // GNU Affero General Public License for more details. | ||||
| // | ||||
| // You should have received a copy of the GNU Affero General Public License | ||||
| // along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||
|  | ||||
| const std = @import("std"); | ||||
| const builtin = @import("builtin"); | ||||
| const build_config = @import("build_config"); | ||||
| @@ -5,19 +23,14 @@ const build_config = @import("build_config"); | ||||
| const Thread = std.Thread; | ||||
| const Allocator = std.mem.Allocator; | ||||
|  | ||||
| const LogLevel: Level = blk: { | ||||
| const log_evel: Level = blk: { | ||||
|     if (builtin.is_test) break :blk .err; | ||||
|     break :blk @enumFromInt(@intFromEnum(build_config.log_level)); | ||||
| }; | ||||
|  | ||||
| var pool: Pool = undefined; | ||||
| pub fn init(allocator: Allocator, opts: Opts) !void { | ||||
|     pool = try Pool.init(allocator, 3, opts); | ||||
| } | ||||
|  | ||||
| pub fn deinit(allocator: Allocator) void { | ||||
|     pool.deinit(allocator); | ||||
| } | ||||
| const format: Format = blk: { | ||||
|     if (builtin.is_test or builtin.mode != .Debug) break :blk .logfmt; | ||||
|     break :blk .pretty; | ||||
| }; | ||||
|  | ||||
| // synchronizes writes to the output | ||||
| var out_lock: Thread.Mutex = .{}; | ||||
| @@ -28,7 +41,7 @@ var last_log_lock: Thread.Mutex = .{}; | ||||
| pub fn enabled(comptime scope: @Type(.enum_literal), comptime level: Level) bool { | ||||
|     // TODO scope disabling | ||||
|     _ = scope; | ||||
|     return @intFromEnum(level) >= @intFromEnum(LogLevel); | ||||
|     return @intFromEnum(level) >= @intFromEnum(log_evel); | ||||
| } | ||||
|  | ||||
| pub const Level = enum { | ||||
| @@ -39,10 +52,6 @@ pub const Level = enum { | ||||
|     fatal, | ||||
| }; | ||||
|  | ||||
| const Opts = struct { | ||||
|     format: Format = if (!builtin.is_test and builtin.mode == .Debug) .pretty else .logfmt, | ||||
| }; | ||||
|  | ||||
| pub const Format = enum { | ||||
|     logfmt, | ||||
|     pretty, | ||||
| @@ -72,260 +81,141 @@ pub fn log(comptime scope: @Type(.enum_literal), comptime level: Level, comptime | ||||
|     if (comptime enabled(scope, level) == false) { | ||||
|         return; | ||||
|     } | ||||
|     const logger = pool.acquire(); | ||||
|     defer pool.release(logger); | ||||
|     logger.log(scope, level, msg, data) catch |log_err| { | ||||
|     tryLog(scope, level, msg, data) catch |log_err| { | ||||
|         std.debug.print("$time={d} $level=fatal $scope={s} $msg=\"log err\" err={s} log_msg=\"{s}\"", .{ timestamp(), @errorName(log_err), @tagName(scope), msg }); | ||||
|     }; | ||||
| } | ||||
|  | ||||
| // Generic so that we can test it against an ArrayList | ||||
| fn LogT(comptime Out: type) type { | ||||
|     return struct { | ||||
|         out: Out, | ||||
|         format: Format, | ||||
|         allocator: Allocator, | ||||
|         buffer: std.ArrayListUnmanaged(u8), | ||||
|  | ||||
|         const Self = @This(); | ||||
|  | ||||
|         fn init(allocator: Allocator, opts: Opts, out: Out) !Self { | ||||
|             var buffer: std.ArrayListUnmanaged(u8) = .{}; | ||||
|             try buffer.ensureTotalCapacity(allocator, 2048); | ||||
|  | ||||
|             return .{ | ||||
|                 .out = out, | ||||
|                 .buffer = buffer, | ||||
|                 .format = opts.format, | ||||
|                 .allocator = allocator, | ||||
|             }; | ||||
| fn tryLog(comptime scope: @Type(.enum_literal), comptime level: Level, comptime msg: []const u8, data: anytype) !void { | ||||
|     comptime { | ||||
|         if (msg.len > 30) { | ||||
|             @compileError("log msg cannot be more than 30 characters: '" ++ msg ++ "'"); | ||||
|         } | ||||
|  | ||||
|         fn deinit(self: *Self) void { | ||||
|             self.buffer.deinit(self.allocator); | ||||
|         if (@tagName(scope).len > 15) { | ||||
|             @compileError("log scope cannot be more than 15 characters: '" ++ @tagName(scope) ++ "'"); | ||||
|         } | ||||
|  | ||||
|         fn log(self: *Self, comptime scope: @Type(.enum_literal), comptime level: Level, comptime msg: []const u8, data: anytype) !void { | ||||
|             comptime { | ||||
|                 if (msg.len > 30) { | ||||
|                     @compileError("log msg cannot be more than 30 characters: '" ++ msg ++ "'"); | ||||
|                 } | ||||
|                 if (@tagName(scope).len > 15) { | ||||
|                     @compileError("log scope cannot be more than 15 characters: '" ++ @tagName(scope) ++ "'"); | ||||
|                 } | ||||
|                 for (msg) |b| { | ||||
|                     switch (b) { | ||||
|                         'A'...'Z', 'a'...'z', ' ', '0'...'9', '_', '-', '.', '{', '}' => {}, | ||||
|                         else => @compileError("log msg contains an invalid character '" ++ msg ++ "'"), | ||||
|                     } | ||||
|                 } | ||||
|         for (msg) |b| { | ||||
|             switch (b) { | ||||
|                 'A'...'Z', 'a'...'z', ' ', '0'...'9', '_', '-', '.', '{', '}' => {}, | ||||
|                 else => @compileError("log msg contains an invalid character '" ++ msg ++ "'"), | ||||
|             } | ||||
|  | ||||
|             defer self.buffer.clearRetainingCapacity(); | ||||
|             switch (self.format) { | ||||
|                 .logfmt => try self.logfmt(scope, level, msg, data), | ||||
|                 .pretty => try self.pretty(scope, level, msg, data), | ||||
|             } | ||||
|  | ||||
|             out_lock.lock(); | ||||
|             defer out_lock.unlock(); | ||||
|             try self.out.writeAll(self.buffer.items); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|         fn logfmt(self: *Self, comptime scope: @Type(.enum_literal), comptime level: Level, comptime msg: []const u8, data: anytype) !void { | ||||
|             const buffer = &self.buffer; | ||||
|             const allocator = self.allocator; | ||||
|     out_lock.lock(); | ||||
|     defer out_lock.unlock(); | ||||
|  | ||||
|             buffer.appendSliceAssumeCapacity("$time="); | ||||
|             try std.fmt.format(buffer.writer(allocator), "{d}", .{timestamp()}); | ||||
|     const stderr = std.io.getStdErr().writer(); | ||||
|     var bw = std.io.bufferedWriter(stderr); | ||||
|  | ||||
|             buffer.appendSliceAssumeCapacity(" $scope="); | ||||
|             buffer.appendSliceAssumeCapacity(@tagName(scope)); | ||||
|  | ||||
|             const level_and_msg = comptime blk: { | ||||
|                 const l = if (level == .err) "error" else @tagName(level); | ||||
|                 // only wrap msg in quotes if it contains a space | ||||
|                 const lm = " $level=" ++ l ++ " $msg="; | ||||
|                 if (std.mem.indexOfScalar(u8, msg, ' ') == null) { | ||||
|                     break :blk lm ++ msg; | ||||
|                 } | ||||
|                 break :blk lm ++ "\"" ++ msg ++ "\""; | ||||
|             }; | ||||
|             buffer.appendSliceAssumeCapacity(level_and_msg); | ||||
|             inline for (@typeInfo(@TypeOf(data)).@"struct".fields) |f| { | ||||
|                 const key = " " ++ f.name ++ "="; | ||||
|                 try buffer.ensureUnusedCapacity(allocator, key.len); | ||||
|                 buffer.appendSliceAssumeCapacity(key); | ||||
|                 try writeValue(allocator, buffer, true, @field(data, f.name)); | ||||
|             } | ||||
|             try buffer.append(allocator, '\n'); | ||||
|         } | ||||
|  | ||||
|         fn pretty(self: *Self, comptime scope: @Type(.enum_literal), comptime level: Level, comptime msg: []const u8, data: anytype) !void { | ||||
|             const buffer = &self.buffer; | ||||
|             const allocator = self.allocator; | ||||
|  | ||||
|             buffer.appendSliceAssumeCapacity(switch (level) { | ||||
|                 .debug => "\x1b[0;36mDEBUG\x1b[0m ", | ||||
|                 .info => "\x1b[0;32mINFO\x1b[0m  ", | ||||
|                 .warn => "\x1b[0;33mWARN\x1b[0m  ", | ||||
|                 .err => "\x1b[0;31mERROR\x1b[0m ", | ||||
|                 .fatal => "\x1b[0;35mFATAL\x1b[0m ", | ||||
|             }); | ||||
|  | ||||
|             const prefix = @tagName(scope) ++ " : " ++ msg; | ||||
|             buffer.appendSliceAssumeCapacity(prefix); | ||||
|  | ||||
|             { | ||||
|                 // msg.len cannot be > 30, and @tagName(scope).len cannot be > 15 | ||||
|                 // so this is safe | ||||
|                 const padding = 55 - prefix.len; | ||||
|                 for (0..padding / 2) |_| { | ||||
|                     buffer.appendSliceAssumeCapacity(" ."); | ||||
|                 } | ||||
|                 if (@mod(padding, 2) == 1) { | ||||
|                     buffer.appendAssumeCapacity(' '); | ||||
|                 } | ||||
|                 try buffer.writer(allocator).print(" [+{d}ms]", .{elapsed()}); | ||||
|                 buffer.appendAssumeCapacity('\n'); | ||||
|             } | ||||
|  | ||||
|             inline for (@typeInfo(@TypeOf(data)).@"struct".fields) |f| { | ||||
|                 const key = "      " ++ f.name ++ " = "; | ||||
|  | ||||
|                 // + 5 covers null/true/false | ||||
|                 try buffer.ensureUnusedCapacity(allocator, key.len + 5); | ||||
|                 buffer.appendSliceAssumeCapacity(key); | ||||
|                 try writeValue(allocator, buffer, false, @field(data, f.name)); | ||||
|                 try buffer.append(allocator, '\n'); | ||||
|             } | ||||
|             try buffer.append(allocator, '\n'); | ||||
|         } | ||||
|     }; | ||||
|     switch (format) { | ||||
|         .logfmt => try logLogfmt(scope, level, msg, data, bw.writer()), | ||||
|         .pretty => try logPretty(scope, level, msg, data, bw.writer()), | ||||
|     } | ||||
|     bw.flush() catch return; | ||||
| } | ||||
|  | ||||
| const Pool = struct { | ||||
|     loggers: []*Log, | ||||
|     available: usize, | ||||
|     mutex: Thread.Mutex, | ||||
|     cond: Thread.Condition, | ||||
| fn logLogfmt(comptime scope: @Type(.enum_literal), comptime level: Level, comptime msg: []const u8, data: anytype, writer: anytype) !void { | ||||
|     try writer.writeAll("$time="); | ||||
|     try writer.print("{d}", .{timestamp()}); | ||||
|  | ||||
|     const Self = @This(); | ||||
|     const Log = LogT(std.fs.File); | ||||
|     try writer.writeAll(" $scope="); | ||||
|     try writer.writeAll(@tagName(scope)); | ||||
|  | ||||
|     pub fn init(allocator: Allocator, count: usize, opts: Opts) !Self { | ||||
|         const loggers = try allocator.alloc(*Log, count); | ||||
|         errdefer allocator.free(loggers); | ||||
|  | ||||
|         var started: usize = 0; | ||||
|         errdefer for (0..started) |i| { | ||||
|             loggers[i].deinit(); | ||||
|             allocator.destroy(loggers[i]); | ||||
|         }; | ||||
|  | ||||
|         const out = std.io.getStdOut(); | ||||
|         for (0..count) |i| { | ||||
|             const logger = try allocator.create(Log); | ||||
|             errdefer allocator.destroy(logger); | ||||
|             logger.* = try Log.init(allocator, opts, out); | ||||
|             loggers[i] = logger; | ||||
|             started += 1; | ||||
|     const level_and_msg = comptime blk: { | ||||
|         const l = if (level == .err) "error" else @tagName(level); | ||||
|         // only wrap msg in quotes if it contains a space | ||||
|         const lm = " $level=" ++ l ++ " $msg="; | ||||
|         if (std.mem.indexOfScalar(u8, msg, ' ') == null) { | ||||
|             break :blk lm ++ msg; | ||||
|         } | ||||
|  | ||||
|         return .{ | ||||
|             .cond = .{}, | ||||
|             .mutex = .{}, | ||||
|             .loggers = loggers, | ||||
|             .available = count, | ||||
|         }; | ||||
|         break :blk lm ++ "\"" ++ msg ++ "\""; | ||||
|     }; | ||||
|     try writer.writeAll(level_and_msg); | ||||
|     inline for (@typeInfo(@TypeOf(data)).@"struct".fields) |f| { | ||||
|         const key = " " ++ f.name ++ "="; | ||||
|         try writer.writeAll(key); | ||||
|         try writeValue(true, @field(data, f.name), writer); | ||||
|     } | ||||
|     try writer.writeByte('\n'); | ||||
| } | ||||
|  | ||||
|     pub fn deinit(self: *Self, allocator: Allocator) void { | ||||
|         for (self.loggers) |logger| { | ||||
|             logger.deinit(); | ||||
|             allocator.destroy(logger); | ||||
| fn logPretty(comptime scope: @Type(.enum_literal), comptime level: Level, comptime msg: []const u8, data: anytype, writer: anytype) !void { | ||||
|     try writer.writeAll(switch (level) { | ||||
|         .debug => "\x1b[0;36mDEBUG\x1b[0m ", | ||||
|         .info => "\x1b[0;32mINFO\x1b[0m  ", | ||||
|         .warn => "\x1b[0;33mWARN\x1b[0m  ", | ||||
|         .err => "\x1b[0;31mERROR\x1b[0m ", | ||||
|         .fatal => "\x1b[0;35mFATAL\x1b[0m ", | ||||
|     }); | ||||
|  | ||||
|     const prefix = @tagName(scope) ++ " : " ++ msg; | ||||
|     try writer.writeAll(prefix); | ||||
|  | ||||
|     { | ||||
|         // msg.len cannot be > 30, and @tagName(scope).len cannot be > 15 | ||||
|         // so this is safe | ||||
|         const padding = 55 - prefix.len; | ||||
|         for (0..padding / 2) |_| { | ||||
|             try writer.writeAll(" ."); | ||||
|         } | ||||
|         allocator.free(self.loggers); | ||||
|     } | ||||
|  | ||||
|     pub fn acquire(self: *Self) *Log { | ||||
|         self.mutex.lock(); | ||||
|         while (true) { | ||||
|             const loggers = self.loggers; | ||||
|             const available = self.available; | ||||
|             if (available == 0) { | ||||
|                 self.cond.wait(&self.mutex); | ||||
|                 continue; | ||||
|             } | ||||
|             const index = available - 1; | ||||
|             const logger = loggers[index]; | ||||
|             self.available = index; | ||||
|             self.mutex.unlock(); | ||||
|             return logger; | ||||
|         if (@mod(padding, 2) == 1) { | ||||
|             try writer.writeByte(' '); | ||||
|         } | ||||
|         try writer.print(" [+{d}ms]", .{elapsed()}); | ||||
|         try writer.writeByte('\n'); | ||||
|     } | ||||
|  | ||||
|     pub fn release(self: *Self, logger: *Log) void { | ||||
|         self.mutex.lock(); | ||||
|         var loggers = self.loggers; | ||||
|         const available = self.available; | ||||
|         loggers[available] = logger; | ||||
|         self.available = available + 1; | ||||
|         self.mutex.unlock(); | ||||
|         self.cond.signal(); | ||||
|     inline for (@typeInfo(@TypeOf(data)).@"struct".fields) |f| { | ||||
|         const key = "      " ++ f.name ++ " = "; | ||||
|         try writer.writeAll(key); | ||||
|         try writeValue(false, @field(data, f.name), writer); | ||||
|         try writer.writeByte('\n'); | ||||
|     } | ||||
| }; | ||||
|     try writer.writeByte('\n'); | ||||
| } | ||||
|  | ||||
| pub fn writeValue(allocator: Allocator, buffer: *std.ArrayListUnmanaged(u8), escape_string: bool, value: anytype) !void { | ||||
| pub fn writeValue(escape_string: bool, value: anytype, writer: anytype) !void { | ||||
|     const T = @TypeOf(value); | ||||
|     switch (@typeInfo(T)) { | ||||
|         .optional => { | ||||
|             if (value) |v| { | ||||
|                 return writeValue(allocator, buffer, escape_string, v); | ||||
|                 return writeValue(escape_string, v, writer); | ||||
|             } | ||||
|             return buffer.appendSlice(allocator, "null"); | ||||
|             return writer.writeAll("null"); | ||||
|         }, | ||||
|         .comptime_int, .int, .comptime_float, .float => { | ||||
|             return std.fmt.format(buffer.writer(allocator), "{d}", .{value}); | ||||
|             return writer.print("{d}", .{value}); | ||||
|         }, | ||||
|         .bool => { | ||||
|             return buffer.appendSlice(allocator, if (value) "true" else "false"); | ||||
|             return writer.writeAll(if (value) "true" else "false"); | ||||
|         }, | ||||
|         .error_set => return buffer.appendSlice(allocator, @errorName(value)), | ||||
|         .@"enum" => return buffer.appendSlice(allocator, @tagName(value)), | ||||
|         .array => return writeValue(allocator, buffer, escape_string, &value), | ||||
|         .error_set => return writer.writeAll(@errorName(value)), | ||||
|         .@"enum" => return writer.writeAll(@tagName(value)), | ||||
|         .array => return writeValue(escape_string, &value, writer), | ||||
|         .pointer => |ptr| switch (ptr.size) { | ||||
|             .slice => switch (ptr.child) { | ||||
|                 u8 => return writeString(allocator, buffer, escape_string, value), | ||||
|                 u8 => return writeString(escape_string, value, writer), | ||||
|                 else => {}, | ||||
|             }, | ||||
|             .one => switch (@typeInfo(ptr.child)) { | ||||
|                 .array => |arr| if (arr.child == u8) { | ||||
|                     return writeString(allocator, buffer, escape_string, value); | ||||
|                 }, | ||||
|                 else => { | ||||
|                     var writer = buffer.writer(allocator); | ||||
|                     return writer.print("{}", .{value}); | ||||
|                     return writeString(escape_string, value, writer); | ||||
|                 }, | ||||
|                 else => return writer.print("{}", .{value}), | ||||
|             }, | ||||
|             else => {}, | ||||
|         }, | ||||
|         .@"union" => { | ||||
|             var writer = buffer.writer(allocator); | ||||
|             return writer.print("{}", .{value}); | ||||
|         }, | ||||
|         .@"struct" => { | ||||
|             var writer = buffer.writer(allocator); | ||||
|             return writer.print("{}", .{value}); | ||||
|         }, | ||||
|         .@"union" => return writer.print("{}", .{value}), | ||||
|         .@"struct" => return writer.print("{}", .{value}), | ||||
|         else => {}, | ||||
|     } | ||||
|     @compileError("cannot log a: " ++ @typeName(T)); | ||||
| } | ||||
|  | ||||
| fn writeString(allocator: Allocator, buffer: *std.ArrayListUnmanaged(u8), escape: bool, value: []const u8) !void { | ||||
| fn writeString(escape: bool, value: []const u8, writer: anytype) !void { | ||||
|     if (escape == false) { | ||||
|         return buffer.appendSlice(allocator, value); | ||||
|         return writer.writeAll(value); | ||||
|     } | ||||
|  | ||||
|     var space_count: usize = 0; | ||||
| @@ -343,44 +233,38 @@ fn writeString(allocator: Allocator, buffer: *std.ArrayListUnmanaged(u8), escape | ||||
|  | ||||
|     if (binary_count > 0) { | ||||
|         // TODO: use a different encoding if the ratio of binary data / printable is low | ||||
|         return std.base64.standard_no_pad.Encoder.encodeWriter(buffer.writer(allocator), value); | ||||
|         return std.base64.standard_no_pad.Encoder.encodeWriter(writer, value); | ||||
|     } | ||||
|  | ||||
|     if (escape_count == 0) { | ||||
|         if (space_count == 0) { | ||||
|             return buffer.appendSlice(allocator, value); | ||||
|             return writer.writeAll(value); | ||||
|         } | ||||
|         try buffer.ensureUnusedCapacity(allocator, 2 + value.len); | ||||
|         buffer.appendAssumeCapacity('"'); | ||||
|         buffer.appendSliceAssumeCapacity(value); | ||||
|         buffer.appendAssumeCapacity('"'); | ||||
|         try writer.writeByte('"'); | ||||
|         try writer.writeAll(value); | ||||
|         try writer.writeByte('"'); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     // + 2 for the quotes | ||||
|     // + escape_count because every character that needs escaping is + 1 | ||||
|     try buffer.ensureUnusedCapacity(allocator, 2 + value.len + escape_count); | ||||
|  | ||||
|     buffer.appendAssumeCapacity('"'); | ||||
|     try writer.writeByte('"'); | ||||
|  | ||||
|     var rest = value; | ||||
|     while (rest.len > 0) { | ||||
|         const pos = std.mem.indexOfAny(u8, rest, "\r\n\"") orelse { | ||||
|             buffer.appendSliceAssumeCapacity(rest); | ||||
|             try writer.writeAll(rest); | ||||
|             break; | ||||
|         }; | ||||
|         buffer.appendSliceAssumeCapacity(rest[0..pos]); | ||||
|         buffer.appendAssumeCapacity('\\'); | ||||
|         try writer.writeAll(rest[0..pos]); | ||||
|         try writer.writeByte('\\'); | ||||
|         switch (rest[pos]) { | ||||
|             '"' => buffer.appendAssumeCapacity('"'), | ||||
|             '\r' => buffer.appendAssumeCapacity('r'), | ||||
|             '\n' => buffer.appendAssumeCapacity('n'), | ||||
|             '"' => try writer.writeByte('"'), | ||||
|             '\r' => try writer.writeByte('r'), | ||||
|             '\n' => try writer.writeByte('n'), | ||||
|             else => unreachable, | ||||
|         } | ||||
|         rest = rest[pos + 1 ..]; | ||||
|     } | ||||
|  | ||||
|     buffer.appendAssumeCapacity('"'); | ||||
|     return writer.writeByte('"'); | ||||
| } | ||||
|  | ||||
| fn timestamp() i64 { | ||||
| @@ -408,64 +292,64 @@ fn elapsed() i64 { | ||||
|     return now - previous; | ||||
| } | ||||
|  | ||||
| const testing = @import("testing.zig"); | ||||
| test "log: data" { | ||||
|     var buf: std.ArrayListUnmanaged(u8) = .{}; | ||||
|     defer buf.deinit(testing.allocator); | ||||
| // const testing = @import("testing.zig"); | ||||
| // test "log: data" { | ||||
| //     var buf: std.ArrayListUnmanaged(u8) = .{}; | ||||
| //     defer buf.deinit(testing.allocator); | ||||
|  | ||||
|     var logger = try TestLogger.init(testing.allocator, .{ .format = .logfmt }, buf.writer(testing.allocator)); | ||||
|     defer logger.deinit(); | ||||
| //     var logger = try TestLogger.init(testing.allocator, .{ .format = .logfmt }, buf.writer(testing.allocator)); | ||||
| //     defer logger.deinit(); | ||||
|  | ||||
|     { | ||||
|         try logger.log(.t_scope, .err, "nope", .{}); | ||||
|         try testing.expectEqual("$time=1739795092929 $scope=t_scope $level=error $msg=nope\n", buf.items); | ||||
|     } | ||||
| //     { | ||||
| //         try logger.log(.t_scope, .err, "nope", .{}); | ||||
| //         try testing.expectEqual("$time=1739795092929 $scope=t_scope $level=error $msg=nope\n", buf.items); | ||||
| //     } | ||||
|  | ||||
|     { | ||||
|         buf.clearRetainingCapacity(); | ||||
|         const string = try testing.allocator.dupe(u8, "spice_must_flow"); | ||||
|         defer testing.allocator.free(string); | ||||
| //     { | ||||
| //         buf.clearRetainingCapacity(); | ||||
| //         const string = try testing.allocator.dupe(u8, "spice_must_flow"); | ||||
| //         defer testing.allocator.free(string); | ||||
|  | ||||
|         try logger.log(.scope_2, .warn, "a msg", .{ | ||||
|             .cint = 5, | ||||
|             .cfloat = 3.43, | ||||
|             .int = @as(i16, -49), | ||||
|             .float = @as(f32, 0.0003232), | ||||
|             .bt = true, | ||||
|             .bf = false, | ||||
|             .nn = @as(?i32, 33), | ||||
|             .n = @as(?i32, null), | ||||
|             .lit = "over9000!", | ||||
|             .slice = string, | ||||
|             .err = error.Nope, | ||||
|             .level = Level.warn, | ||||
|         }); | ||||
| //         try logger.log(.scope_2, .warn, "a msg", .{ | ||||
| //             .cint = 5, | ||||
| //             .cfloat = 3.43, | ||||
| //             .int = @as(i16, -49), | ||||
| //             .float = @as(f32, 0.0003232), | ||||
| //             .bt = true, | ||||
| //             .bf = false, | ||||
| //             .nn = @as(?i32, 33), | ||||
| //             .n = @as(?i32, null), | ||||
| //             .lit = "over9000!", | ||||
| //             .slice = string, | ||||
| //             .err = error.Nope, | ||||
| //             .level = Level.warn, | ||||
| //         }); | ||||
|  | ||||
|         try testing.expectEqual("$time=1739795092929 $scope=scope_2 $level=warn $msg=\"a msg\" " ++ | ||||
|             "cint=5 cfloat=3.43 int=-49 float=0.0003232 bt=true bf=false " ++ | ||||
|             "nn=33 n=null lit=over9000! slice=spice_must_flow " ++ | ||||
|             "err=Nope level=warn\n", buf.items); | ||||
|     } | ||||
| } | ||||
| //         try testing.expectEqual("$time=1739795092929 $scope=scope_2 $level=warn $msg=\"a msg\" " ++ | ||||
| //             "cint=5 cfloat=3.43 int=-49 float=0.0003232 bt=true bf=false " ++ | ||||
| //             "nn=33 n=null lit=over9000! slice=spice_must_flow " ++ | ||||
| //             "err=Nope level=warn\n", buf.items); | ||||
| //     } | ||||
| // } | ||||
|  | ||||
| test "log: string escape" { | ||||
|     var buf: std.ArrayListUnmanaged(u8) = .{}; | ||||
|     defer buf.deinit(testing.allocator); | ||||
| // test "log: string escape" { | ||||
| //     var buf: std.ArrayListUnmanaged(u8) = .{}; | ||||
| //     defer buf.deinit(testing.allocator); | ||||
|  | ||||
|     var logger = try TestLogger.init(testing.allocator, .{ .format = .logfmt }, buf.writer(testing.allocator)); | ||||
|     defer logger.deinit(); | ||||
| //     var logger = try TestLogger.init(testing.allocator, .{ .format = .logfmt }, buf.writer(testing.allocator)); | ||||
| //     defer logger.deinit(); | ||||
|  | ||||
|     const prefix = "$time=1739795092929 $scope=scope $level=error $msg=test "; | ||||
|     { | ||||
|         try logger.log(.scope, .err, "test", .{ .string = "hello world" }); | ||||
|         try testing.expectEqual(prefix ++ "string=\"hello world\"\n", buf.items); | ||||
|     } | ||||
| //     const prefix = "$time=1739795092929 $scope=scope $level=error $msg=test "; | ||||
| //     { | ||||
| //         try logger.log(.scope, .err, "test", .{ .string = "hello world" }); | ||||
| //         try testing.expectEqual(prefix ++ "string=\"hello world\"\n", buf.items); | ||||
| //     } | ||||
|  | ||||
|     { | ||||
|         buf.clearRetainingCapacity(); | ||||
|         try logger.log(.scope, .err, "test", .{ .string = "\n \thi  \" \" " }); | ||||
|         try testing.expectEqual(prefix ++ "string=\"\\n \thi  \\\" \\\" \"\n", buf.items); | ||||
|     } | ||||
| } | ||||
| //     { | ||||
| //         buf.clearRetainingCapacity(); | ||||
| //         try logger.log(.scope, .err, "test", .{ .string = "\n \thi  \" \" " }); | ||||
| //         try testing.expectEqual(prefix ++ "string=\"\\n \thi  \\\" \\\" \"\n", buf.items); | ||||
| //     } | ||||
| // } | ||||
|  | ||||
| const TestLogger = LogT(std.ArrayListUnmanaged(u8).Writer); | ||||
| // const TestLogger = LogT(std.ArrayListUnmanaged(u8).Writer); | ||||
|   | ||||
| @@ -40,9 +40,6 @@ pub fn main() !void { | ||||
|         if (gpa.detectLeaks()) std.posix.exit(1); | ||||
|     }; | ||||
|  | ||||
|     try log.init(alloc, .{}); | ||||
|     defer log.deinit(alloc); | ||||
|  | ||||
|     run(alloc) catch |err| { | ||||
|         log.fatal(.main, "exit", .{ .err = err }); | ||||
|         std.posix.exit(1); | ||||
| @@ -422,7 +419,6 @@ test { | ||||
| var test_wg: std.Thread.WaitGroup = .{}; | ||||
| test "tests:beforeAll" { | ||||
|     try parser.init(); | ||||
|     try log.init(std.testing.allocator, .{}); | ||||
|  | ||||
|     test_wg.startMany(3); | ||||
|     _ = try Platform.init(); | ||||
|   | ||||
| @@ -223,6 +223,7 @@ pub const Client = struct { | ||||
|     } | ||||
|  | ||||
|     fn close(self: *Self) void { | ||||
|         log.info(.server, "client disconected", .{}); | ||||
|         self.connected = false; | ||||
|         // recv only, because we might have pending writes we'd like to get | ||||
|         // out (like the HTTP error response) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Karl Seguin
					Karl Seguin