Simplify DevelopmentLogger, add ProductionLogger

Add auth helper to create a user from CLI:

```
jetzig auth user:create user@example.com
```
This commit is contained in:
Bob Farrell 2024-11-11 22:25:35 +00:00
parent d219a3ce83
commit a6d1b92f5e
16 changed files with 441 additions and 87 deletions

View File

@ -127,8 +127,6 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
return error.ZmplVersionNotSupported; return error.ZmplVersionNotSupported;
} }
_ = b.option([]const u8, "seed", "Internal test seed");
const target = b.host; const target = b.host;
const optimize = exe.root_module.optimize orelse .Debug; const optimize = exe.root_module.optimize orelse .Debug;
@ -333,6 +331,25 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
.optimize = optimize, .optimize = optimize,
}); });
const auth_user_create_step = b.step("jetzig:auth:user:create", "List all routes in your app");
const exe_auth = b.addExecutable(.{
.name = "auth",
.root_source_file = jetzig_dep.path("src/commands/auth.zig"),
.target = target,
.optimize = optimize,
});
exe_auth.root_module.addImport("jetquery", jetquery_module);
exe_auth.root_module.addImport("jetzig", jetzig_module);
exe_auth.root_module.addImport("jetcommon", jetcommon_module);
exe_auth.root_module.addImport("Schema", schema_module);
exe_auth.root_module.addImport("main", main_module);
const run_auth_user_create_cmd = b.addRunArtifact(exe_auth);
auth_user_create_step.dependOn(&run_auth_user_create_cmd.step);
run_auth_user_create_cmd.addArg("user:create");
if (b.option([]const u8, "auth_username", "Auth username")) |username| {
run_auth_user_create_cmd.addArg(username);
}
const exe_database = b.addExecutable(.{ const exe_database = b.addExecutable(.{
.name = "database", .name = "database",
.root_source_file = jetzig_dep.path("src/commands/database.zig"), .root_source_file = jetzig_dep.path("src/commands/database.zig"),

View File

@ -15,8 +15,8 @@
.hash = "12201d75d73aad5e1c996de4d5ae87a00e58479c8d469bc2eeb5fdeeac8857bc09af", .hash = "12201d75d73aad5e1c996de4d5ae87a00e58479c8d469bc2eeb5fdeeac8857bc09af",
}, },
.jetquery = .{ .jetquery = .{
.url = "https://github.com/jetzig-framework/jetquery/archive/c9df298b5a2d0713257c99d89096f6f981cbc6b6.tar.gz", .url = "https://github.com/jetzig-framework/jetquery/archive/23c9741a407b6c4f93d9d21508f6568be747bdcc.tar.gz",
.hash = "12200417dd5948bd2942c36b4965fb7b68a3e4582a7a74dc05bc30801597ace00849", .hash = "122020374e5fd67d5836c0f3d7a8f814262aa076e3b90c1043f44184da1c2997e0bb",
}, },
.jetcommon = .{ .jetcommon = .{
.url = "https://github.com/jetzig-framework/jetcommon/archive/a248776ba56d6cc2b160d593ac3305756adcd26e.tar.gz", .url = "https://github.com/jetzig-framework/jetcommon/archive/a248776ba56d6cc2b160d593ac3305756adcd26e.tar.gz",

View File

@ -1,5 +1,6 @@
const std = @import("std"); const std = @import("std");
const args = @import("args"); const args = @import("args");
const util = @import("../util.zig");
/// Command line options for the `update` command. /// Command line options for the `update` command.
pub const Options = struct { pub const Options = struct {
@ -31,9 +32,9 @@ pub fn run(
defer arena.deinit(); defer arena.deinit();
const alloc = arena.allocator(); const alloc = arena.allocator();
const Action = enum { password }; const Action = enum { user_create };
const map = std.StaticStringMap(Action).initComptime(.{ const map = std.StaticStringMap(Action).initComptime(.{
.{ "password", .password }, .{ "user:create", .user_create },
}); });
const action = if (main_options.positionals.len > 0) const action = if (main_options.positionals.len > 0)
@ -54,26 +55,19 @@ pub fn run(
break :blk error.JetzigCommandError; break :blk error.JetzigCommandError;
} else if (action) |capture| } else if (action) |capture|
switch (capture) { switch (capture) {
.password => blk: { .user_create => blk: {
if (sub_args.len < 1) { if (sub_args.len < 1) {
std.debug.print("Missing argument. Expected a password paramater.\n", .{}); std.debug.print("Missing argument. Expected an email/username parameter.\n", .{});
break :blk error.JetzigCommandError; break :blk error.JetzigCommandError;
} else { } else {
const hash = try hashPassword(alloc, sub_args[0]); try util.execCommand(allocator, &.{
try std.io.getStdOut().writer().print("Password hash: {s}\n", .{hash}); "zig",
"build",
util.environmentBuildOption(main_options.options.environment),
try std.mem.concat(allocator, u8, &.{ "-Dauth_username=", sub_args[0] }),
"jetzig:auth:user:create",
});
} }
}, },
}; };
} }
pub fn hashPassword(allocator: std.mem.Allocator, password: []const u8) ![]const u8 {
const buf = try allocator.alloc(u8, 128);
return try std.crypto.pwhash.argon2.strHash(
password,
.{
.allocator = allocator,
.params = .{ .t = 3, .m = 32, .p = 4 },
},
buf,
);
}

View File

@ -78,6 +78,16 @@ pub const jetzig_options = struct {
/// Database Schema. Set to `@import("Schema")` to load `src/app/database/Schema.zig`. /// Database Schema. Set to `@import("Schema")` to load `src/app/database/Schema.zig`.
pub const Schema = @import("Schema"); pub const Schema = @import("Schema");
/// HTTP cookie configuration
pub const cookies: jetzig.http.Cookies.CookieOptions = .{
.domain = switch (jetzig.environment) {
.development => "localhost",
.testing => "localhost",
.production => "www.example.com",
},
.path = "/",
};
/// Key-value store options. Set backend to `.file` to use a file-based store. /// Key-value store options. Set backend to `.file` to use a file-based store.
/// When using `.file` backend, you must also set `.file_options`. /// When using `.file` backend, you must also set `.file_options`.
/// The key-value store is exposed as `request.store` in views and is also available in as /// The key-value store is exposed as `request.store` in views and is also available in as

72
src/commands/auth.zig Normal file
View File

@ -0,0 +1,72 @@
const std = @import("std");
const build_options = @import("build_options");
const jetquery = @import("jetquery");
const jetzig = @import("jetzig");
const Schema = @import("Schema");
const Action = enum { @"user:create" };
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer std.debug.assert(gpa.deinit() == .ok);
const gpa_allocator = gpa.allocator();
var arena = std.heap.ArenaAllocator.init(gpa_allocator);
defer arena.deinit();
const allocator = arena.allocator();
const args = try std.process.argsAlloc(allocator);
if (args.len < 3) return error.JetzigMissingArgument;
const map = std.StaticStringMap(Action).initComptime(.{
.{ "user:create", .@"user:create" },
});
const action = map.get(args[1]) orelse return error.JetzigUnrecognizedArgument;
const env = try jetzig.Environment.init(allocator, .{ .silent = true });
switch (action) {
.@"user:create" => {
const Repo = jetzig.jetquery.Repo(jetzig.database.adapter, Schema);
var repo = try Repo.loadConfig(
allocator,
std.enums.nameCast(jetzig.jetquery.Environment, jetzig.environment),
.{ .env = try jetzig.database.repoEnv(env), .context = .cli },
);
const model = comptime jetzig.config.get(jetzig.auth.AuthOptions, "auth").user_model;
const stdin = std.io.getStdIn();
const reader = stdin.reader();
if (stdin.isTty()) std.debug.print("Enter password: ", .{});
var buf: [1024]u8 = undefined;
if (try reader.readUntilDelimiterOrEof(&buf, '\n')) |input| {
const password = std.mem.trim(u8, input, &std.ascii.whitespace);
const email = args[2];
try repo.insert(std.enums.nameCast(std.meta.DeclEnum(Schema), model), .{
.email = email,
.password_hash = try hashPassword(allocator, password),
});
std.debug.print("Created user: `{s}`.\n", .{email});
} else {
std.debug.print("Blank password. Exiting.\n", .{});
}
},
}
}
fn hashPassword(allocator: std.mem.Allocator, password: []const u8) ![]const u8 {
const buf = try allocator.alloc(u8, 128);
return try std.crypto.pwhash.argon2.strHash(
password,
.{
.allocator = allocator,
.params = .{ .t = 3, .m = 32, .p = 4 },
},
buf,
);
}

View File

@ -28,7 +28,7 @@ pub fn main() !void {
const args = try std.process.argsAlloc(allocator); const args = try std.process.argsAlloc(allocator);
if (args.len < 2) return error.JetzigMissingDatabaseArgument; if (args.len < 2) return error.JetzigMissingArgument;
const map = std.StaticStringMap(Action).initComptime(.{ const map = std.StaticStringMap(Action).initComptime(.{
.{ "migrate", .migrate }, .{ "migrate", .migrate },
@ -37,7 +37,7 @@ pub fn main() !void {
.{ "drop", .drop }, .{ "drop", .drop },
.{ "reflect", .reflect }, .{ "reflect", .reflect },
}); });
const action = map.get(args[1]) orelse return error.JetzigUnrecognizedDatabaseArgument; const action = map.get(args[1]) orelse return error.JetzigUnrecognizedArgument;
switch (action) { switch (action) {
.migrate => { .migrate => {

View File

@ -13,7 +13,7 @@ pub fn main() !void {
log("Jetzig Routes:", .{}); log("Jetzig Routes:", .{});
const environment = jetzig.Environment.init(undefined); const environment = jetzig.Environment.init(allocator, .{ .silent = true });
const initHook: ?*const fn (*jetzig.App) anyerror!void = if (@hasDecl(app, "init")) app.init else null; const initHook: ?*const fn (*jetzig.App) anyerror!void = if (@hasDecl(app, "init")) app.init else null;
inline for (routes.routes) |route| max_uri_path_len = @max(route.uri_path.len + 5, max_uri_path_len); inline for (routes.routes) |route| max_uri_path_len = @max(route.uri_path.len + 5, max_uri_path_len);

View File

@ -28,7 +28,7 @@ pub const Time = jetcommon.types.Time;
pub const Date = jetcommon.types.Date; pub const Date = jetcommon.types.Date;
pub const build_options = @import("build_options"); pub const build_options = @import("build_options");
pub const environment = build_options.environment; pub const environment = std.enums.nameCast(Environment.EnvironmentName, build_options.environment);
/// The primary interface for a Jetzig application. Create an `App` in your application's /// The primary interface for a Jetzig application. Create an `App` in your application's
/// `src/main.zig` and call `start` to launch the application. /// `src/main.zig` and call `start` to launch the application.
@ -85,7 +85,7 @@ pub const initHook: ?*const fn (*App) anyerror!void = if (@hasDecl(root, "init")
/// Initialize a new Jetzig app. Call this from `src/main.zig` and then call /// Initialize a new Jetzig app. Call this from `src/main.zig` and then call
/// `start(@import("routes").routes)` on the returned value. /// `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 env = try Environment.init(allocator); const env = try Environment.init(allocator, .{});
return .{ return .{
.env = env, .env = env,

View File

@ -21,16 +21,61 @@ log_queue: *jetzig.loggers.LogQueue,
pub const EnvironmentName = enum { development, production, testing }; pub const EnvironmentName = enum { development, production, testing };
pub const Vars = struct { pub const Vars = struct {
env_map: std.process.EnvMap, env_map: std.process.EnvMap,
env_file: ?EnvFile,
pub const EnvFile = struct {
allocator: std.mem.Allocator,
hashmap: *std.StringHashMap([]const u8),
content: []const u8,
pub fn init(allocator: std.mem.Allocator, file: std.fs.File) !EnvFile {
const stat = try file.stat();
const content = try file.readToEndAlloc(allocator, stat.size);
file.close();
const hashmap = try allocator.create(std.StringHashMap([]const u8));
hashmap.* = std.StringHashMap([]const u8).init(allocator);
var it = std.mem.tokenizeScalar(u8, content, '\n');
while (it.next()) |line| {
const stripped = jetzig.util.strip(line);
if (std.mem.startsWith(u8, stripped, "#")) continue;
const equals_index = std.mem.indexOfScalar(u8, stripped, '=') orelse continue;
const name = stripped[0..equals_index];
const value = if (equals_index + 1 < stripped.len) stripped[equals_index + 1 ..] else "";
try hashmap.put(name, jetzig.util.unquote(value));
}
return .{ .allocator = allocator, .hashmap = hashmap, .content = content };
}
pub fn deinit(self: EnvFile) void {
self.hashmap.deinit();
self.allocator.destroy(self.hashmap);
self.allocator.free(self.content);
}
};
pub fn init(allocator: std.mem.Allocator, env_file: ?std.fs.File) !Vars {
return .{
.env_file = if (env_file) |file| try EnvFile.init(allocator, file) else null,
.env_map = try std.process.getEnvMap(allocator),
};
}
pub fn deinit(self: Vars) void {
var env_map = self.env_map;
env_map.deinit();
}
pub fn get(self: Vars, key: []const u8) ?[]const u8 { pub fn get(self: Vars, key: []const u8) ?[]const u8 {
return self.env_map.get(key); const env_file = self.env_file orelse return self.env_map.get(key);
return env_file.hashmap.get(key) orelse self.env_map.get(key);
} }
pub fn getT(self: Vars, T: type, key: []const u8) !switch (@typeInfo(T)) { pub fn getT(self: Vars, T: type, key: []const u8) !switch (@typeInfo(T)) {
.bool => T, .bool => T,
else => ?T, else => ?T,
} { } {
const value = self.env_map.get(key) orelse return if (@typeInfo(T) == .bool) const value = self.get(key) orelse return if (@typeInfo(T) == .bool)
false false
else else
null; null;
@ -48,11 +93,6 @@ pub const Vars = struct {
}; };
} }
pub fn deinit(self: Vars) void {
var env_map = self.env_map;
env_map.deinit();
}
fn parseEnum(E: type, value: []const u8) ?E { fn parseEnum(E: type, value: []const u8) ?E {
return std.meta.stringToEnum(E, value); return std.meta.stringToEnum(E, value);
} }
@ -65,8 +105,11 @@ const Options = struct {
log: []const u8 = "-", log: []const u8 = "-",
@"log-error": []const u8 = "-", @"log-error": []const u8 = "-",
@"log-level": ?jetzig.loggers.LogLevel = null, @"log-level": ?jetzig.loggers.LogLevel = null,
// TODO: Create a production logger and select default logger based on environment. @"log-format": jetzig.loggers.LogFormat = switch (jetzig.environment) {
@"log-format": jetzig.loggers.LogFormat = .development, .development, .testing => .development,
.production => .production,
},
@"env-file": []const u8 = ".env",
detach: bool = false, detach: bool = false,
pub const shorthands = .{ pub const shorthands = .{
@ -95,6 +138,9 @@ const Options = struct {
.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)
, ,
.@"env-file" =
\\Load environment variables from a file. Variables defined in this file take precedence over process environment variables.
,
.help = "Print help and exit", .help = "Print help and exit",
}, },
}; };
@ -103,6 +149,7 @@ const Options = struct {
const LaunchLogger = struct { const LaunchLogger = struct {
stdout: std.fs.File, stdout: std.fs.File,
stderr: std.fs.File, stderr: std.fs.File,
silent: bool = false,
pub fn log( pub fn log(
self: LaunchLogger, self: LaunchLogger,
@ -110,6 +157,8 @@ const LaunchLogger = struct {
comptime message: []const u8, comptime message: []const u8,
log_args: anytype, log_args: anytype,
) !void { ) !void {
if (self.silent) return;
const target = @field(self, @tagName(jetzig.loggers.logTarget(level))); const target = @field(self, @tagName(jetzig.loggers.logTarget(level)));
const writer = target.writer(); const writer = target.writer();
try writer.print( try writer.print(
@ -119,7 +168,11 @@ const LaunchLogger = struct {
} }
}; };
pub fn init(parent_allocator: std.mem.Allocator) !Environment { pub const EnvironmentOptions = struct {
silent: bool = false,
};
pub fn init(parent_allocator: std.mem.Allocator, env_options: EnvironmentOptions) !Environment {
const arena = try parent_allocator.create(std.heap.ArenaAllocator); const arena = try parent_allocator.create(std.heap.ArenaAllocator);
arena.* = std.heap.ArenaAllocator.init(parent_allocator); arena.* = std.heap.ArenaAllocator.init(parent_allocator);
const allocator = arena.allocator(); const allocator = arena.allocator();
@ -127,12 +180,13 @@ pub fn init(parent_allocator: std.mem.Allocator) !Environment {
const options = try args.parseForCurrentProcess(Options, allocator, .print); const options = try args.parseForCurrentProcess(Options, allocator, .print);
defer options.deinit(); defer options.deinit();
const stdout = try getLogFile(.stdout, options.options);
const stderr = try getLogFile(.stdout, options.options);
const log_queue = try allocator.create(jetzig.loggers.LogQueue); const log_queue = try allocator.create(jetzig.loggers.LogQueue);
log_queue.* = jetzig.loggers.LogQueue.init(allocator); log_queue.* = jetzig.loggers.LogQueue.init(allocator);
try log_queue.setFiles(
try getLogFile(.stdout, options.options), try log_queue.setFiles(stdout, stderr);
try getLogFile(.stderr, options.options),
);
if (options.options.help) { if (options.options.help) {
const writer = std.io.getStdErr().writer(); const writer = std.io.getStdErr().writer();
@ -140,26 +194,40 @@ pub fn init(parent_allocator: std.mem.Allocator) !Environment {
std.process.exit(0); std.process.exit(0);
} }
const environment = std.enums.nameCast(EnvironmentName, jetzig.environment); const env_file = std.fs.cwd().openFile(options.options.@"env-file", .{}) catch |err|
const vars = Vars{ .env_map = try std.process.getEnvMap(allocator) }; switch (err) {
error.FileNotFound => null,
else => return err,
};
const vars = try Vars.init(allocator, env_file);
var launch_logger = LaunchLogger{ var launch_logger = LaunchLogger{
.stdout = try getLogFile(.stdout, options.options), .stdout = stdout,
.stderr = try getLogFile(.stdout, options.options), .stderr = stderr,
.silent = env_options.silent,
}; };
const logger = switch (options.options.@"log-format") { const logger = switch (options.options.@"log-format") {
.development => jetzig.loggers.Logger{ .development => jetzig.loggers.Logger{
.development_logger = jetzig.loggers.DevelopmentLogger.init( .development_logger = jetzig.loggers.DevelopmentLogger.init(
allocator, allocator,
resolveLogLevel(options.options.@"log-level", environment), resolveLogLevel(options.options.@"log-level", jetzig.environment),
stdout,
stderr,
),
},
.production => jetzig.loggers.Logger{
.production_logger = jetzig.loggers.ProductionLogger.init(
allocator,
resolveLogLevel(options.options.@"log-level", jetzig.environment),
log_queue, log_queue,
), ),
}, },
.json => jetzig.loggers.Logger{ .json => jetzig.loggers.Logger{
.json_logger = jetzig.loggers.JsonLogger.init( .json_logger = jetzig.loggers.JsonLogger.init(
allocator, allocator,
resolveLogLevel(options.options.@"log-level", environment), resolveLogLevel(options.options.@"log-level", jetzig.environment),
log_queue, log_queue,
), ),
}, },
@ -171,7 +239,7 @@ pub fn init(parent_allocator: std.mem.Allocator) !Environment {
} }
const secret_len = jetzig.http.Session.Cipher.key_length; const secret_len = jetzig.http.Session.Cipher.key_length;
const secret_value = try getSecret(allocator, launch_logger, secret_len, environment); const secret_value = try getSecret(allocator, launch_logger, secret_len, jetzig.environment);
const secret = if (secret_value.len > secret_len) secret_value[0..secret_len] else secret_value; const secret = if (secret_value.len > secret_len) secret_value[0..secret_len] else secret_value;
if (secret.len != secret_len) { if (secret.len != secret_len) {
@ -200,8 +268,9 @@ pub fn init(parent_allocator: std.mem.Allocator) !Environment {
"Using `{s}` database adapter with database: `{s}`.", "Using `{s}` database adapter with database: `{s}`.",
.{ .{
@tagName(jetzig.database.adapter), @tagName(jetzig.database.adapter),
switch (environment) { switch (jetzig.environment) {
inline else => |tag| @field(jetzig.jetquery.config.database, @tagName(tag)).database, inline else => |tag| vars.get("JETQUERY_DATABASE") orelse
@field(jetzig.jetquery.config.database, @tagName(tag)).database,
}, },
}, },
); );
@ -216,7 +285,7 @@ pub fn init(parent_allocator: std.mem.Allocator) !Environment {
.bind = try allocator.dupe(u8, options.options.bind), .bind = try allocator.dupe(u8, options.options.bind),
.port = options.options.port, .port = options.options.port,
.detach = options.options.detach, .detach = options.options.detach,
.environment = environment, .environment = jetzig.environment,
.vars = vars, .vars = vars,
.log_queue = log_queue, .log_queue = log_queue,
}; };

View File

@ -2,7 +2,11 @@ const std = @import("std");
const jetzig = @import("../jetzig.zig"); const jetzig = @import("../jetzig.zig");
pub const adapter = @field(jetzig.jetquery.config.database, @tagName(jetzig.environment)).adapter; pub const adapter = std.enums.nameCast(
jetzig.jetquery.adapters.Name,
@field(jetzig.jetquery.config.database, @tagName(jetzig.environment)).adapter,
);
pub const Schema = jetzig.config.get(type, "Schema"); pub const Schema = jetzig.config.get(type, "Schema");
pub const Repo = jetzig.jetquery.Repo(adapter, Schema); pub const Repo = jetzig.jetquery.Repo(adapter, Schema);
@ -11,7 +15,6 @@ pub fn Query(comptime model: anytype) type {
} }
pub fn repo(allocator: std.mem.Allocator, app: anytype) !Repo { pub fn repo(allocator: std.mem.Allocator, app: anytype) !Repo {
// XXX: Is this terrible ?
const Callback = struct { const Callback = struct {
var jetzig_app: @TypeOf(app) = undefined; var jetzig_app: @TypeOf(app) = undefined;
pub fn callbackFn(event: jetzig.jetquery.events.Event) !void { pub fn callbackFn(event: jetzig.jetquery.events.Event) !void {
@ -25,7 +28,12 @@ pub fn repo(allocator: std.mem.Allocator, app: anytype) !Repo {
std.enums.nameCast(jetzig.jetquery.Environment, jetzig.environment), std.enums.nameCast(jetzig.jetquery.Environment, jetzig.environment),
.{ .{
.eventCallback = Callback.callbackFn, .eventCallback = Callback.callbackFn,
.lazy_connect = jetzig.environment == .development, .lazy_connect = switch (jetzig.environment) {
.development, .production => true,
.testing => false,
},
// Checking field presence here makes setting up test App a bit simpler.
.env = if (@hasField(@TypeOf(app), "env")) try repoEnv(app.env) else .{},
}, },
); );
} }
@ -36,3 +44,16 @@ fn eventCallback(event: jetzig.jetquery.events.Event, app: anytype) !void {
try app.server.logger.ERROR("[database] {?s}", .{err.message}); try app.server.logger.ERROR("[database] {?s}", .{err.message});
} }
} }
pub fn repoEnv(env: jetzig.Environment) !Repo.AdapterOptions {
return switch (comptime adapter) {
.null => .{},
.postgresql => .{
.hostname = @as(?[]const u8, env.vars.get("JETQUERY_HOSTNAME")),
.port = @as(?u16, try env.vars.getT(u16, "JETQUERY_PORT")),
.username = @as(?[]const u8, env.vars.get("JETQUERY_USERNAME")),
.password = @as(?[]const u8, env.vars.get("JETQUERY_PASSWORD")),
.database = @as(?[]const u8, env.vars.get("JETQUERY_DATABASE")),
},
};
}

View File

@ -7,10 +7,12 @@ 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 JsonLogger = @import("loggers/JsonLogger.zig");
pub const TestLogger = @import("loggers/TestLogger.zig"); pub const TestLogger = @import("loggers/TestLogger.zig");
pub const ProductionLogger = @import("loggers/ProductionLogger.zig");
pub const LogQueue = @import("loggers/LogQueue.zig"); pub const LogQueue = @import("loggers/LogQueue.zig");
pub const LogLevel = enum(u4) { 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 LogFormat = enum { development, production, json };
/// Infer a log target (stdout or stderr) from a given log level. /// Infer a log target (stdout or stderr) from a given log level.
pub inline fn logTarget(comptime level: LogLevel) LogQueue.Target { pub inline fn logTarget(comptime level: LogLevel) LogQueue.Target {
@ -23,6 +25,7 @@ pub const Logger = union(enum) {
development_logger: DevelopmentLogger, development_logger: DevelopmentLogger,
json_logger: JsonLogger, json_logger: JsonLogger,
test_logger: TestLogger, test_logger: TestLogger,
production_logger: ProductionLogger,
/// 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 {

View File

@ -8,25 +8,30 @@ const Timestamp = jetzig.types.Timestamp;
const LogLevel = jetzig.loggers.LogLevel; const LogLevel = jetzig.loggers.LogLevel;
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
stdout: std.fs.File,
stderr: std.fs.File,
stdout_colorized: bool, stdout_colorized: bool,
stderr_colorized: bool, stderr_colorized: bool,
level: LogLevel, level: LogLevel,
log_queue: *jetzig.loggers.LogQueue,
mutex: *std.Thread.Mutex, mutex: *std.Thread.Mutex,
/// Initialize a new Development Logger. /// Initialize a new Development Logger.
pub fn init( pub fn init(
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
level: LogLevel, level: LogLevel,
log_queue: *jetzig.loggers.LogQueue, stdout: std.fs.File,
stderr: std.fs.File,
) DevelopmentLogger { ) DevelopmentLogger {
const mutex = allocator.create(std.Thread.Mutex) catch unreachable; const mutex = allocator.create(std.Thread.Mutex) catch unreachable;
mutex.* = std.Thread.Mutex{};
return .{ return .{
.allocator = allocator, .allocator = allocator,
.level = level, .level = level,
.log_queue = log_queue, .stdout = stdout,
.stdout_colorized = log_queue.stdout_is_tty, .stderr = stderr,
.stderr_colorized = log_queue.stderr_is_tty, .stdout_colorized = stdout.isTty(),
.stderr_colorized = stderr.isTty(),
.mutex = mutex, .mutex = mutex,
}; };
} }
@ -38,6 +43,9 @@ pub fn log(
comptime message: []const u8, comptime message: []const u8,
args: anytype, args: anytype,
) !void { ) !void {
self.mutex.lock();
defer self.mutex.unlock();
if (@intFromEnum(level) < @intFromEnum(self.level)) return; if (@intFromEnum(level) < @intFromEnum(self.level)) return;
const output = try std.fmt.allocPrint(self.allocator, message, args); const output = try std.fmt.allocPrint(self.allocator, message, args);
@ -47,13 +55,11 @@ pub fn log(
var timestamp_buf: [256]u8 = undefined; var timestamp_buf: [256]u8 = undefined;
const iso8601 = try timestamp.iso8601(&timestamp_buf); const iso8601 = try timestamp.iso8601(&timestamp_buf);
const target = jetzig.loggers.logTarget(level);
const formatted_level = colorizedLogLevel(level); const formatted_level = colorizedLogLevel(level);
try self.log_queue.print( try self.logWriter(level).print(
"{s: >5} [{s}] {s}\n", "{s: >5} [{s}] {s}\n",
.{ formatted_level, iso8601, output }, .{ formatted_level, iso8601, output },
target,
); );
} }
@ -87,7 +93,7 @@ pub fn logRequest(self: DevelopmentLogger, request: *const jetzig.http.Request)
const formatted_level = if (self.stdout_colorized) colorizedLogLevel(.INFO) else @tagName(.INFO); const formatted_level = if (self.stdout_colorized) colorizedLogLevel(.INFO) else @tagName(.INFO);
try self.log_queue.print("{s: >5} [{s}] [{s}/{s}/{s}]{s}{s}{s}{s}{s}{s}{s}{s}{s}{s} {s}\n", .{ try self.logWriter(.INFO).print("{s: >5} [{s}] [{s}/{s}/{s}]{s}{s}{s}{s}{s}{s}{s}{s}{s}{s} {s}\n", .{
formatted_level, formatted_level,
iso8601, iso8601,
formatted_duration, formatted_duration,
@ -104,22 +110,15 @@ pub fn logRequest(self: DevelopmentLogger, request: *const jetzig.http.Request)
if (request.middleware_rendered) |_| jetzig.colors.codes.escape ++ jetzig.colors.codes.reset else "", if (request.middleware_rendered) |_| jetzig.colors.codes.escape ++ jetzig.colors.codes.reset else "",
if (request.middleware_rendered) |_| "]" else "", if (request.middleware_rendered) |_| "]" else "",
request.path.path, request.path.path,
}, .stdout); });
} }
pub fn logSql(self: *const DevelopmentLogger, event: jetzig.jetquery.events.Event) !void { pub fn logSql(self: *const DevelopmentLogger, event: jetzig.jetquery.events.Event) !void {
self.mutex.lock();
defer self.mutex.unlock();
// XXX: This function does not make any effort to prevent log messages clobbering each other // XXX: This function does not make any effort to prevent log messages clobbering each other
// from multiple threads. JSON logger etc. write in one call and the logger's mutex prevents // from multiple threads. JSON logger etc. write in one call and the log queue prevents
// clobbering, but this is not the case here. // clobbering, but this is not the case here.
const formatted_level = if (self.stdout_colorized) colorizedLogLevel(.INFO) else @tagName(.INFO); const formatted_level = if (self.stdout_colorized) colorizedLogLevel(.INFO) else @tagName(.INFO);
try self.log_queue.print( try self.logWriter(.INFO).print("{s} [database] ", .{formatted_level});
"{s} [database] ",
.{formatted_level},
.stdout,
);
try self.printSql(event.sql orelse ""); try self.printSql(event.sql orelse "");
var duration_buf: [256]u8 = undefined; var duration_buf: [256]u8 = undefined;
@ -129,10 +128,9 @@ pub fn logSql(self: *const DevelopmentLogger, event: jetzig.jetquery.events.Even
self.stdout_colorized, self.stdout_colorized,
) else ""; ) else "";
try self.log_queue.print( try self.logWriter(.INFO).print(
std.fmt.comptimePrint(" [{s}]\n", .{jetzig.colors.cyan("{s}")}), std.fmt.comptimePrint(" [{s}]\n", .{jetzig.colors.cyan("{s}")}),
.{formatted_duration}, .{formatted_duration},
.stdout,
); );
} }
@ -170,6 +168,9 @@ fn printSql(self: *const DevelopmentLogger, sql: []const u8) !void {
const string_color = jetzig.colors.codes.escape ++ jetzig.colors.codes.green; const string_color = jetzig.colors.codes.escape ++ jetzig.colors.codes.green;
const identifier_color = jetzig.colors.codes.escape ++ jetzig.colors.codes.yellow; const identifier_color = jetzig.colors.codes.escape ++ jetzig.colors.codes.yellow;
const reset_color = jetzig.colors.codes.escape ++ jetzig.colors.codes.reset; const reset_color = jetzig.colors.codes.escape ++ jetzig.colors.codes.reset;
var buf: [4096]u8 = undefined;
var stream = std.io.fixedBufferStream(&buf);
const writer = stream.writer();
var index: usize = 0; var index: usize = 0;
var single_quote: bool = false; var single_quote: bool = false;
@ -181,9 +182,9 @@ fn printSql(self: *const DevelopmentLogger, sql: []const u8) !void {
if (!single_quote) { if (!single_quote) {
double_quote = !double_quote; double_quote = !double_quote;
if (double_quote) { if (double_quote) {
try self.log_queue.print(identifier_color ++ "\"", .{}, .stdout); try writer.print(identifier_color ++ "\"", .{});
} else { } else {
try self.log_queue.print("\"" ++ reset_color, .{}, .stdout); try writer.print("\"" ++ reset_color, .{});
} }
index += 1; index += 1;
} }
@ -192,16 +193,16 @@ fn printSql(self: *const DevelopmentLogger, sql: []const u8) !void {
if (!double_quote) { if (!double_quote) {
single_quote = !single_quote; single_quote = !single_quote;
if (single_quote) { if (single_quote) {
try self.log_queue.print(string_color ++ "'", .{}, .stdout); try writer.print(string_color ++ "'", .{});
} else { } else {
try self.log_queue.print("'" ++ reset_color, .{}, .stdout); try writer.print("'" ++ reset_color, .{});
} }
} }
index += 1; index += 1;
}, },
'$' => { '$' => {
if (double_quote or single_quote) { if (double_quote or single_quote) {
try self.log_queue.print("{c}", .{sql[index]}, .stdout); try writer.print("{c}", .{sql[index]});
index += 1; index += 1;
} else { } else {
const param = sql[index..][0 .. std.mem.indexOfAny( const param = sql[index..][0 .. std.mem.indexOfAny(
@ -209,29 +210,30 @@ fn printSql(self: *const DevelopmentLogger, sql: []const u8) !void {
sql[index..], sql[index..],
&std.ascii.whitespace, &std.ascii.whitespace,
) orelse sql.len - index]; ) orelse sql.len - index];
try self.log_queue.print(jetzig.colors.magenta("{s}"), .{param}, .stdout); try writer.print(jetzig.colors.magenta("{s}"), .{param});
index += param.len; index += param.len;
} }
}, },
else => { else => {
if (double_quote or single_quote) { if (double_quote or single_quote) {
try self.log_queue.print("{c}", .{sql[index]}, .stdout); try writer.print("{c}", .{sql[index]});
index += 1; index += 1;
} else { } else {
inline for (sql_tokens) |token| { inline for (sql_tokens) |token| {
if (std.mem.startsWith(u8, sql[index..], token)) { if (std.mem.startsWith(u8, sql[index..], token)) {
try self.log_queue.print(jetzig.colors.cyan(token), .{}, .stdout); try writer.print(jetzig.colors.cyan(token), .{});
index += token.len; index += token.len;
break; break;
} }
} else { } else {
try self.log_queue.print("{c}", .{sql[index]}, .stdout); try writer.print("{c}", .{sql[index]});
index += 1; index += 1;
} }
} }
}, },
} }
} }
try self.logWriter(.INFO).print("{s}", .{stream.getWritten()});
} }
pub fn logError(self: *const DevelopmentLogger, err: anyerror) !void { pub fn logError(self: *const DevelopmentLogger, err: anyerror) !void {
@ -247,6 +249,14 @@ pub fn logError(self: *const DevelopmentLogger, err: anyerror) !void {
try self.log(.ERROR, "Encountered Error: {s}", .{@errorName(err)}); try self.log(.ERROR, "Encountered Error: {s}", .{@errorName(err)});
} }
fn logWriter(self: DevelopmentLogger, comptime level: jetzig.loggers.LogLevel) std.fs.File.Writer {
const target = comptime jetzig.loggers.logTarget(level);
return switch (target) {
.stdout => self.stdout,
.stderr => self.stderr,
}.writer();
}
inline fn colorizedLogLevel(comptime level: LogLevel) []const u8 { inline fn colorizedLogLevel(comptime level: LogLevel) []const u8 {
return switch (level) { return switch (level) {
.TRACE => jetzig.colors.white(@tagName(level)), .TRACE => jetzig.colors.white(@tagName(level)),

View File

@ -0,0 +1,146 @@
const std = @import("std");
const jetzig = @import("../../jetzig.zig");
const ProductionLogger = @This();
const Timestamp = jetzig.types.Timestamp;
const LogLevel = jetzig.loggers.LogLevel;
allocator: std.mem.Allocator,
level: LogLevel,
log_queue: *jetzig.loggers.LogQueue,
/// Initialize a new Development Logger.
pub fn init(
allocator: std.mem.Allocator,
level: LogLevel,
log_queue: *jetzig.loggers.LogQueue,
) ProductionLogger {
return .{
.allocator = allocator,
.level = level,
.log_queue = log_queue,
};
}
/// Generic log function, receives log level, message (format string), and args for format string.
pub fn log(
self: *const ProductionLogger,
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());
var timestamp_buf: [256]u8 = undefined;
const iso8601 = try timestamp.iso8601(&timestamp_buf);
const target = jetzig.loggers.logTarget(level);
try self.log_queue.print(
"{s} [{s}] {s}\n",
.{ @tagName(level), iso8601, output },
target,
);
}
/// Log a one-liner including response status code, path, method, duration, etc.
pub fn logRequest(self: ProductionLogger, request: *const jetzig.http.Request) !void {
if (@intFromEnum(LogLevel.INFO) < @intFromEnum(self.level)) return;
var duration_buf: [256]u8 = undefined;
const formatted_duration = try jetzig.colors.duration(
&duration_buf,
jetzig.util.duration(request.start_time),
false,
);
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_status = status.getFormatted(.{});
const timestamp = Timestamp.init(std.time.timestamp());
var timestamp_buf: [256]u8 = undefined;
const iso8601 = try timestamp.iso8601(&timestamp_buf);
const formatted_level = @tagName(.INFO);
try self.log_queue.print("{s} [{s}] [{s}/{s}/{s}]{s}{s}{s}{s}{s}{s}{s} {s}\n", .{
formatted_level,
iso8601,
formatted_duration,
request.fmtMethod(false),
formatted_status,
if (request.middleware_rendered) |_| " [" else "",
if (request.middleware_rendered) |middleware| middleware.name else "",
if (request.middleware_rendered) |_| ":" else "",
if (request.middleware_rendered) |middleware| middleware.action else "",
if (request.middleware_rendered) |_| ":" else "",
if (request.middleware_rendered) |_| if (request.redirected) "redirect" else "render" else "",
if (request.middleware_rendered) |_| "]" else "",
request.path.path,
}, .stdout);
}
pub fn logSql(self: *const ProductionLogger, event: jetzig.jetquery.events.Event) !void {
var duration_buf: [256]u8 = undefined;
const formatted_duration = if (event.duration) |duration| try jetzig.colors.duration(
&duration_buf,
duration,
false,
) else "";
const timestamp = Timestamp.init(std.time.timestamp());
var timestamp_buf: [256]u8 = undefined;
const iso8601 = try timestamp.iso8601(&timestamp_buf);
try self.log_queue.print(
"{s} [{s}] [database] [sql:{s}] [duration:{s}]\n",
.{ @tagName(.INFO), iso8601, event.sql orelse "", formatted_duration },
.stdout,
);
}
const sql_tokens = .{
"SELECT",
"INSERT",
"UPDATE",
"DELETE",
"WHERE",
"SET",
"ANY",
"FROM",
"INTO",
"IN",
"ON",
"IS",
"NOT",
"NULL",
"LIMIT",
"ORDER BY",
"GROUP BY",
"HAVING",
"LEFT OUTER JOIN",
"INNER JOIN",
"ASC",
"DESC",
"MAX",
"MIN",
"COUNT",
"SUM",
"VALUES",
};
pub fn logError(self: *const ProductionLogger, err: anyerror) !void {
try self.log(.ERROR, "Encountered Error: {s}", .{@errorName(err)});
}

View File

@ -361,7 +361,7 @@ test "environment SMTP config" {
try env_map.put("JETZIG_SMTP_USERNAME", "example-username"); try env_map.put("JETZIG_SMTP_USERNAME", "example-username");
try env_map.put("JETZIG_SMTP_PASSWORD", "example-password"); try env_map.put("JETZIG_SMTP_PASSWORD", "example-password");
env.vars = jetzig.Environment.Vars{ .env_map = env_map }; env.vars = jetzig.Environment.Vars{ .env_map = env_map, .env_file = null };
const mail = Mail{ const mail = Mail{
.allocator = undefined, .allocator = undefined,

View File

@ -114,7 +114,10 @@ pub fn request(
// We init the `std.process.EnvMap` directly here (instead of calling `std.process.getEnvMap` // We init the `std.process.EnvMap` directly here (instead of calling `std.process.getEnvMap`
// to ensure that tests run in a clean environment. Users can manually add items to the // to ensure that tests run in a clean environment. Users can manually add items to the
// environment within a test if required. // environment within a test if required.
const vars = jetzig.Environment.Vars{ .env_map = std.process.EnvMap.init(allocator) }; const vars = jetzig.Environment.Vars{
.env_map = std.process.EnvMap.init(allocator),
.env_file = null,
};
var server = jetzig.http.Server{ var server = jetzig.http.Server{
.allocator = allocator, .allocator = allocator,
.logger = self.logger, .logger = self.logger,

View File

@ -57,6 +57,15 @@ pub inline fn strip(input: []const u8) []const u8 {
return std.mem.trim(u8, input, &std.ascii.whitespace); return std.mem.trim(u8, input, &std.ascii.whitespace);
} }
pub inline fn unquote(input: []const u8) []const u8 {
return if (std.mem.startsWith(u8, input, "\"") and std.mem.endsWith(u8, input, "\""))
std.mem.trim(u8, input, "\"")
else if (std.mem.startsWith(u8, input, "'") and std.mem.endsWith(u8, input, "'"))
std.mem.trim(u8, input, "'")
else
input;
}
/// Generate a secure random string of `len` characters (for cryptographic purposes). /// Generate a secure random string of `len` characters (for cryptographic purposes).
pub fn generateSecret(allocator: std.mem.Allocator, comptime len: u10) ![]const u8 { pub fn generateSecret(allocator: std.mem.Allocator, comptime len: u10) ![]const u8 {
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";