diff --git a/src/jetzig/App.zig b/src/jetzig/App.zig index 59af1df..9bc812a 100644 --- a/src/jetzig/App.zig +++ b/src/jetzig/App.zig @@ -75,10 +75,20 @@ pub fn start(self: App, routes_module: type, options: AppOptions) !void { ); defer cache.deinit(); - const server_options = try self.environment.getServerOptions(); + var log_queue = try jetzig.loggers.LogQueue.init(self.allocator); + try log_queue.setFile(std.io.getStdOut()); + + const server_options = try self.environment.getServerOptions(&log_queue); defer self.allocator.free(server_options.bind); defer self.allocator.free(server_options.secret); + var log_thread = try std.Thread.spawn( + .{ .allocator = self.allocator }, + jetzig.loggers.LogQueue.Reader.publish, + .{log_queue.reader}, + ); + defer log_thread.join(); + if (server_options.detach) { const argv = try std.process.argsAlloc(self.allocator); defer std.process.argsFree(self.allocator, argv); diff --git a/src/jetzig/Environment.zig b/src/jetzig/Environment.zig index 47e8b1f..7e43a0d 100644 --- a/src/jetzig/Environment.zig +++ b/src/jetzig/Environment.zig @@ -59,7 +59,10 @@ pub fn init(allocator: std.mem.Allocator) Environment { } /// Generate server initialization options using command line args with defaults. -pub fn getServerOptions(self: Environment) !jetzig.http.Server.ServerOptions { +pub fn getServerOptions( + self: Environment, + log_queue: *jetzig.loggers.LogQueue, +) !jetzig.http.Server.ServerOptions { const options = try args.parseForCurrentProcess(Options, self.allocator, .print); defer options.deinit(); @@ -72,22 +75,24 @@ pub fn getServerOptions(self: Environment) !jetzig.http.Server.ServerOptions { const environment = options.options.environment; var logger = switch (options.options.@"log-format") { - .development => jetzig.loggers.Logger{ + .development, .json => jetzig.loggers.Logger{ .development_logger = jetzig.loggers.DevelopmentLogger.init( self.allocator, resolveLogLevel(options.options.@"log-level", environment), - try getLogFile(.stdout, options.options), - try getLogFile(.stderr, options.options), - ), - }, - .json => jetzig.loggers.Logger{ - .json_logger = jetzig.loggers.JsonLogger.init( - self.allocator, - resolveLogLevel(options.options.@"log-level", environment), - try getLogFile(.stdout, options.options), - try getLogFile(.stderr, options.options), + log_queue, + // try getLogFile(.stdout, options.options), + // try getLogFile(.stderr, options.options), ), }, + // TODO + // .json => jetzig.loggers.Logger{ + // .json_logger = jetzig.loggers.JsonLogger.init( + // self.allocator, + // resolveLogLevel(options.options.@"log-level", environment), + // try getLogFile(.stdout, options.options), + // try getLogFile(.stderr, options.options), + // ), + // }, }; if (options.options.detach and std.mem.eql(u8, options.options.log, "-")) { diff --git a/src/jetzig/loggers.zig b/src/jetzig/loggers.zig index 84b354a..b2cab7b 100644 --- a/src/jetzig/loggers.zig +++ b/src/jetzig/loggers.zig @@ -6,6 +6,7 @@ const Self = @This(); pub const DevelopmentLogger = @import("loggers/DevelopmentLogger.zig"); pub const JsonLogger = @import("loggers/JsonLogger.zig"); +pub const LogQueue = @import("loggers/LogQueue.zig"); pub const LogLevel = enum(u4) { TRACE, DEBUG, INFO, WARN, ERROR, FATAL }; pub const LogFormat = enum { development, json }; diff --git a/src/jetzig/loggers/DevelopmentLogger.zig b/src/jetzig/loggers/DevelopmentLogger.zig index 3108065..0f3998c 100644 --- a/src/jetzig/loggers/DevelopmentLogger.zig +++ b/src/jetzig/loggers/DevelopmentLogger.zig @@ -8,28 +8,23 @@ const Timestamp = jetzig.types.Timestamp; const LogLevel = jetzig.loggers.LogLevel; allocator: std.mem.Allocator, -stdout: std.fs.File, -stderr: std.fs.File, stdout_colorized: bool, stderr_colorized: bool, level: LogLevel, -mutex: std.Thread.Mutex, +log_queue: *jetzig.loggers.LogQueue, /// Initialize a new Development Logger. pub fn init( allocator: std.mem.Allocator, level: LogLevel, - stdout: std.fs.File, - stderr: std.fs.File, + log_queue: *jetzig.loggers.LogQueue, ) DevelopmentLogger { return .{ .allocator = allocator, .level = level, - .stdout = stdout, - .stderr = stderr, - .stdout_colorized = stdout.isTty(), - .stderr_colorized = stderr.isTty(), - .mutex = std.Thread.Mutex{}, + .log_queue = log_queue, + .stdout_colorized = true, // TODO + .stderr_colorized = true, // TODO }; } @@ -53,19 +48,13 @@ pub fn log( .TRACE, .DEBUG, .INFO => self.stdout_colorized, .WARN, .ERROR, .FATAL => self.stderr_colorized, }; - const file = switch (level) { - .TRACE, .DEBUG, .INFO => self.stdout, - .WARN, .ERROR, .FATAL => self.stderr, + const writer = switch (level) { + .TRACE, .DEBUG, .INFO => self.log_queue.writer, + .WARN, .ERROR, .FATAL => self.log_queue.writer, }; - const writer = file.writer(); const level_formatted = if (colorized) colorizedLogLevel(level) else @tagName(level); - @constCast(self).mutex.lock(); - defer @constCast(self).mutex.unlock(); - try writer.print("{s: >5} [{s}] {s}\n", .{ level_formatted, iso8601, output }); - - if (!file.isTty()) try file.sync(); } /// Log a one-liner including response status code, path, method, duration, etc. diff --git a/src/jetzig/loggers/LogQueue.zig b/src/jetzig/loggers/LogQueue.zig new file mode 100644 index 0000000..0bdd238 --- /dev/null +++ b/src/jetzig/loggers/LogQueue.zig @@ -0,0 +1,92 @@ +const std = @import("std"); + +const List = std.DoublyLinkedList([]const u8); +allocator: std.mem.Allocator, +list: *List, +read_write_mutex: *std.Thread.Mutex, +condition: *std.Thread.Condition, +condition_mutex: *std.Thread.Mutex, +writer: *Writer = undefined, +reader: *Reader = undefined, + +const LogQueue = @This(); + +pub fn init(allocator: std.mem.Allocator) !LogQueue { + const list = try allocator.create(std.DoublyLinkedList([]const u8)); + list.* = .{}; + + return .{ + .allocator = allocator, + .list = list, + .condition = try allocator.create(std.Thread.Condition), + .condition_mutex = try allocator.create(std.Thread.Mutex), + .read_write_mutex = try allocator.create(std.Thread.Mutex), + }; +} + +pub fn setFile(self: *LogQueue, file: std.fs.File) !void { + self.writer = try self.allocator.create(Writer); + self.writer.* = Writer{ .file = file, .queue = self }; + self.reader = try self.allocator.create(Reader); + self.reader.* = Reader{ .file = file, .queue = self }; +} + +pub const Writer = struct { + file: std.fs.File, + queue: *LogQueue, + + pub fn isTty(self: Writer) bool { + return self.file.isTty(); + } + + pub fn print(self: *Writer, comptime message: []const u8, args: anytype) !void { + const output = try std.fmt.allocPrint(self.queue.allocator, message, args); + defer self.queue.allocator.free(output); + try self.queue.append(output); + } +}; + +pub const Reader = struct { + file: std.fs.File, + queue: *LogQueue, + + pub fn publish(self: *Reader) !void { + const writer = self.file.writer(); + while (true) { + self.queue.condition_mutex.lock(); + defer self.queue.condition_mutex.unlock(); + + self.queue.condition.wait(self.queue.condition_mutex); + + while (try self.queue.popFirst()) |message| { + defer self.queue.allocator.free(message); + try writer.writeAll(message); + } + + if (!self.file.isTty()) try self.file.sync(); + } + } +}; + +pub fn append(self: *LogQueue, message: []const u8) !void { + self.read_write_mutex.lock(); + defer self.read_write_mutex.unlock(); + + const node = try self.allocator.create(List.Node); + node.* = .{ .data = try self.allocator.dupe(u8, message) }; + self.list.append(node); + self.condition.signal(); +} + +pub fn popFirst(self: *LogQueue) !?[]const u8 { + self.read_write_mutex.lock(); + defer self.read_write_mutex.unlock(); + + if (self.list.popFirst()) |node| { + const value = node.data; + self.allocator.destroy(node); + return value; + } else { + return null; + } +}