mirror of
https://github.com/jetzig-framework/jetzig.git
synced 2025-05-14 22:16:08 +00:00
WIP
This commit is contained in:
parent
997ac4ec50
commit
055da0f85a
@ -95,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("jetkv", jetkv_dep.module("jetkv"));
|
||||
main_tests.root_module.addImport("httpz", httpz_dep.module("httpz"));
|
||||
const run_main_tests = b.addRunArtifact(main_tests);
|
||||
|
||||
const test_step = b.step("test", "Run library tests");
|
||||
|
@ -11,8 +11,8 @@
|
||||
.hash = "122093b741ef4aff151e916fc6005cb0c2aed747a34b77c0d4b45099ea2b561df9c7",
|
||||
},
|
||||
.jetkv = .{
|
||||
.url = "https://github.com/jetzig-framework/jetkv/archive/50016e13c89e86c89b7f7ae93d4f0a31d3be303b.tar.gz",
|
||||
.hash = "122090b828d2cdd4915d242cb3761fe9142b145e49a2341f8b29343839945d6ab256",
|
||||
.url = "https://github.com/jetzig-framework/jetkv/archive/6fc375b1ece563ae6d16849bb7c0441ff2883a04.tar.gz",
|
||||
.hash = "122079edca9ea46ebb5ce8f05ea2c58ee957cf2d73fcfd9a0fd6a50f65879f3bf88f",
|
||||
},
|
||||
.args = .{
|
||||
.url = "https://github.com/MasterQ32/zig-args/archive/01d72b9a0128c474aeeb9019edd48605fa6d95f7.tar.gz",
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -4,7 +4,9 @@ const jetzig = @import("jetzig");
|
||||
pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
|
||||
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);
|
||||
} else {
|
||||
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 {
|
||||
_ = data;
|
||||
const params = try request.params();
|
||||
var session = try request.session();
|
||||
|
||||
if (params.get("message")) |message| {
|
||||
try request.session.put("message", message);
|
||||
try session.put("message", message);
|
||||
}
|
||||
|
||||
return request.redirect("/session", .moved_permanently);
|
||||
|
@ -12,7 +12,7 @@ pub const jetzig_options = struct {
|
||||
pub const middleware: []const type = &.{
|
||||
// htmx middleware skips layouts when `HX-Target` header is present and issues
|
||||
// `HX-Redirect` instead of a regular HTTP redirect when `request.redirect` is called.
|
||||
jetzig.middleware.HtmxMiddleware,
|
||||
// jetzig.middleware.HtmxMiddleware,
|
||||
// Demo middleware included with new projects. Remove once you are familiar with Jetzig's
|
||||
// middleware system.
|
||||
// @import("app/middleware/DemoMiddleware.zig"),
|
||||
@ -27,6 +27,16 @@ pub const jetzig_options = struct {
|
||||
// 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);
|
||||
|
||||
// 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;
|
||||
|
||||
// Path relative to cwd() to serve public content from. Symlinks are not followed.
|
||||
// pub const public_content_path = "public";
|
||||
|
||||
|
@ -78,6 +78,16 @@ pub const config = struct {
|
||||
/// 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);
|
||||
|
||||
/// 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;
|
||||
|
||||
/// Path relative to cwd() to serve public content from. Symlinks are not followed.
|
||||
pub const public_content_path = "public";
|
||||
|
||||
|
@ -75,17 +75,14 @@ pub fn start(self: App, routes_module: type, options: AppOptions) !void {
|
||||
);
|
||||
defer cache.deinit();
|
||||
|
||||
var log_queue = try jetzig.loggers.LogQueue.init(self.allocator);
|
||||
try log_queue.setFile(std.io.getStdOut());
|
||||
|
||||
const server_options = try self.environment.getServerOptions(&log_queue);
|
||||
const server_options = try self.environment.getServerOptions();
|
||||
defer self.allocator.free(server_options.bind);
|
||||
defer self.allocator.free(server_options.secret);
|
||||
|
||||
var log_thread = try std.Thread.spawn(
|
||||
.{ .allocator = self.allocator },
|
||||
jetzig.loggers.LogQueue.Reader.publish,
|
||||
.{log_queue.reader},
|
||||
.{server_options.log_queue.reader},
|
||||
);
|
||||
defer log_thread.join();
|
||||
|
||||
|
@ -59,13 +59,17 @@ pub fn init(allocator: std.mem.Allocator) Environment {
|
||||
}
|
||||
|
||||
/// Generate server initialization options using command line args with defaults.
|
||||
pub fn getServerOptions(
|
||||
self: Environment,
|
||||
log_queue: *jetzig.loggers.LogQueue,
|
||||
) !jetzig.http.Server.ServerOptions {
|
||||
pub fn getServerOptions(self: Environment) !jetzig.http.Server.ServerOptions {
|
||||
const options = try args.parseForCurrentProcess(Options, self.allocator, .print);
|
||||
defer options.deinit();
|
||||
|
||||
const log_queue = try self.allocator.create(jetzig.loggers.LogQueue);
|
||||
log_queue.* = try jetzig.loggers.LogQueue.init(self.allocator);
|
||||
try log_queue.setFiles(
|
||||
try getLogFile(.stdout, options.options),
|
||||
try getLogFile(.stderr, options.options),
|
||||
);
|
||||
|
||||
if (options.options.help) {
|
||||
const writer = std.io.getStdErr().writer();
|
||||
try args.printHelp(Options, options.executable_name orelse "<app-name>", writer);
|
||||
@ -75,24 +79,20 @@ pub fn getServerOptions(
|
||||
const environment = options.options.environment;
|
||||
|
||||
var logger = switch (options.options.@"log-format") {
|
||||
.development, .json => jetzig.loggers.Logger{
|
||||
.development => jetzig.loggers.Logger{
|
||||
.development_logger = jetzig.loggers.DevelopmentLogger.init(
|
||||
self.allocator,
|
||||
resolveLogLevel(options.options.@"log-level", environment),
|
||||
log_queue,
|
||||
// try getLogFile(.stdout, options.options),
|
||||
// try getLogFile(.stderr, options.options),
|
||||
),
|
||||
},
|
||||
// TODO
|
||||
// .json => jetzig.loggers.Logger{
|
||||
// .json_logger = jetzig.loggers.JsonLogger.init(
|
||||
// self.allocator,
|
||||
// resolveLogLevel(options.options.@"log-level", environment),
|
||||
// try getLogFile(.stdout, options.options),
|
||||
// try getLogFile(.stderr, options.options),
|
||||
// ),
|
||||
// },
|
||||
.json => jetzig.loggers.Logger{
|
||||
.json_logger = jetzig.loggers.JsonLogger.init(
|
||||
self.allocator,
|
||||
resolveLogLevel(options.options.@"log-level", environment),
|
||||
log_queue,
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
if (options.options.detach and std.mem.eql(u8, options.options.log, "-")) {
|
||||
@ -116,6 +116,7 @@ pub fn getServerOptions(
|
||||
.port = options.options.port,
|
||||
.detach = options.options.detach,
|
||||
.environment = environment,
|
||||
.log_queue = log_queue,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,7 @@ allocator: std.mem.Allocator,
|
||||
cookie_string: []const u8,
|
||||
buf: std.ArrayList(u8),
|
||||
cookies: std.StringArrayHashMap(*Cookie),
|
||||
modified: bool = false,
|
||||
|
||||
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 {
|
||||
self.modified = true;
|
||||
|
||||
if (self.cookies.fetchSwapRemove(key)) |entry| {
|
||||
self.allocator.free(entry.key);
|
||||
self.allocator.free(entry.value.value);
|
||||
|
@ -1,120 +1,100 @@
|
||||
const std = @import("std");
|
||||
|
||||
const httpz = @import("httpz");
|
||||
|
||||
const jetzig = @import("../../jetzig.zig");
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
headers: HeadersArray,
|
||||
httpz_headers: *HttpzKeyValue,
|
||||
new_headers: std.ArrayList(Header),
|
||||
|
||||
const Headers = @This();
|
||||
pub const max_headers = 25;
|
||||
const HeadersArray = std.ArrayListUnmanaged(std.http.Header);
|
||||
const Header = struct { name: []const u8, value: []const u8 };
|
||||
const HttpzKeyValue = std.meta.fieldInfo(httpz.Request, std.meta.FieldEnum(httpz.Request).headers).type;
|
||||
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: *HttpzKeyValue) Headers {
|
||||
return .{
|
||||
.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 {
|
||||
self.headers.deinit(self.allocator);
|
||||
}
|
||||
self.httpz_headers.deinit(self.allocator);
|
||||
|
||||
/// Gets the first value for a given header identified by `name`. Names are case insensitive.
|
||||
pub fn get(self: Headers, name: []const u8) ?[]const u8 {
|
||||
for (self.headers.items) |header| {
|
||||
if (jetzig.util.equalStringsCaseInsensitive(name, header.name)) return header.value;
|
||||
for (self.new_headers.items) |header| {
|
||||
self.allocator.free(header.name);
|
||||
self.allocator.free(header.value);
|
||||
}
|
||||
return null;
|
||||
|
||||
self.new_headers.deinit();
|
||||
}
|
||||
|
||||
/// Gets the first value for a given header identified by `name`. Names are case insensitive.
|
||||
/// 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 {
|
||||
std.debug.assert(name.len <= max_bytes_header_name);
|
||||
|
||||
var buf: [max_bytes_header_name]u8 = undefined;
|
||||
const lower = std.ascii.lowerString(&buf, name);
|
||||
|
||||
return self.httpz_headers.get(lower);
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
var headers = std.ArrayList([]const u8).init(self.allocator);
|
||||
|
||||
for (self.headers.items) |header| {
|
||||
if (jetzig.util.equalStringsCaseInsensitive(name, header.name)) {
|
||||
headers.append(header.value) catch @panic("OOM");
|
||||
}
|
||||
for (self.httpz_headers.keys, 0..) |key, index| {
|
||||
var buf: [max_bytes_header_name]u8 = undefined;
|
||||
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");
|
||||
}
|
||||
|
||||
// Deprecated
|
||||
/// Deprecated
|
||||
pub fn getFirstValue(self: *const Headers, name: []const u8) ?[]const u8 {
|
||||
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 {
|
||||
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.
|
||||
pub fn remove(self: *Headers, name: []const u8) void {
|
||||
if (self.headers.items.len == 0) return;
|
||||
|
||||
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,
|
||||
const header = .{
|
||||
.name = try self.allocator.dupe(u8, lower),
|
||||
.value = try self.allocator.dupe(u8, value),
|
||||
};
|
||||
|
||||
/// Returns the next item in the current iteration of headers.
|
||||
pub fn next(self: *Iterator) ?Header {
|
||||
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;
|
||||
}
|
||||
}
|
||||
};
|
||||
try self.new_headers.append(header);
|
||||
self.httpz_headers.add(header.name, header.value);
|
||||
}
|
||||
|
||||
test "append" {
|
||||
test "append (deprecated)" {
|
||||
const allocator = std.testing.allocator;
|
||||
var headers = Headers.init(allocator);
|
||||
var headers = Headers.init(allocator, try HttpzKeyValue.init(allocator, 10));
|
||||
defer headers.deinit();
|
||||
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 headers = Headers.init(allocator, try HttpzKeyValue.init(allocator, 10));
|
||||
defer headers.deinit();
|
||||
try headers.append("foo", "bar");
|
||||
try std.testing.expectEqualStrings(headers.get("foo").?, "bar");
|
||||
}
|
||||
|
||||
test "get with multiple headers (bugfix regression test)" {
|
||||
const allocator = std.testing.allocator;
|
||||
var headers = Headers.init(allocator);
|
||||
var headers = Headers.init(allocator, try HttpzKeyValue.init(allocator, 10));
|
||||
defer headers.deinit();
|
||||
try headers.append("foo", "bar");
|
||||
try headers.append("bar", "baz");
|
||||
@ -123,75 +103,29 @@ test "get with multiple headers (bugfix regression test)" {
|
||||
|
||||
test "getAll" {
|
||||
const allocator = std.testing.allocator;
|
||||
var headers = Headers.init(allocator);
|
||||
var headers = Headers.init(allocator, try HttpzKeyValue.init(allocator, 10));
|
||||
defer headers.deinit();
|
||||
try headers.append("foo", "bar");
|
||||
try headers.append("foo", "baz");
|
||||
try headers.append("bar", "qux");
|
||||
const all = headers.getAll("foo");
|
||||
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;
|
||||
var headers = Headers.init(allocator);
|
||||
var headers = Headers.init(allocator, try HttpzKeyValue.init(allocator, 10));
|
||||
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"));
|
||||
}
|
||||
|
||||
test "case-insensitive matching" {
|
||||
const allocator = std.testing.allocator;
|
||||
var headers = Headers.init(allocator);
|
||||
var headers = Headers.init(allocator, try HttpzKeyValue.init(allocator, 10));
|
||||
defer headers.deinit();
|
||||
try headers.append("Content-Type", "bar");
|
||||
try std.testing.expectEqualStrings(headers.getFirstValue("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);
|
||||
try std.testing.expectEqualStrings(headers.get("content-type").?, "bar");
|
||||
}
|
||||
|
@ -23,8 +23,8 @@ status_code: jetzig.http.status_codes.StatusCode = .not_found,
|
||||
response_data: *jetzig.data.Data,
|
||||
query_params: ?*jetzig.http.Query = null,
|
||||
query_body: ?*jetzig.http.Query = null,
|
||||
cookies: *jetzig.http.Cookies = undefined,
|
||||
session: *jetzig.http.Session = undefined,
|
||||
_cookies: ?*jetzig.http.Cookies = null,
|
||||
_session: ?*jetzig.http.Session = null,
|
||||
body: []const u8 = undefined,
|
||||
processed: bool = false,
|
||||
layout: ?[]const u8 = null,
|
||||
@ -114,7 +114,7 @@ pub fn init(
|
||||
.allocator = allocator,
|
||||
.path = jetzig.http.Path.init(httpz_request.url.raw),
|
||||
.method = method,
|
||||
.headers = jetzig.http.Headers.init(allocator),
|
||||
.headers = jetzig.http.Headers.init(allocator, &httpz_request.headers),
|
||||
.server = server,
|
||||
.response = response,
|
||||
.response_data = response_data,
|
||||
@ -134,37 +134,8 @@ pub fn deinit(self: *Request) void {
|
||||
if (self.processed) self.allocator.free(self.body);
|
||||
}
|
||||
|
||||
/// Process request, read body if present, parse headers
|
||||
/// Process request, read body if present.
|
||||
pub fn process(self: *Request) !void {
|
||||
var cookie: ?[]const u8 = null;
|
||||
|
||||
var header_index: usize = 0;
|
||||
while (header_index < self.httpz_request.headers.len) : (header_index += 1) {
|
||||
const name = self.httpz_request.headers.keys[header_index];
|
||||
const value = self.httpz_request.headers.values[header_index];
|
||||
try self.headers.append(name, value);
|
||||
if (std.mem.eql(u8, name, "Cookie")) cookie = 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,
|
||||
}
|
||||
};
|
||||
|
||||
self.body = self.httpz_request.body() orelse "";
|
||||
self.processed = true;
|
||||
}
|
||||
@ -188,15 +159,7 @@ pub fn respond(
|
||||
) !void {
|
||||
if (!self.processed) unreachable;
|
||||
|
||||
var cookie_it = self.cookies.headerIterator();
|
||||
while (try cookie_it.next()) |header| {
|
||||
// FIXME: Skip setting cookies that are already present ?
|
||||
try self.response.headers.append("Set-Cookie", header);
|
||||
}
|
||||
|
||||
for (self.response.headers.headers.items) |header| {
|
||||
self.httpz_response.header(header.name, header.value);
|
||||
}
|
||||
try self.setCookieHeaders();
|
||||
|
||||
const status = jetzig.http.status_codes.get(self.response.status_code);
|
||||
self.httpz_response.status = try status.getCodeInt();
|
||||
@ -239,8 +202,15 @@ pub fn redirect(
|
||||
|
||||
self.response_data.reset();
|
||||
|
||||
self.response.headers.remove("Location");
|
||||
self.response.headers.append("Location", location) catch @panic("OOM");
|
||||
self.response.headers.append("Location", location) catch |err| {
|
||||
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 };
|
||||
return self.rendered_view.?;
|
||||
@ -328,7 +298,7 @@ pub fn queryParams(self: *Request) !*jetzig.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 {
|
||||
if (self.body.len == 0) return try self.queryParams();
|
||||
if (self.query_body) |parsed| return parsed.data.value.?;
|
||||
@ -345,7 +315,46 @@ fn parseQuery(self: *Request) !*jetzig.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.background()` to run the job outside of the request/response flow.
|
||||
/// e.g.:
|
||||
@ -448,35 +457,23 @@ fn extensionFormat(self: *const Request) ?jetzig.http.Request.Format {
|
||||
}
|
||||
|
||||
pub fn acceptHeaderFormat(self: *const Request) ?jetzig.http.Request.Format {
|
||||
const acceptHeader = self.getHeader("Accept");
|
||||
|
||||
if (acceptHeader) |item| {
|
||||
if (std.mem.eql(u8, item, "text/html")) return .HTML;
|
||||
if (std.mem.eql(u8, item, "application/json")) return .JSON;
|
||||
if (self.httpz_request.headers.get("accept")) |value| {
|
||||
if (std.mem.eql(u8, value, "text/html")) return .HTML;
|
||||
if (std.mem.eql(u8, value, "application/json")) return .JSON;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn contentTypeHeaderFormat(self: *const Request) ?jetzig.http.Request.Format {
|
||||
const acceptHeader = self.getHeader("content-type");
|
||||
|
||||
if (acceptHeader) |item| {
|
||||
if (std.mem.eql(u8, item, "text/html")) return .HTML;
|
||||
if (std.mem.eql(u8, item, "application/json")) return .JSON;
|
||||
if (self.httpz_request.headers.get("content-type")) |value| {
|
||||
if (std.mem.eql(u8, value, "text/html")) return .HTML;
|
||||
if (std.mem.eql(u8, value, "application/json")) return .JSON;
|
||||
}
|
||||
|
||||
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 {
|
||||
if (!colorized) return @tagName(self.method);
|
||||
|
||||
@ -521,6 +518,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.
|
||||
pub fn match(self: *Request, route: jetzig.views.Route) !bool {
|
||||
return switch (self.method) {
|
||||
|
@ -1,27 +1,28 @@
|
||||
const std = @import("std");
|
||||
|
||||
const httpz = @import("httpz");
|
||||
|
||||
const jetzig = @import("../../jetzig.zig");
|
||||
const http = @import("../http.zig");
|
||||
|
||||
const Self = @This();
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
headers: *jetzig.http.Headers,
|
||||
headers: jetzig.http.Headers,
|
||||
content: []const u8,
|
||||
status_code: http.status_codes.StatusCode,
|
||||
content_type: []const u8,
|
||||
|
||||
pub fn init(
|
||||
allocator: std.mem.Allocator,
|
||||
httpz_response: *httpz.Response,
|
||||
) !Self {
|
||||
const headers = try allocator.create(jetzig.http.Headers);
|
||||
headers.* = jetzig.http.Headers.init(allocator);
|
||||
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.status_code = .no_content,
|
||||
.content_type = "application/octet-stream",
|
||||
.content = "",
|
||||
.headers = headers,
|
||||
.headers = jetzig.http.Headers.init(allocator, &httpz_response.headers),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,7 @@ pub const ServerOptions = struct {
|
||||
secret: []const u8,
|
||||
detach: bool,
|
||||
environment: jetzig.Environment.EnvironmentName,
|
||||
log_queue: *jetzig.loggers.LogQueue,
|
||||
};
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
@ -121,7 +122,7 @@ fn processNextRequest(
|
||||
|
||||
const start_time = std.time.nanoTimestamp();
|
||||
|
||||
var response = try jetzig.http.Response.init(allocator);
|
||||
var response = try jetzig.http.Response.init(allocator, httpz_response);
|
||||
var request = try jetzig.http.Request.init(
|
||||
allocator,
|
||||
self,
|
||||
@ -136,7 +137,7 @@ fn processNextRequest(
|
||||
var middleware_data = try jetzig.http.middleware.afterRequest(&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);
|
||||
|
||||
@ -192,7 +193,17 @@ fn renderHTML(
|
||||
};
|
||||
return request.setResponse(rendered, .{});
|
||||
} 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 {
|
||||
if (try self.renderMarkdown(request)) |rendered| {
|
||||
|
@ -28,6 +28,7 @@ pub fn init(
|
||||
};
|
||||
}
|
||||
|
||||
/// Parse session cookie.
|
||||
pub fn parse(self: *Self) !void {
|
||||
if (self.cookies.get(cookie_name)) |cookie| {
|
||||
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 {
|
||||
self.data.reset();
|
||||
_ = try self.data.object();
|
||||
@ -43,12 +45,14 @@ pub fn reset(self: *Self) !void {
|
||||
try self.save();
|
||||
}
|
||||
|
||||
/// Free allocated memory.
|
||||
pub fn deinit(self: *Self) void {
|
||||
if (self.state != .parsed) return;
|
||||
|
||||
self.data.deinit();
|
||||
}
|
||||
|
||||
/// Get a value from the session.
|
||||
pub fn get(self: *Self, key: []const u8) !?*jetzig.data.Value {
|
||||
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 {
|
||||
if (self.state != .parsed) return error.UnparsedSessionCookie;
|
||||
|
||||
|
@ -11,6 +11,13 @@ pub const LogQueue = @import("loggers/LogQueue.zig");
|
||||
pub const LogLevel = enum(u4) { TRACE, DEBUG, INFO, WARN, ERROR, FATAL };
|
||||
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) {
|
||||
development_logger: DevelopmentLogger,
|
||||
json_logger: JsonLogger,
|
||||
|
@ -22,9 +22,9 @@ pub fn init(
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.level = level,
|
||||
.log_queue = log_queue, // TODO: stdout/stderr queues
|
||||
.stdout_colorized = log_queue.is_tty,
|
||||
.stderr_colorized = log_queue.is_tty,
|
||||
.log_queue = log_queue,
|
||||
.stdout_colorized = log_queue.stdout_is_tty,
|
||||
.stderr_colorized = log_queue.stderr_is_tty,
|
||||
};
|
||||
}
|
||||
|
||||
@ -48,13 +48,14 @@ pub fn log(
|
||||
.TRACE, .DEBUG, .INFO => self.stdout_colorized,
|
||||
.WARN, .ERROR, .FATAL => self.stderr_colorized,
|
||||
};
|
||||
const writer = switch (level) {
|
||||
.TRACE, .DEBUG, .INFO => self.log_queue.writer,
|
||||
.WARN, .ERROR, .FATAL => self.log_queue.writer,
|
||||
};
|
||||
const level_formatted = if (colorized) colorizedLogLevel(level) else @tagName(level);
|
||||
const target = jetzig.loggers.logTarget(level);
|
||||
|
||||
try writer.print("{s: >5} [{s}] {s}\n", .{ level_formatted, iso8601, output });
|
||||
try self.log_queue.writer.print(
|
||||
"{s: >5} [{s}] {s}\n",
|
||||
.{ level_formatted, iso8601, output },
|
||||
target,
|
||||
);
|
||||
}
|
||||
|
||||
/// Log a one-liner including response status code, path, method, duration, etc.
|
||||
@ -86,23 +87,22 @@ pub fn logRequest(self: DevelopmentLogger, request: *const jetzig.http.Request)
|
||||
var timestamp_buf: [256]u8 = undefined;
|
||||
const iso8601 = try timestamp.iso8601(×tamp_buf);
|
||||
|
||||
const writer = self.log_queue.writer;
|
||||
try writer.print("{s: >5} [{s}] [{s}/{s}/{s}] {s}\n", .{
|
||||
try self.log_queue.writer.print("{s: >5} [{s}] [{s}/{s}/{s}] {s}\n", .{
|
||||
if (self.stdout_colorized) colorizedLogLevel(.INFO) else @tagName(.INFO),
|
||||
iso8601,
|
||||
formatted_duration,
|
||||
request.fmtMethod(self.stdout_colorized),
|
||||
formatted_status,
|
||||
request.path.path,
|
||||
});
|
||||
}, .stdout);
|
||||
}
|
||||
|
||||
fn colorizedLogLevel(comptime level: LogLevel) []const u8 {
|
||||
return switch (level) {
|
||||
.TRACE => jetzig.colors.white(@tagName(level)),
|
||||
.DEBUG => jetzig.colors.cyan(@tagName(level)),
|
||||
.INFO => jetzig.colors.blue(@tagName(level) ++ " "),
|
||||
.WARN => jetzig.colors.yellow(@tagName(level) ++ " "),
|
||||
.INFO => jetzig.colors.blue(@tagName(level) ++ " "), // Keep logs neatly aligned
|
||||
.WARN => jetzig.colors.yellow(@tagName(level) ++ " "), // "
|
||||
.ERROR => jetzig.colors.red(@tagName(level)),
|
||||
.FATAL => jetzig.colors.red(@tagName(level)),
|
||||
};
|
||||
|
@ -11,6 +11,7 @@ const LogMessage = struct {
|
||||
timestamp: []const u8,
|
||||
message: []const u8,
|
||||
};
|
||||
|
||||
const RequestLogMessage = struct {
|
||||
level: []const u8,
|
||||
timestamp: []const u8,
|
||||
@ -21,24 +22,19 @@ const RequestLogMessage = struct {
|
||||
};
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
stdout: std.fs.File,
|
||||
stderr: std.fs.File,
|
||||
log_queue: *jetzig.loggers.LogQueue,
|
||||
level: LogLevel,
|
||||
mutex: std.Thread.Mutex,
|
||||
|
||||
/// Initialize a new JSON Logger.
|
||||
pub fn init(
|
||||
allocator: std.mem.Allocator,
|
||||
level: LogLevel,
|
||||
stdout: std.fs.File,
|
||||
stderr: std.fs.File,
|
||||
log_queue: *jetzig.loggers.LogQueue,
|
||||
) JsonLogger {
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.level = level,
|
||||
.stdout = stdout,
|
||||
.stderr = stderr,
|
||||
.mutex = std.Thread.Mutex{},
|
||||
.log_queue = log_queue,
|
||||
};
|
||||
}
|
||||
|
||||
@ -58,20 +54,12 @@ pub fn log(
|
||||
var timestamp_buf: [256]u8 = undefined;
|
||||
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 json = try std.json.stringifyAlloc(self.allocator, log_message, .{ .whitespace = .minified });
|
||||
defer self.allocator.free(json);
|
||||
|
||||
@constCast(self).mutex.lock();
|
||||
defer @constCast(self).mutex.unlock();
|
||||
|
||||
try writer.writeAll(json);
|
||||
try writer.writeByte('\n');
|
||||
|
||||
if (!file.isTty()) try file.sync(); // Make configurable ?
|
||||
try self.log_queue.writer.print("{s}\n", .{json}, jetzig.loggers.logTarget(level));
|
||||
}
|
||||
|
||||
/// Log a one-liner including response status code, path, method, duration, etc.
|
||||
@ -91,6 +79,7 @@ pub fn logRequest(self: *const JsonLogger, request: *const jetzig.http.Request)
|
||||
.{},
|
||||
),
|
||||
};
|
||||
|
||||
const message = RequestLogMessage{
|
||||
.level = @tagName(level),
|
||||
.timestamp = iso8601,
|
||||
@ -99,19 +88,17 @@ pub fn logRequest(self: *const JsonLogger, request: *const jetzig.http.Request)
|
||||
.path = request.path.path,
|
||||
.duration = duration,
|
||||
};
|
||||
const json = try std.json.stringifyAlloc(self.allocator, message, .{ .whitespace = .minified });
|
||||
defer self.allocator.free(json);
|
||||
|
||||
const file = self.getFile(level);
|
||||
const writer = file.writer();
|
||||
var buf: [4096]u8 = undefined;
|
||||
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();
|
||||
defer @constCast(self).mutex.unlock();
|
||||
|
||||
try writer.writeAll(json);
|
||||
try writer.writeByte('\n');
|
||||
|
||||
if (!file.isTty()) try file.sync(); // Make configurable ?
|
||||
try self.log_queue.writer.print("{s}\n", .{stream.getWritten()}, .stdout);
|
||||
}
|
||||
|
||||
fn getFile(self: JsonLogger, level: LogLevel) std.fs.File {
|
||||
|
@ -1,10 +1,12 @@
|
||||
const std = @import("std");
|
||||
|
||||
const List = std.DoublyLinkedList(*[buffer_size:0]u8);
|
||||
const buffer_size = 4096;
|
||||
const jetzig = @import("../../jetzig.zig");
|
||||
|
||||
const List = std.DoublyLinkedList(Event);
|
||||
const buffer_size = jetzig.config.get(usize, "log_message_buffer_len");
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
list: *List,
|
||||
list: List,
|
||||
read_write_mutex: *std.Thread.Mutex,
|
||||
condition: *std.Thread.Condition,
|
||||
condition_mutex: *std.Thread.Mutex,
|
||||
@ -13,17 +15,25 @@ reader: *Reader = undefined,
|
||||
node_pool: std.ArrayList(*List.Node),
|
||||
buffer_pool: std.ArrayList(*[buffer_size:0]u8),
|
||||
position: usize,
|
||||
is_tty: bool = undefined,
|
||||
stdout_is_tty: bool = undefined,
|
||||
stderr_is_tty: bool = undefined,
|
||||
state: enum { pending, ready } = .pending,
|
||||
|
||||
const LogQueue = @This();
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) !LogQueue {
|
||||
const list = try allocator.create(std.DoublyLinkedList(*[buffer_size:0]u8));
|
||||
list.* = .{};
|
||||
pub const Target = enum { stdout, stderr };
|
||||
|
||||
const Event = struct {
|
||||
message: *[buffer_size:0]u8,
|
||||
len: usize,
|
||||
target: Target,
|
||||
ptr: ?[]const u8,
|
||||
};
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) !LogQueue {
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.list = list,
|
||||
.list = List{},
|
||||
.condition = try allocator.create(std.Thread.Condition),
|
||||
.condition_mutex = try allocator.create(std.Thread.Mutex),
|
||||
.read_write_mutex = try allocator.create(std.Thread.Mutex),
|
||||
@ -33,35 +43,63 @@ pub fn init(allocator: std.mem.Allocator) !LogQueue {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn setFile(self: *LogQueue, file: std.fs.File) !void {
|
||||
/// Set the stdout and stderr outputs.
|
||||
pub fn setFiles(self: *LogQueue, stdout_file: std.fs.File, stderr_file: std.fs.File) !void {
|
||||
self.writer = try self.allocator.create(Writer);
|
||||
self.writer.* = Writer{ .file = file, .queue = self, .mutex = try self.allocator.create(std.Thread.Mutex) };
|
||||
self.writer.* = Writer{
|
||||
.queue = self,
|
||||
.mutex = try self.allocator.create(std.Thread.Mutex),
|
||||
};
|
||||
self.reader = try self.allocator.create(Reader);
|
||||
self.reader.* = Reader{ .file = file, .queue = self };
|
||||
self.is_tty = file.isTty();
|
||||
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.state = .ready;
|
||||
}
|
||||
|
||||
/// Writer for `LogQueue`. Receives log events and publishes to the queue.
|
||||
pub const Writer = struct {
|
||||
file: std.fs.File,
|
||||
queue: *LogQueue,
|
||||
position: usize = 0,
|
||||
mutex: *std.Thread.Mutex,
|
||||
|
||||
/// True if target output file is a TTY.
|
||||
pub fn isTty(self: Writer) bool {
|
||||
return self.file.isTty();
|
||||
}
|
||||
/// 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 {
|
||||
std.debug.assert(self.queue.state == .ready);
|
||||
|
||||
/// Print a log event.
|
||||
pub fn print(self: *Writer, comptime message: []const u8, args: anytype) !void {
|
||||
self.mutex.lock();
|
||||
defer self.mutex.unlock();
|
||||
|
||||
const buf = try self.getBuffer();
|
||||
self.position += 1;
|
||||
_ = try std.fmt.bufPrintZ(buf, message, args);
|
||||
try self.queue.append(buf);
|
||||
var ptr: ?[]const u8 = null;
|
||||
|
||||
const result = std.fmt.bufPrintZ(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_size:0]u8 {
|
||||
@ -76,40 +114,69 @@ pub const Writer = struct {
|
||||
|
||||
/// Reader for `LogQueue`. Reads log events from the queue.
|
||||
pub const Reader = struct {
|
||||
file: std.fs.File,
|
||||
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) !void {
|
||||
const writer = self.file.writer();
|
||||
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();
|
||||
|
||||
self.queue.condition.wait(self.queue.condition_mutex);
|
||||
|
||||
while (try self.queue.popFirst()) |message| {
|
||||
var stdout_written = false;
|
||||
var stderr_written = false;
|
||||
|
||||
while (try self.queue.popFirst()) |event| {
|
||||
self.queue.writer.mutex.lock();
|
||||
defer self.queue.writer.mutex.unlock();
|
||||
|
||||
try writer.writeAll(message[0..std.mem.indexOfSentinel(u8, 0, message)]);
|
||||
const writer = switch (event.target) {
|
||||
.stdout => blk: {
|
||||
stdout_written = true;
|
||||
break :blk stdout_writer;
|
||||
},
|
||||
.stderr => blk: {
|
||||
stderr_written = true;
|
||||
break :blk stderr_writer;
|
||||
},
|
||||
};
|
||||
|
||||
if (event.ptr) |ptr| {
|
||||
// Log message spilled to heap
|
||||
defer self.queue.allocator.free(ptr);
|
||||
try writer.writeAll(ptr);
|
||||
continue;
|
||||
}
|
||||
|
||||
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] = message;
|
||||
self.queue.buffer_pool.items[self.queue.writer.position] = event.message;
|
||||
} else {
|
||||
try self.queue.buffer_pool.append(message);
|
||||
try self.queue.buffer_pool.append(event.message); // TODO: Prevent unlimited inflation
|
||||
}
|
||||
}
|
||||
|
||||
if (!self.file.isTty()) try self.file.sync();
|
||||
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();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 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, message: *[buffer_size:0]u8) !void {
|
||||
fn append(self: *LogQueue, event: Event) !void {
|
||||
self.read_write_mutex.lock();
|
||||
defer self.read_write_mutex.unlock();
|
||||
|
||||
@ -120,14 +187,14 @@ fn append(self: *LogQueue, message: *[buffer_size:0]u8) !void {
|
||||
|
||||
self.position += 1;
|
||||
|
||||
node.* = .{ .data = message };
|
||||
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) !?*[buffer_size:0]u8 {
|
||||
fn popFirst(self: *LogQueue) !?Event {
|
||||
self.read_write_mutex.lock();
|
||||
defer self.read_write_mutex.unlock();
|
||||
|
||||
@ -144,3 +211,18 @@ fn popFirst(self: *LogQueue) !?*[buffer_size:0]u8 {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
test "setFiles" {
|
||||
var log_queue = try LogQueue.init(std.testing.allocator);
|
||||
|
||||
var tmp_dir = std.testing.tmpDir(.{});
|
||||
defer tmp_dir.cleanup();
|
||||
|
||||
const stdout = tmp_dir.dir.createFile("stdout.log", .{});
|
||||
defer stdout.close();
|
||||
|
||||
const stderr = tmp_dir.dir.createFile("stderr.log", .{});
|
||||
defer stderr.close();
|
||||
|
||||
try log_queue.setFiles(stdout, stderr);
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ pub fn iso8601(self: *const Self, buf: *[256]u8) ![]const u8 {
|
||||
const u32_hour: u32 = @intCast(self.hour());
|
||||
const u32_minute: u32 = @intCast(self.minute());
|
||||
const u32_second: u32 = @intCast(self.second());
|
||||
return try std.fmt.bufPrint(buf, "{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_month,
|
||||
u32_day_of_month,
|
||||
|
@ -1,67 +0,0 @@
|
||||
const std = @import("std");
|
||||
const Server = @import("http/Server.zig");
|
||||
const jetzig = @import("../jetzig.zig");
|
||||
|
||||
pub fn listen(self: *Server) !void {
|
||||
const address = try std.net.Address.parseIp(self.options.bind, self.options.port);
|
||||
self.std_net_server = try address.listen(.{ .reuse_port = true });
|
||||
|
||||
self.initialized = true;
|
||||
|
||||
try self.logger.INFO("Listening on http://{s}:{} [{s}]", .{
|
||||
self.options.bind,
|
||||
self.options.port,
|
||||
@tagName(self.options.environment),
|
||||
});
|
||||
try processRequests(self);
|
||||
}
|
||||
|
||||
fn processRequests(self: *Server) !void {
|
||||
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();
|
||||
|
||||
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();
|
||||
|
||||
processNextRequest(self, allocator, &std_http_server) catch |err| {
|
||||
if (Server.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 {
|
||||
const start_time = std.time.nanoTimestamp();
|
||||
|
||||
const std_http_request = try std_http_server.receiveHead();
|
||||
if (std_http_server.state == .receiving_head) return error.JetzigParseHeadError;
|
||||
|
||||
var response = try jetzig.http.Response.init(allocator);
|
||||
var request = try jetzig.http.Request.init(allocator, self, start_time, std_http_request, &response);
|
||||
|
||||
try request.process();
|
||||
|
||||
var middleware_data = try jetzig.http.middleware.afterRequest(&request);
|
||||
|
||||
try self.renderResponse(&request);
|
||||
try request.response.headers.append("content-type", response.content_type);
|
||||
|
||||
try jetzig.http.middleware.beforeResponse(&middleware_data, &request);
|
||||
|
||||
try request.respond();
|
||||
|
||||
try jetzig.http.middleware.afterResponse(&middleware_data, &request);
|
||||
jetzig.http.middleware.deinit(&middleware_data, &request);
|
||||
|
||||
try self.logger.logRequest(&request);
|
||||
}
|
@ -6,4 +6,5 @@ test {
|
||||
_ = @import("jetzig/http/Path.zig");
|
||||
_ = @import("jetzig/jobs/Job.zig");
|
||||
_ = @import("jetzig/mail/Mail.zig");
|
||||
_ = @import("jetzig/loggers/LogQueue.zig");
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user