server: every dns listener shares one budget of 32 queries, stream listeners take 16 connections, the task pool is capped from those bounds, and work it cannot start is counted by site
This commit is contained in:
+58
-10
@@ -26,6 +26,7 @@ const Allocator = std.mem.Allocator;
|
||||
const cert_store = @import("../server/cert_store.zig");
|
||||
const client_names = @import("../server/client_names.zig");
|
||||
const clients = @import("../server/clients.zig");
|
||||
const concurrency = @import("../platform/concurrency.zig");
|
||||
const dns_cache = @import("../cache/dns_cache.zig");
|
||||
const dns_handler = @import("../server/handler.zig");
|
||||
const disk_monitor = @import("../storage/disk_monitor.zig");
|
||||
@@ -93,15 +94,18 @@ pub const DiskSample = struct {
|
||||
};
|
||||
|
||||
/// The DoH listener counters this exposition exports (milestone-10 ruling 10):
|
||||
/// the four every TLS listener keeps, plus DoH's `bad_requests`. A subset of
|
||||
/// `doh_server.Snapshot` on purpose — the accept-side refusal counters stay
|
||||
/// internal, exactly as they do for the DoT listener and TCP/53.
|
||||
/// the ones every TLS listener keeps, plus DoH's `bad_requests`. A subset of
|
||||
/// `doh_server.Snapshot` on purpose: of the accept-side refusals only
|
||||
/// `rejected_at_capacity` is exported, the one a full connection cap explains,
|
||||
/// exactly as for the DoT listener.
|
||||
pub const DohListenerSample = struct {
|
||||
connections: u64,
|
||||
rejected_at_capacity: u64,
|
||||
tls_handshake_failures: u64,
|
||||
idle_timeouts: u64,
|
||||
connection_errors: u64,
|
||||
bad_requests: u64,
|
||||
dropped_no_permit: u64,
|
||||
};
|
||||
|
||||
/// One upstream, with every string owned by the caller's arena.
|
||||
@@ -152,6 +156,9 @@ pub const Sample = struct {
|
||||
dns: DnsCounters = @splat(0),
|
||||
logger: LoggerCounters = .{},
|
||||
log_sink: logging.Stats = .{},
|
||||
/// Process-wide, like the log sink: every `concurrency.Site`, always
|
||||
/// rendered, so a zero reads as "never happened" rather than "not wired".
|
||||
concurrency: concurrency.Snapshot = .initFill(0),
|
||||
cache: ?CacheSample = null,
|
||||
limiter: ?LimiterSample = null,
|
||||
tracker: ?TrackerSample = null,
|
||||
@@ -212,7 +219,7 @@ pub fn handle(
|
||||
}
|
||||
|
||||
pub fn collect(state: *server.WebState, io: std.Io, arena: Allocator) Allocator.Error!Sample {
|
||||
var sample: Sample = .{ .log_sink = logging.stats() };
|
||||
var sample: Sample = .{ .log_sink = logging.stats(), .concurrency = concurrency.snapshot() };
|
||||
|
||||
if (state.handler) |handler| {
|
||||
sample.dns = dnsCounters(&handler.stats);
|
||||
@@ -290,10 +297,12 @@ pub fn collect(state: *server.WebState, io: std.Io, arena: Allocator) Allocator.
|
||||
const snapshot = listener.snapshotStats();
|
||||
sample.doh_listener = .{
|
||||
.connections = snapshot.connections,
|
||||
.rejected_at_capacity = snapshot.rejected_at_capacity,
|
||||
.tls_handshake_failures = snapshot.tls_handshake_failures,
|
||||
.idle_timeouts = snapshot.idle_timeouts,
|
||||
.connection_errors = snapshot.connection_errors,
|
||||
.bad_requests = snapshot.bad_requests,
|
||||
.dropped_no_permit = snapshot.dropped_no_permit,
|
||||
};
|
||||
}
|
||||
if (state.dot_listener) |listener| sample.dot_listener = listener.snapshotStats();
|
||||
@@ -414,6 +423,16 @@ pub fn render(w: *std.Io.Writer, sample: Sample) std.Io.Writer.Error!void {
|
||||
try counterGroup(w, "nxdns_querylog_", "Query log writer counter", sample.logger);
|
||||
try counterGroup(w, "nxdns_log_", "Diagnostic log sink counter", sample.log_sink);
|
||||
|
||||
try labeledHead(
|
||||
w,
|
||||
"nxdns_concurrency_unavailable_total",
|
||||
"Work lost because the task pool was at its limit or could not start a thread, by where it happened.",
|
||||
"counter",
|
||||
);
|
||||
for (std.enums.values(concurrency.Site)) |site| {
|
||||
try w.print("nxdns_concurrency_unavailable_total{{site=\"{t}\"}} {d}\n", .{ site, sample.concurrency.get(site) });
|
||||
}
|
||||
|
||||
if (sample.cache) |cache| {
|
||||
try counterGroup(w, "nxdns_cache_", "DNS cache counter", cache.stats);
|
||||
try gauge(w, "nxdns_cache_entries", "Responses currently held in the DNS cache.", cache.entries);
|
||||
@@ -1048,16 +1067,20 @@ test "every HELP line has a TYPE line and a sample, and every sample a name" {
|
||||
try testing.expect(std.mem.startsWith(u8, line, "nxdns_"));
|
||||
}
|
||||
}
|
||||
// Every family a bare state has is one sample, except the concurrency
|
||||
// family, which has one per site.
|
||||
const sites = std.enums.values(concurrency.Site).len;
|
||||
try testing.expectEqual(helps, types);
|
||||
try testing.expectEqual(helps, samples);
|
||||
try testing.expectEqual(helps - 1 + sites, samples);
|
||||
// The families a bare state still has: `nxdns_up`, every DNS counter, the
|
||||
// query log writer's, and the diagnostic log sink's. Derived rather than
|
||||
// counted, so a new counter in any of those structs extends the exposition
|
||||
// and this assertion together.
|
||||
// query log writer's, the diagnostic log sink's and the concurrency sites.
|
||||
// Derived rather than counted, so a new counter in any of those structs
|
||||
// extends the exposition and this assertion together.
|
||||
try testing.expectEqual(
|
||||
1 + dns_stat_fields.len +
|
||||
@typeInfo(LoggerCounters).@"struct".fields.len +
|
||||
@typeInfo(logging.Stats).@"struct".fields.len,
|
||||
@typeInfo(logging.Stats).@"struct".fields.len +
|
||||
sites,
|
||||
samples,
|
||||
);
|
||||
// The reflective walk names the counters, so a renamed `Handler.Stats`
|
||||
@@ -1141,6 +1164,7 @@ test "the plain-DNS listener families carry every counter of both listeners" {
|
||||
.accept_errors = 8,
|
||||
.connection_errors = 9,
|
||||
.idle_timeouts = 10,
|
||||
.dropped_no_permit = 11,
|
||||
},
|
||||
});
|
||||
defer testing.allocator.free(text);
|
||||
@@ -1155,6 +1179,7 @@ test "the plain-DNS listener families carry every counter of both listeners" {
|
||||
try testing.expect(std.mem.containsAtLeast(u8, text, 1, "nxdns_tcp_server_connections_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_dropped_no_permit_total 11\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"));
|
||||
@@ -1223,10 +1248,12 @@ test "listener counters render only for the wired servers" {
|
||||
const doh_only = try renderToString(testing.allocator, .{
|
||||
.doh_listener = .{
|
||||
.connections = 9,
|
||||
.rejected_at_capacity = 6,
|
||||
.tls_handshake_failures = 2,
|
||||
.idle_timeouts = 1,
|
||||
.connection_errors = 0,
|
||||
.bad_requests = 4,
|
||||
.dropped_no_permit = 7,
|
||||
},
|
||||
});
|
||||
defer testing.allocator.free(doh_only);
|
||||
@@ -1236,14 +1263,18 @@ test "listener counters render only for the wired servers" {
|
||||
try testing.expect(std.mem.containsAtLeast(u8, doh_only, 1, "nxdns_doh_server_idle_timeouts_total 1\n"));
|
||||
try testing.expect(std.mem.containsAtLeast(u8, doh_only, 1, "nxdns_doh_server_connection_errors_total 0\n"));
|
||||
try testing.expect(std.mem.containsAtLeast(u8, doh_only, 1, "nxdns_doh_server_bad_requests_total 4\n"));
|
||||
try testing.expect(std.mem.containsAtLeast(u8, doh_only, 1, "nxdns_doh_server_rejected_at_capacity_total 6\n"));
|
||||
try testing.expect(std.mem.containsAtLeast(u8, doh_only, 1, "nxdns_doh_server_dropped_no_permit_total 7\n"));
|
||||
try testing.expect(!std.mem.containsAtLeast(u8, doh_only, 1, "nxdns_dot_server_"));
|
||||
|
||||
const dot_only = try renderToString(testing.allocator, .{
|
||||
.dot_listener = .{
|
||||
.connections = 5,
|
||||
.rejected_at_capacity = 2,
|
||||
.tls_handshake_failures = 0,
|
||||
.idle_timeouts = 3,
|
||||
.connection_errors = 1,
|
||||
.dropped_no_permit = 4,
|
||||
},
|
||||
});
|
||||
defer testing.allocator.free(dot_only);
|
||||
@@ -1251,11 +1282,28 @@ test "listener counters render only for the wired servers" {
|
||||
try testing.expect(std.mem.containsAtLeast(u8, dot_only, 1, "nxdns_dot_server_connections_total 5\n"));
|
||||
try testing.expect(std.mem.containsAtLeast(u8, dot_only, 1, "nxdns_dot_server_idle_timeouts_total 3\n"));
|
||||
try testing.expect(std.mem.containsAtLeast(u8, dot_only, 1, "nxdns_dot_server_connection_errors_total 1\n"));
|
||||
try testing.expect(std.mem.containsAtLeast(u8, dot_only, 1, "nxdns_dot_server_rejected_at_capacity_total 2\n"));
|
||||
try testing.expect(std.mem.containsAtLeast(u8, dot_only, 1, "nxdns_dot_server_dropped_no_permit_total 4\n"));
|
||||
// The DoT listener has no HTTP layer, so no bad_requests family.
|
||||
try testing.expect(!std.mem.containsAtLeast(u8, dot_only, 1, "nxdns_dot_server_bad_requests_total"));
|
||||
try testing.expect(!std.mem.containsAtLeast(u8, dot_only, 1, "nxdns_doh_server_"));
|
||||
}
|
||||
|
||||
test "every concurrency site renders under its own label, zero included" {
|
||||
var sample: Sample = .{};
|
||||
sample.concurrency.set(.querylog_wait, 3);
|
||||
const text = try renderToString(testing.allocator, sample);
|
||||
defer testing.allocator.free(text);
|
||||
|
||||
try testing.expect(std.mem.containsAtLeast(u8, text, 1, "# TYPE nxdns_concurrency_unavailable_total counter\n"));
|
||||
try testing.expect(std.mem.containsAtLeast(u8, text, 1, "nxdns_concurrency_unavailable_total{site=\"querylog_wait\"} 3\n"));
|
||||
for (std.enums.values(concurrency.Site)) |site| {
|
||||
var line: [96]u8 = undefined;
|
||||
const prefix = try std.fmt.bufPrint(&line, "nxdns_concurrency_unavailable_total{{site=\"{t}\"}} ", .{site});
|
||||
try testing.expect(std.mem.containsAtLeast(u8, text, 1, prefix));
|
||||
}
|
||||
}
|
||||
|
||||
test "cert reload counters render per endpoint, only for the wired stores" {
|
||||
const one = try renderToString(testing.allocator, .{
|
||||
.doh_certs = .{ .reloads = 2, .reload_failures = 1, .last_reload_unix = 1_700_000_000 },
|
||||
@@ -1474,7 +1522,7 @@ test "two upstreams on one host stay two series" {
|
||||
keys[count] = key;
|
||||
count += 1;
|
||||
}
|
||||
try testing.expectEqual(@as(usize, 22), count);
|
||||
try testing.expectEqual(@as(usize, 22 + std.enums.values(concurrency.Site).len), count);
|
||||
}
|
||||
|
||||
test "an upstream url is redacted before it reaches an open endpoint's label" {
|
||||
|
||||
Reference in New Issue
Block a user