This commit is contained in:
Bob Farrell 2024-05-12 14:09:28 +01:00
parent bd02977600
commit 2fbd2f2f6a
6 changed files with 150 additions and 66 deletions

View File

@ -12,7 +12,7 @@ pub const jetzig_options = struct {
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,
jetzig.middleware.HtmxMiddleware,
// Demo middleware included with new projects. Remove once you are familiar with Jetzig's
// middleware system.
// @import("app/middleware/DemoMiddleware.zig"),

View File

@ -4,48 +4,83 @@ const builtin = @import("builtin");
const types = @import("types.zig");
// Must be consistent with `std.io.tty.Color` for Windows compatibility.
const codes = .{
.escape = "\x1b[",
.reset = "0;0",
.black = "0;30",
.red = "0;31",
.green = "0;32",
.yellow = "0;33",
.blue = "0;34",
.purple = "0;35",
.cyan = "0;36",
.white = "0;37",
.black = "30m",
.red = "31m",
.green = "32m",
.yellow = "33m",
.blue = "34m",
.magenta = "35m",
.cyan = "36m",
.white = "37m",
.bright_black = "90m",
.bright_red = "91m",
.bright_green = "92m",
.bright_yellow = "93m",
.bright_blue = "94m",
.bright_magenta = "95m",
.bright_cyan = "96m",
.bright_white = "97m",
.bold = "1m",
.dim = "2m",
.reset = "0m",
};
pub fn colorize(color: std.io.tty.Color, buf: []u8, input: []const u8, target: std.fs.File) ![]const u8 {
const config = std.io.tty.detectConfig(target);
/// Map color codes generated by `std.io.tty.Config.setColor` back to `std.io.tty.Color`. Used by
/// `jetzig.loggers.LogQueue.writeWindows` to parse escape codes so they can be passed to
/// `std.io.tty.Config.setColor` (using Windows API to set console color mode).
pub const codes_map = std.StaticStringMap(std.io.tty.Color).initComptime(.{
.{ "30", .black },
.{ "31", .red },
.{ "32", .green },
.{ "33", .yellow },
.{ "34", .blue },
.{ "35", .magenta },
.{ "36", .cyan },
.{ "37", .white },
.{ "90", .bright_black },
.{ "91", .bright_red },
.{ "92", .bright_green },
.{ "93", .bright_yellow },
.{ "94", .bright_blue },
.{ "95", .bright_magenta },
.{ "96", .bright_cyan },
.{ "97", .bright_white },
.{ "1", .bold },
.{ "2", .dim },
.{ "0", .reset },
});
/// Colorize a log message. Note that we force `.escape_codes` when we are a TTY even on Windows.
/// `jetzig.loggers.LogQueue` parses the ANSI codes and uses `std.io.tty.Config.setColor` to
/// invoke the appropriate Windows API call to set the terminal color before writing each token.
/// We must do it this way because Windows colors are set by API calls at the time of write, not
/// encoded into the message string.
pub fn colorize(color: std.io.tty.Color, buf: []u8, input: []const u8, is_colorized: bool) ![]const u8 {
if (!is_colorized) return input;
const config: std.io.tty.Config = .escape_codes;
var stream = std.io.fixedBufferStream(buf);
const writer = stream.writer();
try config.setColor(writer, color);
try writer.writeAll(input);
try config.setColor(writer, .white);
try config.setColor(writer, .reset);
return stream.getWritten();
}
fn wrap(comptime attribute: []const u8, comptime message: []const u8) []const u8 {
if (builtin.os.tag == .windows) {
return message;
} else {
return codes.escape ++ attribute ++ "m" ++ message ++ codes.escape ++ codes.reset ++ "m";
}
return codes.escape ++ attribute ++ message ++ codes.escape ++ codes.reset;
}
fn runtimeWrap(allocator: std.mem.Allocator, attribute: []const u8, message: []const u8) ![]const u8 {
if (builtin.os.tag == .windows) {
return try allocator.dupe(u8, message);
} else {
return try std.mem.join(
allocator,
"",
&[_][]const u8{ codes.escape, attribute, "m", message, codes.escape, codes.reset, "m" },
);
}
return try std.mem.join(
allocator,
"",
&[_][]const u8{ codes.escape, attribute, message, codes.escape, codes.reset },
);
}
pub fn black(comptime message: []const u8) []const u8 {
@ -88,12 +123,12 @@ pub fn runtimeBlue(allocator: std.mem.Allocator, message: []const u8) ![]const u
return try runtimeWrap(allocator, codes.blue, message);
}
pub fn purple(comptime message: []const u8) []const u8 {
return wrap(codes.purple, message);
pub fn magenta(comptime message: []const u8) []const u8 {
return wrap(codes.magenta, message);
}
pub fn runtimePurple(allocator: std.mem.Allocator, message: []const u8) ![]const u8 {
return try runtimeWrap(allocator, codes.purple, message);
pub fn runtimeMagenta(allocator: std.mem.Allocator, message: []const u8) ![]const u8 {
return try runtimeWrap(allocator, codes.magenta, message);
}
pub fn cyan(comptime message: []const u8) []const u8 {
@ -112,16 +147,26 @@ pub fn runtimeWhite(allocator: std.mem.Allocator, message: []const u8) ![]const
return try runtimeWrap(allocator, codes.white, message);
}
pub fn duration(buf: *[256]u8, delta: i64) ![]const u8 {
const code = if (delta < 1000000)
codes.green
pub fn duration(buf: *[256]u8, delta: i64, is_colorized: bool) ![]const u8 {
if (!is_colorized) {
return try std.fmt.bufPrint(
buf,
"{}",
.{std.fmt.fmtDurationSigned(delta)},
);
}
const color: std.io.tty.Color = if (delta < 1000000)
.green
else if (delta < 5000000)
codes.yellow
.yellow
else
codes.red;
return try std.fmt.bufPrint(
buf,
"{s}{s}m{}{s}{s}m",
.{ codes.escape, code, std.fmt.fmtDurationSigned(delta), codes.escape, codes.reset },
.red;
var duration_buf: [256]u8 = undefined;
const formatted_duration = try std.fmt.bufPrint(
&duration_buf,
"{}",
.{std.fmt.fmtDurationSigned(delta)},
);
return try colorize(color, buf, formatted_duration, true);
}

View File

@ -107,6 +107,8 @@ fn processNextRequest(
httpz_request: *httpz.Request,
httpz_response: *httpz.Response,
) !void {
const start_time = std.time.nanoTimestamp();
const state = try self.allocator.create(jetzig.http.Request.CallbackState);
const arena = try self.allocator.create(std.heap.ArenaAllocator);
arena.* = std.heap.ArenaAllocator.init(self.allocator);
@ -120,8 +122,6 @@ fn processNextRequest(
const allocator = state.arena.allocator();
const start_time = std.time.nanoTimestamp();
var response = try jetzig.http.Response.init(allocator, httpz_response);
var request = try jetzig.http.Request.init(
allocator,

View File

@ -44,10 +44,8 @@ pub fn log(
var timestamp_buf: [256]u8 = undefined;
const iso8601 = try timestamp.iso8601(&timestamp_buf);
var level_buf: [16]u8 = undefined;
const formatted_level = try colorizedLogLevel(level, &level_buf, self.log_queue.reader.stdout_file);
const target = jetzig.loggers.logTarget(level);
const formatted_level = colorizedLogLevel(level);
try self.log_queue.print(
"{s: >5} [{s}] {s}\n",
@ -59,14 +57,11 @@ pub fn log(
/// Log a one-liner including response status code, path, method, duration, etc.
pub fn logRequest(self: DevelopmentLogger, request: *const jetzig.http.Request) !void {
var duration_buf: [256]u8 = undefined;
const formatted_duration = if (self.stdout_colorized)
try jetzig.colors.duration(&duration_buf, jetzig.util.duration(request.start_time))
else
try std.fmt.bufPrint(
&duration_buf,
"{}",
.{std.fmt.fmtDurationSigned(jetzig.util.duration(request.start_time))},
);
const formatted_duration = try jetzig.colors.duration(
&duration_buf,
jetzig.util.duration(request.start_time),
self.stdout_colorized,
);
const status: jetzig.http.status_codes.TaggedStatusCode = switch (request.response.status_code) {
inline else => |status_code| @unionInit(
@ -85,8 +80,7 @@ pub fn logRequest(self: DevelopmentLogger, request: *const jetzig.http.Request)
var timestamp_buf: [256]u8 = undefined;
const iso8601 = try timestamp.iso8601(&timestamp_buf);
var level_buf: [16]u8 = undefined;
const formatted_level = try colorizedLogLevel(.INFO, &level_buf, self.log_queue.reader.stdout_file);
const formatted_level = if (self.stdout_colorized) colorizedLogLevel(.INFO) else @tagName(.INFO);
try self.log_queue.print("{s: >5} [{s}] [{s}/{s}/{s}] {s}\n", .{
formatted_level,
@ -98,13 +92,13 @@ pub fn logRequest(self: DevelopmentLogger, request: *const jetzig.http.Request)
}, .stdout);
}
fn colorizedLogLevel(comptime level: LogLevel, buf: []u8, file: std.fs.File) ![]const u8 {
inline fn colorizedLogLevel(comptime level: LogLevel) []const u8 {
return switch (level) {
.TRACE => jetzig.colors.colorize(.white, buf, @tagName(level), file),
.DEBUG => jetzig.colors.colorize(.cyan, buf, @tagName(level), file),
.INFO => jetzig.colors.colorize(.blue, buf, @tagName(level) ++ " ", file),
.WARN => jetzig.colors.colorize(.yellow, buf, @tagName(level) ++ " ", file),
.ERROR => jetzig.colors.colorize(.red, buf, @tagName(level), file),
.FATAL => jetzig.colors.colorize(.red, buf, @tagName(level), file),
.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

@ -1,4 +1,5 @@
const std = @import("std");
const builtin = @import("builtin");
const jetzig = @import("../../jetzig.zig");
@ -81,8 +82,10 @@ pub fn setFiles(self: *LogQueue, stdout_file: std.fs.File, stderr_file: std.fs.F
};
self.stdout_is_tty = stdout_file.isTty();
self.stderr_is_tty = stderr_file.isTty();
self.stdout_colorize = std.io.tty.detectConfig(stdout_file) != .no_color;
self.stderr_colorize = std.io.tty.detectConfig(stderr_file) != .no_color;
self.state = .ready;
}
@ -164,6 +167,8 @@ pub const Reader = struct {
var stdout_written = false;
var stderr_written = false;
var file: std.fs.File = undefined;
var colorize = false;
while (try self.queue.popFirst()) |event| {
self.queue.writer.mutex.lock();
@ -172,10 +177,18 @@ pub const Reader = struct {
const writer = switch (event.target) {
.stdout => blk: {
stdout_written = true;
if (builtin.os.tag == .windows) {
file = self.stdout_file;
colorize = self.queue.stdout_colorize;
}
break :blk stdout_writer;
},
.stderr => blk: {
stderr_written = true;
if (builtin.os.tag == .windows) {
file = self.stderr_file;
colorize = self.queue.stderr_colorize;
}
break :blk stderr_writer;
},
};
@ -187,7 +200,11 @@ pub const Reader = struct {
continue;
}
try writer.writeAll(event.message[0..event.len]);
if (builtin.os.tag == .windows and colorize) {
try writeWindows(file, writer, event);
} else {
try writer.writeAll(event.message[0..event.len]);
}
self.queue.writer.position -= 1;
@ -244,6 +261,35 @@ fn popFirst(self: *LogQueue) !?Event {
}
}
fn writeWindows(file: std.fs.File, writer: anytype, event: Event) !void {
var info: std.os.windows.CONSOLE_SCREEN_BUFFER_INFO = undefined;
const config: std.io.tty.Config = if (std.os.windows.kernel32.GetConsoleScreenBufferInfo(
file.handle,
&info,
) != std.os.windows.TRUE)
.no_color
else
.{ .windows_api = .{
.handle = file.handle,
.reset_attributes = info.wAttributes,
} };
var it = std.mem.tokenizeSequence(u8, event.message[0..event.len], "\x1b[");
while (it.next()) |token| {
if (std.mem.indexOfScalar(u8, token, 'm')) |index| {
if (index > 0 and index + 1 < token.len) {
if (jetzig.colors.codes_map.get(token[0..index])) |color| {
try config.setColor(writer, color);
try writer.writeAll(token[index + 1 ..]);
continue;
}
}
}
// Fallback
try writer.writeAll(token);
}
}
test "print to stdout and stderr" {
var log_queue = LogQueue.init(std.testing.allocator);
defer log_queue.deinit();

View File

@ -29,10 +29,9 @@ pub fn afterRequest(self: *Self, request: *jetzig.http.Request) !void {
pub fn beforeResponse(self: *Self, request: *jetzig.http.Request, response: *jetzig.http.Response) !void {
_ = self;
if (response.status_code != .moved_permanently and response.status_code != .found) return;
if (request.headers.getFirstValue("HX-Request") == null) return;
if (request.headers.get("HX-Request") == null) return;
if (response.headers.getFirstValue("Location")) |location| {
response.headers.remove("Location");
if (response.headers.get("Location")) |location| {
response.status_code = .ok;
request.response_data.reset();
try response.headers.append("HX-Redirect", location);