milestone 16: behavioral fixes for silent failures, locks, counters and the query log
This commit is contained in:
@@ -36,6 +36,8 @@ const rate_limiter = @import("../server/rate_limiter.zig");
|
||||
const retention_mod = @import("../storage/retention.zig");
|
||||
const safe_url = @import("../safe_url.zig");
|
||||
const server = @import("server.zig");
|
||||
const tcp_server = @import("../server/tcp_server.zig");
|
||||
const udp_server = @import("../server/udp_server.zig");
|
||||
|
||||
/// The exposition format version, as the 0.0.4 specification writes it.
|
||||
pub const content_type = "text/plain; version=0.0.4; charset=utf-8";
|
||||
@@ -133,6 +135,13 @@ pub const Sample = struct {
|
||||
/// disabled or its bind failed, like every other unwired collaborator.
|
||||
doh_listener: ?DohListenerSample = null,
|
||||
dot_listener: ?dot_server.StatsSnapshot = null,
|
||||
/// The plain-DNS listener families (ruling 13). The app binds one listener
|
||||
/// per address family, and both answer the same port for the same reason,
|
||||
/// so their counters are summed into one family rather than labelled: an
|
||||
/// operator asks how much UDP/53 dropped, not how much of it arrived over
|
||||
/// IPv6. Absent when no listener is wired, like every other collaborator.
|
||||
udp_listener: ?udp_server.Snapshot = null,
|
||||
tcp_listener: ?tcp_server.Snapshot = null,
|
||||
upstreams: []const UpstreamSample = &.{},
|
||||
};
|
||||
|
||||
@@ -214,11 +223,36 @@ pub fn collect(state: *server.WebState, io: std.Io, arena: Allocator) Allocator.
|
||||
}
|
||||
if (state.dot_listener) |listener| sample.dot_listener = listener.snapshotStats();
|
||||
|
||||
sample.udp_listener = sumListeners(udp_server.Snapshot, udp_server.UdpServer, state.udp_listeners);
|
||||
sample.tcp_listener = sumListeners(tcp_server.Snapshot, tcp_server.TcpServer, state.tcp_listeners);
|
||||
|
||||
if (state.pool) |pool| sample.upstreams = try upstreams(pool, io, arena);
|
||||
|
||||
return sample;
|
||||
}
|
||||
|
||||
/// Adds one snapshot per listener field by field. Null for an empty slice, so
|
||||
/// an unbound listener omits its family rather than reporting zeros.
|
||||
///
|
||||
/// A `u64` counter cannot realistically overflow the sum of four of them, and
|
||||
/// wrapping addition would be a worse answer than a wrong-looking large one, so
|
||||
/// the addition is the ordinary checked one.
|
||||
fn sumListeners(comptime Snapshot: type, comptime Server: type, listeners: []const *Server) ?Snapshot {
|
||||
if (listeners.len == 0) return null;
|
||||
|
||||
var total: Snapshot = undefined;
|
||||
inline for (@typeInfo(Snapshot).@"struct".fields) |field| {
|
||||
@field(total, field.name) = 0;
|
||||
}
|
||||
for (listeners) |listener| {
|
||||
const one = listener.snapshotStats();
|
||||
inline for (@typeInfo(Snapshot).@"struct".fields) |field| {
|
||||
@field(total, field.name) += @field(one, field.name);
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
fn dnsCounters(stats: *const dns_handler.Handler.Stats) DnsCounters {
|
||||
var out: DnsCounters = undefined;
|
||||
inline for (dns_stat_fields, 0..) |field, i| {
|
||||
@@ -339,6 +373,12 @@ pub fn render(w: *std.Io.Writer, sample: Sample) std.Io.Writer.Error!void {
|
||||
);
|
||||
}
|
||||
|
||||
if (sample.udp_listener) |listener| {
|
||||
try counterGroup(w, "nxdns_udp_server_", "UDP/53 listener counter", listener);
|
||||
}
|
||||
if (sample.tcp_listener) |listener| {
|
||||
try counterGroup(w, "nxdns_tcp_server_", "TCP/53 listener counter", listener);
|
||||
}
|
||||
if (sample.doh_listener) |listener| {
|
||||
try counterGroup(w, "nxdns_doh_server_", "DoH listener counter", listener);
|
||||
}
|
||||
@@ -705,6 +745,103 @@ test "an unwired collaborator omits its family rather than reporting zeros" {
|
||||
try testing.expect(!std.mem.containsAtLeast(u8, text, 1, "nxdns_cert_"));
|
||||
try testing.expect(!std.mem.containsAtLeast(u8, text, 1, "nxdns_doh_server_"));
|
||||
try testing.expect(!std.mem.containsAtLeast(u8, text, 1, "nxdns_dot_server_"));
|
||||
try testing.expect(!std.mem.containsAtLeast(u8, text, 1, "nxdns_udp_server_"));
|
||||
try testing.expect(!std.mem.containsAtLeast(u8, text, 1, "nxdns_tcp_server_"));
|
||||
}
|
||||
|
||||
test "the plain-DNS listener families carry every counter of both listeners" {
|
||||
const text = try renderToString(testing.allocator, .{
|
||||
.udp_listener = .{
|
||||
.received = 90,
|
||||
.dropped_oversize = 1,
|
||||
.dropped_no_slot = 2,
|
||||
.dropped_handler = 3,
|
||||
.receive_errors = 4,
|
||||
.send_errors = 5,
|
||||
},
|
||||
.tcp_listener = .{
|
||||
.accepted = 12,
|
||||
.rejected_at_capacity = 6,
|
||||
.rejected_at_shutdown = 7,
|
||||
.accept_errors = 8,
|
||||
.connection_errors = 9,
|
||||
.idle_timeouts = 10,
|
||||
},
|
||||
});
|
||||
defer testing.allocator.free(text);
|
||||
|
||||
try testing.expect(std.mem.containsAtLeast(u8, text, 1, "nxdns_udp_server_received_total 90\n"));
|
||||
try testing.expect(std.mem.containsAtLeast(u8, text, 1, "nxdns_udp_server_dropped_oversize_total 1\n"));
|
||||
try testing.expect(std.mem.containsAtLeast(u8, text, 1, "nxdns_udp_server_dropped_no_slot_total 2\n"));
|
||||
try testing.expect(std.mem.containsAtLeast(u8, text, 1, "nxdns_udp_server_dropped_handler_total 3\n"));
|
||||
try testing.expect(std.mem.containsAtLeast(u8, text, 1, "nxdns_udp_server_receive_errors_total 4\n"));
|
||||
try testing.expect(std.mem.containsAtLeast(u8, text, 1, "nxdns_udp_server_send_errors_total 5\n"));
|
||||
|
||||
try testing.expect(std.mem.containsAtLeast(u8, text, 1, "nxdns_tcp_server_accepted_total 12\n"));
|
||||
try testing.expect(std.mem.containsAtLeast(u8, text, 1, "nxdns_tcp_server_rejected_at_capacity_total 6\n"));
|
||||
try testing.expect(std.mem.containsAtLeast(u8, text, 1, "nxdns_tcp_server_rejected_at_shutdown_total 7\n"));
|
||||
try testing.expect(std.mem.containsAtLeast(u8, text, 1, "nxdns_tcp_server_accept_errors_total 8\n"));
|
||||
try testing.expect(std.mem.containsAtLeast(u8, text, 1, "nxdns_tcp_server_connection_errors_total 9\n"));
|
||||
try testing.expect(std.mem.containsAtLeast(u8, text, 1, "nxdns_tcp_server_idle_timeouts_total 10\n"));
|
||||
|
||||
// One family per listener kind, whatever the number of listeners behind it:
|
||||
// the counters are summed, not labelled.
|
||||
try testing.expectEqual(
|
||||
@as(usize, 1),
|
||||
std.mem.count(u8, text, "# TYPE nxdns_udp_server_received_total counter\n"),
|
||||
);
|
||||
}
|
||||
|
||||
test "one family covers all four listeners, summed" {
|
||||
// Only `stats` is read, so the listeners need no socket: `snapshotStats`
|
||||
// loads counters and touches nothing else.
|
||||
var udp6: udp_server.UdpServer = undefined;
|
||||
udp6.stats = .{};
|
||||
udp6.stats.received.store(10, .monotonic);
|
||||
udp6.stats.dropped_no_slot.store(1, .monotonic);
|
||||
|
||||
var udp4: udp_server.UdpServer = undefined;
|
||||
udp4.stats = .{};
|
||||
udp4.stats.received.store(7, .monotonic);
|
||||
udp4.stats.dropped_no_slot.store(2, .monotonic);
|
||||
|
||||
var tcp6: tcp_server.TcpServer = undefined;
|
||||
tcp6.stats = .{};
|
||||
tcp6.stats.accepted.store(4, .monotonic);
|
||||
|
||||
var tcp4: tcp_server.TcpServer = undefined;
|
||||
tcp4.stats = .{};
|
||||
tcp4.stats.accepted.store(5, .monotonic);
|
||||
tcp4.stats.idle_timeouts.store(3, .monotonic);
|
||||
|
||||
const udp = sumListeners(udp_server.Snapshot, udp_server.UdpServer, &.{ &udp6, &udp4 }).?;
|
||||
try testing.expectEqual(@as(u64, 17), udp.received);
|
||||
try testing.expectEqual(@as(u64, 3), udp.dropped_no_slot);
|
||||
try testing.expectEqual(@as(u64, 0), udp.send_errors);
|
||||
|
||||
const tcp = sumListeners(tcp_server.Snapshot, tcp_server.TcpServer, &.{ &tcp6, &tcp4 }).?;
|
||||
try testing.expectEqual(@as(u64, 9), tcp.accepted);
|
||||
try testing.expectEqual(@as(u64, 3), tcp.idle_timeouts);
|
||||
|
||||
// No listener at all is a missing family, not a family of zeros.
|
||||
try testing.expectEqual(
|
||||
@as(?udp_server.Snapshot, null),
|
||||
sumListeners(udp_server.Snapshot, udp_server.UdpServer, &.{}),
|
||||
);
|
||||
}
|
||||
|
||||
test "the three forward-client counters reach the DNS families" {
|
||||
var dns: DnsCounters = @splat(0);
|
||||
dns[fieldIndex("forward_udp_truncated")] = 2;
|
||||
dns[fieldIndex("forward_foreign_datagrams")] = 3;
|
||||
dns[fieldIndex("forward_failures")] = 4;
|
||||
|
||||
const text = try renderToString(testing.allocator, .{ .dns = dns });
|
||||
defer testing.allocator.free(text);
|
||||
|
||||
try testing.expect(std.mem.containsAtLeast(u8, text, 1, "nxdns_dns_forward_udp_truncated_total 2\n"));
|
||||
try testing.expect(std.mem.containsAtLeast(u8, text, 1, "nxdns_dns_forward_foreign_datagrams_total 3\n"));
|
||||
try testing.expect(std.mem.containsAtLeast(u8, text, 1, "nxdns_dns_forward_failures_total 4\n"));
|
||||
}
|
||||
|
||||
test "listener counters render only for the wired servers" {
|
||||
|
||||
Reference in New Issue
Block a user