mirror of
https://github.com/jetzig-framework/jetzig.git
synced 2025-05-14 22:16:08 +00:00
Merge pull request #67 from jetzig-framework/http.zig-and-misc-optimizations
Switch to http.zig
This commit is contained in:
commit
19cc2a1714
@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
_Jetzig_ is a web framework written in 100% pure [Zig](https://ziglang.org) :lizard: for _Linux_, _OS X_, _Windows_, and any _OS_ that can compile _Zig_ code.
|
_Jetzig_ is a web framework written in 100% pure [Zig](https://ziglang.org) :lizard: for _Linux_, _OS X_, _Windows_, and any _OS_ that can compile _Zig_ code.
|
||||||
|
|
||||||
The framework is under active development and is currently in an alpha state.
|
|
||||||
|
|
||||||
Official website: [jetzig.dev](https://www.jetzig.dev/)
|
Official website: [jetzig.dev](https://www.jetzig.dev/)
|
||||||
|
|
||||||
_Jetzig_ aims to provide a rich set of user-friendly tools for building modern web applications quickly. See the checklist below.
|
_Jetzig_ aims to provide a rich set of user-friendly tools for building modern web applications quickly. See the checklist below.
|
||||||
@ -13,7 +11,7 @@ Join us on Discord ! [https://discord.gg/eufqssz7X6](https://discord.gg/eufqssz7
|
|||||||
If you are interested in _Jetzig_ you will probably find these tools interesting too:
|
If you are interested in _Jetzig_ you will probably find these tools interesting too:
|
||||||
|
|
||||||
* [Zap](https://github.com/zigzap/zap)
|
* [Zap](https://github.com/zigzap/zap)
|
||||||
* [http.zig](https://github.com/karlseguin/http.zig)
|
* [http.zig](https://github.com/karlseguin/http.zig) (_Jetzig_'s backend)
|
||||||
* [tokamak](https://github.com/cztomsik/tokamak)
|
* [tokamak](https://github.com/cztomsik/tokamak)
|
||||||
* [zig-router](https://github.com/Cloudef/zig-router)
|
* [zig-router](https://github.com/Cloudef/zig-router)
|
||||||
* [zig-webui](https://github.com/webui-dev/zig-webui/)
|
* [zig-webui](https://github.com/webui-dev/zig-webui/)
|
||||||
|
@ -57,6 +57,7 @@ pub fn build(b: *std.Build) !void {
|
|||||||
|
|
||||||
const jetkv_dep = b.dependency("jetkv", .{ .target = target, .optimize = optimize });
|
const jetkv_dep = b.dependency("jetkv", .{ .target = target, .optimize = optimize });
|
||||||
const zmd_dep = b.dependency("zmd", .{ .target = target, .optimize = optimize });
|
const zmd_dep = b.dependency("zmd", .{ .target = target, .optimize = optimize });
|
||||||
|
const httpz_dep = b.dependency("httpz", .{ .target = target, .optimize = optimize });
|
||||||
|
|
||||||
// This is the way to make it look nice in the zig build script
|
// This is the way to make it look nice in the zig build script
|
||||||
// If we would do it the other way around, we would have to do
|
// If we would do it the other way around, we would have to do
|
||||||
@ -75,6 +76,7 @@ pub fn build(b: *std.Build) !void {
|
|||||||
jetzig_module.addImport("zmd", zmd_dep.module("zmd"));
|
jetzig_module.addImport("zmd", zmd_dep.module("zmd"));
|
||||||
jetzig_module.addImport("jetkv", jetkv_dep.module("jetkv"));
|
jetzig_module.addImport("jetkv", jetkv_dep.module("jetkv"));
|
||||||
jetzig_module.addImport("smtp", smtp_client_dep.module("smtp_client"));
|
jetzig_module.addImport("smtp", smtp_client_dep.module("smtp_client"));
|
||||||
|
jetzig_module.addImport("httpz", httpz_dep.module("httpz"));
|
||||||
|
|
||||||
const main_tests = b.addTest(.{
|
const main_tests = b.addTest(.{
|
||||||
.root_source_file = .{ .path = "src/tests.zig" },
|
.root_source_file = .{ .path = "src/tests.zig" },
|
||||||
@ -93,6 +95,7 @@ pub fn build(b: *std.Build) !void {
|
|||||||
|
|
||||||
main_tests.root_module.addImport("zmpl", zmpl_dep.module("zmpl"));
|
main_tests.root_module.addImport("zmpl", zmpl_dep.module("zmpl"));
|
||||||
main_tests.root_module.addImport("jetkv", jetkv_dep.module("jetkv"));
|
main_tests.root_module.addImport("jetkv", jetkv_dep.module("jetkv"));
|
||||||
|
main_tests.root_module.addImport("httpz", httpz_dep.module("httpz"));
|
||||||
const run_main_tests = b.addRunArtifact(main_tests);
|
const run_main_tests = b.addRunArtifact(main_tests);
|
||||||
|
|
||||||
const test_step = b.step("test", "Run library tests");
|
const test_step = b.step("test", "Run library tests");
|
||||||
|
@ -7,20 +7,24 @@
|
|||||||
.hash = "1220482f07f2bbaef335f20d6890c15a1e14739950b784232bc69182423520e058a5",
|
.hash = "1220482f07f2bbaef335f20d6890c15a1e14739950b784232bc69182423520e058a5",
|
||||||
},
|
},
|
||||||
.zmpl = .{
|
.zmpl = .{
|
||||||
.url = "https://github.com/jetzig-framework/zmpl/archive/c14683521ca48c1de0a9b2d61dfb145e1bc4dac1.tar.gz",
|
.url = "https://github.com/jetzig-framework/zmpl/archive/ef04bf3579e176f9fa3a02effc4ffcbbb5d080d8.tar.gz",
|
||||||
.hash = "122093b741ef4aff151e916fc6005cb0c2aed747a34b77c0d4b45099ea2b561df9c7",
|
.hash = "12209bd490ef2c841d607a6260be9cc40e20dc76786cb99d0fcd72cfef4a253a840d",
|
||||||
},
|
},
|
||||||
.jetkv = .{
|
.jetkv = .{
|
||||||
.url = "https://github.com/jetzig-framework/jetkv/archive/50016e13c89e86c89b7f7ae93d4f0a31d3be303b.tar.gz",
|
.url = "https://github.com/jetzig-framework/jetkv/archive/6fc375b1ece563ae6d16849bb7c0441ff2883a04.tar.gz",
|
||||||
.hash = "122090b828d2cdd4915d242cb3761fe9142b145e49a2341f8b29343839945d6ab256",
|
.hash = "122079edca9ea46ebb5ce8f05ea2c58ee957cf2d73fcfd9a0fd6a50f65879f3bf88f",
|
||||||
},
|
},
|
||||||
.args = .{
|
.args = .{
|
||||||
.url = "https://github.com/MasterQ32/zig-args/archive/01d72b9a0128c474aeeb9019edd48605fa6d95f7.tar.gz",
|
.url = "https://github.com/MasterQ32/zig-args/archive/01d72b9a0128c474aeeb9019edd48605fa6d95f7.tar.gz",
|
||||||
.hash = "12208a1de366740d11de525db7289345949f5fd46527db3f89eecc7bb49b012c0732",
|
.hash = "12208a1de366740d11de525db7289345949f5fd46527db3f89eecc7bb49b012c0732",
|
||||||
},
|
},
|
||||||
.smtp_client = .{
|
.smtp_client = .{
|
||||||
.url = "https://github.com/bobf/smtp_client.zig/archive/86315adf5527e6add304fea3f1ff110613126283.tar.gz",
|
.url = "https://github.com/karlseguin/smtp_client.zig/archive/964152ad4e19dc1d22f6def6f659c86df60e7832.tar.gz",
|
||||||
.hash = "12203ccdd3f7145f305c6f88b18ecd407d27b36051a523f8f579f0099db6a17cd757",
|
.hash = "1220d4f1c2472769b0d689ea878f41f0a66cb07f28569a138aea2c0a648a5c90dd4e",
|
||||||
|
},
|
||||||
|
.httpz = .{
|
||||||
|
.url = "https://github.com/karlseguin/http.zig/archive/34f1aa8a1486478414e876f65364a501d73c8a76.tar.gz",
|
||||||
|
.hash = "12205404dd8bdd98c659e844385154eb28116c1be073103fc94739bc99fa912323e8",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -20,11 +20,11 @@ const jetzig = @import("jetzig");
|
|||||||
/// can also be modified.
|
/// can also be modified.
|
||||||
my_custom_value: []const u8,
|
my_custom_value: []const u8,
|
||||||
|
|
||||||
const Self = @This();
|
const DemoMiddleware = @This();
|
||||||
|
|
||||||
/// Initialize middleware.
|
/// Initialize middleware.
|
||||||
pub fn init(request: *jetzig.http.Request) !*Self {
|
pub fn init(request: *jetzig.http.Request) !*DemoMiddleware {
|
||||||
var middleware = try request.allocator.create(Self);
|
var middleware = try request.allocator.create(DemoMiddleware);
|
||||||
middleware.my_custom_value = "initial value";
|
middleware.my_custom_value = "initial value";
|
||||||
return middleware;
|
return middleware;
|
||||||
}
|
}
|
||||||
@ -32,7 +32,7 @@ pub fn init(request: *jetzig.http.Request) !*Self {
|
|||||||
/// Invoked immediately after the request is received but before it has started processing.
|
/// Invoked immediately after the request is received but before it has started processing.
|
||||||
/// Any calls to `request.render` or `request.redirect` will prevent further processing of the
|
/// Any calls to `request.render` or `request.redirect` will prevent further processing of the
|
||||||
/// request, including any other middleware in the chain.
|
/// request, including any other middleware in the chain.
|
||||||
pub fn afterRequest(self: *Self, request: *jetzig.http.Request) !void {
|
pub fn afterRequest(self: *DemoMiddleware, request: *jetzig.http.Request) !void {
|
||||||
try request.server.logger.DEBUG(
|
try request.server.logger.DEBUG(
|
||||||
"[DemoMiddleware:afterRequest] my_custom_value: {s}",
|
"[DemoMiddleware:afterRequest] my_custom_value: {s}",
|
||||||
.{self.my_custom_value},
|
.{self.my_custom_value},
|
||||||
@ -42,7 +42,11 @@ pub fn afterRequest(self: *Self, request: *jetzig.http.Request) !void {
|
|||||||
|
|
||||||
/// Invoked immediately before the response renders to the client.
|
/// Invoked immediately before the response renders to the client.
|
||||||
/// The response can be modified here if needed.
|
/// The response can be modified here if needed.
|
||||||
pub fn beforeResponse(self: *Self, request: *jetzig.http.Request, response: *jetzig.http.Response) !void {
|
pub fn beforeResponse(
|
||||||
|
self: *DemoMiddleware,
|
||||||
|
request: *jetzig.http.Request,
|
||||||
|
response: *jetzig.http.Response,
|
||||||
|
) !void {
|
||||||
try request.server.logger.DEBUG(
|
try request.server.logger.DEBUG(
|
||||||
"[DemoMiddleware:beforeResponse] my_custom_value: {s}, response status: {s}",
|
"[DemoMiddleware:beforeResponse] my_custom_value: {s}, response status: {s}",
|
||||||
.{ self.my_custom_value, @tagName(response.status_code) },
|
.{ self.my_custom_value, @tagName(response.status_code) },
|
||||||
@ -51,7 +55,11 @@ pub fn beforeResponse(self: *Self, request: *jetzig.http.Request, response: *jet
|
|||||||
|
|
||||||
/// Invoked immediately after the response has been finalized and sent to the client.
|
/// Invoked immediately after the response has been finalized and sent to the client.
|
||||||
/// Response data can be accessed for logging, but any modifications will have no impact.
|
/// Response data can be accessed for logging, but any modifications will have no impact.
|
||||||
pub fn afterResponse(self: *Self, request: *jetzig.http.Request, response: *jetzig.http.Response) !void {
|
pub fn afterResponse(
|
||||||
|
self: *DemoMiddleware,
|
||||||
|
request: *jetzig.http.Request,
|
||||||
|
response: *jetzig.http.Response,
|
||||||
|
) !void {
|
||||||
_ = self;
|
_ = self;
|
||||||
_ = response;
|
_ = response;
|
||||||
try request.server.logger.DEBUG("[DemoMiddleware:afterResponse] response completed", .{});
|
try request.server.logger.DEBUG("[DemoMiddleware:afterResponse] response completed", .{});
|
||||||
@ -60,6 +68,6 @@ pub fn afterResponse(self: *Self, request: *jetzig.http.Request, response: *jetz
|
|||||||
/// Invoked after `afterResponse` is called. Use this function to do any clean-up.
|
/// Invoked after `afterResponse` is called. Use this function to do any clean-up.
|
||||||
/// Note that `request.allocator` is an arena allocator, so any allocations are automatically
|
/// Note that `request.allocator` is an arena allocator, so any allocations are automatically
|
||||||
/// freed before the next request starts processing.
|
/// freed before the next request starts processing.
|
||||||
pub fn deinit(self: *Self, request: *jetzig.http.Request) void {
|
pub fn deinit(self: *DemoMiddleware, request: *jetzig.http.Request) void {
|
||||||
request.allocator.destroy(self);
|
request.allocator.destroy(self);
|
||||||
}
|
}
|
||||||
|
7
demo/src/app/views/basic.zig
Normal file
7
demo/src/app/views/basic.zig
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const jetzig = @import("jetzig");
|
||||||
|
|
||||||
|
pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
|
||||||
|
_ = data;
|
||||||
|
return request.render(.ok);
|
||||||
|
}
|
3
demo/src/app/views/basic/index.zmpl
Normal file
3
demo/src/app/views/basic/index.zmpl
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<div>
|
||||||
|
<span>Content goes here</span>
|
||||||
|
</div>
|
@ -10,7 +10,7 @@ pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
|
|||||||
// * `src/app/mailers/welcome/html.zmpl`
|
// * `src/app/mailers/welcome/html.zmpl`
|
||||||
// * `src/app/mailers/welcome/text.zmpl`
|
// * `src/app/mailers/welcome/text.zmpl`
|
||||||
// All mailer templates have access to the same template data as a view template.
|
// All mailer templates have access to the same template data as a view template.
|
||||||
const mail = request.mail("welcome", .{ .to = &.{"hello.dev"} });
|
const mail = request.mail("welcome", .{ .to = &.{"hello@jetzig.dev"} });
|
||||||
|
|
||||||
// Deliver the email asynchronously via a built-in mail Job. Use `.now` to send the email
|
// Deliver the email asynchronously via a built-in mail Job. Use `.now` to send the email
|
||||||
// synchronously (i.e. before the request has returned).
|
// synchronously (i.e. before the request has returned).
|
||||||
|
@ -16,5 +16,6 @@ pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try request.response.headers.append("foobar", "hello");
|
||||||
return request.render(.ok);
|
return request.render(.ok);
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,9 @@ const jetzig = @import("jetzig");
|
|||||||
pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
|
pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
|
||||||
var root = try data.object();
|
var root = try data.object();
|
||||||
|
|
||||||
if (try request.session.get("message")) |message| {
|
const session = try request.session();
|
||||||
|
|
||||||
|
if (try session.get("message")) |message| {
|
||||||
try root.put("message", message);
|
try root.put("message", message);
|
||||||
} else {
|
} else {
|
||||||
try root.put("message", data.string("No message saved yet"));
|
try root.put("message", data.string("No message saved yet"));
|
||||||
@ -16,9 +18,10 @@ pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
|
|||||||
pub fn post(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
|
pub fn post(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
|
||||||
_ = data;
|
_ = data;
|
||||||
const params = try request.params();
|
const params = try request.params();
|
||||||
|
var session = try request.session();
|
||||||
|
|
||||||
if (params.get("message")) |message| {
|
if (params.get("message")) |message| {
|
||||||
try request.session.put("message", message);
|
try session.put("message", message);
|
||||||
}
|
}
|
||||||
|
|
||||||
return request.redirect("/session", .moved_permanently);
|
return request.redirect("/session", .moved_permanently);
|
||||||
|
@ -15,7 +15,7 @@ pub const jetzig_options = struct {
|
|||||||
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"),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Maximum bytes to allow in request body.
|
// Maximum bytes to allow in request body.
|
||||||
@ -27,6 +27,22 @@ pub const jetzig_options = struct {
|
|||||||
// Maximum filesize for `static/` content (applies only to apps using `jetzig.http.StaticRequest`).
|
// Maximum filesize for `static/` content (applies only to apps using `jetzig.http.StaticRequest`).
|
||||||
// pub const max_bytes_static_content: usize = std.math.pow(usize, 2, 18);
|
// pub const max_bytes_static_content: usize = std.math.pow(usize, 2, 18);
|
||||||
|
|
||||||
|
// Maximum length of a header name. There is no limit imposed by the HTTP specification but
|
||||||
|
// AWS load balancers reference 40 as a limit so we use that as a baseline:
|
||||||
|
// https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_HttpHeaderConditionConfig.html
|
||||||
|
// This can be increased if needed.
|
||||||
|
// pub const max_bytes_header_name: u16 = 40;
|
||||||
|
|
||||||
|
// Log message buffer size. Log messages exceeding this size spill to heap with degraded
|
||||||
|
// performance. Log messages should aim to fit in the message buffer.
|
||||||
|
// pub const log_message_buffer_len: usize = 4096;
|
||||||
|
|
||||||
|
// Maximum log pool size. When a log buffer is no longer required it is returned to a pool
|
||||||
|
// for recycling. When logging i/o is slow, a high volume of requests will result in this
|
||||||
|
// pool growing. When the pool size reaches the maximum value defined here, log events are
|
||||||
|
// freed instead of recycled.
|
||||||
|
// pub const max_log_pool_len: usize = 256;
|
||||||
|
|
||||||
// Path relative to cwd() to serve public content from. Symlinks are not followed.
|
// Path relative to cwd() to serve public content from. Symlinks are not followed.
|
||||||
// pub const public_content_path = "public";
|
// pub const public_content_path = "public";
|
||||||
|
|
||||||
|
@ -78,6 +78,22 @@ pub const config = struct {
|
|||||||
/// Maximum filesize for `static/` content (applies only to apps using `jetzig.http.StaticRequest`).
|
/// Maximum filesize for `static/` content (applies only to apps using `jetzig.http.StaticRequest`).
|
||||||
pub const max_bytes_static_content: usize = std.math.pow(usize, 2, 18);
|
pub const max_bytes_static_content: usize = std.math.pow(usize, 2, 18);
|
||||||
|
|
||||||
|
/// Maximum length of a header name. There is no limit imposed by the HTTP specification but
|
||||||
|
/// AWS load balancers reference 40 as a limit so we use that as a baseline:
|
||||||
|
/// https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_HttpHeaderConditionConfig.html
|
||||||
|
/// This can be increased if needed.
|
||||||
|
pub const max_bytes_header_name: u16 = 40;
|
||||||
|
|
||||||
|
/// Log message buffer size. Log messages exceeding this size spill to heap with degraded
|
||||||
|
/// performance. Log messages should aim to fit in the message buffer.
|
||||||
|
pub const log_message_buffer_len: usize = 4096;
|
||||||
|
|
||||||
|
/// Maximum log pool size. When a log buffer is no longer required it is returned to a pool
|
||||||
|
/// for recycling. When logging i/o is slow, a high volume of requests will result in this
|
||||||
|
/// pool growing. When the pool size reaches the maximum value defined here, log events are
|
||||||
|
/// freed instead of recycled.
|
||||||
|
pub const max_log_pool_len: usize = 256;
|
||||||
|
|
||||||
/// Path relative to cwd() to serve public content from. Symlinks are not followed.
|
/// Path relative to cwd() to serve public content from. Symlinks are not followed.
|
||||||
pub const public_content_path = "public";
|
pub const public_content_path = "public";
|
||||||
|
|
||||||
|
@ -79,6 +79,13 @@ pub fn start(self: App, routes_module: type, options: AppOptions) !void {
|
|||||||
defer self.allocator.free(server_options.bind);
|
defer self.allocator.free(server_options.bind);
|
||||||
defer self.allocator.free(server_options.secret);
|
defer self.allocator.free(server_options.secret);
|
||||||
|
|
||||||
|
var log_thread = try std.Thread.spawn(
|
||||||
|
.{ .allocator = self.allocator },
|
||||||
|
jetzig.loggers.LogQueue.Reader.publish,
|
||||||
|
.{ &server_options.log_queue.reader, .{} },
|
||||||
|
);
|
||||||
|
defer log_thread.join();
|
||||||
|
|
||||||
if (server_options.detach) {
|
if (server_options.detach) {
|
||||||
const argv = try std.process.argsAlloc(self.allocator);
|
const argv = try std.process.argsAlloc(self.allocator);
|
||||||
defer std.process.argsFree(self.allocator, argv);
|
defer std.process.argsFree(self.allocator, argv);
|
||||||
@ -105,7 +112,6 @@ pub fn start(self: App, routes_module: type, options: AppOptions) !void {
|
|||||||
&job_queue,
|
&job_queue,
|
||||||
&cache,
|
&cache,
|
||||||
);
|
);
|
||||||
defer server.deinit();
|
|
||||||
|
|
||||||
var mutex = std.Thread.Mutex{};
|
var mutex = std.Thread.Mutex{};
|
||||||
var worker_pool = jetzig.jobs.Pool.init(
|
var worker_pool = jetzig.jobs.Pool.init(
|
||||||
|
@ -63,6 +63,13 @@ pub fn getServerOptions(self: Environment) !jetzig.http.Server.ServerOptions {
|
|||||||
const options = try args.parseForCurrentProcess(Options, self.allocator, .print);
|
const options = try args.parseForCurrentProcess(Options, self.allocator, .print);
|
||||||
defer options.deinit();
|
defer options.deinit();
|
||||||
|
|
||||||
|
const log_queue = try self.allocator.create(jetzig.loggers.LogQueue);
|
||||||
|
log_queue.* = jetzig.loggers.LogQueue.init(self.allocator);
|
||||||
|
try log_queue.setFiles(
|
||||||
|
try getLogFile(.stdout, options.options),
|
||||||
|
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();
|
||||||
try args.printHelp(Options, options.executable_name orelse "<app-name>", writer);
|
try args.printHelp(Options, options.executable_name orelse "<app-name>", writer);
|
||||||
@ -76,16 +83,14 @@ pub fn getServerOptions(self: Environment) !jetzig.http.Server.ServerOptions {
|
|||||||
.development_logger = jetzig.loggers.DevelopmentLogger.init(
|
.development_logger = jetzig.loggers.DevelopmentLogger.init(
|
||||||
self.allocator,
|
self.allocator,
|
||||||
resolveLogLevel(options.options.@"log-level", environment),
|
resolveLogLevel(options.options.@"log-level", environment),
|
||||||
try getLogFile(.stdout, options.options),
|
log_queue,
|
||||||
try getLogFile(.stderr, options.options),
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
.json => jetzig.loggers.Logger{
|
.json => jetzig.loggers.Logger{
|
||||||
.json_logger = jetzig.loggers.JsonLogger.init(
|
.json_logger = jetzig.loggers.JsonLogger.init(
|
||||||
self.allocator,
|
self.allocator,
|
||||||
resolveLogLevel(options.options.@"log-level", environment),
|
resolveLogLevel(options.options.@"log-level", environment),
|
||||||
try getLogFile(.stdout, options.options),
|
log_queue,
|
||||||
try getLogFile(.stderr, options.options),
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -111,6 +116,7 @@ pub fn getServerOptions(self: Environment) !jetzig.http.Server.ServerOptions {
|
|||||||
.port = options.options.port,
|
.port = options.options.port,
|
||||||
.detach = options.options.detach,
|
.detach = options.options.detach,
|
||||||
.environment = environment,
|
.environment = environment,
|
||||||
|
.log_queue = log_queue,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,38 +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",
|
||||||
};
|
};
|
||||||
|
|
||||||
fn wrap(comptime attribute: []const u8, comptime message: []const u8) []const u8 {
|
/// Map color codes generated by `std.io.tty.Config.setColor` back to `std.io.tty.Color`. Used by
|
||||||
if (builtin.os.tag == .windows) {
|
/// `jetzig.loggers.LogQueue.writeWindows` to parse escape codes so they can be passed to
|
||||||
return message;
|
/// `std.io.tty.Config.setColor` (using Windows API to set console color mode).
|
||||||
} else {
|
pub const codes_map = std.StaticStringMap(std.io.tty.Color).initComptime(.{
|
||||||
return codes.escape ++ attribute ++ "m" ++ message ++ codes.escape ++ codes.reset ++ "m";
|
.{ "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, .reset);
|
||||||
|
|
||||||
|
return stream.getWritten();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn wrap(comptime attribute: []const u8, comptime message: []const u8) []const u8 {
|
||||||
|
return codes.escape ++ attribute ++ message ++ codes.escape ++ codes.reset;
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
@ -77,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 {
|
||||||
@ -101,15 +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(allocator: std.mem.Allocator, delta: i64) ![]const u8 {
|
pub fn duration(buf: *[256]u8, delta: i64, is_colorized: bool) ![]const u8 {
|
||||||
var buf: [1024]u8 = undefined;
|
if (!is_colorized) {
|
||||||
const formatted_duration = try std.fmt.bufPrint(&buf, "{}", .{std.fmt.fmtDurationSigned(delta)});
|
return try std.fmt.bufPrint(
|
||||||
|
buf,
|
||||||
|
"{}",
|
||||||
|
.{std.fmt.fmtDurationSigned(delta)},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (delta < 1000000) {
|
const color: std.io.tty.Color = if (delta < 1000000)
|
||||||
return try runtimeGreen(allocator, formatted_duration);
|
.green
|
||||||
} else if (delta < 5000000) {
|
else if (delta < 5000000)
|
||||||
return try runtimeYellow(allocator, formatted_duration);
|
.yellow
|
||||||
} else {
|
else
|
||||||
return try runtimeRed(allocator, formatted_duration);
|
.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);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
pub const Server = @import("http/Server.zig");
|
pub const Server = @import("http/Server.zig");
|
||||||
pub const Request = @import("http/Request.zig");
|
pub const Request = @import("http/Request.zig");
|
||||||
|
@ -6,6 +6,7 @@ allocator: std.mem.Allocator,
|
|||||||
cookie_string: []const u8,
|
cookie_string: []const u8,
|
||||||
buf: std.ArrayList(u8),
|
buf: std.ArrayList(u8),
|
||||||
cookies: std.StringArrayHashMap(*Cookie),
|
cookies: std.StringArrayHashMap(*Cookie),
|
||||||
|
modified: bool = false,
|
||||||
|
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
@ -38,6 +39,8 @@ pub fn get(self: *Self, key: []const u8) ?*Cookie {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn put(self: *Self, key: []const u8, value: Cookie) !void {
|
pub fn put(self: *Self, key: []const u8, value: Cookie) !void {
|
||||||
|
self.modified = true;
|
||||||
|
|
||||||
if (self.cookies.fetchSwapRemove(key)) |entry| {
|
if (self.cookies.fetchSwapRemove(key)) |entry| {
|
||||||
self.allocator.free(entry.key);
|
self.allocator.free(entry.key);
|
||||||
self.allocator.free(entry.value.value);
|
self.allocator.free(entry.value.value);
|
||||||
|
@ -1,120 +1,102 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
|
const httpz = @import("httpz");
|
||||||
|
|
||||||
const jetzig = @import("../../jetzig.zig");
|
const jetzig = @import("../../jetzig.zig");
|
||||||
|
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
headers: HeadersArray,
|
httpz_headers: *httpz.key_value.KeyValue,
|
||||||
|
new_headers: std.ArrayList(Header),
|
||||||
|
|
||||||
const Headers = @This();
|
const Headers = @This();
|
||||||
pub const max_headers = 25;
|
const Header = struct { name: []const u8, value: []const u8 };
|
||||||
const HeadersArray = std.ArrayListUnmanaged(std.http.Header);
|
const max_bytes_header_name = jetzig.config.get(u8, "max_bytes_header_name");
|
||||||
|
|
||||||
pub fn init(allocator: std.mem.Allocator) Headers {
|
pub fn init(allocator: std.mem.Allocator, httpz_headers: *httpz.key_value.KeyValue) Headers {
|
||||||
return .{
|
return .{
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
.headers = HeadersArray.initCapacity(allocator, max_headers) catch @panic("OOM"),
|
.httpz_headers = httpz_headers,
|
||||||
|
.new_headers = std.ArrayList(Header).init(allocator),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Headers) void {
|
pub fn deinit(self: *Headers) void {
|
||||||
self.headers.deinit(self.allocator);
|
self.httpz_headers.deinit(self.allocator);
|
||||||
|
|
||||||
|
for (self.new_headers.items) |header| {
|
||||||
|
self.allocator.free(header.name);
|
||||||
|
self.allocator.free(header.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the first value for a given header identified by `name`. Names are case insensitive.
|
self.new_headers.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the first value for a given header identified by `name`. Names are case insensitive.
|
||||||
pub fn get(self: Headers, name: []const u8) ?[]const u8 {
|
pub fn get(self: Headers, name: []const u8) ?[]const u8 {
|
||||||
for (self.headers.items) |header| {
|
std.debug.assert(name.len <= max_bytes_header_name);
|
||||||
if (jetzig.util.equalStringsCaseInsensitive(name, header.name)) return header.value;
|
|
||||||
}
|
var buf: [max_bytes_header_name]u8 = undefined;
|
||||||
return null;
|
const lower = std.ascii.lowerString(&buf, name);
|
||||||
|
|
||||||
|
return self.httpz_headers.get(lower);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the first value for a given header identified by `name`. Names are case insensitive.
|
/// Get all values for a given header identified by `name`. Names are case insensitive.
|
||||||
pub fn getAll(self: Headers, name: []const u8) []const []const u8 {
|
pub fn getAll(self: Headers, name: []const u8) []const []const u8 {
|
||||||
var headers = std.ArrayList([]const u8).init(self.allocator);
|
var headers = std.ArrayList([]const u8).init(self.allocator);
|
||||||
|
|
||||||
for (self.headers.items) |header| {
|
for (self.httpz_headers.keys, 0..) |key, index| {
|
||||||
if (jetzig.util.equalStringsCaseInsensitive(name, header.name)) {
|
var buf: [max_bytes_header_name]u8 = undefined;
|
||||||
headers.append(header.value) catch @panic("OOM");
|
const lower = std.ascii.lowerString(&buf, name);
|
||||||
}
|
|
||||||
|
if (std.mem.eql(u8, lower, key)) headers.append(self.httpz_headers.values[index]) catch @panic("OOM");
|
||||||
}
|
}
|
||||||
return headers.toOwnedSlice() catch @panic("OOM");
|
return headers.toOwnedSlice() catch @panic("OOM");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deprecated
|
/// Deprecated
|
||||||
pub fn getFirstValue(self: *const Headers, name: []const u8) ?[]const u8 {
|
pub fn getFirstValue(self: *const Headers, name: []const u8) ?[]const u8 {
|
||||||
return self.get(name);
|
return self.get(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Appends `name` and `value` to headers.
|
/// Add `name` and `value` to headers.
|
||||||
pub fn append(self: *Headers, name: []const u8, value: []const u8) !void {
|
pub fn append(self: *Headers, name: []const u8, value: []const u8) !void {
|
||||||
if (self.headers.items.len >= 25) return error.JetzigTooManyHeaders;
|
if (self.httpz_headers.len >= self.httpz_headers.keys.len) return error.JetzigTooManyHeaders;
|
||||||
|
|
||||||
self.headers.appendAssumeCapacity(.{ .name = name, .value = value });
|
var buf: [max_bytes_header_name]u8 = undefined;
|
||||||
}
|
const lower = std.ascii.lowerString(&buf, name);
|
||||||
|
|
||||||
/// Removes **all** header entries matching `name`. Names are case-insensitive.
|
const header = .{
|
||||||
pub fn remove(self: *Headers, name: []const u8) void {
|
.name = try self.allocator.dupe(u8, lower),
|
||||||
if (self.headers.items.len == 0) return;
|
.value = try self.allocator.dupe(u8, value),
|
||||||
|
|
||||||
var index: usize = self.headers.items.len;
|
|
||||||
|
|
||||||
while (index > 0) {
|
|
||||||
index -= 1;
|
|
||||||
if (jetzig.util.equalStringsCaseInsensitive(name, self.headers.items[index].name)) {
|
|
||||||
_ = self.headers.orderedRemove(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns an iterator which implements `next()` returning each name/value of the stored headers.
|
|
||||||
pub fn iterator(self: Headers) Iterator {
|
|
||||||
return Iterator{ .headers = self.headers };
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns an array of `std.http.Header`, can be used to set response headers directly.
|
|
||||||
/// Caller owns memory.
|
|
||||||
pub fn stdHeaders(self: *Headers) !std.ArrayListUnmanaged(std.http.Header) {
|
|
||||||
var array = try std.ArrayListUnmanaged(std.http.Header).initCapacity(self.allocator, max_headers);
|
|
||||||
|
|
||||||
var it = self.iterator();
|
|
||||||
while (it.next()) |header| {
|
|
||||||
array.appendAssumeCapacity(.{ .name = header.name, .value = header.value });
|
|
||||||
}
|
|
||||||
return array;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Iterates through stored headers yielidng a `Header` on each call to `next()`
|
|
||||||
const Iterator = struct {
|
|
||||||
headers: HeadersArray,
|
|
||||||
index: usize = 0,
|
|
||||||
|
|
||||||
const Header = struct {
|
|
||||||
name: []const u8,
|
|
||||||
value: []const u8,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Returns the next item in the current iteration of headers.
|
try self.new_headers.append(header);
|
||||||
pub fn next(self: *Iterator) ?Header {
|
self.httpz_headers.add(header.name, header.value);
|
||||||
if (self.headers.items.len > self.index) {
|
|
||||||
const std_header = self.headers.items[self.index];
|
|
||||||
self.index += 1;
|
|
||||||
return .{ .name = std_header.name, .value = std_header.value };
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
test "append" {
|
test "append (deprecated)" {
|
||||||
const allocator = std.testing.allocator;
|
const allocator = std.testing.allocator;
|
||||||
var headers = Headers.init(allocator);
|
var httpz_headers = try httpz.key_value.KeyValue.init(allocator, 10);
|
||||||
|
var headers = Headers.init(allocator, &httpz_headers);
|
||||||
defer headers.deinit();
|
defer headers.deinit();
|
||||||
try headers.append("foo", "bar");
|
try headers.append("foo", "bar");
|
||||||
try std.testing.expectEqualStrings(headers.getFirstValue("foo").?, "bar");
|
try std.testing.expectEqualStrings(headers.get("foo").?, "bar");
|
||||||
|
}
|
||||||
|
|
||||||
|
test "add" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
var httpz_headers = try httpz.key_value.KeyValue.init(allocator, 10);
|
||||||
|
var headers = Headers.init(allocator, &httpz_headers);
|
||||||
|
defer headers.deinit();
|
||||||
|
try headers.append("foo", "bar");
|
||||||
|
try std.testing.expectEqualStrings(headers.get("foo").?, "bar");
|
||||||
}
|
}
|
||||||
|
|
||||||
test "get with multiple headers (bugfix regression test)" {
|
test "get with multiple headers (bugfix regression test)" {
|
||||||
const allocator = std.testing.allocator;
|
const allocator = std.testing.allocator;
|
||||||
var headers = Headers.init(allocator);
|
var httpz_headers = try httpz.key_value.KeyValue.init(allocator, 10);
|
||||||
|
var headers = Headers.init(allocator, &httpz_headers);
|
||||||
defer headers.deinit();
|
defer headers.deinit();
|
||||||
try headers.append("foo", "bar");
|
try headers.append("foo", "bar");
|
||||||
try headers.append("bar", "baz");
|
try headers.append("bar", "baz");
|
||||||
@ -123,75 +105,32 @@ test "get with multiple headers (bugfix regression test)" {
|
|||||||
|
|
||||||
test "getAll" {
|
test "getAll" {
|
||||||
const allocator = std.testing.allocator;
|
const allocator = std.testing.allocator;
|
||||||
var headers = Headers.init(allocator);
|
var httpz_headers = try httpz.key_value.KeyValue.init(allocator, 10);
|
||||||
|
var headers = Headers.init(allocator, &httpz_headers);
|
||||||
defer headers.deinit();
|
defer headers.deinit();
|
||||||
try headers.append("foo", "bar");
|
try headers.append("foo", "bar");
|
||||||
try headers.append("foo", "baz");
|
try headers.append("foo", "baz");
|
||||||
try headers.append("bar", "qux");
|
try headers.append("bar", "qux");
|
||||||
const all = headers.getAll("foo");
|
const all = headers.getAll("foo");
|
||||||
defer allocator.free(all);
|
defer allocator.free(all);
|
||||||
try std.testing.expectEqualSlices([]const u8, all, &[_][]const u8{ "bar", "baz" });
|
try std.testing.expectEqualDeep(all, &[_][]const u8{ "bar", "baz" });
|
||||||
}
|
}
|
||||||
|
|
||||||
test "append too many headers" {
|
test "add too many headers" {
|
||||||
const allocator = std.testing.allocator;
|
const allocator = std.testing.allocator;
|
||||||
var headers = Headers.init(allocator);
|
var httpz_headers = try httpz.key_value.KeyValue.init(allocator, 10);
|
||||||
|
var headers = Headers.init(allocator, &httpz_headers);
|
||||||
defer headers.deinit();
|
defer headers.deinit();
|
||||||
for (0..25) |_| try headers.append("foo", "bar");
|
for (0..10) |_| try headers.append("foo", "bar");
|
||||||
|
|
||||||
try std.testing.expectError(error.JetzigTooManyHeaders, headers.append("foo", "bar"));
|
try std.testing.expectError(error.JetzigTooManyHeaders, headers.append("foo", "bar"));
|
||||||
}
|
}
|
||||||
|
|
||||||
test "case-insensitive matching" {
|
test "case-insensitive matching" {
|
||||||
const allocator = std.testing.allocator;
|
const allocator = std.testing.allocator;
|
||||||
var headers = Headers.init(allocator);
|
var httpz_headers = try httpz.key_value.KeyValue.init(allocator, 10);
|
||||||
|
var headers = Headers.init(allocator, &httpz_headers);
|
||||||
defer headers.deinit();
|
defer headers.deinit();
|
||||||
try headers.append("Content-Type", "bar");
|
try headers.append("Content-Type", "bar");
|
||||||
try std.testing.expectEqualStrings(headers.getFirstValue("content-type").?, "bar");
|
try std.testing.expectEqualStrings(headers.get("content-type").?, "bar");
|
||||||
}
|
|
||||||
|
|
||||||
test "iterator" {
|
|
||||||
const allocator = std.testing.allocator;
|
|
||||||
var headers = Headers.init(allocator);
|
|
||||||
defer headers.deinit();
|
|
||||||
|
|
||||||
try headers.append("foo", "bar");
|
|
||||||
|
|
||||||
var it = headers.iterator();
|
|
||||||
while (it.next()) |header| {
|
|
||||||
try std.testing.expectEqualStrings("foo", header.name);
|
|
||||||
try std.testing.expectEqualStrings("bar", header.value);
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
try std.testing.expect(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test "remove" {
|
|
||||||
const allocator = std.testing.allocator;
|
|
||||||
var headers = Headers.init(allocator);
|
|
||||||
defer headers.deinit();
|
|
||||||
try headers.append("foo", "baz");
|
|
||||||
try headers.append("foo", "qux");
|
|
||||||
try headers.append("bar", "quux");
|
|
||||||
headers.remove("Foo"); // Headers are case-insensitive.
|
|
||||||
try std.testing.expect(headers.getFirstValue("foo") == null);
|
|
||||||
try std.testing.expectEqualStrings(headers.getFirstValue("bar").?, "quux");
|
|
||||||
}
|
|
||||||
|
|
||||||
test "stdHeaders" {
|
|
||||||
const allocator = std.testing.allocator;
|
|
||||||
var headers = Headers.init(allocator);
|
|
||||||
defer headers.deinit();
|
|
||||||
|
|
||||||
try headers.append("foo", "bar");
|
|
||||||
try headers.append("baz", "qux");
|
|
||||||
|
|
||||||
var std_headers = try headers.stdHeaders();
|
|
||||||
defer std_headers.deinit(allocator);
|
|
||||||
|
|
||||||
try std.testing.expectEqualStrings("foo", std_headers.items[0].name);
|
|
||||||
try std.testing.expectEqualStrings("bar", std_headers.items[0].value);
|
|
||||||
try std.testing.expectEqualStrings("baz", std_headers.items[1].name);
|
|
||||||
try std.testing.expectEqualStrings("qux", std_headers.items[1].value);
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
|
const httpz = @import("httpz");
|
||||||
|
|
||||||
const jetzig = @import("../../jetzig.zig");
|
const jetzig = @import("../../jetzig.zig");
|
||||||
|
|
||||||
const Request = @This();
|
const Request = @This();
|
||||||
@ -14,14 +16,15 @@ path: jetzig.http.Path,
|
|||||||
method: Method,
|
method: Method,
|
||||||
headers: jetzig.http.Headers,
|
headers: jetzig.http.Headers,
|
||||||
server: *jetzig.http.Server,
|
server: *jetzig.http.Server,
|
||||||
std_http_request: std.http.Server.Request,
|
httpz_request: *httpz.Request,
|
||||||
|
httpz_response: *httpz.Response,
|
||||||
response: *jetzig.http.Response,
|
response: *jetzig.http.Response,
|
||||||
status_code: jetzig.http.status_codes.StatusCode = .not_found,
|
status_code: jetzig.http.status_codes.StatusCode = .not_found,
|
||||||
response_data: *jetzig.data.Data,
|
response_data: *jetzig.data.Data,
|
||||||
query_params: ?*jetzig.http.Query = null,
|
query_params: ?*jetzig.http.Query = null,
|
||||||
query_body: ?*jetzig.http.Query = null,
|
query_body: ?*jetzig.http.Query = null,
|
||||||
cookies: *jetzig.http.Cookies = undefined,
|
_cookies: ?*jetzig.http.Cookies = null,
|
||||||
session: *jetzig.http.Session = undefined,
|
_session: ?*jetzig.http.Session = null,
|
||||||
body: []const u8 = undefined,
|
body: []const u8 = undefined,
|
||||||
processed: bool = false,
|
processed: bool = false,
|
||||||
layout: ?[]const u8 = null,
|
layout: ?[]const u8 = null,
|
||||||
@ -90,20 +93,18 @@ pub fn init(
|
|||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
server: *jetzig.http.Server,
|
server: *jetzig.http.Server,
|
||||||
start_time: i128,
|
start_time: i128,
|
||||||
std_http_request: std.http.Server.Request,
|
httpz_request: *httpz.Request,
|
||||||
|
httpz_response: *httpz.Response,
|
||||||
response: *jetzig.http.Response,
|
response: *jetzig.http.Response,
|
||||||
) !Request {
|
) !Request {
|
||||||
const method = switch (std_http_request.head.method) {
|
const method = switch (httpz_request.method) {
|
||||||
.DELETE => Method.DELETE,
|
.DELETE => Method.DELETE,
|
||||||
.GET => Method.GET,
|
.GET => Method.GET,
|
||||||
.PATCH => Method.PATCH,
|
.PATCH => Method.PATCH,
|
||||||
.POST => Method.POST,
|
.POST => Method.POST,
|
||||||
.HEAD => Method.HEAD,
|
.HEAD => Method.HEAD,
|
||||||
.PUT => Method.PUT,
|
.PUT => Method.PUT,
|
||||||
.CONNECT => Method.CONNECT,
|
|
||||||
.OPTIONS => Method.OPTIONS,
|
.OPTIONS => Method.OPTIONS,
|
||||||
.TRACE => Method.TRACE,
|
|
||||||
_ => return error.JetzigUnsupportedHttpMethod,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const response_data = try allocator.create(jetzig.data.Data);
|
const response_data = try allocator.create(jetzig.data.Data);
|
||||||
@ -111,13 +112,14 @@ pub fn init(
|
|||||||
|
|
||||||
return .{
|
return .{
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
.path = jetzig.http.Path.init(std_http_request.head.target),
|
.path = jetzig.http.Path.init(httpz_request.url.raw),
|
||||||
.method = method,
|
.method = method,
|
||||||
.headers = jetzig.http.Headers.init(allocator),
|
.headers = jetzig.http.Headers.init(allocator, &httpz_request.headers),
|
||||||
.server = server,
|
.server = server,
|
||||||
.response = response,
|
.response = response,
|
||||||
.response_data = response_data,
|
.response_data = response_data,
|
||||||
.std_http_request = std_http_request,
|
.httpz_request = httpz_request,
|
||||||
|
.httpz_response = httpz_response,
|
||||||
.start_time = start_time,
|
.start_time = start_time,
|
||||||
.store = .{ .store = server.store, .allocator = allocator },
|
.store = .{ .store = server.store, .allocator = allocator },
|
||||||
.cache = .{ .store = server.cache, .allocator = allocator },
|
.cache = .{ .store = server.cache, .allocator = allocator },
|
||||||
@ -132,63 +134,37 @@ pub fn deinit(self: *Request) void {
|
|||||||
if (self.processed) self.allocator.free(self.body);
|
if (self.processed) self.allocator.free(self.body);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Process request, read body if present, parse headers (TODO)
|
/// Process request, read body if present.
|
||||||
pub fn process(self: *Request) !void {
|
pub fn process(self: *Request) !void {
|
||||||
var headers_it = self.std_http_request.iterateHeaders();
|
self.body = self.httpz_request.body() orelse "";
|
||||||
var cookie: ?[]const u8 = null;
|
|
||||||
|
|
||||||
while (headers_it.next()) |header| {
|
|
||||||
try self.headers.append(header.name, header.value);
|
|
||||||
if (std.mem.eql(u8, header.name, "Cookie")) cookie = header.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.cookies = try self.allocator.create(jetzig.http.Cookies);
|
|
||||||
self.cookies.* = jetzig.http.Cookies.init(
|
|
||||||
self.allocator,
|
|
||||||
cookie orelse "",
|
|
||||||
);
|
|
||||||
try self.cookies.parse();
|
|
||||||
|
|
||||||
self.session = try self.allocator.create(jetzig.http.Session);
|
|
||||||
self.session.* = jetzig.http.Session.init(self.allocator, self.cookies, self.server.options.secret);
|
|
||||||
self.session.parse() catch |err| {
|
|
||||||
switch (err) {
|
|
||||||
error.JetzigInvalidSessionCookie => {
|
|
||||||
try self.server.logger.DEBUG("Invalid session cookie detected. Resetting session.", .{});
|
|
||||||
try self.session.reset();
|
|
||||||
},
|
|
||||||
else => return err,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const reader = try self.std_http_request.reader();
|
|
||||||
self.body = try reader.readAllAlloc(self.allocator, jetzig.config.get(usize, "max_bytes_request_body"));
|
|
||||||
self.processed = true;
|
self.processed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set response headers, write response payload, and finalize the response.
|
pub const CallbackState = struct {
|
||||||
pub fn respond(self: *Request) !void {
|
arena: *std.heap.ArenaAllocator,
|
||||||
if (!self.processed) unreachable;
|
allocator: std.mem.Allocator,
|
||||||
|
};
|
||||||
|
|
||||||
var cookie_it = self.cookies.headerIterator();
|
pub fn responseCompleteCallback(ptr: *anyopaque) void {
|
||||||
while (try cookie_it.next()) |header| {
|
var state: *CallbackState = @ptrCast(@alignCast(ptr));
|
||||||
// FIXME: Skip setting cookies that are already present ?
|
state.arena.deinit();
|
||||||
try self.response.headers.append("Set-Cookie", header);
|
state.allocator.destroy(state.arena);
|
||||||
|
state.allocator.destroy(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
var std_response_headers = try self.response.headers.stdHeaders();
|
/// Set response headers, write response payload, and finalize the response.
|
||||||
defer std_response_headers.deinit(self.allocator);
|
pub fn respond(
|
||||||
|
self: *Request,
|
||||||
|
state: *CallbackState,
|
||||||
|
) !void {
|
||||||
|
if (!self.processed) unreachable;
|
||||||
|
|
||||||
try self.std_http_request.respond(
|
try self.setCookieHeaders();
|
||||||
self.response.content,
|
|
||||||
.{
|
const status = jetzig.http.status_codes.get(self.response.status_code);
|
||||||
.keep_alive = false,
|
self.httpz_response.status = try status.getCodeInt();
|
||||||
.status = switch (self.response.status_code) {
|
self.httpz_response.body = self.response.content;
|
||||||
inline else => |tag| @field(std.http.Status, @tagName(tag)),
|
self.httpz_response.callback(responseCompleteCallback, @ptrCast(state));
|
||||||
},
|
|
||||||
.extra_headers = std_response_headers.items,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render a response. This function can only be called once per request (repeat calls will
|
/// Render a response. This function can only be called once per request (repeat calls will
|
||||||
@ -226,8 +202,15 @@ pub fn redirect(
|
|||||||
|
|
||||||
self.response_data.reset();
|
self.response_data.reset();
|
||||||
|
|
||||||
self.response.headers.remove("Location");
|
self.response.headers.append("Location", location) catch |err| {
|
||||||
self.response.headers.append("Location", location) catch @panic("OOM");
|
switch (err) {
|
||||||
|
error.JetzigTooManyHeaders => std.debug.print(
|
||||||
|
"Header limit reached. Unable to add redirect header.\n",
|
||||||
|
.{},
|
||||||
|
),
|
||||||
|
else => @panic("OOM"),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
self.rendered_view = .{ .data = self.response_data, .status_code = status_code };
|
self.rendered_view = .{ .data = self.response_data, .status_code = status_code };
|
||||||
return self.rendered_view.?;
|
return self.rendered_view.?;
|
||||||
@ -315,7 +298,7 @@ pub fn queryParams(self: *Request) !*jetzig.data.Value {
|
|||||||
return self.query_params.?.data.value.?;
|
return self.query_params.?.data.value.?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parses request body as params if present, otherwise delegates to `queryParams`.
|
// Parse request body as params if present, otherwise delegate to `queryParams`.
|
||||||
fn parseQuery(self: *Request) !*jetzig.data.Value {
|
fn parseQuery(self: *Request) !*jetzig.data.Value {
|
||||||
if (self.body.len == 0) return try self.queryParams();
|
if (self.body.len == 0) return try self.queryParams();
|
||||||
if (self.query_body) |parsed| return parsed.data.value.?;
|
if (self.query_body) |parsed| return parsed.data.value.?;
|
||||||
@ -332,7 +315,46 @@ fn parseQuery(self: *Request) !*jetzig.data.Value {
|
|||||||
return self.query_body.?.data.value.?;
|
return self.query_body.?.data.value.?;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new Job. Receives a job name which must resolve to `src/app/jobs/<name>.zig`
|
/// Parse `Cookie` header into separate cookies.
|
||||||
|
pub fn cookies(self: *Request) !*jetzig.http.Cookies {
|
||||||
|
if (self._cookies) |capture| return capture;
|
||||||
|
|
||||||
|
const cookie = self.httpz_request.headers.get("cookie");
|
||||||
|
|
||||||
|
const local_cookies = try self.allocator.create(jetzig.http.Cookies);
|
||||||
|
local_cookies.* = jetzig.http.Cookies.init(
|
||||||
|
self.allocator,
|
||||||
|
cookie orelse "",
|
||||||
|
);
|
||||||
|
try local_cookies.parse();
|
||||||
|
|
||||||
|
self._cookies = local_cookies;
|
||||||
|
|
||||||
|
return local_cookies;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse cookies, decrypt Jetzig cookie (`jetzig.http.Session.cookie_name`) and return a mutable
|
||||||
|
/// `jetzig.http.Session`.
|
||||||
|
pub fn session(self: *Request) !*jetzig.http.Session {
|
||||||
|
if (self._session) |capture| return capture;
|
||||||
|
|
||||||
|
const local_session = try self.allocator.create(jetzig.http.Session);
|
||||||
|
local_session.* = jetzig.http.Session.init(self.allocator, try self.cookies(), self.server.options.secret);
|
||||||
|
local_session.parse() catch |err| {
|
||||||
|
switch (err) {
|
||||||
|
error.JetzigInvalidSessionCookie => {
|
||||||
|
try self.server.logger.DEBUG("Invalid session cookie detected. Resetting session.", .{});
|
||||||
|
try local_session.reset();
|
||||||
|
},
|
||||||
|
else => return err,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self._session = local_session;
|
||||||
|
return local_session;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new Job. Receives a job name which must resolve to `src/app/jobs/<name>.zig`
|
||||||
/// Call `Job.put(...)` to set job params.
|
/// Call `Job.put(...)` to set job params.
|
||||||
/// Call `Job.background()` to run the job outside of the request/response flow.
|
/// Call `Job.background()` to run the job outside of the request/response flow.
|
||||||
/// e.g.:
|
/// e.g.:
|
||||||
@ -415,6 +437,14 @@ const RequestMail = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Create a new email from the mailer named `name` (`app/mailers/<name>.zig`). Pass delivery
|
||||||
|
/// params to override defaults defined my mailer (`to`, `from`, `subject`, etc.).
|
||||||
|
/// Must call `deliver` on the returned `RequestMail` to send the email.
|
||||||
|
/// Example:
|
||||||
|
/// ```zig
|
||||||
|
/// const mail = request.mail("welcome", .{ .to = &.{"hello@jetzig.dev"} });
|
||||||
|
/// try mail.deliver(.background, .{});
|
||||||
|
/// ```
|
||||||
pub fn mail(self: *Request, name: []const u8, mail_params: jetzig.mail.MailParams) RequestMail {
|
pub fn mail(self: *Request, name: []const u8, mail_params: jetzig.mail.MailParams) RequestMail {
|
||||||
return .{
|
return .{
|
||||||
.request = self,
|
.request = self,
|
||||||
@ -435,35 +465,23 @@ fn extensionFormat(self: *const Request) ?jetzig.http.Request.Format {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn acceptHeaderFormat(self: *const Request) ?jetzig.http.Request.Format {
|
pub fn acceptHeaderFormat(self: *const Request) ?jetzig.http.Request.Format {
|
||||||
const acceptHeader = self.getHeader("Accept");
|
if (self.httpz_request.headers.get("accept")) |value| {
|
||||||
|
if (std.mem.eql(u8, value, "text/html")) return .HTML;
|
||||||
if (acceptHeader) |item| {
|
if (std.mem.eql(u8, value, "application/json")) return .JSON;
|
||||||
if (std.mem.eql(u8, item, "text/html")) return .HTML;
|
|
||||||
if (std.mem.eql(u8, item, "application/json")) return .JSON;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn contentTypeHeaderFormat(self: *const Request) ?jetzig.http.Request.Format {
|
pub fn contentTypeHeaderFormat(self: *const Request) ?jetzig.http.Request.Format {
|
||||||
const acceptHeader = self.getHeader("content-type");
|
if (self.httpz_request.headers.get("content-type")) |value| {
|
||||||
|
if (std.mem.eql(u8, value, "text/html")) return .HTML;
|
||||||
if (acceptHeader) |item| {
|
if (std.mem.eql(u8, value, "application/json")) return .JSON;
|
||||||
if (std.mem.eql(u8, item, "text/html")) return .HTML;
|
|
||||||
if (std.mem.eql(u8, item, "application/json")) return .JSON;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn hash(self: *Request) ![]const u8 {
|
|
||||||
return try std.fmt.allocPrint(
|
|
||||||
self.allocator,
|
|
||||||
"{s}-{s}-{s}",
|
|
||||||
.{ @tagName(self.method), self.path, @tagName(self.requestFormat()) },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fmtMethod(self: *const Request, colorized: bool) []const u8 {
|
pub fn fmtMethod(self: *const Request, colorized: bool) []const u8 {
|
||||||
if (!colorized) return @tagName(self.method);
|
if (!colorized) return @tagName(self.method);
|
||||||
|
|
||||||
@ -508,6 +526,17 @@ pub fn setResponse(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn setCookieHeaders(self: *Request) !void {
|
||||||
|
const local_cookies = self._cookies orelse return;
|
||||||
|
if (!local_cookies.modified) return;
|
||||||
|
|
||||||
|
var cookie_it = local_cookies.headerIterator();
|
||||||
|
while (try cookie_it.next()) |header| {
|
||||||
|
// FIXME: Skip setting cookies that are already present ?
|
||||||
|
try self.response.headers.append("Set-Cookie", header);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Determine if a given route matches the current request.
|
// Determine if a given route matches the current request.
|
||||||
pub fn match(self: *Request, route: jetzig.views.Route) !bool {
|
pub fn match(self: *Request, route: jetzig.views.Route) !bool {
|
||||||
return switch (self.method) {
|
return switch (self.method) {
|
||||||
|
@ -1,27 +1,28 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
|
const httpz = @import("httpz");
|
||||||
|
|
||||||
const jetzig = @import("../../jetzig.zig");
|
const jetzig = @import("../../jetzig.zig");
|
||||||
const http = @import("../http.zig");
|
const http = @import("../http.zig");
|
||||||
|
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
headers: *jetzig.http.Headers,
|
headers: jetzig.http.Headers,
|
||||||
content: []const u8,
|
content: []const u8,
|
||||||
status_code: http.status_codes.StatusCode,
|
status_code: http.status_codes.StatusCode,
|
||||||
content_type: []const u8,
|
content_type: []const u8,
|
||||||
|
|
||||||
pub fn init(
|
pub fn init(
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
|
httpz_response: *httpz.Response,
|
||||||
) !Self {
|
) !Self {
|
||||||
const headers = try allocator.create(jetzig.http.Headers);
|
|
||||||
headers.* = jetzig.http.Headers.init(allocator);
|
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
.status_code = .no_content,
|
.status_code = .no_content,
|
||||||
.content_type = "application/octet-stream",
|
.content_type = "application/octet-stream",
|
||||||
.content = "",
|
.content = "",
|
||||||
.headers = headers,
|
.headers = jetzig.http.Headers.init(allocator, &httpz_response.headers),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
const jetzig = @import("../../jetzig.zig");
|
const jetzig = @import("../../jetzig.zig");
|
||||||
const zmpl = @import("zmpl");
|
const zmpl = @import("zmpl");
|
||||||
const zmd = @import("zmd");
|
const zmd = @import("zmd");
|
||||||
|
const httpz = @import("httpz");
|
||||||
|
|
||||||
pub const ServerOptions = struct {
|
pub const ServerOptions = struct {
|
||||||
logger: jetzig.loggers.Logger,
|
logger: jetzig.loggers.Logger,
|
||||||
@ -11,6 +13,7 @@ pub const ServerOptions = struct {
|
|||||||
secret: []const u8,
|
secret: []const u8,
|
||||||
detach: bool,
|
detach: bool,
|
||||||
environment: jetzig.Environment.EnvironmentName,
|
environment: jetzig.Environment.EnvironmentName,
|
||||||
|
log_queue: *jetzig.loggers.LogQueue,
|
||||||
};
|
};
|
||||||
|
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
@ -59,64 +62,86 @@ pub fn deinit(self: *Server) void {
|
|||||||
self.allocator.free(self.options.bind);
|
self.allocator.free(self.options.bind);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn listen(self: *Server) !void {
|
const Dispatcher = struct {
|
||||||
const address = try std.net.Address.parseIp(self.options.bind, self.options.port);
|
server: *Server,
|
||||||
self.std_net_server = try address.listen(.{ .reuse_port = true });
|
|
||||||
|
|
||||||
self.initialized = true;
|
pub fn handle(self: Dispatcher, request: *httpz.Request, response: *httpz.Response) void {
|
||||||
|
self.server.processNextRequest(request, response) catch |err| {
|
||||||
|
self.server.errorHandlerFn(request, response, err);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn listen(self: *Server) !void {
|
||||||
|
var httpz_server = try httpz.ServerCtx(Dispatcher, Dispatcher).init(
|
||||||
|
self.allocator,
|
||||||
|
.{
|
||||||
|
.port = self.options.port,
|
||||||
|
.address = self.options.bind,
|
||||||
|
.thread_pool = .{ .count = @intCast(try std.Thread.getCpuCount()) },
|
||||||
|
},
|
||||||
|
Dispatcher{ .server = self },
|
||||||
|
);
|
||||||
|
defer httpz_server.deinit();
|
||||||
|
|
||||||
try self.logger.INFO("Listening on http://{s}:{} [{s}]", .{
|
try self.logger.INFO("Listening on http://{s}:{} [{s}]", .{
|
||||||
self.options.bind,
|
self.options.bind,
|
||||||
self.options.port,
|
self.options.port,
|
||||||
@tagName(self.options.environment),
|
@tagName(self.options.environment),
|
||||||
});
|
});
|
||||||
try self.processRequests();
|
|
||||||
|
self.initialized = true;
|
||||||
|
|
||||||
|
return try httpz_server.listen();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn processRequests(self: *Server) !void {
|
pub fn errorHandlerFn(self: *Server, request: *httpz.Request, response: *httpz.Response, err: anyerror) void {
|
||||||
// TODO: Keepalive
|
if (isBadHttpError(err)) return;
|
||||||
while (true) {
|
|
||||||
var arena = std.heap.ArenaAllocator.init(self.allocator);
|
|
||||||
errdefer arena.deinit();
|
|
||||||
const allocator = arena.allocator();
|
|
||||||
|
|
||||||
const connection = try self.std_net_server.accept();
|
self.logger.ERROR("Encountered error: {s} {s}", .{ @errorName(err), request.url.raw }) catch {};
|
||||||
|
response.body = "500 Internal Server Error";
|
||||||
var buf: [jetzig.config.get(usize, "http_buffer_size")]u8 = undefined;
|
|
||||||
var std_http_server = std.http.Server.init(connection, &buf);
|
|
||||||
errdefer std_http_server.connection.stream.close();
|
|
||||||
|
|
||||||
self.processNextRequest(allocator, &std_http_server) catch |err| {
|
|
||||||
if (isBadHttpError(err)) {
|
|
||||||
std_http_server.connection.stream.close();
|
|
||||||
continue;
|
|
||||||
} else return err;
|
|
||||||
};
|
|
||||||
|
|
||||||
std_http_server.connection.stream.close();
|
|
||||||
arena.deinit();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn processNextRequest(self: *Server, allocator: std.mem.Allocator, std_http_server: *std.http.Server) !void {
|
fn processNextRequest(
|
||||||
|
self: *Server,
|
||||||
|
httpz_request: *httpz.Request,
|
||||||
|
httpz_response: *httpz.Response,
|
||||||
|
) !void {
|
||||||
const start_time = std.time.nanoTimestamp();
|
const start_time = std.time.nanoTimestamp();
|
||||||
|
|
||||||
const std_http_request = try std_http_server.receiveHead();
|
const state = try self.allocator.create(jetzig.http.Request.CallbackState);
|
||||||
if (std_http_server.state == .receiving_head) return error.JetzigParseHeadError;
|
const arena = try self.allocator.create(std.heap.ArenaAllocator);
|
||||||
|
arena.* = std.heap.ArenaAllocator.init(self.allocator);
|
||||||
|
state.* = .{
|
||||||
|
.arena = arena,
|
||||||
|
.allocator = self.allocator,
|
||||||
|
};
|
||||||
|
|
||||||
var response = try jetzig.http.Response.init(allocator);
|
// Regular arena deinit occurs in jetzig.http.Request.responseCompletCallback
|
||||||
var request = try jetzig.http.Request.init(allocator, self, start_time, std_http_request, &response);
|
errdefer state.arena.deinit();
|
||||||
|
|
||||||
|
const allocator = state.arena.allocator();
|
||||||
|
|
||||||
|
var response = try jetzig.http.Response.init(allocator, httpz_response);
|
||||||
|
var request = try jetzig.http.Request.init(
|
||||||
|
allocator,
|
||||||
|
self,
|
||||||
|
start_time,
|
||||||
|
httpz_request,
|
||||||
|
httpz_response,
|
||||||
|
&response,
|
||||||
|
);
|
||||||
|
|
||||||
try request.process();
|
try request.process();
|
||||||
|
|
||||||
var middleware_data = try jetzig.http.middleware.afterRequest(&request);
|
var middleware_data = try jetzig.http.middleware.afterRequest(&request);
|
||||||
|
|
||||||
try self.renderResponse(&request);
|
try self.renderResponse(&request);
|
||||||
try request.response.headers.append("content-type", response.content_type);
|
try request.response.headers.append("Content-Type", response.content_type);
|
||||||
|
|
||||||
try jetzig.http.middleware.beforeResponse(&middleware_data, &request);
|
try jetzig.http.middleware.beforeResponse(&middleware_data, &request);
|
||||||
|
|
||||||
try request.respond();
|
try request.respond(state);
|
||||||
|
|
||||||
try jetzig.http.middleware.afterResponse(&middleware_data, &request);
|
try jetzig.http.middleware.afterResponse(&middleware_data, &request);
|
||||||
jetzig.http.middleware.deinit(&middleware_data, &request);
|
jetzig.http.middleware.deinit(&middleware_data, &request);
|
||||||
@ -168,7 +193,17 @@ fn renderHTML(
|
|||||||
};
|
};
|
||||||
return request.setResponse(rendered, .{});
|
return request.setResponse(rendered, .{});
|
||||||
} else {
|
} else {
|
||||||
return request.setResponse(try self.renderNotFound(request), .{});
|
// Try rendering without a template to see if we get a redirect.
|
||||||
|
const rendered = self.renderView(matched_route, request, null) catch |err| {
|
||||||
|
if (isUnhandledError(err)) return err;
|
||||||
|
const rendered_error = try self.renderInternalServerError(request, err);
|
||||||
|
return request.setResponse(rendered_error, .{});
|
||||||
|
};
|
||||||
|
|
||||||
|
return if (request.redirected)
|
||||||
|
request.setResponse(rendered, .{})
|
||||||
|
else
|
||||||
|
request.setResponse(try self.renderNotFound(request), .{});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (try self.renderMarkdown(request)) |rendered| {
|
if (try self.renderMarkdown(request)) |rendered| {
|
||||||
|
@ -28,6 +28,7 @@ pub fn init(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parse session cookie.
|
||||||
pub fn parse(self: *Self) !void {
|
pub fn parse(self: *Self) !void {
|
||||||
if (self.cookies.get(cookie_name)) |cookie| {
|
if (self.cookies.get(cookie_name)) |cookie| {
|
||||||
try self.parseSessionCookie(cookie.value);
|
try self.parseSessionCookie(cookie.value);
|
||||||
@ -36,6 +37,7 @@ pub fn parse(self: *Self) !void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reset session to an empty state.
|
||||||
pub fn reset(self: *Self) !void {
|
pub fn reset(self: *Self) !void {
|
||||||
self.data.reset();
|
self.data.reset();
|
||||||
_ = try self.data.object();
|
_ = try self.data.object();
|
||||||
@ -43,12 +45,14 @@ pub fn reset(self: *Self) !void {
|
|||||||
try self.save();
|
try self.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Free allocated memory.
|
||||||
pub fn deinit(self: *Self) void {
|
pub fn deinit(self: *Self) void {
|
||||||
if (self.state != .parsed) return;
|
if (self.state != .parsed) return;
|
||||||
|
|
||||||
self.data.deinit();
|
self.data.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get a value from the session.
|
||||||
pub fn get(self: *Self, key: []const u8) !?*jetzig.data.Value {
|
pub fn get(self: *Self, key: []const u8) !?*jetzig.data.Value {
|
||||||
if (self.state != .parsed) return error.UnparsedSessionCookie;
|
if (self.state != .parsed) return error.UnparsedSessionCookie;
|
||||||
|
|
||||||
@ -58,6 +62,7 @@ pub fn get(self: *Self, key: []const u8) !?*jetzig.data.Value {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Put a value into the session.
|
||||||
pub fn put(self: *Self, key: []const u8, value: *jetzig.data.Value) !void {
|
pub fn put(self: *Self, key: []const u8, value: *jetzig.data.Value) !void {
|
||||||
if (self.state != .parsed) return error.UnparsedSessionCookie;
|
if (self.state != .parsed) return error.UnparsedSessionCookie;
|
||||||
|
|
||||||
|
@ -178,6 +178,12 @@ pub const TaggedStatusCode = union(StatusCode) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn getCodeInt(self: Self) !u16 {
|
||||||
|
return switch (self) {
|
||||||
|
inline else => |capture| try std.fmt.parseInt(u16, capture.code, 10),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
pub fn getMessage(self: Self) []const u8 {
|
pub fn getMessage(self: Self) []const u8 {
|
||||||
return switch (self) {
|
return switch (self) {
|
||||||
inline else => |capture| capture.message,
|
inline else => |capture| capture.message,
|
||||||
|
@ -6,10 +6,18 @@ 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 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, json };
|
||||||
|
|
||||||
|
/// Infer a log target (stdout or stderr) from a given log level.
|
||||||
|
pub inline fn logTarget(comptime level: LogLevel) LogQueue.Target {
|
||||||
|
return switch (level) {
|
||||||
|
.TRACE, .DEBUG, .INFO => .stdout,
|
||||||
|
.WARN, .ERROR, .FATAL => .stderr,
|
||||||
|
};
|
||||||
|
}
|
||||||
pub const Logger = union(enum) {
|
pub const Logger = union(enum) {
|
||||||
development_logger: DevelopmentLogger,
|
development_logger: DevelopmentLogger,
|
||||||
json_logger: JsonLogger,
|
json_logger: JsonLogger,
|
||||||
|
@ -8,28 +8,23 @@ 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,
|
||||||
mutex: std.Thread.Mutex,
|
log_queue: *jetzig.loggers.LogQueue,
|
||||||
|
|
||||||
/// 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,
|
||||||
stdout: std.fs.File,
|
log_queue: *jetzig.loggers.LogQueue,
|
||||||
stderr: std.fs.File,
|
|
||||||
) DevelopmentLogger {
|
) DevelopmentLogger {
|
||||||
return .{
|
return .{
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
.level = level,
|
.level = level,
|
||||||
.stdout = stdout,
|
.log_queue = log_queue,
|
||||||
.stderr = stderr,
|
.stdout_colorized = log_queue.stdout_is_tty,
|
||||||
.stdout_colorized = stdout.isTty(),
|
.stderr_colorized = log_queue.stderr_is_tty,
|
||||||
.stderr_colorized = stderr.isTty(),
|
|
||||||
.mutex = std.Thread.Mutex{},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,40 +40,28 @@ pub fn log(
|
|||||||
const output = try std.fmt.allocPrint(self.allocator, message, args);
|
const output = try std.fmt.allocPrint(self.allocator, message, args);
|
||||||
defer self.allocator.free(output);
|
defer self.allocator.free(output);
|
||||||
|
|
||||||
const timestamp = Timestamp.init(std.time.timestamp(), self.allocator);
|
const timestamp = Timestamp.init(std.time.timestamp());
|
||||||
const iso8601 = try timestamp.iso8601();
|
var timestamp_buf: [256]u8 = undefined;
|
||||||
defer self.allocator.free(iso8601);
|
const iso8601 = try timestamp.iso8601(×tamp_buf);
|
||||||
|
|
||||||
const colorized = switch (level) {
|
const target = jetzig.loggers.logTarget(level);
|
||||||
.TRACE, .DEBUG, .INFO => self.stdout_colorized,
|
const formatted_level = colorizedLogLevel(level);
|
||||||
.WARN, .ERROR, .FATAL => self.stderr_colorized,
|
|
||||||
};
|
|
||||||
const file = switch (level) {
|
|
||||||
.TRACE, .DEBUG, .INFO => self.stdout,
|
|
||||||
.WARN, .ERROR, .FATAL => self.stderr,
|
|
||||||
};
|
|
||||||
const writer = file.writer();
|
|
||||||
const level_formatted = if (colorized) colorizedLogLevel(level) else @tagName(level);
|
|
||||||
|
|
||||||
@constCast(self).mutex.lock();
|
try self.log_queue.print(
|
||||||
defer @constCast(self).mutex.unlock();
|
"{s: >5} [{s}] {s}\n",
|
||||||
|
.{ formatted_level, iso8601, output },
|
||||||
try writer.print("{s: >5} [{s}] {s}\n", .{ level_formatted, iso8601, output });
|
target,
|
||||||
|
);
|
||||||
if (!file.isTty()) try file.sync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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 {
|
||||||
const formatted_duration = if (self.stdout_colorized)
|
var duration_buf: [256]u8 = undefined;
|
||||||
try jetzig.colors.duration(self.allocator, jetzig.util.duration(request.start_time))
|
const formatted_duration = try jetzig.colors.duration(
|
||||||
else
|
&duration_buf,
|
||||||
try std.fmt.allocPrint(
|
jetzig.util.duration(request.start_time),
|
||||||
self.allocator,
|
self.stdout_colorized,
|
||||||
"{}",
|
|
||||||
.{std.fmt.fmtDurationSigned(jetzig.util.duration(request.start_time))},
|
|
||||||
);
|
);
|
||||||
defer self.allocator.free(formatted_duration);
|
|
||||||
|
|
||||||
const status: jetzig.http.status_codes.TaggedStatusCode = switch (request.response.status_code) {
|
const status: jetzig.http.status_codes.TaggedStatusCode = switch (request.response.status_code) {
|
||||||
inline else => |status_code| @unionInit(
|
inline else => |status_code| @unionInit(
|
||||||
@ -93,17 +76,23 @@ pub fn logRequest(self: DevelopmentLogger, request: *const jetzig.http.Request)
|
|||||||
else
|
else
|
||||||
status.getFormatted(.{});
|
status.getFormatted(.{});
|
||||||
|
|
||||||
const message = try std.fmt.allocPrint(self.allocator, "[{s}/{s}/{s}] {s}", .{
|
const timestamp = Timestamp.init(std.time.timestamp());
|
||||||
|
var timestamp_buf: [256]u8 = undefined;
|
||||||
|
const iso8601 = try timestamp.iso8601(×tamp_buf);
|
||||||
|
|
||||||
|
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,
|
||||||
|
iso8601,
|
||||||
formatted_duration,
|
formatted_duration,
|
||||||
request.fmtMethod(self.stdout_colorized),
|
request.fmtMethod(self.stdout_colorized),
|
||||||
formatted_status,
|
formatted_status,
|
||||||
request.path.path,
|
request.path.path,
|
||||||
});
|
}, .stdout);
|
||||||
defer self.allocator.free(message);
|
|
||||||
try self.log(.INFO, "{s}", .{message});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)),
|
||||||
.DEBUG => jetzig.colors.cyan(@tagName(level)),
|
.DEBUG => jetzig.colors.cyan(@tagName(level)),
|
||||||
|
@ -11,6 +11,7 @@ const LogMessage = struct {
|
|||||||
timestamp: []const u8,
|
timestamp: []const u8,
|
||||||
message: []const u8,
|
message: []const u8,
|
||||||
};
|
};
|
||||||
|
|
||||||
const RequestLogMessage = struct {
|
const RequestLogMessage = struct {
|
||||||
level: []const u8,
|
level: []const u8,
|
||||||
timestamp: []const u8,
|
timestamp: []const u8,
|
||||||
@ -21,24 +22,19 @@ const RequestLogMessage = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
stdout: std.fs.File,
|
log_queue: *jetzig.loggers.LogQueue,
|
||||||
stderr: std.fs.File,
|
|
||||||
level: LogLevel,
|
level: LogLevel,
|
||||||
mutex: std.Thread.Mutex,
|
|
||||||
|
|
||||||
/// Initialize a new JSON Logger.
|
/// Initialize a new JSON Logger.
|
||||||
pub fn init(
|
pub fn init(
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
level: LogLevel,
|
level: LogLevel,
|
||||||
stdout: std.fs.File,
|
log_queue: *jetzig.loggers.LogQueue,
|
||||||
stderr: std.fs.File,
|
|
||||||
) JsonLogger {
|
) JsonLogger {
|
||||||
return .{
|
return .{
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
.level = level,
|
.level = level,
|
||||||
.stdout = stdout,
|
.log_queue = log_queue,
|
||||||
.stderr = stderr,
|
|
||||||
.mutex = std.Thread.Mutex{},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,24 +50,16 @@ pub fn log(
|
|||||||
const output = try std.fmt.allocPrint(self.allocator, message, args);
|
const output = try std.fmt.allocPrint(self.allocator, message, args);
|
||||||
defer self.allocator.free(output);
|
defer self.allocator.free(output);
|
||||||
|
|
||||||
const timestamp = Timestamp.init(std.time.timestamp(), self.allocator);
|
const timestamp = Timestamp.init(std.time.timestamp());
|
||||||
const iso8601 = try timestamp.iso8601();
|
var timestamp_buf: [256]u8 = undefined;
|
||||||
defer self.allocator.free(iso8601);
|
const iso8601 = try timestamp.iso8601(×tamp_buf);
|
||||||
|
|
||||||
const file = self.getFile(level);
|
|
||||||
const writer = file.writer();
|
|
||||||
const log_message = LogMessage{ .level = @tagName(level), .timestamp = iso8601, .message = output };
|
const log_message = LogMessage{ .level = @tagName(level), .timestamp = iso8601, .message = output };
|
||||||
|
|
||||||
const json = try std.json.stringifyAlloc(self.allocator, log_message, .{ .whitespace = .minified });
|
const json = try std.json.stringifyAlloc(self.allocator, log_message, .{ .whitespace = .minified });
|
||||||
defer self.allocator.free(json);
|
defer self.allocator.free(json);
|
||||||
|
|
||||||
@constCast(self).mutex.lock();
|
try self.log_queue.print("{s}\n", .{json}, jetzig.loggers.logTarget(level));
|
||||||
defer @constCast(self).mutex.unlock();
|
|
||||||
|
|
||||||
try writer.writeAll(json);
|
|
||||||
try writer.writeByte('\n');
|
|
||||||
|
|
||||||
if (!file.isTty()) try file.sync(); // Make configurable ?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Log a one-liner including response status code, path, method, duration, etc.
|
/// Log a one-liner including response status code, path, method, duration, etc.
|
||||||
@ -80,9 +68,9 @@ pub fn logRequest(self: *const JsonLogger, request: *const jetzig.http.Request)
|
|||||||
|
|
||||||
const duration = jetzig.util.duration(request.start_time);
|
const duration = jetzig.util.duration(request.start_time);
|
||||||
|
|
||||||
const timestamp = Timestamp.init(std.time.timestamp(), self.allocator);
|
const timestamp = Timestamp.init(std.time.timestamp());
|
||||||
const iso8601 = try timestamp.iso8601();
|
var timestamp_buf: [256]u8 = undefined;
|
||||||
defer self.allocator.free(iso8601);
|
const iso8601 = try timestamp.iso8601(×tamp_buf);
|
||||||
|
|
||||||
const status = switch (request.response.status_code) {
|
const status = switch (request.response.status_code) {
|
||||||
inline else => |status_code| @unionInit(
|
inline else => |status_code| @unionInit(
|
||||||
@ -91,6 +79,7 @@ pub fn logRequest(self: *const JsonLogger, request: *const jetzig.http.Request)
|
|||||||
.{},
|
.{},
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
const message = RequestLogMessage{
|
const message = RequestLogMessage{
|
||||||
.level = @tagName(level),
|
.level = @tagName(level),
|
||||||
.timestamp = iso8601,
|
.timestamp = iso8601,
|
||||||
@ -99,19 +88,17 @@ pub fn logRequest(self: *const JsonLogger, request: *const jetzig.http.Request)
|
|||||||
.path = request.path.path,
|
.path = request.path.path,
|
||||||
.duration = duration,
|
.duration = duration,
|
||||||
};
|
};
|
||||||
const json = try std.json.stringifyAlloc(self.allocator, message, .{ .whitespace = .minified });
|
|
||||||
defer self.allocator.free(json);
|
|
||||||
|
|
||||||
const file = self.getFile(level);
|
var buf: [4096]u8 = undefined;
|
||||||
const writer = file.writer();
|
var stream = std.io.fixedBufferStream(&buf);
|
||||||
|
std.json.stringify(message, .{ .whitespace = .minified }, stream.writer()) catch |err| {
|
||||||
|
switch (err) {
|
||||||
|
error.NoSpaceLeft => {}, // TODO: Spill to heap
|
||||||
|
else => return err,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
@constCast(self).mutex.lock();
|
try self.log_queue.print("{s}\n", .{stream.getWritten()}, .stdout);
|
||||||
defer @constCast(self).mutex.unlock();
|
|
||||||
|
|
||||||
try writer.writeAll(json);
|
|
||||||
try writer.writeByte('\n');
|
|
||||||
|
|
||||||
if (!file.isTty()) try file.sync(); // Make configurable ?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn getFile(self: JsonLogger, level: LogLevel) std.fs.File {
|
fn getFile(self: JsonLogger, level: LogLevel) std.fs.File {
|
||||||
|
370
src/jetzig/loggers/LogQueue.zig
Normal file
370
src/jetzig/loggers/LogQueue.zig
Normal file
@ -0,0 +1,370 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
|
const jetzig = @import("../../jetzig.zig");
|
||||||
|
|
||||||
|
const buffer_size = jetzig.config.get(usize, "log_message_buffer_len");
|
||||||
|
const max_pool_len = jetzig.config.get(usize, "max_log_pool_len");
|
||||||
|
|
||||||
|
const List = std.DoublyLinkedList(Event);
|
||||||
|
const Buffer = [buffer_size]u8;
|
||||||
|
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
node_allocator: std.heap.MemoryPool(List.Node),
|
||||||
|
buffer_allocator: std.heap.MemoryPool(Buffer),
|
||||||
|
list: List,
|
||||||
|
read_write_mutex: std.Thread.Mutex,
|
||||||
|
condition: std.Thread.Condition,
|
||||||
|
condition_mutex: std.Thread.Mutex,
|
||||||
|
writer: Writer = undefined,
|
||||||
|
reader: Reader = undefined,
|
||||||
|
node_pool: std.ArrayList(*List.Node),
|
||||||
|
buffer_pool: std.ArrayList(*Buffer),
|
||||||
|
position: usize,
|
||||||
|
stdout_is_tty: bool = undefined,
|
||||||
|
stderr_is_tty: bool = undefined,
|
||||||
|
stdout_colorize: bool = undefined,
|
||||||
|
stderr_colorize: bool = undefined,
|
||||||
|
state: enum { pending, ready } = .pending,
|
||||||
|
|
||||||
|
const LogQueue = @This();
|
||||||
|
|
||||||
|
pub const Target = enum { stdout, stderr };
|
||||||
|
|
||||||
|
const Event = struct {
|
||||||
|
message: *Buffer,
|
||||||
|
len: usize,
|
||||||
|
target: Target,
|
||||||
|
ptr: ?[]const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Create a new `LogQueue`.
|
||||||
|
pub fn init(allocator: std.mem.Allocator) LogQueue {
|
||||||
|
return .{
|
||||||
|
.allocator = allocator,
|
||||||
|
.node_allocator = initPool(allocator, List.Node),
|
||||||
|
.buffer_allocator = initPool(allocator, Buffer),
|
||||||
|
.list = List{},
|
||||||
|
.condition = std.Thread.Condition{},
|
||||||
|
.condition_mutex = std.Thread.Mutex{},
|
||||||
|
.read_write_mutex = std.Thread.Mutex{},
|
||||||
|
.node_pool = std.ArrayList(*List.Node).init(allocator),
|
||||||
|
.buffer_pool = std.ArrayList(*Buffer).init(allocator),
|
||||||
|
.position = 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Free allocated resources and return to `pending` state.
|
||||||
|
pub fn deinit(self: *LogQueue) void {
|
||||||
|
self.node_pool.deinit();
|
||||||
|
self.buffer_pool.deinit();
|
||||||
|
|
||||||
|
self.buffer_allocator.deinit();
|
||||||
|
self.node_allocator.deinit();
|
||||||
|
|
||||||
|
self.state = .pending;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the stdout and stderr outputs. Must be called before `print`.
|
||||||
|
pub fn setFiles(self: *LogQueue, stdout_file: std.fs.File, stderr_file: std.fs.File) !void {
|
||||||
|
self.writer = Writer{
|
||||||
|
.queue = self,
|
||||||
|
.mutex = std.Thread.Mutex{},
|
||||||
|
};
|
||||||
|
self.reader = Reader{
|
||||||
|
.stdout_file = stdout_file,
|
||||||
|
.stderr_file = stderr_file,
|
||||||
|
.queue = self,
|
||||||
|
};
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print(self: *LogQueue, comptime message: []const u8, args: anytype, target: Target) !void {
|
||||||
|
std.debug.assert(self.state == .ready);
|
||||||
|
|
||||||
|
try self.writer.print(message, args, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writer for `LogQueue`. Receives log events and publishes to the queue.
|
||||||
|
pub const Writer = struct {
|
||||||
|
queue: *LogQueue,
|
||||||
|
position: usize = 0,
|
||||||
|
mutex: std.Thread.Mutex,
|
||||||
|
|
||||||
|
/// Print a log event. Messages longer than `jetzig.config.get(usize, "log_message_buffer_len")`
|
||||||
|
/// spill to heap with degraded performance. Adjust buffer length or limit long entries to
|
||||||
|
/// ensure fast logging performance.
|
||||||
|
/// `target` must be `.stdout` or `.stderr`.
|
||||||
|
pub fn print(
|
||||||
|
self: *Writer,
|
||||||
|
comptime message: []const u8,
|
||||||
|
args: anytype,
|
||||||
|
target: Target,
|
||||||
|
) !void {
|
||||||
|
self.mutex.lock();
|
||||||
|
defer self.mutex.unlock();
|
||||||
|
|
||||||
|
const buf = try self.getBuffer();
|
||||||
|
self.position += 1;
|
||||||
|
var ptr: ?[]const u8 = null;
|
||||||
|
|
||||||
|
const result = std.fmt.bufPrint(buf, message, args) catch |err| switch (err) {
|
||||||
|
error.NoSpaceLeft => blk: {
|
||||||
|
ptr = try std.fmt.allocPrint(self.queue.allocator, message, args);
|
||||||
|
self.position -= 1;
|
||||||
|
break :blk null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try self.queue.append(.{
|
||||||
|
.message = buf,
|
||||||
|
.target = target,
|
||||||
|
.len = if (ptr) |capture| capture.len else result.?.len,
|
||||||
|
.ptr = ptr,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getBuffer(self: *Writer) !*Buffer {
|
||||||
|
const buffer = if (self.position >= self.queue.buffer_pool.items.len)
|
||||||
|
try self.queue.buffer_allocator.create()
|
||||||
|
else
|
||||||
|
self.queue.buffer_pool.items[self.position];
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Reader for `LogQueue`. Reads log events from the queue and writes them to the designated
|
||||||
|
/// target (stdout or stderr).
|
||||||
|
pub const Reader = struct {
|
||||||
|
stdout_file: std.fs.File,
|
||||||
|
stderr_file: std.fs.File,
|
||||||
|
queue: *LogQueue,
|
||||||
|
|
||||||
|
/// Publish log events from the queue. Invoke from a dedicated thread. Sleeps when log queue
|
||||||
|
/// is empty, wakes up when a new event is published.
|
||||||
|
pub fn publish(self: *Reader, options: struct { oneshot: bool = false }) !void {
|
||||||
|
std.debug.assert(self.queue.state == .ready);
|
||||||
|
|
||||||
|
const stdout_writer = self.stdout_file.writer();
|
||||||
|
const stderr_writer = self.stderr_file.writer();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
self.queue.condition_mutex.lock();
|
||||||
|
defer self.queue.condition_mutex.unlock();
|
||||||
|
|
||||||
|
if (!options.oneshot) self.queue.condition.wait(&self.queue.condition_mutex);
|
||||||
|
|
||||||
|
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();
|
||||||
|
defer self.queue.writer.mutex.unlock();
|
||||||
|
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (event.ptr) |ptr| {
|
||||||
|
// Log message spilled to heap
|
||||||
|
defer self.queue.allocator.free(ptr);
|
||||||
|
try writer.writeAll(ptr);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
if (self.queue.writer.position < self.queue.buffer_pool.items.len) {
|
||||||
|
self.queue.buffer_pool.items[self.queue.writer.position] = event.message;
|
||||||
|
} else {
|
||||||
|
if (self.queue.buffer_pool.items.len >= max_pool_len) {
|
||||||
|
self.queue.buffer_allocator.destroy(@alignCast(event.message));
|
||||||
|
self.queue.writer.position += 1;
|
||||||
|
} else {
|
||||||
|
try self.queue.buffer_pool.append(event.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stdout_written and !self.queue.stdout_is_tty) try self.stdout_file.sync();
|
||||||
|
if (stderr_written and !self.queue.stderr_is_tty) try self.stderr_file.sync();
|
||||||
|
|
||||||
|
if (options.oneshot) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Append a log event to the queue. Signal the publish loop thread to wake up. Recycle nodes if
|
||||||
|
// available in the pool, otherwise create a new one.
|
||||||
|
fn append(self: *LogQueue, event: Event) !void {
|
||||||
|
self.read_write_mutex.lock();
|
||||||
|
defer self.read_write_mutex.unlock();
|
||||||
|
|
||||||
|
const node = if (self.position >= self.node_pool.items.len)
|
||||||
|
try self.node_allocator.create()
|
||||||
|
else
|
||||||
|
self.node_pool.items[self.position];
|
||||||
|
|
||||||
|
self.position += 1;
|
||||||
|
|
||||||
|
node.* = .{ .data = event };
|
||||||
|
self.list.append(node);
|
||||||
|
|
||||||
|
self.condition.signal();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pop a log event from the queue. Return node to the pool for re-use.
|
||||||
|
fn popFirst(self: *LogQueue) !?Event {
|
||||||
|
self.read_write_mutex.lock();
|
||||||
|
defer self.read_write_mutex.unlock();
|
||||||
|
|
||||||
|
if (self.list.popFirst()) |node| {
|
||||||
|
const value = node.data;
|
||||||
|
self.position -= 1;
|
||||||
|
if (self.position < self.node_pool.items.len) {
|
||||||
|
self.node_pool.items[self.position] = node;
|
||||||
|
} else {
|
||||||
|
if (self.node_pool.items.len >= max_pool_len) {
|
||||||
|
self.node_allocator.destroy(node);
|
||||||
|
self.position += 1;
|
||||||
|
} else {
|
||||||
|
try self.node_pool.append(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn initPool(allocator: std.mem.Allocator, T: type) std.heap.MemoryPool(T) {
|
||||||
|
return std.heap.MemoryPool(T).initPreheated(allocator, max_pool_len) catch @panic("OOM");
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
var tmp_dir = std.testing.tmpDir(.{});
|
||||||
|
defer tmp_dir.cleanup();
|
||||||
|
|
||||||
|
const stdout = try tmp_dir.dir.createFile("stdout.log", .{ .read = true });
|
||||||
|
defer stdout.close();
|
||||||
|
|
||||||
|
const stderr = try tmp_dir.dir.createFile("stderr.log", .{ .read = true });
|
||||||
|
defer stderr.close();
|
||||||
|
|
||||||
|
try log_queue.setFiles(stdout, stderr);
|
||||||
|
try log_queue.print("foo {s}\n", .{"bar"}, .stdout);
|
||||||
|
try log_queue.print("baz {s}\n", .{"qux"}, .stderr);
|
||||||
|
try log_queue.print("quux {s}\n", .{"corge"}, .stdout);
|
||||||
|
try log_queue.print("grault {s}\n", .{"garply"}, .stderr);
|
||||||
|
try log_queue.print("waldo {s}\n", .{"fred"}, .stderr);
|
||||||
|
try log_queue.print("plugh {s}\n", .{"zyzzy"}, .stdout);
|
||||||
|
|
||||||
|
try log_queue.reader.publish(.{ .oneshot = true });
|
||||||
|
|
||||||
|
try stdout.seekTo(0);
|
||||||
|
var buf: [1024]u8 = undefined;
|
||||||
|
var len = try stdout.readAll(&buf);
|
||||||
|
|
||||||
|
try std.testing.expectEqualStrings(
|
||||||
|
\\foo bar
|
||||||
|
\\quux corge
|
||||||
|
\\plugh zyzzy
|
||||||
|
\\
|
||||||
|
, buf[0..len]);
|
||||||
|
|
||||||
|
try stderr.seekTo(0);
|
||||||
|
len = try stderr.readAll(&buf);
|
||||||
|
try std.testing.expectEqualStrings(
|
||||||
|
\\baz qux
|
||||||
|
\\grault garply
|
||||||
|
\\waldo fred
|
||||||
|
\\
|
||||||
|
, buf[0..len]);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "long messages" {
|
||||||
|
var log_queue = LogQueue.init(std.testing.allocator);
|
||||||
|
defer log_queue.deinit();
|
||||||
|
|
||||||
|
var tmp_dir = std.testing.tmpDir(.{});
|
||||||
|
defer tmp_dir.cleanup();
|
||||||
|
|
||||||
|
const stdout = try tmp_dir.dir.createFile("stdout.log", .{ .read = true });
|
||||||
|
defer stdout.close();
|
||||||
|
|
||||||
|
const stderr = try tmp_dir.dir.createFile("stderr.log", .{ .read = true });
|
||||||
|
defer stderr.close();
|
||||||
|
|
||||||
|
try log_queue.setFiles(stdout, stderr);
|
||||||
|
try log_queue.print("foo" ** buffer_size, .{}, .stdout);
|
||||||
|
|
||||||
|
try log_queue.reader.publish(.{ .oneshot = true });
|
||||||
|
|
||||||
|
try stdout.seekTo(0);
|
||||||
|
var buf: [buffer_size * 3]u8 = undefined;
|
||||||
|
const len = try stdout.readAll(&buf);
|
||||||
|
|
||||||
|
try std.testing.expectEqualStrings("foo" ** buffer_size, buf[0..len]);
|
||||||
|
}
|
@ -15,7 +15,7 @@ pub fn init(request: *jetzig.http.Request) !*Self {
|
|||||||
/// content rendered directly by the view function.
|
/// content rendered directly by the view function.
|
||||||
pub fn afterRequest(self: *Self, request: *jetzig.http.Request) !void {
|
pub fn afterRequest(self: *Self, request: *jetzig.http.Request) !void {
|
||||||
_ = self;
|
_ = self;
|
||||||
if (request.getHeader("HX-Target")) |target| {
|
if (request.headers.get("HX-Target")) |target| {
|
||||||
try request.server.logger.DEBUG(
|
try request.server.logger.DEBUG(
|
||||||
"[middleware-htmx] htmx request detected, disabling layout. (#{s})",
|
"[middleware-htmx] htmx request detected, disabling layout. (#{s})",
|
||||||
.{target},
|
.{target},
|
||||||
@ -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);
|
||||||
|
@ -3,7 +3,6 @@ const std = @import("std");
|
|||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
timestamp: i64,
|
timestamp: i64,
|
||||||
allocator: std.mem.Allocator,
|
|
||||||
|
|
||||||
const constants = struct {
|
const constants = struct {
|
||||||
pub const seconds_in_day: i64 = 60 * 60 * 24;
|
pub const seconds_in_day: i64 = 60 * 60 * 24;
|
||||||
@ -12,18 +11,18 @@ const constants = struct {
|
|||||||
pub const epoch_year: i64 = 1970;
|
pub const epoch_year: i64 = 1970;
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn init(timestamp: i64, allocator: std.mem.Allocator) Self {
|
pub fn init(timestamp: i64) Self {
|
||||||
return .{ .allocator = allocator, .timestamp = timestamp };
|
return .{ .timestamp = timestamp };
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn iso8601(self: *const Self) ![]const u8 {
|
pub fn iso8601(self: *const Self, buf: *[256]u8) ![]const u8 {
|
||||||
const u32_year: u32 = @intCast(self.year());
|
const u32_year: u32 = @intCast(self.year());
|
||||||
const u32_month: u32 = @intCast(self.month());
|
const u32_month: u32 = @intCast(self.month());
|
||||||
const u32_day_of_month: u32 = @intCast(self.dayOfMonth());
|
const u32_day_of_month: u32 = @intCast(self.dayOfMonth());
|
||||||
const u32_hour: u32 = @intCast(self.hour());
|
const u32_hour: u32 = @intCast(self.hour());
|
||||||
const u32_minute: u32 = @intCast(self.minute());
|
const u32_minute: u32 = @intCast(self.minute());
|
||||||
const u32_second: u32 = @intCast(self.second());
|
const u32_second: u32 = @intCast(self.second());
|
||||||
return try std.fmt.allocPrint(self.allocator, "{d:0>4}-{d:0>2}-{d:0>2} {d:0>2}:{d:0>2}:{d:0>2}", .{
|
return try std.fmt.bufPrint(buf, "{d:0>4}-{d:0>2}-{d:0>2}T{d:0>2}:{d:0>2}:{d:0>2}", .{
|
||||||
u32_year,
|
u32_year,
|
||||||
u32_month,
|
u32_month,
|
||||||
u32_day_of_month,
|
u32_day_of_month,
|
||||||
|
@ -6,4 +6,5 @@ test {
|
|||||||
_ = @import("jetzig/http/Path.zig");
|
_ = @import("jetzig/http/Path.zig");
|
||||||
_ = @import("jetzig/jobs/Job.zig");
|
_ = @import("jetzig/jobs/Job.zig");
|
||||||
_ = @import("jetzig/mail/Mail.zig");
|
_ = @import("jetzig/mail/Mail.zig");
|
||||||
|
_ = @import("jetzig/loggers/LogQueue.zig");
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user