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 = &.{ pub const middleware: []const type = &.{
// htmx middleware skips layouts when `HX-Target` header is present and issues // 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. // `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 // Demo middleware included with new projects. Remove once you are familiar with Jetzig's
// middleware system. // middleware system.
// @import("app/middleware/DemoMiddleware.zig"), // @import("app/middleware/DemoMiddleware.zig"),

View File

@ -4,49 +4,84 @@ const builtin = @import("builtin");
const types = @import("types.zig"); const types = @import("types.zig");
// Must be consistent with `std.io.tty.Color` for Windows compatibility.
const codes = .{ const codes = .{
.escape = "\x1b[", .escape = "\x1b[",
.reset = "0;0", .black = "30m",
.black = "0;30", .red = "31m",
.red = "0;31", .green = "32m",
.green = "0;32", .yellow = "33m",
.yellow = "0;33", .blue = "34m",
.blue = "0;34", .magenta = "35m",
.purple = "0;35", .cyan = "36m",
.cyan = "0;36", .white = "37m",
.white = "0;37", .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 { /// Map color codes generated by `std.io.tty.Config.setColor` back to `std.io.tty.Color`. Used by
const config = std.io.tty.detectConfig(target); /// `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); var stream = std.io.fixedBufferStream(buf);
const writer = stream.writer(); const writer = stream.writer();
try config.setColor(writer, color); try config.setColor(writer, color);
try writer.writeAll(input); try writer.writeAll(input);
try config.setColor(writer, .white); try config.setColor(writer, .reset);
return stream.getWritten(); return stream.getWritten();
} }
fn wrap(comptime attribute: []const u8, comptime message: []const u8) []const u8 { fn wrap(comptime attribute: []const u8, comptime message: []const u8) []const u8 {
if (builtin.os.tag == .windows) { return codes.escape ++ attribute ++ message ++ codes.escape ++ codes.reset;
return message;
} else {
return codes.escape ++ attribute ++ "m" ++ message ++ codes.escape ++ codes.reset ++ "m";
}
} }
fn runtimeWrap(allocator: std.mem.Allocator, attribute: []const u8, message: []const u8) ![]const u8 { 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( return try std.mem.join(
allocator, allocator,
"", "",
&[_][]const u8{ codes.escape, attribute, "m", message, codes.escape, codes.reset, "m" }, &[_][]const u8{ codes.escape, attribute, message, codes.escape, codes.reset },
); );
} }
}
pub fn black(comptime message: []const u8) []const u8 { pub fn black(comptime message: []const u8) []const u8 {
return wrap(codes.black, message); return wrap(codes.black, message);
@ -88,12 +123,12 @@ pub fn runtimeBlue(allocator: std.mem.Allocator, message: []const u8) ![]const u
return try runtimeWrap(allocator, codes.blue, message); return try runtimeWrap(allocator, codes.blue, message);
} }
pub fn purple(comptime message: []const u8) []const u8 { pub fn magenta(comptime message: []const u8) []const u8 {
return wrap(codes.purple, message); return wrap(codes.magenta, message);
} }
pub fn runtimePurple(allocator: std.mem.Allocator, message: []const u8) ![]const u8 { pub fn runtimeMagenta(allocator: std.mem.Allocator, message: []const u8) ![]const u8 {
return try runtimeWrap(allocator, codes.purple, message); return try runtimeWrap(allocator, codes.magenta, message);
} }
pub fn cyan(comptime message: []const u8) []const u8 { 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); return try runtimeWrap(allocator, codes.white, message);
} }
pub fn duration(buf: *[256]u8, delta: i64) ![]const u8 { pub fn duration(buf: *[256]u8, delta: i64, is_colorized: bool) ![]const u8 {
const code = if (delta < 1000000) if (!is_colorized) {
codes.green
else if (delta < 5000000)
codes.yellow
else
codes.red;
return try std.fmt.bufPrint( return try std.fmt.bufPrint(
buf, buf,
"{s}{s}m{}{s}{s}m", "{}",
.{ codes.escape, code, std.fmt.fmtDurationSigned(delta), codes.escape, codes.reset }, .{std.fmt.fmtDurationSigned(delta)},
); );
} }
const color: std.io.tty.Color = if (delta < 1000000)
.green
else if (delta < 5000000)
.yellow
else
.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_request: *httpz.Request,
httpz_response: *httpz.Response, httpz_response: *httpz.Response,
) !void { ) !void {
const start_time = std.time.nanoTimestamp();
const state = try self.allocator.create(jetzig.http.Request.CallbackState); const state = try self.allocator.create(jetzig.http.Request.CallbackState);
const arena = try self.allocator.create(std.heap.ArenaAllocator); const arena = try self.allocator.create(std.heap.ArenaAllocator);
arena.* = std.heap.ArenaAllocator.init(self.allocator); arena.* = std.heap.ArenaAllocator.init(self.allocator);
@ -120,8 +122,6 @@ fn processNextRequest(
const allocator = state.arena.allocator(); const allocator = state.arena.allocator();
const start_time = std.time.nanoTimestamp();
var response = try jetzig.http.Response.init(allocator, httpz_response); var response = try jetzig.http.Response.init(allocator, httpz_response);
var request = try jetzig.http.Request.init( var request = try jetzig.http.Request.init(
allocator, allocator,

View File

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

View File

@ -1,4 +1,5 @@
const std = @import("std"); const std = @import("std");
const builtin = @import("builtin");
const jetzig = @import("../../jetzig.zig"); 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.stdout_is_tty = stdout_file.isTty();
self.stderr_is_tty = stderr_file.isTty(); self.stderr_is_tty = stderr_file.isTty();
self.stdout_colorize = std.io.tty.detectConfig(stdout_file) != .no_color; self.stdout_colorize = std.io.tty.detectConfig(stdout_file) != .no_color;
self.stderr_colorize = std.io.tty.detectConfig(stderr_file) != .no_color; self.stderr_colorize = std.io.tty.detectConfig(stderr_file) != .no_color;
self.state = .ready; self.state = .ready;
} }
@ -164,6 +167,8 @@ pub const Reader = struct {
var stdout_written = false; var stdout_written = false;
var stderr_written = false; var stderr_written = false;
var file: std.fs.File = undefined;
var colorize = false;
while (try self.queue.popFirst()) |event| { while (try self.queue.popFirst()) |event| {
self.queue.writer.mutex.lock(); self.queue.writer.mutex.lock();
@ -172,10 +177,18 @@ pub const Reader = struct {
const writer = switch (event.target) { const writer = switch (event.target) {
.stdout => blk: { .stdout => blk: {
stdout_written = true; stdout_written = true;
if (builtin.os.tag == .windows) {
file = self.stdout_file;
colorize = self.queue.stdout_colorize;
}
break :blk stdout_writer; break :blk stdout_writer;
}, },
.stderr => blk: { .stderr => blk: {
stderr_written = true; stderr_written = true;
if (builtin.os.tag == .windows) {
file = self.stderr_file;
colorize = self.queue.stderr_colorize;
}
break :blk stderr_writer; break :blk stderr_writer;
}, },
}; };
@ -187,7 +200,11 @@ pub const Reader = struct {
continue; continue;
} }
if (builtin.os.tag == .windows and colorize) {
try writeWindows(file, writer, event);
} else {
try writer.writeAll(event.message[0..event.len]); try writer.writeAll(event.message[0..event.len]);
}
self.queue.writer.position -= 1; 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" { test "print to stdout and stderr" {
var log_queue = LogQueue.init(std.testing.allocator); var log_queue = LogQueue.init(std.testing.allocator);
defer log_queue.deinit(); 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 { pub fn beforeResponse(self: *Self, request: *jetzig.http.Request, response: *jetzig.http.Response) !void {
_ = self; _ = self;
if (response.status_code != .moved_permanently and response.status_code != .found) return; 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| { if (response.headers.get("Location")) |location| {
response.headers.remove("Location");
response.status_code = .ok; response.status_code = .ok;
request.response_data.reset(); request.response_data.reset();
try response.headers.append("HX-Redirect", location); try response.headers.append("HX-Redirect", location);