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",
|
.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",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -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",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -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 {
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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) {
|
||||||
|
@ -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,
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -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 {
|
||||||
|
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);
|
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