mirror of
https://github.com/jetzig-framework/jetzig.git
synced 2025-05-14 14:06:08 +00:00
Overhaul logging, implement JSON logger
Log errors to separate files, specify minimum log level, implement JSON logging.
This commit is contained in:
parent
87bcc4c9e0
commit
381c38d85d
@ -7,7 +7,7 @@
|
||||
.hash = "1220e5ede084ca6b94defd466a8f8779aab151d37bf688fefb928fded6f02cde4135",
|
||||
},
|
||||
.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",
|
||||
},
|
||||
},
|
||||
|
@ -5,7 +5,7 @@
|
||||
|
||||
.dependencies = .{
|
||||
.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",
|
||||
},
|
||||
},
|
||||
|
@ -3,6 +3,7 @@ const std = @import("std");
|
||||
pub const jetzig = @import("jetzig");
|
||||
pub const routes = @import("routes").routes;
|
||||
|
||||
// Override default settings in jetzig.config here:
|
||||
pub const jetzig_options = struct {
|
||||
pub const middleware: []const type = &.{
|
||||
// htmx middleware skips layouts when `HX-Target` header is present and issues
|
||||
@ -10,6 +11,11 @@ pub const jetzig_options = struct {
|
||||
jetzig.middleware.HtmxMiddleware,
|
||||
@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 {
|
||||
|
@ -35,13 +35,48 @@ pub const Data = data.Data;
|
||||
/// generate a `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 {
|
||||
/// Maximum bytes to allow in request body.
|
||||
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);
|
||||
|
||||
/// 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 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 {
|
||||
const args = try std.process.argsAlloc(allocator);
|
||||
defer std.process.argsFree(allocator, args);
|
||||
|
@ -12,17 +12,20 @@ const Options = struct {
|
||||
help: bool = false,
|
||||
bind: []const u8 = "127.0.0.1",
|
||||
port: u16 = 8080,
|
||||
environment: []const u8 = "development",
|
||||
// TODO:
|
||||
// environment: []const u8 = "development",
|
||||
log: []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,
|
||||
|
||||
pub const shorthands = .{
|
||||
.h = "help",
|
||||
.b = "bind",
|
||||
.p = "port",
|
||||
.e = "environment",
|
||||
// TODO:
|
||||
// .e = "environment",
|
||||
.d = "detach",
|
||||
};
|
||||
|
||||
@ -32,13 +35,17 @@ const Options = struct {
|
||||
.option_docs = .{
|
||||
.bind = "IP address/hostname to bind to (default: 127.0.0.1)",
|
||||
.port = "Port to listen on (default: 8080)",
|
||||
.environment = "Load an environment configuration from src/app/environments/<environment>.zig",
|
||||
.log = "Path to log file. Use '-' for stdout (default: -)",
|
||||
// TODO:
|
||||
// .environment = "Load an environment configuration from src/app/environments/<environment>.zig",
|
||||
.log = "Path to log file. Use '-' for stdout (default: '-')",
|
||||
.@"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" =
|
||||
\\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 =
|
||||
\\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);
|
||||
}
|
||||
|
||||
var logger = jetzig.loggers.Logger{
|
||||
var logger = switch (options.options.@"log-format") {
|
||||
.development => jetzig.loggers.Logger{
|
||||
.development_logger = jetzig.loggers.DevelopmentLogger.init(
|
||||
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, "-")) {
|
||||
@ -93,8 +112,16 @@ pub fn getServerOptions(self: Environment) !jetzig.http.Server.ServerOptions {
|
||||
};
|
||||
}
|
||||
|
||||
fn getLogFile(path: []const u8) !std.fs.File {
|
||||
if (std.mem.eql(u8, path, "-")) return std.io.getStdOut();
|
||||
fn getLogFile(stream: enum { stdout, stderr }, options: Options) !std.fs.File {
|
||||
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 });
|
||||
try file.seekFromEnd(0);
|
||||
|
@ -30,10 +30,12 @@ rendered: bool = false,
|
||||
redirected: bool = false,
|
||||
rendered_multiple: bool = false,
|
||||
rendered_view: ?jetzig.views.View = null,
|
||||
start_time: i128,
|
||||
|
||||
pub fn init(
|
||||
allocator: std.mem.Allocator,
|
||||
server: *jetzig.http.Server,
|
||||
start_time: i128,
|
||||
std_http_request: std.http.Server.Request,
|
||||
response: *jetzig.http.Response,
|
||||
) !Self {
|
||||
@ -69,6 +71,7 @@ pub fn init(
|
||||
.query_data = query_data,
|
||||
.query = query,
|
||||
.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();
|
||||
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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
return switch (self.method) {
|
||||
|
@ -3,13 +3,6 @@ const std = @import("std");
|
||||
const jetzig = @import("../../jetzig.zig");
|
||||
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 {
|
||||
logger: jetzig.loggers.Logger,
|
||||
bind: []const u8,
|
||||
@ -21,7 +14,6 @@ pub const ServerOptions = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
logger: jetzig.loggers.Logger,
|
||||
options: ServerOptions,
|
||||
start_time: i128 = undefined,
|
||||
routes: []*jetzig.views.Route,
|
||||
mime_map: *jetzig.http.mime.MimeMap,
|
||||
std_net_server: std.net.Server = undefined,
|
||||
@ -69,7 +61,7 @@ fn processRequests(self: *Self) !void {
|
||||
|
||||
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);
|
||||
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 {
|
||||
self.start_time = std.time.nanoTimestamp();
|
||||
const start_time = std.time.nanoTimestamp();
|
||||
|
||||
const std_http_request = try std_http_server.receiveHead();
|
||||
if (std_http_server.state == .receiving_head) return error.JetzigParseHeadError;
|
||||
|
||||
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();
|
||||
|
||||
@ -108,9 +100,7 @@ fn processNextRequest(self: *Self, allocator: std.mem.Allocator, std_http_server
|
||||
try jetzig.http.middleware.afterResponse(&middleware_data, &request);
|
||||
jetzig.http.middleware.deinit(&middleware_data, &request);
|
||||
|
||||
const log_message = try self.requestLogMessage(&request);
|
||||
defer self.allocator.free(log_message);
|
||||
try self.logger.INFO("{s}", .{log_message});
|
||||
try self.logger.logRequest(&request);
|
||||
}
|
||||
|
||||
fn renderResponse(self: *Self, request: *jetzig.http.Request) !void {
|
||||
@ -327,34 +317,7 @@ fn logStackTrace(
|
||||
defer buf.deinit();
|
||||
const writer = buf.writer();
|
||||
try stack.format("", .{}, writer);
|
||||
try self.logger.ERROR("{s}", .{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);
|
||||
try self.logger.ERROR("{s}\n", .{buf.items});
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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 },
|
||||
) catch |err| {
|
||||
switch (err) {
|
||||
@ -415,7 +378,7 @@ fn matchPublicContent(self: *Self, request: *jetzig.http.Request) !?StaticResour
|
||||
const content = try iterable_dir.readFileAlloc(
|
||||
request.allocator,
|
||||
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 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(
|
||||
request.allocator,
|
||||
capture,
|
||||
jetzig.config.max_bytes_static_content,
|
||||
jetzig.config.get(usize, "max_bytes_static_content"),
|
||||
) catch |err| {
|
||||
switch (err) {
|
||||
error.FileNotFound => return null,
|
||||
|
@ -1,12 +1,7 @@
|
||||
const std = @import("std");
|
||||
const jetzig = @import("../../jetzig.zig");
|
||||
|
||||
const server_options = jetzig.http.Server.jetzig_server_options;
|
||||
|
||||
const middlewares: []const type = if (@hasDecl(server_options, "middleware"))
|
||||
server_options.middleware
|
||||
else
|
||||
&.{};
|
||||
const middlewares: []const type = jetzig.config.get([]const type, "middleware");
|
||||
|
||||
const MiddlewareData = std.BoundedArray(*anyopaque, middlewares.len);
|
||||
|
||||
|
@ -164,4 +164,10 @@ pub const TaggedStatusCode = union(StatusCode) {
|
||||
inline else => |capture| capture.format(colorized),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getCode(self: Self) []const u8 {
|
||||
return switch (self) {
|
||||
inline else => |capture| capture.code,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
@ -1,59 +1,64 @@
|
||||
const std = @import("std");
|
||||
|
||||
const jetzig = @import("../jetzig.zig");
|
||||
|
||||
const Self = @This();
|
||||
|
||||
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) {
|
||||
development_logger: DevelopmentLogger,
|
||||
|
||||
pub fn isColorized(self: Logger) bool {
|
||||
switch (self) {
|
||||
inline else => |logger| return logger.isColorized(),
|
||||
}
|
||||
}
|
||||
json_logger: JsonLogger,
|
||||
|
||||
/// Log a TRACE level message to the configured logger.
|
||||
pub fn TRACE(self: *const Logger, comptime message: []const u8, args: anytype) !void {
|
||||
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.
|
||||
pub fn DEBUG(self: *const Logger, comptime message: []const u8, args: anytype) !void {
|
||||
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.
|
||||
pub fn INFO(self: *const Logger, comptime message: []const u8, args: anytype) !void {
|
||||
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.
|
||||
pub fn WARN(self: *const Logger, comptime message: []const u8, args: anytype) !void {
|
||||
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.
|
||||
pub fn ERROR(self: *const Logger, comptime message: []const u8, args: anytype) !void {
|
||||
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.
|
||||
pub fn FATAL(self: *const Logger, comptime message: []const u8, args: anytype) !void {
|
||||
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),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -2,64 +2,91 @@ const std = @import("std");
|
||||
|
||||
const jetzig = @import("../../jetzig.zig");
|
||||
|
||||
const Self = @This();
|
||||
const DevelopmentLogger = @This();
|
||||
|
||||
const Timestamp = jetzig.types.Timestamp;
|
||||
const LogLevel = jetzig.loggers.LogLevel;
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
file: std.fs.File,
|
||||
colorized: bool,
|
||||
stdout: std.fs.File,
|
||||
stderr: std.fs.File,
|
||||
stdout_colorized: bool,
|
||||
stderr_colorized: bool,
|
||||
level: LogLevel,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, file: std.fs.File) Self {
|
||||
return .{ .allocator = allocator, .file = file, .colorized = file.isTty() };
|
||||
/// Initialize a new Development Logger.
|
||||
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)
|
||||
pub fn isColorized(self: Self) bool {
|
||||
return self.colorized;
|
||||
}
|
||||
/// Generic log function, receives log level, message (format string), and args for format string.
|
||||
pub fn log(
|
||||
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);
|
||||
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 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 });
|
||||
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 {
|
||||
|
114
src/jetzig/loggers/JsonLogger.zig
Normal file
114
src/jetzig/loggers/JsonLogger.zig
Normal 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,
|
||||
};
|
||||
}
|
@ -44,3 +44,8 @@ pub fn generateSecret(allocator: std.mem.Allocator, comptime len: u10) ![]const
|
||||
|
||||
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);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user