Provide tooling for running Jetzig in deployment

This commit is contained in:
Bob Farrell 2024-03-19 23:03:55 +00:00
parent adba4756c5
commit 87bcc4c9e0
22 changed files with 365 additions and 251 deletions

View File

@ -39,7 +39,7 @@ pub fn build(b: *std.Build) !void {
lib.root_module.addImport("zmpl", zmpl_dep.module("zmpl"));
jetzig_module.addImport("zmpl", zmpl_dep.module("zmpl"));
lib.root_module.addImport("args", zig_args_dep.module("args"));
jetzig_module.addImport("args", zig_args_dep.module("args"));
// This is the way to make it look nice in the zig build script
// If we would do it the other way around, we would have to do

View File

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

View File

@ -5,8 +5,8 @@
.dependencies = .{
.args = .{
.url = "https://github.com/MasterQ32/zig-args/archive/89f18a104d9c13763b90e97d6b4ce133da8a3e2b.tar.gz",
.hash = "12203ded54c85878eea7f12744066dcb4397177395ac49a7b2aa365bf6047b623829",
.url = "https://github.com/bobf/zig-args/archive/e827c93f00e8bd95bd4b970c59593f393a6b08d5.tar.gz",
.hash = "12208a1de366740d11de525db7289345949f5fd46527db3f89eecc7bb49b012c0732",
},
},
.paths = .{

View File

@ -4,12 +4,13 @@ const view = @import("generate/view.zig");
const partial = @import("generate/partial.zig");
const layout = @import("generate/layout.zig");
const middleware = @import("generate/middleware.zig");
const secret = @import("generate/secret.zig");
const util = @import("../util.zig");
/// Command line options for the `generate` command.
pub const Options = struct {
pub const meta = .{
.usage_summary = "[view|partial|layout|middleware] [options]",
.usage_summary = "[view|partial|layout|middleware|secret] [options]",
.full_text =
\\Generates scaffolding for views, middleware, and other objects in future.
\\
@ -44,7 +45,7 @@ pub fn run(
try args.printHelp(Options, "jetzig generate", writer);
return;
}
var generate_type: ?enum { view, partial, layout, middleware } = null;
var generate_type: ?enum { view, partial, layout, middleware, secret } = null;
var sub_args = std.ArrayList([]const u8).init(allocator);
defer sub_args.deinit();
@ -57,6 +58,8 @@ pub fn run(
generate_type = .layout;
} else if (generate_type == null and std.mem.eql(u8, arg, "middleware")) {
generate_type = .middleware;
} else if (generate_type == null and std.mem.eql(u8, arg, "secret")) {
generate_type = .secret;
} else if (generate_type == null) {
std.debug.print("Unknown generator command: {s}\n", .{arg});
return error.JetzigCommandError;
@ -71,6 +74,7 @@ pub fn run(
.partial => partial.run(allocator, cwd, sub_args.items),
.layout => layout.run(allocator, cwd, sub_args.items),
.middleware => middleware.run(allocator, cwd, sub_args.items),
.secret => secret.run(allocator, cwd, sub_args.items),
};
} else {
std.debug.print("Missing sub-command. Expected: [view|partial|layout|middleware]\n", .{});

View File

@ -0,0 +1,16 @@
const std = @import("std");
/// Generate a secure random secret and output to stdout.
pub fn run(allocator: std.mem.Allocator, cwd: std.fs.Dir, args: [][]const u8) !void {
_ = allocator;
_ = args;
_ = cwd;
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
var secret: [44]u8 = undefined;
for (0..44) |index| {
secret[index] = chars[std.crypto.random.intRangeAtMost(u8, 0, chars.len)];
}
std.debug.print("{s}\n", .{secret});
}

View File

@ -33,14 +33,17 @@ pub fn init(request: *jetzig.http.Request) !*Self {
/// Any calls to `request.render` or `request.redirect` will prevent further processing of the
/// request, including any other middleware in the chain.
pub fn afterRequest(self: *Self, request: *jetzig.http.Request) !void {
request.server.logger.debug("[DemoMiddleware:afterRequest] my_custom_value: {s}", .{self.my_custom_value});
try request.server.logger.DEBUG(
"[DemoMiddleware:afterRequest] my_custom_value: {s}",
.{self.my_custom_value},
);
self.my_custom_value = @tagName(request.method);
}
/// Invoked immediately before the response renders to the client.
/// The response can be modified here if needed.
pub fn beforeResponse(self: *Self, request: *jetzig.http.Request, response: *jetzig.http.Response) !void {
request.server.logger.debug(
try request.server.logger.DEBUG(
"[DemoMiddleware:beforeResponse] my_custom_value: {s}, response status: {s}",
.{ self.my_custom_value, @tagName(response.status_code) },
);
@ -51,7 +54,7 @@ pub fn beforeResponse(self: *Self, request: *jetzig.http.Request, response: *jet
pub fn afterResponse(self: *Self, request: *jetzig.http.Request, response: *jetzig.http.Response) !void {
_ = self;
_ = response;
request.server.logger.debug("[DemoMiddleware:afterResponse] response completed", .{});
try request.server.logger.DEBUG("[DemoMiddleware:afterResponse] response completed", .{});
}
/// Invoked after `afterResponse` is called. Use this function to do any clean-up.

View File

@ -5,16 +5,19 @@ pub const zmpl = @import("zmpl").zmpl;
pub const http = @import("jetzig/http.zig");
pub const loggers = @import("jetzig/loggers.zig");
pub const data = @import("jetzig/data.zig");
pub const caches = @import("jetzig/caches.zig");
pub const views = @import("jetzig/views.zig");
pub const colors = @import("jetzig/colors.zig");
pub const middleware = @import("jetzig/middleware.zig");
pub const util = @import("jetzig/util.zig");
pub const types = @import("jetzig/types.zig");
/// The primary interface for a Jetzig application. Create an `App` in your application's
/// `src/main.zig` and call `start` to launch the application.
pub const App = @import("jetzig/App.zig");
/// Configuration options for the application server with command-line argument parsing.
pub const Environment = @import("jetzig/Environment.zig");
/// An HTTP request which is passed to (dynamic) view functions and provides access to params,
/// headers, and functions to render a response.
pub const Request = http.Request;
@ -43,36 +46,11 @@ pub fn init(allocator: std.mem.Allocator) !App {
const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);
const host: []const u8 = if (args.len > 1)
try allocator.dupe(u8, args[1])
else
try allocator.dupe(u8, "127.0.0.1");
// TODO: Fix this up with proper arg parsing
const port: u16 = if (args.len > 2) try std.fmt.parseInt(u16, args[2], 10) else 8080;
const use_cache: bool = args.len > 3 and std.mem.eql(u8, args[3], "--cache");
const server_cache = switch (use_cache) {
true => caches.Cache{ .memory_cache = caches.MemoryCache.init(allocator) },
false => caches.Cache{ .null_cache = caches.NullCache.init(allocator) },
};
var logger = loggers.Logger{ .development_logger = loggers.DevelopmentLogger.init(allocator) };
const secret = try generateSecret(allocator);
logger.debug(
"Running in development mode, using auto-generated cookie encryption key:\n {s}",
.{secret},
);
const server_options = http.Server.ServerOptions{
.cache = server_cache,
.logger = logger,
.secret = secret,
};
const environment = Environment.init(allocator);
return .{
.server_options = server_options,
.server_options = try environment.getServerOptions(),
.allocator = allocator,
.host = host,
.port = port,
};
}
@ -154,36 +132,3 @@ pub fn route(comptime routes: anytype) []views.Route {
return &detected;
}
pub fn generateSecret(allocator: std.mem.Allocator) ![]const u8 {
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
var secret: [64]u8 = undefined;
for (0..64) |index| {
secret[index] = chars[std.crypto.random.intRangeAtMost(u8, 0, chars.len)];
}
return try allocator.dupe(u8, &secret);
}
pub fn base64Encode(allocator: std.mem.Allocator, string: []const u8) ![]const u8 {
const encoder = std.base64.Base64Encoder.init(
std.base64.url_safe_no_pad.alphabet_chars,
std.base64.url_safe_no_pad.pad_char,
);
const size = encoder.calcSize(string.len);
const ptr = try allocator.alloc(u8, size);
_ = encoder.encode(ptr, string);
return ptr;
}
pub fn base64Decode(allocator: std.mem.Allocator, string: []const u8) ![]const u8 {
const decoder = std.base64.Base64Decoder.init(
std.base64.url_safe_no_pad.alphabet_chars,
std.base64.url_safe_no_pad.pad_char,
);
const size = try decoder.calcSizeForSlice(string);
const ptr = try allocator.alloc(u8, size);
try decoder.decode(ptr, string);
return ptr;
}

View File

@ -1,5 +1,7 @@
const std = @import("std");
const args = @import("args");
const jetzig = @import("../jetzig.zig");
const mime_types = @import("mime_types").mime_types; // Generated at build time.
@ -7,8 +9,6 @@ const Self = @This();
server_options: jetzig.http.Server.ServerOptions,
allocator: std.mem.Allocator,
host: []const u8,
port: u16,
pub fn deinit(self: Self) void {
_ = self;
@ -48,30 +48,40 @@ pub fn start(self: Self, comptime_routes: []jetzig.views.Route) !void {
self.allocator.destroy(route);
};
if (self.server_options.detach) {
const argv = try std.process.argsAlloc(self.allocator);
defer std.process.argsFree(self.allocator, argv);
var child_argv = std.ArrayList([]const u8).init(self.allocator);
for (argv) |arg| {
if (!std.mem.eql(u8, "-d", arg) and !std.mem.eql(u8, "--detach", arg)) {
try child_argv.append(arg);
}
}
var child = std.process.Child.init(child_argv.items, self.allocator);
try child.spawn();
std.debug.print("Spawned child process. PID: {}. Exiting.\n", .{child.id});
std.process.exit(0);
}
var server = jetzig.http.Server.init(
self.allocator,
self.host,
self.port,
self.server_options,
routes.items,
&mime_map,
);
defer server.deinit();
defer self.allocator.free(self.host);
defer self.allocator.free(server.options.secret);
server.listen() catch |err| {
switch (err) {
error.AddressInUse => {
server.logger.debug(
try server.logger.ERROR(
"Socket unavailable: {s}:{} - unable to start server.\n",
.{ self.host, self.port },
.{ self.server_options.bind, self.server_options.port },
);
return;
},
else => {
server.logger.debug("Encountered error: {}\nExiting.\n", .{err});
try server.logger.ERROR("Encountered error: {}\nExiting.\n", .{err});
return err;
},
}

124
src/jetzig/Environment.zig Normal file
View File

@ -0,0 +1,124 @@
const std = @import("std");
const args = @import("args");
const jetzig = @import("../jetzig.zig");
const Environment = @This();
allocator: std.mem.Allocator,
const Options = struct {
help: bool = false,
bind: []const u8 = "127.0.0.1",
port: u16 = 8080,
environment: []const u8 = "development",
log: []const u8 = "-",
@"log-error": []const u8 = "-",
@"log-level": jetzig.loggers.LogLevel = .DEBUG,
detach: bool = false,
pub const shorthands = .{
.h = "help",
.b = "bind",
.p = "port",
.e = "environment",
.d = "detach",
};
pub const wrap_len = 80;
pub const meta = .{
.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: -)",
.@"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.
,
.@"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)
,
.detach =
\\Run the server in the background. Must be used in conjunction with --log (default: false)
,
.help = "Print help and exit",
},
};
};
pub fn init(allocator: std.mem.Allocator) Environment {
return .{ .allocator = allocator };
}
/// Generate server initialization options using command line args with defaults.
pub fn getServerOptions(self: Environment) !jetzig.http.Server.ServerOptions {
const options = try args.parseForCurrentProcess(Options, self.allocator, .print);
if (options.options.help) {
const writer = std.io.getStdErr().writer();
try args.printHelp(Options, options.executable_name orelse "<app-name>", writer);
std.process.exit(0);
}
var logger = jetzig.loggers.Logger{
.development_logger = jetzig.loggers.DevelopmentLogger.init(
self.allocator,
try getLogFile(options.options.log),
),
};
if (options.options.detach and std.mem.eql(u8, options.options.log, "-")) {
try logger.ERROR("Must pass `--log` when using `--detach`.", .{});
std.process.exit(1);
}
// TODO: Generate nonce per session - do research to confirm correct best practice.
const secret_len = jetzig.http.Session.Cipher.key_length + jetzig.http.Session.Cipher.nonce_length;
const secret = try self.getSecret(&logger, secret_len);
if (secret.len != secret_len) {
try logger.ERROR("Expected secret length: {}, found: {}.", .{ secret_len, secret.len });
try logger.ERROR("Use `jetzig generate secret` to create a secure secret value.", .{});
std.process.exit(1);
}
return .{
.logger = logger,
.secret = secret,
.bind = try self.allocator.dupe(u8, options.options.bind),
.port = options.options.port,
.detach = options.options.detach,
};
}
fn getLogFile(path: []const u8) !std.fs.File {
if (std.mem.eql(u8, path, "-")) return std.io.getStdOut();
const file = try std.fs.createFileAbsolute(path, .{ .truncate = false });
try file.seekFromEnd(0);
return file;
}
fn getSecret(self: Environment, logger: *jetzig.loggers.Logger, comptime len: u10) ![]const u8 {
return std.process.getEnvVarOwned(self.allocator, "JETZIG_SECRET") catch |err| {
switch (err) {
error.EnvironmentVariableNotFound => {
// TODO: Make this a failure when running in non-development mode.
const secret = try jetzig.util.generateSecret(self.allocator, len);
try logger.WARN(
"Running in development mode, using auto-generated cookie encryption key: {s}",
.{secret},
);
try logger.WARN(
"Run `jetzig generate secret` and set `JETZIG_SECRET` to remove this warning.",
.{},
);
return secret;
},
else => return err,
}
};
}

View File

@ -1,30 +0,0 @@
const std = @import("std");
const http = @import("http.zig");
pub const Result = @import("caches/Result.zig");
pub const MemoryCache = @import("caches/MemoryCache.zig");
pub const NullCache = @import("caches/NullCache.zig");
pub const Cache = union(enum) {
memory_cache: MemoryCache,
null_cache: NullCache,
pub fn deinit(self: *Cache) void {
switch (self.*) {
inline else => |*case| case.deinit(),
}
}
pub fn get(self: *Cache, key: []const u8) ?Result {
return switch (self.*) {
inline else => |*case| case.get(key),
};
}
pub fn put(self: *Cache, key: []const u8, value: http.Response) !Result {
return switch (self.*) {
inline else => |*case| case.put(key, value),
};
}
};

View File

@ -1,5 +0,0 @@
const std = @import("std");
pub const Result = @import("Result.zig");
pub const MemoryCache = @import("MemoryCache.zig");
pub const NullCache = @import("NullCache.zig");

View File

@ -1,39 +0,0 @@
const std = @import("std");
const http = @import("../http.zig");
const Result = @import("Result.zig");
allocator: std.mem.Allocator,
cache: std.StringHashMap(http.Response),
const Self = @This();
pub fn init(allocator: std.mem.Allocator) Self {
const cache = std.StringHashMap(http.Response).init(allocator);
return .{ .allocator = allocator, .cache = cache };
}
pub fn deinit(self: *Self) void {
var iterator = self.cache.keyIterator();
while (iterator.next()) |key| {
self.allocator.free(key.*);
}
self.cache.deinit();
}
pub fn get(self: *Self, key: []const u8) ?Result {
if (self.cache.get(key)) |value| {
return Result.init(self.allocator, value, true);
} else {
return null;
}
}
pub fn put(self: *Self, key: []const u8, value: http.Response) !Result {
const key_dupe = try self.allocator.dupe(u8, key);
const value_dupe = try value.dupe();
try self.cache.put(key_dupe, value_dupe);
return Result.init(self.allocator, value_dupe, true);
}

View File

@ -1,27 +0,0 @@
const std = @import("std");
const http = @import("../http.zig");
const Result = @import("Result.zig");
const Self = @This();
allocator: std.mem.Allocator,
pub fn init(allocator: std.mem.Allocator) Self {
return Self{ .allocator = allocator };
}
pub fn deinit(self: *const Self) void {
_ = self;
}
pub fn get(self: *const Self, key: []const u8) ?Result {
_ = key;
_ = self;
return null;
}
pub fn put(self: *const Self, key: []const u8, value: http.Response) !Result {
_ = key;
return Result{ .value = value, .cached = false, .allocator = self.allocator };
}

View File

@ -1,17 +0,0 @@
const std = @import("std");
const Self = @This();
const jetzig = @import("../../jetzig.zig");
value: jetzig.http.Response,
cached: bool,
allocator: std.mem.Allocator,
pub fn init(allocator: std.mem.Allocator, value: jetzig.http.Response, cached: bool) Self {
return .{ .allocator = allocator, .cached = cached, .value = value };
}
pub fn deinit(self: *const Self) void {
if (!self.cached) self.value.deinit();
}

View File

@ -101,7 +101,7 @@ pub fn process(self: *Self) !void {
self.session.parse() catch |err| {
switch (err) {
error.JetzigInvalidSessionCookie => {
self.server.logger.debug("Invalid session cookie detected. Resetting session.", .{});
try self.server.logger.DEBUG("Invalid session cookie detected. Resetting session.", .{});
try self.session.reset();
},
else => return err,
@ -310,7 +310,9 @@ pub fn hash(self: *Self) ![]const u8 {
);
}
pub fn fmtMethod(self: *Self) []const u8 {
pub fn fmtMethod(self: *Self, colorized: bool) []const u8 {
if (!colorized) return @tagName(self.method);
return switch (self.method) {
.GET => jetzig.colors.cyan("GET"),
.PUT => jetzig.colors.yellow("PUT"),

View File

@ -11,37 +11,32 @@ else
struct {};
pub const ServerOptions = struct {
cache: jetzig.caches.Cache,
logger: jetzig.loggers.Logger,
bind: []const u8,
port: u16,
secret: []const u8,
detach: bool,
};
allocator: std.mem.Allocator,
port: u16,
host: []const u8,
cache: jetzig.caches.Cache,
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,
initialized: bool = false,
const Self = @This();
pub fn init(
allocator: std.mem.Allocator,
host: []const u8,
port: u16,
options: ServerOptions,
routes: []*jetzig.views.Route,
mime_map: *jetzig.http.mime.MimeMap,
) Self {
return .{
.allocator = allocator,
.host = host,
.port = port,
.cache = options.cache,
.logger = options.logger,
.options = options,
.routes = routes,
@ -50,15 +45,18 @@ pub fn init(
}
pub fn deinit(self: *Self) void {
self.std_net_server.deinit();
if (self.initialized) self.std_net_server.deinit();
self.allocator.free(self.options.secret);
self.allocator.free(self.options.bind);
}
pub fn listen(self: *Self) !void {
const address = try std.net.Address.parseIp("127.0.0.1", 8080);
const address = try std.net.Address.parseIp(self.options.bind, self.options.port);
self.std_net_server = try address.listen(.{ .reuse_port = true });
const cache_status = if (self.options.cache == .null_cache) "disabled" else "enabled";
self.logger.debug("Listening on http://{s}:{} [cache:{s}]", .{ self.host, self.port, cache_status });
self.initialized = true;
try self.logger.INFO("Listening on http://{s}:{}", .{ self.options.bind, self.options.port });
try self.processRequests();
}
@ -77,7 +75,6 @@ fn processRequests(self: *Self) !void {
self.processNextRequest(allocator, &std_http_server) catch |err| {
if (isBadHttpError(err)) {
std.debug.print("Encountered HTTP error: {s}\n", .{@errorName(err)});
std_http_server.connection.stream.close();
continue;
} else return err;
@ -113,7 +110,7 @@ fn processNextRequest(self: *Self, allocator: std.mem.Allocator, std_http_server
const log_message = try self.requestLogMessage(&request);
defer self.allocator.free(log_message);
self.logger.debug("{s}", .{log_message});
try self.logger.INFO("{s}", .{log_message});
}
fn renderResponse(self: *Self, request: *jetzig.http.Request) !void {
@ -210,7 +207,7 @@ fn renderView(
// `return request.render(.ok)`, but the actual rendered view is stored in
// `request.rendered_view`.
_ = route.render(route.*, request) catch |err| {
self.logger.debug("Encountered error: {s}", .{@errorName(err)});
try self.logger.ERROR("Encountered error: {s}", .{@errorName(err)});
if (isUnhandledError(err)) return err;
if (isBadRequest(err)) return try self.renderBadRequest(request);
return try self.renderInternalServerError(request, err);
@ -230,7 +227,7 @@ fn renderView(
return .{ .view = rendered_view, .content = "" };
}
} else {
self.logger.debug("`request.render` was not invoked. Rendering empty content.", .{});
try self.logger.WARN("`request.render` was not invoked. Rendering empty content.", .{});
request.response_data.reset();
return .{
.view = .{ .data = request.response_data, .status_code = .no_content },
@ -254,7 +251,7 @@ fn renderTemplateWithLayout(
if (zmpl.manifest.find(prefixed_name)) |layout| {
return try template.renderWithLayout(layout, view.data);
} else {
self.logger.debug("Unknown layout: {s}", .{layout_name});
try self.logger.WARN("Unknown layout: {s}", .{layout_name});
return try template.render(view.data);
}
} else return try template.render(view.data);
@ -299,7 +296,7 @@ fn renderInternalServerError(self: *Self, request: *jetzig.http.Request, err: an
try object.put("error", request.response_data.string(@errorName(err)));
const stack = @errorReturnTrace();
if (stack) |capture| try self.logStackTrace(capture, request, object);
if (stack) |capture| try self.logStackTrace(capture, request);
return .{
.view = jetzig.views.View{ .data = request.response_data, .status_code = .internal_server_error },
@ -324,16 +321,13 @@ fn logStackTrace(
self: *Self,
stack: *std.builtin.StackTrace,
request: *jetzig.http.Request,
object: *jetzig.data.Value,
) !void {
_ = self;
std.debug.print("\nStack Trace:\n{}", .{stack});
var array = std.ArrayList(u8).init(request.allocator);
const writer = array.writer();
try self.logger.ERROR("\nStack Trace:\n{}", .{stack});
var buf = std.ArrayList(u8).init(request.allocator);
defer buf.deinit();
const writer = buf.writer();
try stack.format("", .{}, writer);
// TODO: Generate an array of objects with stack trace in useful data structure instead of
// dumping the whole formatted backtrace as a JSON string:
try object.put("backtrace", request.response_data.string(array.items));
try self.logger.ERROR("{s}", .{buf.items});
}
fn requestLogMessage(self: *Self, request: *jetzig.http.Request) ![]const u8 {
@ -345,13 +339,16 @@ fn requestLogMessage(self: *Self, request: *jetzig.http.Request) ![]const u8 {
),
};
const formatted_duration = try jetzig.colors.duration(self.allocator, self.duration());
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(),
status.format(),
request.fmtMethod(self.logger.isColorized()),
status.format(self.logger.isColorized()),
request.path.path,
});
}

View File

@ -3,7 +3,7 @@ const std = @import("std");
const jetzig = @import("../../jetzig.zig");
pub const cookie_name = "_jetzig-session";
const Cipher = std.crypto.aead.aes_gcm.Aes256Gcm;
pub const Cipher = std.crypto.aead.aes_gcm.Aes256Gcm;
allocator: std.mem.Allocator,
encryption_key: ?[]const u8,
@ -99,7 +99,7 @@ fn save(self: *Self) !void {
}
self.encrypted = try self.encrypt(json);
const encoded = try jetzig.base64Encode(self.allocator, self.encrypted.?);
const encoded = try jetzig.util.base64Encode(self.allocator, self.encrypted.?);
defer self.allocator.free(encoded);
if (self.cookie) |*ptr| self.allocator.free(ptr.*.value);
@ -113,7 +113,7 @@ fn save(self: *Self) !void {
fn parseSessionCookie(self: *Self, cookie_value: []const u8) !void {
self.data = jetzig.data.Data.init(self.allocator);
const decoded = try jetzig.base64Decode(self.allocator, cookie_value);
const decoded = try jetzig.util.base64Decode(self.allocator, cookie_value);
defer self.allocator.free(decoded);
const buf = self.decrypt(decoded) catch |err| {

View File

@ -73,11 +73,12 @@ pub fn StatusCodeType(comptime code: []const u8, comptime message: []const u8) t
const Self = @This();
pub fn format(self: Self) []const u8 {
pub fn format(self: Self, colorized: bool) []const u8 {
_ = self;
const full_message = code ++ " " ++ message;
if (!colorized) return full_message;
if (std.mem.startsWith(u8, code, "2")) {
return jetzig.colors.green(full_message);
} else if (std.mem.startsWith(u8, code, "3")) {
@ -158,9 +159,9 @@ pub const TaggedStatusCode = union(StatusCode) {
const Self = @This();
pub fn format(self: Self) []const u8 {
pub fn format(self: Self, colorized: bool) []const u8 {
return switch (self) {
inline else => |capture| capture.format(),
inline else => |capture| capture.format(colorized),
};
}
};

View File

@ -4,18 +4,56 @@ const Self = @This();
pub const DevelopmentLogger = @import("loggers/DevelopmentLogger.zig");
const LogLevel = enum {
debug,
};
pub const LogLevel = enum { TRACE, DEBUG, INFO, WARN, ERROR, FATAL };
pub const Logger = union(enum) {
development_logger: DevelopmentLogger,
pub fn debug(self: *Logger, comptime message: []const u8, args: anytype) void {
pub fn isColorized(self: Logger) bool {
switch (self) {
inline else => |logger| return logger.isColorized(),
}
}
/// 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 => |*case| case.debug(message, args) catch |err| {
std.debug.print("{}\n", .{err});
},
inline else => |*logger| try logger.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),
}
}
/// 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),
}
}
/// 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),
}
}
/// 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),
}
}
/// 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),
}
}
};

View File

@ -1,19 +1,74 @@
const std = @import("std");
const jetzig = @import("../../jetzig.zig");
const Self = @This();
const Timestamp = @import("../types/Timestamp.zig");
const Timestamp = jetzig.types.Timestamp;
const LogLevel = jetzig.loggers.LogLevel;
allocator: std.mem.Allocator,
file: std.fs.File,
colorized: bool,
pub fn init(allocator: std.mem.Allocator) Self {
return .{ .allocator = allocator };
pub fn init(allocator: std.mem.Allocator, file: std.fs.File) Self {
return .{ .allocator = allocator, .file = file, .colorized = file.isTty() };
}
pub fn debug(self: *Self, comptime message: []const u8, args: anytype) !void {
/// 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;
}
/// 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);
std.debug.print("[{s}] {s}\n", .{ iso8601, output });
const writer = self.file.writer();
const level_formatted = if (self.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();
}
fn colorizedLogLevel(comptime level: LogLevel) []const u8 {
return switch (level) {
.TRACE => jetzig.colors.white(@tagName(level)),
.DEBUG => jetzig.colors.cyan(@tagName(level)),
.INFO => jetzig.colors.blue(@tagName(level) ++ " "),
.WARN => jetzig.colors.yellow(@tagName(level) ++ " "),
.ERROR => jetzig.colors.red(@tagName(level)),
.FATAL => jetzig.colors.red(@tagName(level)),
};
}

View File

@ -16,7 +16,7 @@ pub fn init(request: *jetzig.http.Request) !*Self {
pub fn afterRequest(self: *Self, request: *jetzig.http.Request) !void {
_ = self;
if (request.getHeader("HX-Target")) |target| {
request.server.logger.debug(
try request.server.logger.DEBUG(
"[middleware-htmx] htmx request detected, disabling layout. (#{s})",
.{target},
);

View File

@ -1,5 +1,6 @@
const std = @import("std");
/// Compare two strings with case-insensitive matching.
pub fn equalStringsCaseInsensitive(expected: []const u8, actual: []const u8) bool {
if (expected.len != actual.len) return false;
for (expected, actual) |expected_char, actual_char| {
@ -7,3 +8,39 @@ pub fn equalStringsCaseInsensitive(expected: []const u8, actual: []const u8) boo
}
return true;
}
/// Encode arbitrary input to Base64.
pub fn base64Encode(allocator: std.mem.Allocator, string: []const u8) ![]const u8 {
const encoder = std.base64.Base64Encoder.init(
std.base64.url_safe_no_pad.alphabet_chars,
std.base64.url_safe_no_pad.pad_char,
);
const size = encoder.calcSize(string.len);
const ptr = try allocator.alloc(u8, size);
_ = encoder.encode(ptr, string);
return ptr;
}
/// Decode arbitrary input from Base64.
pub fn base64Decode(allocator: std.mem.Allocator, string: []const u8) ![]const u8 {
const decoder = std.base64.Base64Decoder.init(
std.base64.url_safe_no_pad.alphabet_chars,
std.base64.url_safe_no_pad.pad_char,
);
const size = try decoder.calcSizeForSlice(string);
const ptr = try allocator.alloc(u8, size);
try decoder.decode(ptr, string);
return ptr;
}
/// Generate a secure random string of `len` characters (for cryptographic purposes).
pub fn generateSecret(allocator: std.mem.Allocator, comptime len: u10) ![]const u8 {
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
var secret: [len]u8 = undefined;
for (0..len) |index| {
secret[index] = chars[std.crypto.random.intRangeAtMost(u8, 0, chars.len)];
}
return try allocator.dupe(u8, &secret);
}