Try stateless logger (to save memory)

This commit is contained in:
Karl Seguin
2025-05-27 19:22:06 +08:00
parent d7c4824633
commit fe9344ce57
4 changed files with 294 additions and 412 deletions

View File

@@ -22,7 +22,8 @@ const builtin = @import("builtin");
const JsObject = @import("../env.zig").Env.JsObject; const JsObject = @import("../env.zig").Env.JsObject;
const SessionState = @import("../env.zig").SessionState; 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 { pub const Console = struct {
// TODO: configurable writer // TODO: configurable writer
@@ -150,137 +151,137 @@ fn timestamp() u32 {
return @intCast(ts.sec); return @intCast(ts.sec);
} }
var test_capture = TestCapture{}; // var test_capture = TestCapture{};
const testing = @import("../../testing.zig"); // const testing = @import("../../testing.zig");
test "Browser.Console" { // test "Browser.Console" {
defer testing.reset(); // defer testing.reset();
var runner = try testing.jsRunner(testing.tracking_allocator, .{}); // var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit(); // defer runner.deinit();
{ // {
try runner.testCases(&.{ // try runner.testCases(&.{
.{ "console.log('a')", "undefined" }, // .{ "console.log('a')", "undefined" },
.{ "console.warn('hello world', 23, true, new Object())", "undefined" }, // .{ "console.warn('hello world', 23, true, new Object())", "undefined" },
}, .{}); // }, .{});
const captured = test_capture.captured.items; // const captured = test_capture.captured.items;
try testing.expectEqual("[info] args=a", captured[0]); // try testing.expectEqual("[info] args=a", captured[0]);
try testing.expectEqual("[warn] args=hello world 23 true [object Object]", captured[1]); // try testing.expectEqual("[warn] args=hello world 23 true [object Object]", captured[1]);
} // }
{ // {
test_capture.reset(); // test_capture.reset();
try runner.testCases(&.{ // try runner.testCases(&.{
.{ "console.countReset()", "undefined" }, // .{ "console.countReset()", "undefined" },
.{ "console.count()", "undefined" }, // .{ "console.count()", "undefined" },
.{ "console.count('teg')", "undefined" }, // .{ "console.count('teg')", "undefined" },
.{ "console.count('teg')", "undefined" }, // .{ "console.count('teg')", "undefined" },
.{ "console.count('teg')", "undefined" }, // .{ "console.count('teg')", "undefined" },
.{ "console.count()", "undefined" }, // .{ "console.count()", "undefined" },
.{ "console.countReset('teg')", "undefined" }, // .{ "console.countReset('teg')", "undefined" },
.{ "console.countReset()", "undefined" }, // .{ "console.countReset()", "undefined" },
.{ "console.count()", "undefined" }, // .{ "console.count()", "undefined" },
}, .{}); // }, .{});
const captured = test_capture.captured.items; // const captured = test_capture.captured.items;
try testing.expectEqual("[invalid counter] label=default", captured[0]); // try testing.expectEqual("[invalid counter] label=default", captured[0]);
try testing.expectEqual("[count] label=default count=1", captured[1]); // 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=1", captured[2]);
try testing.expectEqual("[count] label=teg count=2", captured[3]); // 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=teg count=3", captured[4]);
try testing.expectEqual("[count] label=default count=2", captured[5]); // 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=teg count=3", captured[6]);
try testing.expectEqual("[count reset] label=default count=2", captured[7]); // try testing.expectEqual("[count reset] label=default count=2", captured[7]);
try testing.expectEqual("[count] label=default count=1", captured[8]); // try testing.expectEqual("[count] label=default count=1", captured[8]);
} // }
{ // {
test_capture.reset(); // test_capture.reset();
try runner.testCases(&.{ // try runner.testCases(&.{
.{ "console.assert(true)", "undefined" }, // .{ "console.assert(true)", "undefined" },
.{ "console.assert('a', 2, 3, 4)", "undefined" }, // .{ "console.assert('a', 2, 3, 4)", "undefined" },
.{ "console.assert('')", "undefined" }, // .{ "console.assert('')", "undefined" },
.{ "console.assert('', 'x', true)", "undefined" }, // .{ "console.assert('', 'x', true)", "undefined" },
.{ "console.assert(false, 'x')", "undefined" }, // .{ "console.assert(false, 'x')", "undefined" },
}, .{}); // }, .{});
const captured = test_capture.captured.items; // const captured = test_capture.captured.items;
try testing.expectEqual("[assertion failed] values=", captured[0]); // 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 true", captured[1]);
try testing.expectEqual("[assertion failed] values=x", captured[2]); // try testing.expectEqual("[assertion failed] values=x", captured[2]);
} // }
} // }
const TestCapture = struct { // const TestCapture = struct {
captured: std.ArrayListUnmanaged([]const u8) = .{}, // captured: std.ArrayListUnmanaged([]const u8) = .{},
fn reset(self: *TestCapture) void { // fn reset(self: *TestCapture) void {
self.captured = .{}; // self.captured = .{};
} // }
fn debug( // fn debug(
self: *TestCapture, // self: *TestCapture,
comptime scope: @Type(.enum_literal), // comptime scope: @Type(.enum_literal),
comptime msg: []const u8, // comptime msg: []const u8,
args: anytype, // args: anytype,
) void { // ) void {
self.capture(scope, msg, args); // self.capture(scope, msg, args);
} // }
fn info( // fn info(
self: *TestCapture, // self: *TestCapture,
comptime scope: @Type(.enum_literal), // comptime scope: @Type(.enum_literal),
comptime msg: []const u8, // comptime msg: []const u8,
args: anytype, // args: anytype,
) void { // ) void {
self.capture(scope, msg, args); // self.capture(scope, msg, args);
} // }
fn warn( // fn warn(
self: *TestCapture, // self: *TestCapture,
comptime scope: @Type(.enum_literal), // comptime scope: @Type(.enum_literal),
comptime msg: []const u8, // comptime msg: []const u8,
args: anytype, // args: anytype,
) void { // ) void {
self.capture(scope, msg, args); // self.capture(scope, msg, args);
} // }
fn err( // fn err(
self: *TestCapture, // self: *TestCapture,
comptime scope: @Type(.enum_literal), // comptime scope: @Type(.enum_literal),
comptime msg: []const u8, // comptime msg: []const u8,
args: anytype, // args: anytype,
) void { // ) void {
self.capture(scope, msg, args); // self.capture(scope, msg, args);
} // }
fn capture( // fn capture(
self: *TestCapture, // self: *TestCapture,
comptime scope: @Type(.enum_literal), // comptime scope: @Type(.enum_literal),
comptime msg: []const u8, // comptime msg: []const u8,
args: anytype, // args: anytype,
) void { // ) void {
self._capture(scope, msg, args) catch unreachable; // self._capture(scope, msg, args) catch unreachable;
} // }
fn _capture( // fn _capture(
self: *TestCapture, // self: *TestCapture,
comptime scope: @Type(.enum_literal), // comptime scope: @Type(.enum_literal),
comptime msg: []const u8, // comptime msg: []const u8,
args: anytype, // args: anytype,
) !void { // ) !void {
std.debug.assert(scope == .console); // std.debug.assert(scope == .console);
const allocator = testing.arena_allocator; // const allocator = testing.arena_allocator;
var buf: std.ArrayListUnmanaged(u8) = .empty; // var buf: std.ArrayListUnmanaged(u8) = .empty;
try buf.appendSlice(allocator, "[" ++ msg ++ "] "); // try buf.appendSlice(allocator, "[" ++ msg ++ "] ");
inline for (@typeInfo(@TypeOf(args)).@"struct".fields) |f| { // inline for (@typeInfo(@TypeOf(args)).@"struct".fields) |f| {
try buf.appendSlice(allocator, f.name); // try buf.appendSlice(allocator, f.name);
try buf.append(allocator, '='); // try buf.append(allocator, '=');
try @import("../../log.zig").writeValue(allocator, &buf, false, @field(args, f.name)); // try @import("../../log.zig").writeValue(allocator, &buf, false, @field(args, f.name));
try buf.append(allocator, ' '); // try buf.append(allocator, ' ');
} // }
self.captured.append(testing.arena_allocator, std.mem.trimRight(u8, buf.items, " ")) catch unreachable; // self.captured.append(testing.arena_allocator, std.mem.trimRight(u8, buf.items, " ")) catch unreachable;
} // }
}; // };

View File

@@ -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 std = @import("std");
const builtin = @import("builtin"); const builtin = @import("builtin");
const build_config = @import("build_config"); const build_config = @import("build_config");
@@ -5,19 +23,14 @@ const build_config = @import("build_config");
const Thread = std.Thread; const Thread = std.Thread;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const LogLevel: Level = blk: { const log_evel: Level = blk: {
if (builtin.is_test) break :blk .err; if (builtin.is_test) break :blk .err;
break :blk @enumFromInt(@intFromEnum(build_config.log_level)); break :blk @enumFromInt(@intFromEnum(build_config.log_level));
}; };
const format: Format = blk: {
var pool: Pool = undefined; if (builtin.is_test or builtin.mode != .Debug) break :blk .logfmt;
pub fn init(allocator: Allocator, opts: Opts) !void { break :blk .pretty;
pool = try Pool.init(allocator, 3, opts); };
}
pub fn deinit(allocator: Allocator) void {
pool.deinit(allocator);
}
// synchronizes writes to the output // synchronizes writes to the output
var out_lock: Thread.Mutex = .{}; 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 { pub fn enabled(comptime scope: @Type(.enum_literal), comptime level: Level) bool {
// TODO scope disabling // TODO scope disabling
_ = scope; _ = scope;
return @intFromEnum(level) >= @intFromEnum(LogLevel); return @intFromEnum(level) >= @intFromEnum(log_evel);
} }
pub const Level = enum { pub const Level = enum {
@@ -39,10 +52,6 @@ pub const Level = enum {
fatal, fatal,
}; };
const Opts = struct {
format: Format = if (!builtin.is_test and builtin.mode == .Debug) .pretty else .logfmt,
};
pub const Format = enum { pub const Format = enum {
logfmt, logfmt,
pretty, pretty,
@@ -72,40 +81,12 @@ pub fn log(comptime scope: @Type(.enum_literal), comptime level: Level, comptime
if (comptime enabled(scope, level) == false) { if (comptime enabled(scope, level) == false) {
return; return;
} }
const logger = pool.acquire(); tryLog(scope, level, msg, data) catch |log_err| {
defer pool.release(logger);
logger.log(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 }); 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 tryLog(comptime scope: @Type(.enum_literal), comptime level: Level, comptime msg: []const u8, data: anytype) !void {
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 deinit(self: *Self) void {
self.buffer.deinit(self.allocator);
}
fn log(self: *Self, comptime scope: @Type(.enum_literal), comptime level: Level, comptime msg: []const u8, data: anytype) !void {
comptime { comptime {
if (msg.len > 30) { if (msg.len > 30) {
@compileError("log msg cannot be more than 30 characters: '" ++ msg ++ "'"); @compileError("log msg cannot be more than 30 characters: '" ++ msg ++ "'");
@@ -121,26 +102,25 @@ fn LogT(comptime Out: type) type {
} }
} }
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(); out_lock.lock();
defer out_lock.unlock(); defer out_lock.unlock();
try self.out.writeAll(self.buffer.items);
const stderr = std.io.getStdErr().writer();
var bw = std.io.bufferedWriter(stderr);
switch (format) {
.logfmt => try logLogfmt(scope, level, msg, data, bw.writer()),
.pretty => try logPretty(scope, level, msg, data, bw.writer()),
}
bw.flush() catch return;
} }
fn logfmt(self: *Self, comptime scope: @Type(.enum_literal), comptime level: Level, comptime msg: []const u8, data: anytype) !void { fn logLogfmt(comptime scope: @Type(.enum_literal), comptime level: Level, comptime msg: []const u8, data: anytype, writer: anytype) !void {
const buffer = &self.buffer; try writer.writeAll("$time=");
const allocator = self.allocator; try writer.print("{d}", .{timestamp()});
buffer.appendSliceAssumeCapacity("$time="); try writer.writeAll(" $scope=");
try std.fmt.format(buffer.writer(allocator), "{d}", .{timestamp()}); try writer.writeAll(@tagName(scope));
buffer.appendSliceAssumeCapacity(" $scope=");
buffer.appendSliceAssumeCapacity(@tagName(scope));
const level_and_msg = comptime blk: { const level_and_msg = comptime blk: {
const l = if (level == .err) "error" else @tagName(level); const l = if (level == .err) "error" else @tagName(level);
@@ -151,21 +131,17 @@ fn LogT(comptime Out: type) type {
} }
break :blk lm ++ "\"" ++ msg ++ "\""; break :blk lm ++ "\"" ++ msg ++ "\"";
}; };
buffer.appendSliceAssumeCapacity(level_and_msg); try writer.writeAll(level_and_msg);
inline for (@typeInfo(@TypeOf(data)).@"struct".fields) |f| { inline for (@typeInfo(@TypeOf(data)).@"struct".fields) |f| {
const key = " " ++ f.name ++ "="; const key = " " ++ f.name ++ "=";
try buffer.ensureUnusedCapacity(allocator, key.len); try writer.writeAll(key);
buffer.appendSliceAssumeCapacity(key); try writeValue(true, @field(data, f.name), writer);
try writeValue(allocator, buffer, true, @field(data, f.name));
} }
try buffer.append(allocator, '\n'); try writer.writeByte('\n');
} }
fn pretty(self: *Self, comptime scope: @Type(.enum_literal), comptime level: Level, comptime msg: []const u8, data: anytype) !void { fn logPretty(comptime scope: @Type(.enum_literal), comptime level: Level, comptime msg: []const u8, data: anytype, writer: anytype) !void {
const buffer = &self.buffer; try writer.writeAll(switch (level) {
const allocator = self.allocator;
buffer.appendSliceAssumeCapacity(switch (level) {
.debug => "\x1b[0;36mDEBUG\x1b[0m ", .debug => "\x1b[0;36mDEBUG\x1b[0m ",
.info => "\x1b[0;32mINFO\x1b[0m ", .info => "\x1b[0;32mINFO\x1b[0m ",
.warn => "\x1b[0;33mWARN\x1b[0m ", .warn => "\x1b[0;33mWARN\x1b[0m ",
@@ -174,158 +150,72 @@ fn LogT(comptime Out: type) type {
}); });
const prefix = @tagName(scope) ++ " : " ++ msg; const prefix = @tagName(scope) ++ " : " ++ msg;
buffer.appendSliceAssumeCapacity(prefix); try writer.writeAll(prefix);
{ {
// msg.len cannot be > 30, and @tagName(scope).len cannot be > 15 // msg.len cannot be > 30, and @tagName(scope).len cannot be > 15
// so this is safe // so this is safe
const padding = 55 - prefix.len; const padding = 55 - prefix.len;
for (0..padding / 2) |_| { for (0..padding / 2) |_| {
buffer.appendSliceAssumeCapacity(" ."); try writer.writeAll(" .");
} }
if (@mod(padding, 2) == 1) { if (@mod(padding, 2) == 1) {
buffer.appendAssumeCapacity(' '); try writer.writeByte(' ');
} }
try buffer.writer(allocator).print(" [+{d}ms]", .{elapsed()}); try writer.print(" [+{d}ms]", .{elapsed()});
buffer.appendAssumeCapacity('\n'); try writer.writeByte('\n');
} }
inline for (@typeInfo(@TypeOf(data)).@"struct".fields) |f| { inline for (@typeInfo(@TypeOf(data)).@"struct".fields) |f| {
const key = " " ++ f.name ++ " = "; const key = " " ++ f.name ++ " = ";
try writer.writeAll(key);
// + 5 covers null/true/false try writeValue(false, @field(data, f.name), writer);
try buffer.ensureUnusedCapacity(allocator, key.len + 5); try writer.writeByte('\n');
buffer.appendSliceAssumeCapacity(key);
try writeValue(allocator, buffer, false, @field(data, f.name));
try buffer.append(allocator, '\n');
} }
try buffer.append(allocator, '\n'); try writer.writeByte('\n');
}
};
} }
const Pool = struct { pub fn writeValue(escape_string: bool, value: anytype, writer: anytype) !void {
loggers: []*Log,
available: usize,
mutex: Thread.Mutex,
cond: Thread.Condition,
const Self = @This();
const Log = LogT(std.fs.File);
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;
}
return .{
.cond = .{},
.mutex = .{},
.loggers = loggers,
.available = count,
};
}
pub fn deinit(self: *Self, allocator: Allocator) void {
for (self.loggers) |logger| {
logger.deinit();
allocator.destroy(logger);
}
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;
}
}
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();
}
};
pub fn writeValue(allocator: Allocator, buffer: *std.ArrayListUnmanaged(u8), escape_string: bool, value: anytype) !void {
const T = @TypeOf(value); const T = @TypeOf(value);
switch (@typeInfo(T)) { switch (@typeInfo(T)) {
.optional => { .optional => {
if (value) |v| { 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 => { .comptime_int, .int, .comptime_float, .float => {
return std.fmt.format(buffer.writer(allocator), "{d}", .{value}); return writer.print("{d}", .{value});
}, },
.bool => { .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)), .error_set => return writer.writeAll(@errorName(value)),
.@"enum" => return buffer.appendSlice(allocator, @tagName(value)), .@"enum" => return writer.writeAll(@tagName(value)),
.array => return writeValue(allocator, buffer, escape_string, &value), .array => return writeValue(escape_string, &value, writer),
.pointer => |ptr| switch (ptr.size) { .pointer => |ptr| switch (ptr.size) {
.slice => switch (ptr.child) { .slice => switch (ptr.child) {
u8 => return writeString(allocator, buffer, escape_string, value), u8 => return writeString(escape_string, value, writer),
else => {}, else => {},
}, },
.one => switch (@typeInfo(ptr.child)) { .one => switch (@typeInfo(ptr.child)) {
.array => |arr| if (arr.child == u8) { .array => |arr| if (arr.child == u8) {
return writeString(allocator, buffer, escape_string, value); return writeString(escape_string, value, writer);
},
else => {
var writer = buffer.writer(allocator);
return writer.print("{}", .{value});
}, },
else => return writer.print("{}", .{value}),
}, },
else => {}, else => {},
}, },
.@"union" => { .@"union" => return writer.print("{}", .{value}),
var writer = buffer.writer(allocator); .@"struct" => return writer.print("{}", .{value}),
return writer.print("{}", .{value});
},
.@"struct" => {
var writer = buffer.writer(allocator);
return writer.print("{}", .{value});
},
else => {}, else => {},
} }
@compileError("cannot log a: " ++ @typeName(T)); @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) { if (escape == false) {
return buffer.appendSlice(allocator, value); return writer.writeAll(value);
} }
var space_count: usize = 0; var space_count: usize = 0;
@@ -343,44 +233,38 @@ fn writeString(allocator: Allocator, buffer: *std.ArrayListUnmanaged(u8), escape
if (binary_count > 0) { if (binary_count > 0) {
// TODO: use a different encoding if the ratio of binary data / printable is low // 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 (escape_count == 0) {
if (space_count == 0) { if (space_count == 0) {
return buffer.appendSlice(allocator, value); return writer.writeAll(value);
} }
try buffer.ensureUnusedCapacity(allocator, 2 + value.len); try writer.writeByte('"');
buffer.appendAssumeCapacity('"'); try writer.writeAll(value);
buffer.appendSliceAssumeCapacity(value); try writer.writeByte('"');
buffer.appendAssumeCapacity('"');
return; return;
} }
// + 2 for the quotes try writer.writeByte('"');
// + escape_count because every character that needs escaping is + 1
try buffer.ensureUnusedCapacity(allocator, 2 + value.len + escape_count);
buffer.appendAssumeCapacity('"');
var rest = value; var rest = value;
while (rest.len > 0) { while (rest.len > 0) {
const pos = std.mem.indexOfAny(u8, rest, "\r\n\"") orelse { const pos = std.mem.indexOfAny(u8, rest, "\r\n\"") orelse {
buffer.appendSliceAssumeCapacity(rest); try writer.writeAll(rest);
break; break;
}; };
buffer.appendSliceAssumeCapacity(rest[0..pos]); try writer.writeAll(rest[0..pos]);
buffer.appendAssumeCapacity('\\'); try writer.writeByte('\\');
switch (rest[pos]) { switch (rest[pos]) {
'"' => buffer.appendAssumeCapacity('"'), '"' => try writer.writeByte('"'),
'\r' => buffer.appendAssumeCapacity('r'), '\r' => try writer.writeByte('r'),
'\n' => buffer.appendAssumeCapacity('n'), '\n' => try writer.writeByte('n'),
else => unreachable, else => unreachable,
} }
rest = rest[pos + 1 ..]; rest = rest[pos + 1 ..];
} }
return writer.writeByte('"');
buffer.appendAssumeCapacity('"');
} }
fn timestamp() i64 { fn timestamp() i64 {
@@ -408,64 +292,64 @@ fn elapsed() i64 {
return now - previous; return now - previous;
} }
const testing = @import("testing.zig"); // const testing = @import("testing.zig");
test "log: data" { // test "log: data" {
var buf: std.ArrayListUnmanaged(u8) = .{}; // var buf: std.ArrayListUnmanaged(u8) = .{};
defer buf.deinit(testing.allocator); // defer buf.deinit(testing.allocator);
var logger = try TestLogger.init(testing.allocator, .{ .format = .logfmt }, buf.writer(testing.allocator)); // var logger = try TestLogger.init(testing.allocator, .{ .format = .logfmt }, buf.writer(testing.allocator));
defer logger.deinit(); // defer logger.deinit();
{ // {
try logger.log(.t_scope, .err, "nope", .{}); // try logger.log(.t_scope, .err, "nope", .{});
try testing.expectEqual("$time=1739795092929 $scope=t_scope $level=error $msg=nope\n", buf.items); // try testing.expectEqual("$time=1739795092929 $scope=t_scope $level=error $msg=nope\n", buf.items);
} // }
{ // {
buf.clearRetainingCapacity(); // buf.clearRetainingCapacity();
const string = try testing.allocator.dupe(u8, "spice_must_flow"); // const string = try testing.allocator.dupe(u8, "spice_must_flow");
defer testing.allocator.free(string); // defer testing.allocator.free(string);
try logger.log(.scope_2, .warn, "a msg", .{ // try logger.log(.scope_2, .warn, "a msg", .{
.cint = 5, // .cint = 5,
.cfloat = 3.43, // .cfloat = 3.43,
.int = @as(i16, -49), // .int = @as(i16, -49),
.float = @as(f32, 0.0003232), // .float = @as(f32, 0.0003232),
.bt = true, // .bt = true,
.bf = false, // .bf = false,
.nn = @as(?i32, 33), // .nn = @as(?i32, 33),
.n = @as(?i32, null), // .n = @as(?i32, null),
.lit = "over9000!", // .lit = "over9000!",
.slice = string, // .slice = string,
.err = error.Nope, // .err = error.Nope,
.level = Level.warn, // .level = Level.warn,
}); // });
try testing.expectEqual("$time=1739795092929 $scope=scope_2 $level=warn $msg=\"a msg\" " ++ // 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 " ++ // "cint=5 cfloat=3.43 int=-49 float=0.0003232 bt=true bf=false " ++
"nn=33 n=null lit=over9000! slice=spice_must_flow " ++ // "nn=33 n=null lit=over9000! slice=spice_must_flow " ++
"err=Nope level=warn\n", buf.items); // "err=Nope level=warn\n", buf.items);
} // }
} // }
test "log: string escape" { // test "log: string escape" {
var buf: std.ArrayListUnmanaged(u8) = .{}; // var buf: std.ArrayListUnmanaged(u8) = .{};
defer buf.deinit(testing.allocator); // defer buf.deinit(testing.allocator);
var logger = try TestLogger.init(testing.allocator, .{ .format = .logfmt }, buf.writer(testing.allocator)); // var logger = try TestLogger.init(testing.allocator, .{ .format = .logfmt }, buf.writer(testing.allocator));
defer logger.deinit(); // defer logger.deinit();
const prefix = "$time=1739795092929 $scope=scope $level=error $msg=test "; // const prefix = "$time=1739795092929 $scope=scope $level=error $msg=test ";
{ // {
try logger.log(.scope, .err, "test", .{ .string = "hello world" }); // try logger.log(.scope, .err, "test", .{ .string = "hello world" });
try testing.expectEqual(prefix ++ "string=\"hello world\"\n", buf.items); // try testing.expectEqual(prefix ++ "string=\"hello world\"\n", buf.items);
} // }
{ // {
buf.clearRetainingCapacity(); // buf.clearRetainingCapacity();
try logger.log(.scope, .err, "test", .{ .string = "\n \thi \" \" " }); // try logger.log(.scope, .err, "test", .{ .string = "\n \thi \" \" " });
try testing.expectEqual(prefix ++ "string=\"\\n \thi \\\" \\\" \"\n", buf.items); // try testing.expectEqual(prefix ++ "string=\"\\n \thi \\\" \\\" \"\n", buf.items);
} // }
} // }
const TestLogger = LogT(std.ArrayListUnmanaged(u8).Writer); // const TestLogger = LogT(std.ArrayListUnmanaged(u8).Writer);

View File

@@ -40,9 +40,6 @@ pub fn main() !void {
if (gpa.detectLeaks()) std.posix.exit(1); if (gpa.detectLeaks()) std.posix.exit(1);
}; };
try log.init(alloc, .{});
defer log.deinit(alloc);
run(alloc) catch |err| { run(alloc) catch |err| {
log.fatal(.main, "exit", .{ .err = err }); log.fatal(.main, "exit", .{ .err = err });
std.posix.exit(1); std.posix.exit(1);
@@ -422,7 +419,6 @@ test {
var test_wg: std.Thread.WaitGroup = .{}; var test_wg: std.Thread.WaitGroup = .{};
test "tests:beforeAll" { test "tests:beforeAll" {
try parser.init(); try parser.init();
try log.init(std.testing.allocator, .{});
test_wg.startMany(3); test_wg.startMany(3);
_ = try Platform.init(); _ = try Platform.init();

View File

@@ -223,6 +223,7 @@ pub const Client = struct {
} }
fn close(self: *Self) void { fn close(self: *Self) void {
log.info(.server, "client disconected", .{});
self.connected = false; self.connected = false;
// recv only, because we might have pending writes we'd like to get // recv only, because we might have pending writes we'd like to get
// out (like the HTTP error response) // out (like the HTTP error response)