diff --git a/src/api/router.zig b/src/api/router.zig index 21640b2..865675d 100644 --- a/src/api/router.zig +++ b/src/api/router.zig @@ -52,6 +52,10 @@ pub fn handleHttpRequest( fn handleGet(conn: *websocket.Conn, path: []const u8, query: []const u8, headers: *const websocket.Handshake.KeyValue, ctx: *HttpContext) void { if (std.mem.eql(u8, path, "/_health") or std.mem.eql(u8, path, "/xrpc/_health")) { + _ = ctx.persist.db.exec("SELECT 1", .{}) catch { + h.respondJson(conn, .internal_server_error, "{\"status\":\"error\",\"msg\":\"database unavailable\"}"); + return; + }; h.respondJson(conn, .ok, "{\"status\":\"ok\"}"); } else if (std.mem.eql(u8, path, "/_stats")) { var stats_buf: [4096]u8 = undefined; diff --git a/src/main.zig b/src/main.zig index 505840e..10d9fdf 100644 --- a/src/main.zig +++ b/src/main.zig @@ -19,8 +19,9 @@ //! /admin/hosts/unblock — unblock a host (POST, admin) //! /_health, /_stats — health, stats //! -//! port 3001 (RELAY_METRICS_PORT): internal metrics only +//! port 3001 (RELAY_METRICS_PORT): internal metrics + health //! /metrics — prometheus metrics +//! /_health — liveness probe (DB check) const std = @import("std"); const http = std.http; @@ -48,6 +49,7 @@ const MetricsServer = struct { stats: *broadcaster.Stats, validator: *validator_mod.Validator, data_dir: []const u8, + persist: *event_log_mod.DiskPersist, fn run(self: *MetricsServer) void { while (!shutdown_flag.load(.acquire)) { @@ -56,12 +58,12 @@ const MetricsServer = struct { log.debug("metrics accept error: {s}", .{@errorName(err)}); continue; }; - handleMetricsConn(conn.stream, self.stats, self.validator, self.data_dir); + handleMetricsConn(conn.stream, self.stats, self.validator, self.data_dir, self.persist); } } }; -fn handleMetricsConn(stream: std.net.Stream, stats: *broadcaster.Stats, validator: *validator_mod.Validator, data_dir: []const u8) void { +fn handleMetricsConn(stream: std.net.Stream, stats: *broadcaster.Stats, validator: *validator_mod.Validator, data_dir: []const u8, persist: *event_log_mod.DiskPersist) void { defer stream.close(); var recv_buf: [4096]u8 = undefined; @@ -71,16 +73,32 @@ fn handleMetricsConn(stream: std.net.Stream, stats: *broadcaster.Stats, validato var server = http.Server.init(connection_reader.interface(), &connection_writer.interface); var request = server.receiveHead() catch return; - - const cache_entries = validator.cacheSize(); - const migration_queue_len = validator.migrationQueueLen(); - - var metrics_buf: [8192]u8 = undefined; - const body = broadcaster.formatPrometheusMetrics(stats, cache_entries, migration_queue_len, data_dir, &metrics_buf); - request.respond(body, .{ .status = .ok, .keep_alive = false, .extra_headers = &.{ - .{ .name = "content-type", .value = "text/plain; version=0.0.4; charset=utf-8" }, - .{ .name = "server", .value = "zlay (atproto-relay)" }, - } }) catch {}; + const path = request.head.target; + + if (std.mem.eql(u8, path, "/_health")) { + const db_ok = if (persist.db.exec("SELECT 1", .{})) |_| true else |_| false; + const status: http.Status = if (db_ok) .ok else .internal_server_error; + const body = if (db_ok) "{\"status\":\"ok\"}" else "{\"status\":\"error\",\"msg\":\"database unavailable\"}"; + request.respond(body, .{ .status = status, .keep_alive = false, .extra_headers = &.{ + .{ .name = "content-type", .value = "application/json" }, + .{ .name = "server", .value = "zlay (atproto-relay)" }, + } }) catch {}; + } else if (std.mem.eql(u8, path, "/metrics")) { + const cache_entries = validator.cacheSize(); + const migration_queue_len = validator.migrationQueueLen(); + + var metrics_buf: [8192]u8 = undefined; + const body = broadcaster.formatPrometheusMetrics(stats, cache_entries, migration_queue_len, data_dir, &metrics_buf); + request.respond(body, .{ .status = .ok, .keep_alive = false, .extra_headers = &.{ + .{ .name = "content-type", .value = "text/plain; version=0.0.4; charset=utf-8" }, + .{ .name = "server", .value = "zlay (atproto-relay)" }, + } }) catch {}; + } else { + request.respond("not found", .{ .status = .not_found, .keep_alive = false, .extra_headers = &.{ + .{ .name = "content-type", .value = "text/plain" }, + .{ .name = "server", .value = "zlay (atproto-relay)" }, + } }) catch {}; + } } pub fn main() !void { @@ -182,6 +200,7 @@ pub fn main() !void { .stats = &bc.stats, .validator = &val, .data_dir = data_dir, + .persist = &dp, }; const metrics_thread = try std.Thread.spawn(.{ .stack_size = default_stack_size }, MetricsServer.run, .{&metrics_srv});