Merge pull request #29 from jetzig-framework/deployment

Deployment
This commit is contained in:
bobf 2024-03-20 23:44:20 +00:00 committed by GitHub
commit bf62fdcf5b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 819 additions and 333 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/MasterQ32/zig-args/archive/01d72b9a0128c474aeeb9019edd48605fa6d95f7.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/MasterQ32/zig-args/archive/01d72b9a0128c474aeeb9019edd48605fa6d95f7.tar.gz",
.hash = "12208a1de366740d11de525db7289345949f5fd46527db3f89eecc7bb49b012c0732",
},
},
.paths = .{

View File

@ -4,6 +4,7 @@ const init = @import("commands/init.zig");
const update = @import("commands/update.zig");
const generate = @import("commands/generate.zig");
const server = @import("commands/server.zig");
const bundle = @import("commands/bundle.zig");
const Options = struct {
help: bool = false,
@ -19,6 +20,7 @@ const Options = struct {
.update = "Update current project to latest version of Jetzig",
.generate = "Generate scaffolding",
.server = "Run a development server",
.bundle = "Create a deployment bundle",
.help = "Print help and exit",
},
};
@ -29,8 +31,10 @@ const Verb = union(enum) {
update: update.Options,
generate: generate.Options,
server: server.Options,
bundle: bundle.Options,
g: generate.Options,
s: server.Options,
b: bundle.Options,
};
/// Main entrypoint for `jetzig` executable. Parses command line args and generates a new
@ -52,7 +56,7 @@ pub fn main() !void {
}
};
if (options.options.help or options.verb == null) {
if ((!options.options.help and options.verb == null) or (options.options.help and options.verb == null)) {
try args.printHelp(Options, "jetzig", writer);
try writer.writeAll(
\\
@ -62,6 +66,7 @@ pub fn main() !void {
\\ update Update current project to latest version of Jetzig.
\\ generate Generate scaffolding.
\\ server Run a development server.
\\ bundle Create a deployment bundle.
\\
\\ Pass --help to any command for more information, e.g. `jetzig init --help`
\\
@ -100,6 +105,13 @@ fn run(allocator: std.mem.Allocator, options: args.ParseArgsResult(Options, Verb
options.positionals,
.{ .help = options.options.help },
),
.b, .bundle => |opts| bundle.run(
allocator,
opts,
writer,
options.positionals,
.{ .help = options.options.help },
),
};
}
}

139
cli/commands/bundle.zig Normal file
View File

@ -0,0 +1,139 @@
const std = @import("std");
const builtin = @import("builtin");
const args = @import("args");
const util = @import("../util.zig");
/// Command line options for the `bundle` command.
pub const Options = struct {
optimize: enum { Debug, ReleaseFast, ReleaseSmall } = .ReleaseFast,
arch: enum { x86_64, aarch64, default } = .default,
os: enum { linux, macos, windows, default } = .default,
pub const meta = .{
.full_text =
\\Creates a deployment bundle.
\\
\\On Windows, `tar.exe` is used to generate a `.zip` file.
\\
\\On other operating systems, `tar` is used to generate a `.tar.gz` file.
\\
\\The deployment bundle contains a compiled executable with the `public/` and `static/`
\\directories included. This bundle can be copied to a deployment server, unpacked, and
\\launched in place.
,
.option_docs = .{
.optimize = "Set optimization level, must be one of { Debug, ReleaseFast, ReleaseSmall } (default: ReleaseFast)",
.arch = "Set build target CPU architecture, must be one of { x86_64, aarch64 } (default: Current CPU arch)",
.os = "Set build target operating system, must be one of { linux, macos, windows } (default: Current OS)",
},
};
};
/// Run the deployment bundle generator. Create an archive containing the Jetzig executable,
/// with `public/` and `static/` directories.
pub fn run(
allocator: std.mem.Allocator,
options: Options,
writer: anytype,
positionals: [][]const u8,
other_options: struct { help: bool },
) !void {
_ = positionals;
if (other_options.help) {
try args.printHelp(Options, "jetzig bundle", writer);
return;
}
std.debug.print("Compiling bundle...\n", .{});
var cwd = try util.detectJetzigProjectDir();
defer cwd.close();
const path = try cwd.realpathAlloc(allocator, ".");
defer allocator.free(path);
if (try util.locateExecutable(allocator, cwd, .{ .relative = true })) |executable| {
defer allocator.free(executable);
var tar_argv = std.ArrayList([]const u8).init(allocator);
defer tar_argv.deinit();
var install_argv = std.ArrayList([]const u8).init(allocator);
defer install_argv.deinit();
try install_argv.appendSlice(&[_][]const u8{ "zig", "build" });
switch (builtin.os.tag) {
.windows => try tar_argv.appendSlice(&[_][]const u8{
"tar.exe",
"-a",
"-c",
"-f",
"bundle.zip",
executable,
}),
else => try tar_argv.appendSlice(&[_][]const u8{
"tar",
"--transform=s,^,jetzig/,",
"--transform=s,^jetzig/zig-out/bin/,jetzig/,",
"-zcf",
"bundle.tar.gz",
executable,
}),
}
switch (options.optimize) {
.ReleaseFast => try install_argv.append("-Doptimize=ReleaseFast"),
.ReleaseSmall => try install_argv.append("-Doptimize=ReleaseSmall"),
.Debug => try install_argv.append("-Doptimize=Debug"),
}
var target_buf = std.ArrayList([]const u8).init(allocator);
defer target_buf.deinit();
try target_buf.append("-Dtarget=");
switch (options.arch) {
.x86_64 => try target_buf.append("x86_64"),
.aarch64 => try target_buf.append("aarch64"),
.default => try target_buf.append(@tagName(builtin.cpu.arch)),
}
try target_buf.append("-");
switch (options.os) {
.linux => try target_buf.append("linux"),
.macos => try target_buf.append("macos"),
.windows => try target_buf.append("windows"),
.default => try target_buf.append(@tagName(builtin.os.tag)),
}
const target = try std.mem.concat(allocator, u8, target_buf.items);
defer allocator.free(target);
try install_argv.append(target);
try install_argv.append("install");
var public_dir: ?std.fs.Dir = cwd.openDir("public", .{}) catch null;
defer if (public_dir) |*dir| dir.close();
var static_dir: ?std.fs.Dir = cwd.openDir("static", .{}) catch null;
defer if (static_dir) |*dir| dir.close();
if (public_dir != null) try tar_argv.append("public");
if (static_dir != null) try tar_argv.append("static");
try util.runCommand(allocator, path, install_argv.items);
try util.runCommand(allocator, path, tar_argv.items);
switch (builtin.os.tag) {
.windows => std.debug.print("Bundle `bundle.zip` generated successfully.", .{}),
else => std.debug.print("Bundle `bundle.tar.gz` generated successfully.", .{}),
}
util.printSuccess();
} else {
std.debug.print("Unable to locate compiled executable. Exiting.", .{});
util.printFailure();
std.os.exit(1);
}
}

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

@ -1,11 +1,12 @@
const std = @import("std");
const args = @import("args");
const util = @import("../util.zig");
const builtin = @import("builtin");
pub const watch_changes_pause_duration = 1 * 1000 * 1000 * 1000;
/// Command line options for the `update` command.
/// Command line options for the `server` command.
pub const Options = struct {
reload: bool = true,
@ -68,7 +69,7 @@ pub fn run(
&[_][]const u8{ "zig", "build", "-Djetzig_runner=true", "install" },
);
const exe_path = try locateExecutable(allocator, cwd);
const exe_path = try util.locateExecutable(allocator, cwd, .{});
if (exe_path == null) {
std.debug.print("Unable to locate compiled executable. Exiting.\n", .{});
std.os.exit(1);
@ -137,34 +138,3 @@ fn totalMtime(allocator: std.mem.Allocator, cwd: std.fs.Dir, sub_path: []const u
return sum;
}
fn locateExecutable(allocator: std.mem.Allocator, dir: std.fs.Dir) !?[]const u8 {
const file = dir.openFile(".jetzig", .{}) catch |err| {
switch (err) {
error.FileNotFound => return null,
else => return err,
}
};
const content = try file.readToEndAlloc(allocator, 1024);
defer allocator.free(content);
const exe_name = util.strip(content);
const suffix = if (builtin.os.tag == .windows) ".exe" else "";
const full_name = try std.mem.concat(allocator, u8, &[_][]const u8{ exe_name, suffix });
defer allocator.free(full_name);
// XXX: Will fail if user sets a custom install path.
var bin_dir = try dir.openDir("zig-out/bin", .{ .iterate = true });
defer bin_dir.close();
var walker = try bin_dir.walk(allocator);
defer walker.deinit();
while (try walker.next()) |entry| {
if (entry.kind == .file and std.mem.eql(u8, entry.path, full_name)) {
return try bin_dir.realpathAlloc(allocator, entry.path);
}
}
return null;
}

View File

@ -1,4 +1,5 @@
const std = @import("std");
const builtin = @import("builtin");
/// Decode a base64 string, used for parsing out build artifacts generated by the CLI program's
/// build.zig which are stored in the executable as a module.
@ -167,3 +168,43 @@ pub fn githubUrl(allocator: std.mem.Allocator) ![]const u8 {
},
);
}
/// Attempt to locate the main application executable in `zig-out/bin/`
pub fn locateExecutable(
allocator: std.mem.Allocator,
dir: std.fs.Dir,
options: struct { relative: bool = false },
) !?[]const u8 {
const file = dir.openFile(".jetzig", .{}) catch |err| {
switch (err) {
error.FileNotFound => return null,
else => return err,
}
};
const content = try file.readToEndAlloc(allocator, 1024);
defer allocator.free(content);
const exe_name = strip(content);
const suffix = if (builtin.os.tag == .windows) ".exe" else "";
const full_name = try std.mem.concat(allocator, u8, &[_][]const u8{ exe_name, suffix });
defer allocator.free(full_name);
// XXX: Will fail if user sets a custom install path.
var bin_dir = try dir.openDir("zig-out/bin", .{ .iterate = true });
defer bin_dir.close();
var walker = try bin_dir.walk(allocator);
defer walker.deinit();
while (try walker.next()) |entry| {
if (entry.kind == .file and std.mem.eql(u8, entry.path, full_name)) {
if (options.relative) {
return try std.fs.path.join(allocator, &[_][]const u8{ "zig-out", "bin", entry.path });
} else {
return try bin_dir.realpathAlloc(allocator, entry.path);
}
}
}
return null;
}

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

@ -3,13 +3,33 @@ 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 {
/// Middleware chain. Add any custom middleware here, or use middleware provided in
/// `jetzig.middleware` (e.g. `jetzig.middleware.HtmxMiddleware`).
pub const middleware: []const type = &.{
// htmx middleware skips layouts when `HX-Target` header is present and issues
// `HX-Redirect` instead of a regular HTTP redirect when `request.redirect` is called.
jetzig.middleware.HtmxMiddleware,
// Demo middleware included with new projects. Remove once you are familiar with Jetzig's
// middleware system.
@import("app/middleware/DemoMiddleware.zig"),
};
// 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_public_content: usize = std.math.pow(usize, 2, 20);
// Maximum filesize for `static/` content (applies only to apps using `jetzig.http.StaticRequest`).
// pub const max_bytes_static_content: usize = std.math.pow(usize, 2, 18);
// Path relative to cwd() to serve public content from. Symlinks are not followed.
// pub const public_content_path = "public";
// 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 fn main() !void {

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;
@ -32,47 +35,60 @@ 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);
pub const max_bytes_static_content: usize = std.math.pow(usize, 2, 16);
/// Maximum filesize for `public/` content.
pub const max_bytes_public_content: usize = std.math.pow(usize, 2, 20);
/// Maximum filesize for `static/` content (applies only to apps using `jetzig.http.StaticRequest`).
pub const max_bytes_static_content: usize = std.math.pow(usize, 2, 18);
/// 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);
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 +170,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;
},
}

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

@ -0,0 +1,154 @@
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,
// TODO:
// environment: []const u8 = "development",
log: []const u8 = "-",
@"log-error": []const u8 = "-",
@"log-level": jetzig.loggers.LogLevel = .INFO,
@"log-format": jetzig.loggers.LogFormat = .development,
detach: bool = false,
pub const shorthands = .{
.h = "help",
.b = "bind",
.p = "port",
// TODO:
// .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)",
// 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 stderr. If omitted, errors are logged to the location specified by the `log` option (or stderr if `log` is '-').
,
.@"log-level" =
\\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)
,
.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 = switch (options.options.@"log-format") {
.development => jetzig.loggers.Logger{
.development_logger = jetzig.loggers.DevelopmentLogger.init(
self.allocator,
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, "-")) {
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(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 => if (std.mem.eql(u8, options.log, "-"))
std.io.getStdErr()
else
try std.fs.createFileAbsolute(options.log, .{ .truncate = false }),
};
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

@ -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,
};
}
@ -101,7 +104,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,
@ -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,9 @@ pub fn hash(self: *Self) ![]const u8 {
);
}
pub fn fmtMethod(self: *Self) []const u8 {
pub fn fmtMethod(self: *const 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

@ -3,45 +3,32 @@ 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 {
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 +37,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();
}
@ -71,13 +61,12 @@ 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();
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;
@ -89,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();
@ -111,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);
self.logger.debug("{s}", .{log_message});
try self.logger.logRequest(&request);
}
fn renderResponse(self: *Self, request: *jetzig.http.Request) !void {
@ -210,7 +197,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 +217,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 +241,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 +286,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,40 +311,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));
}
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 = try jetzig.colors.duration(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.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 {
@ -398,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) {
@ -418,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_public_content"),
);
const extension = std.fs.path.extension(file.path);
const mime_type = if (self.mime_map.get(extension)) |mime| mime else "application/octet-stream";
@ -450,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,

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

@ -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);

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,15 @@ 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),
};
}
pub fn getCode(self: Self) []const u8 {
return switch (self) {
inline else => |capture| capture.code,
};
}
};

View File

@ -1,21 +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");
const LogLevel = enum {
debug,
};
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,
json_logger: JsonLogger,
pub fn debug(self: *Logger, comptime message: []const u8, args: anytype) void {
/// 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.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.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.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.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.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.log(.FATAL, message, args),
}
}
pub fn logRequest(self: *const Logger, request: *const jetzig.http.Request) !void {
switch (self.*) {
inline else => |*logger| try logger.logRequest(request),
}
}
};

View File

@ -1,19 +1,105 @@
const std = @import("std");
const Self = @This();
const Timestamp = @import("../types/Timestamp.zig");
const jetzig = @import("../../jetzig.zig");
const DevelopmentLogger = @This();
const Timestamp = jetzig.types.Timestamp;
const LogLevel = jetzig.loggers.LogLevel;
allocator: std.mem.Allocator,
stdout: std.fs.File,
stderr: std.fs.File,
stdout_colorized: bool,
stderr_colorized: bool,
level: LogLevel,
pub fn init(allocator: std.mem.Allocator) Self {
return .{ .allocator = allocator };
/// 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(),
};
}
pub fn debug(self: *Self, comptime message: []const u8, args: anytype) !void {
/// 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;
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 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 (!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,
"{}",
.{std.fmt.fmtDurationSigned(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 {
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

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

View File

@ -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,44 @@ 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);
}
/// 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);
}