Overhaul logging, implement JSON logger

Log errors to separate files, specify minimum log level, implement JSON
logging.
This commit is contained in:
Bob Farrell 2024-03-20 21:17:14 +00:00
parent 87bcc4c9e0
commit 381c38d85d
13 changed files with 312 additions and 126 deletions

View File

@ -7,7 +7,7 @@
.hash = "1220e5ede084ca6b94defd466a8f8779aab151d37bf688fefb928fded6f02cde4135", .hash = "1220e5ede084ca6b94defd466a8f8779aab151d37bf688fefb928fded6f02cde4135",
}, },
.args = .{ .args = .{
.url = "https://github.com/bobf/zig-args/archive/e827c93f00e8bd95bd4b970c59593f393a6b08d5.tar.gz", .url = "https://github.com/MasterQ32/zig-args/archive/01d72b9a0128c474aeeb9019edd48605fa6d95f7.tar.gz",
.hash = "12208a1de366740d11de525db7289345949f5fd46527db3f89eecc7bb49b012c0732", .hash = "12208a1de366740d11de525db7289345949f5fd46527db3f89eecc7bb49b012c0732",
}, },
}, },

View File

@ -5,7 +5,7 @@
.dependencies = .{ .dependencies = .{
.args = .{ .args = .{
.url = "https://github.com/bobf/zig-args/archive/e827c93f00e8bd95bd4b970c59593f393a6b08d5.tar.gz", .url = "https://github.com/MasterQ32/zig-args/archive/01d72b9a0128c474aeeb9019edd48605fa6d95f7.tar.gz",
.hash = "12208a1de366740d11de525db7289345949f5fd46527db3f89eecc7bb49b012c0732", .hash = "12208a1de366740d11de525db7289345949f5fd46527db3f89eecc7bb49b012c0732",
}, },
}, },

View File

@ -3,6 +3,7 @@ const std = @import("std");
pub const jetzig = @import("jetzig"); pub const jetzig = @import("jetzig");
pub const routes = @import("routes").routes; pub const routes = @import("routes").routes;
// Override default settings in jetzig.config here:
pub const jetzig_options = struct { pub const jetzig_options = struct {
pub const middleware: []const type = &.{ pub const middleware: []const type = &.{
// htmx middleware skips layouts when `HX-Target` header is present and issues // htmx middleware skips layouts when `HX-Target` header is present and issues
@ -10,6 +11,11 @@ pub const jetzig_options = struct {
jetzig.middleware.HtmxMiddleware, jetzig.middleware.HtmxMiddleware,
@import("app/middleware/DemoMiddleware.zig"), @import("app/middleware/DemoMiddleware.zig"),
}; };
pub const max_bytes_request_body: usize = std.math.pow(usize, 2, 16);
pub const max_bytes_static_content: usize = std.math.pow(usize, 2, 16);
pub const http_buffer_size: usize = std.math.pow(usize, 2, 16);
pub const public_content_path = "public";
}; };
pub fn main() !void { pub fn main() !void {

View File

@ -35,13 +35,48 @@ pub const Data = data.Data;
/// generate a `View`. /// generate a `View`.
pub const View = views.View; pub const View = views.View;
const root = @import("root");
/// Global configuration. Override these values by defining in `src/main.zig` with:
/// ```zig
/// pub const jetzig_options = struct {
/// // ...
/// }
/// ```
/// All constants defined below can be overridden.
pub const config = struct { pub const config = struct {
/// Maximum bytes to allow in request body.
pub const max_bytes_request_body: usize = std.math.pow(usize, 2, 16); pub const max_bytes_request_body: usize = std.math.pow(usize, 2, 16);
/// Maximum filesize for `public/` content.
pub const max_bytes_static_content: usize = std.math.pow(usize, 2, 16); pub const max_bytes_static_content: usize = std.math.pow(usize, 2, 16);
/// Path relative to cwd() to serve public content from. Symlinks are not followed.
pub const public_content_path = "public";
/// Middleware chain. Add any custom middleware here, or use middleware provided in
/// `jetzig.middleware` (e.g. `jetzig.middleware.HtmxMiddleware`).
pub const middleware = &.{};
/// HTTP buffer. Must be large enough to store all headers. This should typically not be
/// modified.
pub const http_buffer_size: usize = std.math.pow(usize, 2, 16); pub const http_buffer_size: usize = std.math.pow(usize, 2, 16);
pub const public_content = .{ .path = "public" };
/// Reconciles a configuration value from user-defined values and defaults provided by Jetzig.
pub fn get(T: type, comptime key: []const u8) T {
const self = @This();
if (!@hasDecl(self, key)) @panic("Unknown config option: " ++ key);
if (@hasDecl(root, "jetzig_options") and @hasDecl(root.jetzig_options, key)) {
return @field(root.jetzig_options, key);
} else {
return @field(self, key);
}
}
}; };
/// Initialize a new Jetzig app. Call this from `src/main.zig` and then call
/// `start(@import("routes").routes)` on the returned value.
pub fn init(allocator: std.mem.Allocator) !App { pub fn init(allocator: std.mem.Allocator) !App {
const args = try std.process.argsAlloc(allocator); const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args); defer std.process.argsFree(allocator, args);

View File

@ -12,17 +12,20 @@ const Options = struct {
help: bool = false, help: bool = false,
bind: []const u8 = "127.0.0.1", bind: []const u8 = "127.0.0.1",
port: u16 = 8080, port: u16 = 8080,
environment: []const u8 = "development", // TODO:
// environment: []const u8 = "development",
log: []const u8 = "-", log: []const u8 = "-",
@"log-error": []const u8 = "-", @"log-error": []const u8 = "-",
@"log-level": jetzig.loggers.LogLevel = .DEBUG, @"log-level": jetzig.loggers.LogLevel = .INFO,
@"log-format": jetzig.loggers.LogFormat = .development,
detach: bool = false, detach: bool = false,
pub const shorthands = .{ pub const shorthands = .{
.h = "help", .h = "help",
.b = "bind", .b = "bind",
.p = "port", .p = "port",
.e = "environment", // TODO:
// .e = "environment",
.d = "detach", .d = "detach",
}; };
@ -32,13 +35,17 @@ const Options = struct {
.option_docs = .{ .option_docs = .{
.bind = "IP address/hostname to bind to (default: 127.0.0.1)", .bind = "IP address/hostname to bind to (default: 127.0.0.1)",
.port = "Port to listen on (default: 8080)", .port = "Port to listen on (default: 8080)",
.environment = "Load an environment configuration from src/app/environments/<environment>.zig", // TODO:
.log = "Path to log file. Use '-' for stdout (default: -)", // .environment = "Load an environment configuration from src/app/environments/<environment>.zig",
.log = "Path to log file. Use '-' for stdout (default: '-')",
.@"log-error" = .@"log-error" =
\\Optional path to separate error log file. Use '-' for stdout. If omitted, errors are logged to the location specified by the `log` option. \\Optional path to separate error log file. Use '-' for stderr. If omitted, errors are logged to the location specified by the `log` option (or stderr if `log` is '-').
, ,
.@"log-level" = .@"log-level" =
\\Specify the minimum log level. Log events below the given level are ignored. Must be one of: TRACE, DEBUG, INFO, WARN, ERROR, FATAL (default: DEBUG) \\Minimum log level. Log events below the given level are ignored. Must be one of: { TRACE, DEBUG, INFO, WARN, ERROR, FATAL } (default: DEBUG)
,
.@"log-format" =
\\Output logs in the given format. Must be one of: { development, json } (default: development)
, ,
.detach = .detach =
\\Run the server in the background. Must be used in conjunction with --log (default: false) \\Run the server in the background. Must be used in conjunction with --log (default: false)
@ -62,11 +69,23 @@ pub fn getServerOptions(self: Environment) !jetzig.http.Server.ServerOptions {
std.process.exit(0); std.process.exit(0);
} }
var logger = jetzig.loggers.Logger{ var logger = switch (options.options.@"log-format") {
.development => jetzig.loggers.Logger{
.development_logger = jetzig.loggers.DevelopmentLogger.init( .development_logger = jetzig.loggers.DevelopmentLogger.init(
self.allocator, self.allocator,
try getLogFile(options.options.log), options.options.@"log-level",
try getLogFile(.stdout, options.options),
try getLogFile(.stderr, options.options),
), ),
},
.json => jetzig.loggers.Logger{
.json_logger = jetzig.loggers.JsonLogger.init(
self.allocator,
options.options.@"log-level",
try getLogFile(.stdout, options.options),
try getLogFile(.stderr, options.options),
),
},
}; };
if (options.options.detach and std.mem.eql(u8, options.options.log, "-")) { if (options.options.detach and std.mem.eql(u8, options.options.log, "-")) {
@ -93,8 +112,16 @@ pub fn getServerOptions(self: Environment) !jetzig.http.Server.ServerOptions {
}; };
} }
fn getLogFile(path: []const u8) !std.fs.File { fn getLogFile(stream: enum { stdout, stderr }, options: Options) !std.fs.File {
if (std.mem.eql(u8, path, "-")) return std.io.getStdOut(); const path = switch (stream) {
.stdout => options.log,
.stderr => options.@"log-error",
};
if (std.mem.eql(u8, path, "-")) return switch (stream) {
.stdout => std.io.getStdOut(),
.stderr => std.io.getStdErr(),
};
const file = try std.fs.createFileAbsolute(path, .{ .truncate = false }); const file = try std.fs.createFileAbsolute(path, .{ .truncate = false });
try file.seekFromEnd(0); try file.seekFromEnd(0);

View File

@ -30,10 +30,12 @@ rendered: bool = false,
redirected: bool = false, redirected: bool = false,
rendered_multiple: bool = false, rendered_multiple: bool = false,
rendered_view: ?jetzig.views.View = null, rendered_view: ?jetzig.views.View = null,
start_time: i128,
pub fn init( pub fn init(
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
server: *jetzig.http.Server, server: *jetzig.http.Server,
start_time: i128,
std_http_request: std.http.Server.Request, std_http_request: std.http.Server.Request,
response: *jetzig.http.Response, response: *jetzig.http.Response,
) !Self { ) !Self {
@ -69,6 +71,7 @@ pub fn init(
.query_data = query_data, .query_data = query_data,
.query = query, .query = query,
.std_http_request = std_http_request, .std_http_request = std_http_request,
.start_time = start_time,
}; };
} }
@ -109,7 +112,7 @@ pub fn process(self: *Self) !void {
}; };
const reader = try self.std_http_request.reader(); const reader = try self.std_http_request.reader();
self.body = try reader.readAllAlloc(self.allocator, jetzig.config.max_bytes_request_body); self.body = try reader.readAllAlloc(self.allocator, jetzig.config.get(usize, "max_bytes_request_body"));
self.processed = true; self.processed = true;
} }
@ -310,7 +313,7 @@ pub fn hash(self: *Self) ![]const u8 {
); );
} }
pub fn fmtMethod(self: *Self, colorized: bool) []const u8 { pub fn fmtMethod(self: *const Self, colorized: bool) []const u8 {
if (!colorized) return @tagName(self.method); if (!colorized) return @tagName(self.method);
return switch (self.method) { return switch (self.method) {

View File

@ -3,13 +3,6 @@ const std = @import("std");
const jetzig = @import("../../jetzig.zig"); const jetzig = @import("../../jetzig.zig");
const zmpl = @import("zmpl"); const zmpl = @import("zmpl");
const root_file = @import("root");
pub const jetzig_server_options = if (@hasDecl(root_file, "jetzig_options"))
root_file.jetzig_options
else
struct {};
pub const ServerOptions = struct { pub const ServerOptions = struct {
logger: jetzig.loggers.Logger, logger: jetzig.loggers.Logger,
bind: []const u8, bind: []const u8,
@ -21,7 +14,6 @@ pub const ServerOptions = struct {
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
logger: jetzig.loggers.Logger, logger: jetzig.loggers.Logger,
options: ServerOptions, options: ServerOptions,
start_time: i128 = undefined,
routes: []*jetzig.views.Route, routes: []*jetzig.views.Route,
mime_map: *jetzig.http.mime.MimeMap, mime_map: *jetzig.http.mime.MimeMap,
std_net_server: std.net.Server = undefined, std_net_server: std.net.Server = undefined,
@ -69,7 +61,7 @@ fn processRequests(self: *Self) !void {
const connection = try self.std_net_server.accept(); const connection = try self.std_net_server.accept();
var buf: [jetzig.config.http_buffer_size]u8 = undefined; var buf: [jetzig.config.get(usize, "http_buffer_size")]u8 = undefined;
var std_http_server = std.http.Server.init(connection, &buf); var std_http_server = std.http.Server.init(connection, &buf);
errdefer std_http_server.connection.stream.close(); errdefer std_http_server.connection.stream.close();
@ -86,13 +78,13 @@ fn processRequests(self: *Self) !void {
} }
fn processNextRequest(self: *Self, allocator: std.mem.Allocator, std_http_server: *std.http.Server) !void { fn processNextRequest(self: *Self, allocator: std.mem.Allocator, std_http_server: *std.http.Server) !void {
self.start_time = std.time.nanoTimestamp(); const start_time = std.time.nanoTimestamp();
const std_http_request = try std_http_server.receiveHead(); const std_http_request = try std_http_server.receiveHead();
if (std_http_server.state == .receiving_head) return error.JetzigParseHeadError; if (std_http_server.state == .receiving_head) return error.JetzigParseHeadError;
var response = try jetzig.http.Response.init(allocator); var response = try jetzig.http.Response.init(allocator);
var request = try jetzig.http.Request.init(allocator, self, std_http_request, &response); var request = try jetzig.http.Request.init(allocator, self, start_time, std_http_request, &response);
try request.process(); try request.process();
@ -108,9 +100,7 @@ fn processNextRequest(self: *Self, allocator: std.mem.Allocator, std_http_server
try jetzig.http.middleware.afterResponse(&middleware_data, &request); try jetzig.http.middleware.afterResponse(&middleware_data, &request);
jetzig.http.middleware.deinit(&middleware_data, &request); jetzig.http.middleware.deinit(&middleware_data, &request);
const log_message = try self.requestLogMessage(&request); try self.logger.logRequest(&request);
defer self.allocator.free(log_message);
try self.logger.INFO("{s}", .{log_message});
} }
fn renderResponse(self: *Self, request: *jetzig.http.Request) !void { fn renderResponse(self: *Self, request: *jetzig.http.Request) !void {
@ -327,34 +317,7 @@ fn logStackTrace(
defer buf.deinit(); defer buf.deinit();
const writer = buf.writer(); const writer = buf.writer();
try stack.format("", .{}, writer); try stack.format("", .{}, writer);
try self.logger.ERROR("{s}", .{buf.items}); try self.logger.ERROR("{s}\n", .{buf.items});
}
fn requestLogMessage(self: *Self, request: *jetzig.http.Request) ![]const u8 {
const status: jetzig.http.status_codes.TaggedStatusCode = switch (request.response.status_code) {
inline else => |status_code| @unionInit(
jetzig.http.status_codes.TaggedStatusCode,
@tagName(status_code),
.{},
),
};
const formatted_duration = if (self.logger.isColorized())
try jetzig.colors.duration(self.allocator, self.duration())
else
try std.fmt.allocPrint(self.allocator, "{}", .{self.duration()});
defer self.allocator.free(formatted_duration);
return try std.fmt.allocPrint(self.allocator, "[{s}/{s}/{s}] {s}", .{
formatted_duration,
request.fmtMethod(self.logger.isColorized()),
status.format(self.logger.isColorized()),
request.path.path,
});
}
fn duration(self: *Self) i64 {
return @intCast(std.time.nanoTimestamp() - self.start_time);
} }
fn matchRoute(self: *Self, request: *jetzig.http.Request, static: bool) !?*jetzig.views.Route { fn matchRoute(self: *Self, request: *jetzig.http.Request, static: bool) !?*jetzig.views.Route {
@ -395,7 +358,7 @@ fn matchPublicContent(self: *Self, request: *jetzig.http.Request) !?StaticResour
if (request.method != .GET) return null; if (request.method != .GET) return null;
var iterable_dir = std.fs.cwd().openDir( var iterable_dir = std.fs.cwd().openDir(
jetzig.config.public_content.path, jetzig.config.get([]const u8, "public_content_path"),
.{ .iterate = true, .no_follow = true }, .{ .iterate = true, .no_follow = true },
) catch |err| { ) catch |err| {
switch (err) { switch (err) {
@ -415,7 +378,7 @@ fn matchPublicContent(self: *Self, request: *jetzig.http.Request) !?StaticResour
const content = try iterable_dir.readFileAlloc( const content = try iterable_dir.readFileAlloc(
request.allocator, request.allocator,
file.path, file.path,
jetzig.config.max_bytes_static_content, jetzig.config.get(usize, "max_bytes_static_content"),
); );
const extension = std.fs.path.extension(file.path); const extension = std.fs.path.extension(file.path);
const mime_type = if (self.mime_map.get(extension)) |mime| mime else "application/octet-stream"; const mime_type = if (self.mime_map.get(extension)) |mime| mime else "application/octet-stream";
@ -447,7 +410,7 @@ fn matchStaticContent(self: *Self, request: *jetzig.http.Request) !?[]const u8 {
return static_dir.readFileAlloc( return static_dir.readFileAlloc(
request.allocator, request.allocator,
capture, capture,
jetzig.config.max_bytes_static_content, jetzig.config.get(usize, "max_bytes_static_content"),
) catch |err| { ) catch |err| {
switch (err) { switch (err) {
error.FileNotFound => return null, error.FileNotFound => return null,

View File

@ -1,12 +1,7 @@
const std = @import("std"); const std = @import("std");
const jetzig = @import("../../jetzig.zig"); const jetzig = @import("../../jetzig.zig");
const server_options = jetzig.http.Server.jetzig_server_options; const middlewares: []const type = jetzig.config.get([]const type, "middleware");
const middlewares: []const type = if (@hasDecl(server_options, "middleware"))
server_options.middleware
else
&.{};
const MiddlewareData = std.BoundedArray(*anyopaque, middlewares.len); const MiddlewareData = std.BoundedArray(*anyopaque, middlewares.len);

View File

@ -164,4 +164,10 @@ pub const TaggedStatusCode = union(StatusCode) {
inline else => |capture| capture.format(colorized), inline else => |capture| capture.format(colorized),
}; };
} }
pub fn getCode(self: Self) []const u8 {
return switch (self) {
inline else => |capture| capture.code,
};
}
}; };

View File

@ -1,59 +1,64 @@
const std = @import("std"); const std = @import("std");
const jetzig = @import("../jetzig.zig");
const Self = @This(); const Self = @This();
pub const DevelopmentLogger = @import("loggers/DevelopmentLogger.zig"); pub const DevelopmentLogger = @import("loggers/DevelopmentLogger.zig");
pub const JsonLogger = @import("loggers/JsonLogger.zig");
pub const LogLevel = enum { TRACE, DEBUG, INFO, WARN, ERROR, FATAL }; pub const LogLevel = enum(u4) { TRACE, DEBUG, INFO, WARN, ERROR, FATAL };
pub const LogFormat = enum { development, json };
pub const Logger = union(enum) { pub const Logger = union(enum) {
development_logger: DevelopmentLogger, development_logger: DevelopmentLogger,
json_logger: JsonLogger,
pub fn isColorized(self: Logger) bool {
switch (self) {
inline else => |logger| return logger.isColorized(),
}
}
/// Log a TRACE level message to the configured logger. /// Log a TRACE level message to the configured logger.
pub fn TRACE(self: *const Logger, comptime message: []const u8, args: anytype) !void { pub fn TRACE(self: *const Logger, comptime message: []const u8, args: anytype) !void {
switch (self.*) { switch (self.*) {
inline else => |*logger| try logger.TRACE(message, args), inline else => |*logger| try logger.log(.TRACE, message, args),
} }
} }
/// Log a DEBUG level message to the configured logger. /// Log a DEBUG level message to the configured logger.
pub fn DEBUG(self: *const Logger, comptime message: []const u8, args: anytype) !void { pub fn DEBUG(self: *const Logger, comptime message: []const u8, args: anytype) !void {
switch (self.*) { switch (self.*) {
inline else => |*logger| try logger.DEBUG(message, args), inline else => |*logger| try logger.log(.DEBUG, message, args),
} }
} }
/// Log an INFO level message to the configured logger. /// Log an INFO level message to the configured logger.
pub fn INFO(self: *const Logger, comptime message: []const u8, args: anytype) !void { pub fn INFO(self: *const Logger, comptime message: []const u8, args: anytype) !void {
switch (self.*) { switch (self.*) {
inline else => |*logger| try logger.INFO(message, args), inline else => |*logger| try logger.log(.INFO, message, args),
} }
} }
/// Log a WARN level message to the configured logger. /// Log a WARN level message to the configured logger.
pub fn WARN(self: *const Logger, comptime message: []const u8, args: anytype) !void { pub fn WARN(self: *const Logger, comptime message: []const u8, args: anytype) !void {
switch (self.*) { switch (self.*) {
inline else => |*logger| try logger.WARN(message, args), inline else => |*logger| try logger.log(.WARN, message, args),
} }
} }
/// Log an ERROR level message to the configured logger. /// Log an ERROR level message to the configured logger.
pub fn ERROR(self: *const Logger, comptime message: []const u8, args: anytype) !void { pub fn ERROR(self: *const Logger, comptime message: []const u8, args: anytype) !void {
switch (self.*) { switch (self.*) {
inline else => |*logger| try logger.ERROR(message, args), inline else => |*logger| try logger.log(.ERROR, message, args),
} }
} }
/// Log a FATAL level message to the configured logger. /// Log a FATAL level message to the configured logger.
pub fn FATAL(self: *const Logger, comptime message: []const u8, args: anytype) !void { pub fn FATAL(self: *const Logger, comptime message: []const u8, args: anytype) !void {
switch (self.*) { switch (self.*) {
inline else => |*logger| try logger.FATAL(message, args), inline else => |*logger| try logger.log(.FATAL, message, args),
}
}
pub fn logRequest(self: *const Logger, request: *const jetzig.http.Request) !void {
switch (self.*) {
inline else => |*logger| try logger.logRequest(request),
} }
} }
}; };

View File

@ -2,64 +2,91 @@ const std = @import("std");
const jetzig = @import("../../jetzig.zig"); const jetzig = @import("../../jetzig.zig");
const Self = @This(); const DevelopmentLogger = @This();
const Timestamp = jetzig.types.Timestamp; const Timestamp = jetzig.types.Timestamp;
const LogLevel = jetzig.loggers.LogLevel; const LogLevel = jetzig.loggers.LogLevel;
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
file: std.fs.File, stdout: std.fs.File,
colorized: bool, stderr: std.fs.File,
stdout_colorized: bool,
stderr_colorized: bool,
level: LogLevel,
pub fn init(allocator: std.mem.Allocator, file: std.fs.File) Self { /// Initialize a new Development Logger.
return .{ .allocator = allocator, .file = file, .colorized = file.isTty() }; pub fn init(
allocator: std.mem.Allocator,
level: LogLevel,
stdout: std.fs.File,
stderr: std.fs.File,
) DevelopmentLogger {
return .{
.allocator = allocator,
.level = level,
.stdout = stdout,
.stderr = stderr,
.stdout_colorized = stdout.isTty(),
.stderr_colorized = stderr.isTty(),
};
} }
/// Return true if logger was initialized with colorization (i.e. if log file is a tty) /// Generic log function, receives log level, message (format string), and args for format string.
pub fn isColorized(self: Self) bool { pub fn log(
return self.colorized; self: DevelopmentLogger,
} comptime level: LogLevel,
comptime message: []const u8,
args: anytype,
) !void {
if (@intFromEnum(level) < @intFromEnum(self.level)) return;
/// Log a TRACE level message to the configured logger.
pub fn TRACE(self: *const Self, comptime message: []const u8, args: anytype) !void {
try self.log(.DEBUG, message, args);
}
/// Log a DEBUG level message to the configured logger.
pub fn DEBUG(self: *const Self, comptime message: []const u8, args: anytype) !void {
try self.log(.DEBUG, message, args);
}
/// Log an INFO level message to the configured logger.
pub fn INFO(self: *const Self, comptime message: []const u8, args: anytype) !void {
try self.log(.INFO, message, args);
}
/// Log a WARN level message to the configured logger.
pub fn WARN(self: *const Self, comptime message: []const u8, args: anytype) !void {
try self.log(.WARN, message, args);
}
/// Log an ERROR level message to the configured logger.
pub fn ERROR(self: *const Self, comptime message: []const u8, args: anytype) !void {
try self.log(.ERROR, message, args);
}
/// Log a FATAL level message to the configured logger.
pub fn FATAL(self: *const Self, comptime message: []const u8, args: anytype) !void {
try self.log(.FATAL, message, args);
}
pub fn log(self: Self, comptime level: LogLevel, comptime message: []const u8, args: anytype) !void {
const output = try std.fmt.allocPrint(self.allocator, message, args); const output = try std.fmt.allocPrint(self.allocator, message, args);
defer self.allocator.free(output); defer self.allocator.free(output);
const timestamp = Timestamp.init(std.time.timestamp(), self.allocator); const timestamp = Timestamp.init(std.time.timestamp(), self.allocator);
const iso8601 = try timestamp.iso8601(); const iso8601 = try timestamp.iso8601();
defer self.allocator.free(iso8601); defer self.allocator.free(iso8601);
const writer = self.file.writer();
const level_formatted = if (self.colorized) colorizedLogLevel(level) else @tagName(level); const colorized = switch (level) {
.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 = file.writer();
const level_formatted = if (colorized) colorizedLogLevel(level) else @tagName(level);
try writer.print("{s: >5} [{s}] {s}\n", .{ level_formatted, iso8601, output }); try writer.print("{s: >5} [{s}] {s}\n", .{ level_formatted, iso8601, output });
if (!self.file.isTty()) try self.file.sync();
if (!file.isTty()) try file.sync();
}
/// Log a one-liner including response status code, path, method, duration, etc.
pub fn logRequest(self: DevelopmentLogger, request: *const jetzig.http.Request) !void {
const formatted_duration = if (self.stdout_colorized)
try jetzig.colors.duration(self.allocator, jetzig.util.duration(request.start_time))
else
try std.fmt.allocPrint(self.allocator, "{}", .{jetzig.util.duration(request.start_time)});
defer self.allocator.free(formatted_duration);
const status: jetzig.http.status_codes.TaggedStatusCode = switch (request.response.status_code) {
inline else => |status_code| @unionInit(
jetzig.http.status_codes.TaggedStatusCode,
@tagName(status_code),
.{},
),
};
const message = try std.fmt.allocPrint(self.allocator, "[{s}/{s}/{s}] {s}", .{
formatted_duration,
request.fmtMethod(self.stdout_colorized),
status.format(self.stdout_colorized),
request.path.path,
});
defer self.allocator.free(message);
try self.log(.INFO, "{s}", .{message});
} }
fn colorizedLogLevel(comptime level: LogLevel) []const u8 { fn colorizedLogLevel(comptime level: LogLevel) []const u8 {

View File

@ -0,0 +1,114 @@
const std = @import("std");
const jetzig = @import("../../jetzig.zig");
const JsonLogger = @This();
const Timestamp = jetzig.types.Timestamp;
const LogLevel = jetzig.loggers.LogLevel;
const LogMessage = struct {
level: []const u8,
timestamp: []const u8,
message: []const u8,
};
const RequestLogMessage = struct {
level: []const u8,
timestamp: []const u8,
method: []const u8,
status: []const u8,
path: []const u8,
duration: i64,
};
allocator: std.mem.Allocator,
stdout: std.fs.File,
stderr: std.fs.File,
level: LogLevel,
/// Initialize a new JSON Logger.
pub fn init(
allocator: std.mem.Allocator,
level: LogLevel,
stdout: std.fs.File,
stderr: std.fs.File,
) JsonLogger {
return .{
.allocator = allocator,
.level = level,
.stdout = stdout,
.stderr = stderr,
};
}
/// Generic log function, receives log level, message (format string), and args for format string.
pub fn log(
self: JsonLogger,
comptime level: LogLevel,
comptime message: []const u8,
args: anytype,
) !void {
if (@intFromEnum(level) < @intFromEnum(self.level)) return;
const output = try std.fmt.allocPrint(self.allocator, message, args);
defer self.allocator.free(output);
const timestamp = Timestamp.init(std.time.timestamp(), self.allocator);
const iso8601 = try timestamp.iso8601();
defer self.allocator.free(iso8601);
const file = self.getFile(level);
const writer = file.writer();
const log_message = LogMessage{ .level = @tagName(level), .timestamp = iso8601, .message = output };
const json = try std.json.stringifyAlloc(self.allocator, log_message, .{ .whitespace = .minified });
defer self.allocator.free(json);
try writer.writeAll(json);
try writer.writeByte('\n');
if (!file.isTty()) try file.sync(); // Make configurable ?
}
/// Log a one-liner including response status code, path, method, duration, etc.
pub fn logRequest(self: JsonLogger, request: *const jetzig.http.Request) !void {
const level: LogLevel = .INFO;
const duration = jetzig.util.duration(request.start_time);
const timestamp = Timestamp.init(std.time.timestamp(), self.allocator);
const iso8601 = try timestamp.iso8601();
defer self.allocator.free(iso8601);
const status = switch (request.response.status_code) {
inline else => |status_code| @unionInit(
jetzig.http.status_codes.TaggedStatusCode,
@tagName(status_code),
.{},
),
};
const message = RequestLogMessage{
.level = @tagName(level),
.timestamp = iso8601,
.method = @tagName(request.method),
.status = status.getCode(),
.path = request.path.path,
.duration = duration,
};
const json = try std.json.stringifyAlloc(self.allocator, message, .{ .whitespace = .minified });
defer self.allocator.free(json);
const file = self.getFile(level);
const writer = file.writer();
try writer.writeAll(json);
try writer.writeByte('\n');
if (!file.isTty()) try file.sync(); // Make configurable ?
}
fn getFile(self: JsonLogger, level: LogLevel) std.fs.File {
return switch (level) {
.TRACE, .DEBUG, .INFO => self.stdout,
.WARN, .ERROR, .FATAL => self.stderr,
};
}

View File

@ -44,3 +44,8 @@ pub fn generateSecret(allocator: std.mem.Allocator, comptime len: u10) ![]const
return try allocator.dupe(u8, &secret); return try allocator.dupe(u8, &secret);
} }
/// Calculate a duration from a given start time (in nanoseconds) to the current time.
pub fn duration(start_time: i128) i64 {
return @intCast(std.time.nanoTimestamp() - start_time);
}