query log batching: one transaction per flush interval, not per query
Gates / frontend (push) Successful in 1m18s
Gates / test (push) Successful in 2m46s
Gates / test-aarch64 (push) Successful in 7m33s
Gates / package (push) Successful in 5m34s
Gates / container (push) Successful in 17s
CI / gates (push) Successful in 16m16s
Gates / frontend (push) Successful in 1m8s
Gates / container (push) Successful in 9s
Release / gates (push) Successful in 9m15s
Release / guard (push) Successful in 19s
Gates / test (push) Successful in 1m34s
Gates / test-aarch64 (push) Successful in 6m46s
Gates / package (push) Successful in 39s
Release / publish (push) Failing after 4m7s

This commit is contained in:
2026-08-20 20:57:11 +02:00
parent 037f209179
commit addf24f92c
18 changed files with 422 additions and 61 deletions
+6
View File
@@ -231,6 +231,10 @@ pub const Logging = struct {
level: LogLevel = .info,
retention_days: u16 = 30,
query_log_buffer_max: u32 = 10000,
/// How long the query-log writer gathers entries before it commits them.
/// `0` does not wait at all: it flushes the entry that woke the writer plus
/// whatever is already queued.
query_log_flush_interval_s: u16 = 60,
hide_domains: bool = false,
hide_client_ips: bool = false,
output: LogOutput = .stderr,
@@ -565,6 +569,7 @@ const expected_keys = [_][]const u8{
"logging.max_size_mb",
"logging.output",
"logging.query_log_buffer_max",
"logging.query_log_flush_interval_s",
"logging.retention_days",
"upstream.attempt_timeout_ms",
"upstream.read_timeout_ms",
@@ -657,6 +662,7 @@ test "toSettings and fromSettings round-trip a non-default config" {
.level = .err,
.retention_days = 41,
.query_log_buffer_max = 43,
.query_log_flush_interval_s = 44,
.hide_domains = true,
.hide_client_ips = true,
.output = .file,
+32
View File
@@ -93,6 +93,7 @@ pub const ValidateError = error{
BadTtl,
BadCacheSize,
BadRetention,
BadFlushInterval,
BadLogRotation,
BadDiskThresholds,
BadRateLimit,
@@ -335,6 +336,11 @@ const max_rate_window_seconds = 3_600;
/// the box, and nxdns does not try to know that.
const max_boot_entries = 1_000_000;
/// The ceiling on the query-log flush window. An hour of queries is already
/// more history than a crash is allowed to cost; beyond that the setting stops
/// being a batching knob and becomes a way to lose a working day of rows.
const max_flush_interval_s = 3_600;
fn checkScalars(cfg: Config, diags: *Diagnostics) error{OutOfMemory}!void {
const up = cfg.upstream;
try checkTimeout(diags, up.attempt_timeout_ms, "upstream.attempt_timeout_ms");
@@ -464,6 +470,16 @@ fn checkScalars(cfg: Config, diags: *Diagnostics) error{OutOfMemory}!void {
.{ max_boot_entries, cfg.logging.query_log_buffer_max },
);
}
// No floor: 0 is the documented "do not wait" setting, not a mistake.
if (cfg.logging.query_log_flush_interval_s > max_flush_interval_s) {
try diags.add(
error.BadFlushInterval,
"logging.query_log_flush_interval_s",
.{},
"must be at most {d}, got {d}",
.{ max_flush_interval_s, cfg.logging.query_log_flush_interval_s },
);
}
if (cfg.logging.max_size_mb < 1) {
try diags.add(error.BadLogRotation, "logging.max_size_mb", .{}, "must be at least 1", .{});
}
@@ -1946,6 +1962,22 @@ test "error.BadRetention" {
try expectProblem(huge_buffer, error.BadRetention, "logging.query_log_buffer_max");
}
test "error.BadFlushInterval" {
var cfg = baseConfig();
cfg.logging.query_log_flush_interval_s = 3601;
try expectProblem(cfg, error.BadFlushInterval, "logging.query_log_flush_interval_s");
// 0 is the "do not wait" setting and 3600 is the ceiling itself: both are
// legal, and a floor check would reject the first.
var immediate = baseConfig();
immediate.logging.query_log_flush_interval_s = 0;
try expectClean(immediate);
var edge = baseConfig();
edge.logging.query_log_flush_interval_s = max_flush_interval_s;
try expectClean(edge);
}
test "error.BadLogRotation" {
var cfg = baseConfig();
cfg.logging.max_files = 0;