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;
}
_ = b.option([]const u8, "seed", "Internal test seed");
const target = b.host;
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,
});
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(.{
.name = "database",
.root_source_file = jetzig_dep.path("src/commands/database.zig"),

View File

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

View File

@ -1,5 +1,6 @@
const std = @import("std");
const args = @import("args");
const util = @import("../util.zig");
/// Command line options for the `update` command.
pub const Options = struct {
@ -31,9 +32,9 @@ pub fn run(
defer arena.deinit();
const alloc = arena.allocator();
const Action = enum { password };
const Action = enum { user_create };
const map = std.StaticStringMap(Action).initComptime(.{
.{ "password", .password },
.{ "user:create", .user_create },
});
const action = if (main_options.positionals.len > 0)
@ -54,26 +55,19 @@ pub fn run(
break :blk error.JetzigCommandError;
} else if (action) |capture|
switch (capture) {
.password => blk: {
.user_create => blk: {
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;
} else {
const hash = try hashPassword(alloc, sub_args[0]);
try std.io.getStdOut().writer().print("Password hash: {s}\n", .{hash});
try util.execCommand(allocator, &.{
"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`.
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.
/// 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

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

View File

@ -13,7 +13,7 @@ pub fn main() !void {
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;
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 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
/// `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
/// `start(@import("routes").routes)` on the returned value.
pub fn init(allocator: std.mem.Allocator) !App {
const env = try Environment.init(allocator);
const env = try Environment.init(allocator, .{});
return .{
.env = env,

View File

@ -21,16 +21,61 @@ log_queue: *jetzig.loggers.LogQueue,
pub const EnvironmentName = enum { development, production, testing };
pub const Vars = struct {
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 {
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)) {
.bool => 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
else
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 {
return std.meta.stringToEnum(E, value);
}
@ -65,8 +105,11 @@ const Options = struct {
log: []const u8 = "-",
@"log-error": []const u8 = "-",
@"log-level": ?jetzig.loggers.LogLevel = null,
// TODO: Create a production logger and select default logger based on environment.
@"log-format": jetzig.loggers.LogFormat = .development,
@"log-format": jetzig.loggers.LogFormat = switch (jetzig.environment) {
.development, .testing => .development,
.production => .production,
},
@"env-file": []const u8 = ".env",
detach: bool = false,
pub const shorthands = .{
@ -95,6 +138,9 @@ const Options = struct {
.detach =
\\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",
},
};
@ -103,6 +149,7 @@ const Options = struct {
const LaunchLogger = struct {
stdout: std.fs.File,
stderr: std.fs.File,
silent: bool = false,
pub fn log(
self: LaunchLogger,
@ -110,6 +157,8 @@ const LaunchLogger = struct {
comptime message: []const u8,
log_args: anytype,
) !void {
if (self.silent) return;
const target = @field(self, @tagName(jetzig.loggers.logTarget(level)));
const writer = target.writer();
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);
arena.* = std.heap.ArenaAllocator.init(parent_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);
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);
log_queue.* = jetzig.loggers.LogQueue.init(allocator);
try log_queue.setFiles(
try getLogFile(.stdout, options.options),
try getLogFile(.stderr, options.options),
);
try log_queue.setFiles(stdout, stderr);
if (options.options.help) {
const writer = std.io.getStdErr().writer();
@ -140,26 +194,40 @@ pub fn init(parent_allocator: std.mem.Allocator) !Environment {
std.process.exit(0);
}
const environment = std.enums.nameCast(EnvironmentName, jetzig.environment);
const vars = Vars{ .env_map = try std.process.getEnvMap(allocator) };
const env_file = std.fs.cwd().openFile(options.options.@"env-file", .{}) catch |err|
switch (err) {
error.FileNotFound => null,
else => return err,
};
const vars = try Vars.init(allocator, env_file);
var launch_logger = LaunchLogger{
.stdout = try getLogFile(.stdout, options.options),
.stderr = try getLogFile(.stdout, options.options),
.stdout = stdout,
.stderr = stderr,
.silent = env_options.silent,
};
const logger = switch (options.options.@"log-format") {
.development => jetzig.loggers.Logger{
.development_logger = jetzig.loggers.DevelopmentLogger.init(
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,
),
},
.json => jetzig.loggers.Logger{
.json_logger = jetzig.loggers.JsonLogger.init(
allocator,
resolveLogLevel(options.options.@"log-level", environment),
resolveLogLevel(options.options.@"log-level", jetzig.environment),
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_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;
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}`.",
.{
@tagName(jetzig.database.adapter),
switch (environment) {
inline else => |tag| @field(jetzig.jetquery.config.database, @tagName(tag)).database,
switch (jetzig.environment) {
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),
.port = options.options.port,
.detach = options.options.detach,
.environment = environment,
.environment = jetzig.environment,
.vars = vars,
.log_queue = log_queue,
};

View File

@ -2,7 +2,11 @@ const std = @import("std");
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 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 {
// XXX: Is this terrible ?
const Callback = struct {
var jetzig_app: @TypeOf(app) = undefined;
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),
.{
.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});
}
}
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 JsonLogger = @import("loggers/JsonLogger.zig");
pub const TestLogger = @import("loggers/TestLogger.zig");
pub const ProductionLogger = @import("loggers/ProductionLogger.zig");
pub const LogQueue = @import("loggers/LogQueue.zig");
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.
pub inline fn logTarget(comptime level: LogLevel) LogQueue.Target {
@ -23,6 +25,7 @@ pub const Logger = union(enum) {
development_logger: DevelopmentLogger,
json_logger: JsonLogger,
test_logger: TestLogger,
production_logger: ProductionLogger,
/// Log a TRACE level message to the configured logger.
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;
allocator: std.mem.Allocator,
stdout: std.fs.File,
stderr: std.fs.File,
stdout_colorized: bool,
stderr_colorized: bool,
level: LogLevel,
log_queue: *jetzig.loggers.LogQueue,
mutex: *std.Thread.Mutex,
/// Initialize a new Development Logger.
pub fn init(
allocator: std.mem.Allocator,
level: LogLevel,
log_queue: *jetzig.loggers.LogQueue,
stdout: std.fs.File,
stderr: std.fs.File,
) DevelopmentLogger {
const mutex = allocator.create(std.Thread.Mutex) catch unreachable;
mutex.* = std.Thread.Mutex{};
return .{
.allocator = allocator,
.level = level,
.log_queue = log_queue,
.stdout_colorized = log_queue.stdout_is_tty,
.stderr_colorized = log_queue.stderr_is_tty,
.stdout = stdout,
.stderr = stderr,
.stdout_colorized = stdout.isTty(),
.stderr_colorized = stderr.isTty(),
.mutex = mutex,
};
}
@ -38,6 +43,9 @@ pub fn log(
comptime message: []const u8,
args: anytype,
) !void {
self.mutex.lock();
defer self.mutex.unlock();
if (@intFromEnum(level) < @intFromEnum(self.level)) return;
const output = try std.fmt.allocPrint(self.allocator, message, args);
@ -47,13 +55,11 @@ pub fn log(
var timestamp_buf: [256]u8 = undefined;
const iso8601 = try timestamp.iso8601(&timestamp_buf);
const target = jetzig.loggers.logTarget(level);
const formatted_level = colorizedLogLevel(level);
try self.log_queue.print(
try self.logWriter(level).print(
"{s: >5} [{s}] {s}\n",
.{ 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);
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,
iso8601,
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) |_| "]" else "",
request.path.path,
}, .stdout);
});
}
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
// 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.
const formatted_level = if (self.stdout_colorized) colorizedLogLevel(.INFO) else @tagName(.INFO);
try self.log_queue.print(
"{s} [database] ",
.{formatted_level},
.stdout,
);
try self.logWriter(.INFO).print("{s} [database] ", .{formatted_level});
try self.printSql(event.sql orelse "");
var duration_buf: [256]u8 = undefined;
@ -129,10 +128,9 @@ pub fn logSql(self: *const DevelopmentLogger, event: jetzig.jetquery.events.Even
self.stdout_colorized,
) else "";
try self.log_queue.print(
try self.logWriter(.INFO).print(
std.fmt.comptimePrint(" [{s}]\n", .{jetzig.colors.cyan("{s}")}),
.{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 identifier_color = jetzig.colors.codes.escape ++ jetzig.colors.codes.yellow;
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 single_quote: bool = false;
@ -181,9 +182,9 @@ fn printSql(self: *const DevelopmentLogger, sql: []const u8) !void {
if (!single_quote) {
double_quote = !double_quote;
if (double_quote) {
try self.log_queue.print(identifier_color ++ "\"", .{}, .stdout);
try writer.print(identifier_color ++ "\"", .{});
} else {
try self.log_queue.print("\"" ++ reset_color, .{}, .stdout);
try writer.print("\"" ++ reset_color, .{});
}
index += 1;
}
@ -192,16 +193,16 @@ fn printSql(self: *const DevelopmentLogger, sql: []const u8) !void {
if (!double_quote) {
single_quote = !single_quote;
if (single_quote) {
try self.log_queue.print(string_color ++ "'", .{}, .stdout);
try writer.print(string_color ++ "'", .{});
} else {
try self.log_queue.print("'" ++ reset_color, .{}, .stdout);
try writer.print("'" ++ reset_color, .{});
}
}
index += 1;
},
'$' => {
if (double_quote or single_quote) {
try self.log_queue.print("{c}", .{sql[index]}, .stdout);
try writer.print("{c}", .{sql[index]});
index += 1;
} else {
const param = sql[index..][0 .. std.mem.indexOfAny(
@ -209,29 +210,30 @@ fn printSql(self: *const DevelopmentLogger, sql: []const u8) !void {
sql[index..],
&std.ascii.whitespace,
) 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;
}
},
else => {
if (double_quote or single_quote) {
try self.log_queue.print("{c}", .{sql[index]}, .stdout);
try writer.print("{c}", .{sql[index]});
index += 1;
} else {
inline for (sql_tokens) |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;
break;
}
} else {
try self.log_queue.print("{c}", .{sql[index]}, .stdout);
try writer.print("{c}", .{sql[index]});
index += 1;
}
}
},
}
}
try self.logWriter(.INFO).print("{s}", .{stream.getWritten()});
}
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)});
}
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 {
return switch (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_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{
.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`
// to ensure that tests run in a clean environment. Users can manually add items to the
// 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{
.allocator = allocator,
.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);
}
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).
pub fn generateSecret(allocator: std.mem.Allocator, comptime len: u10) ![]const u8 {
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";