diff --git a/src/metrics.zig b/src/metrics.zig index 38beb22..1be570c 100644 --- a/src/metrics.zig +++ b/src/metrics.zig @@ -479,3 +479,122 @@ test "token bucket never partially consumes on a failed acquire" { try std.testing.expectApproxEqAbs(@as(f64, 5), b.availableAt(0), 1e-9); } +test "sliding window limiter smooths across a window boundary" { + const windowNs: u64 = std.time.ns_per_s; + var lim = SlidingWindowLimiter.init(10, windowNs, 0); + + var i: usize = 0; + while (i < 10) : (i += 1) try std.testing.expect(lim.allowAt(0)); + try std.testing.expect(!lim.allowAt(0)); // exhausted current window + + // Halfway into the next window: previous window's 10 requests are + // weighted at 50% overlap (5), so only 5 more should be allowed before + // the weighted total reaches the limit of 10. + const halfwayNextWindow = windowNs + windowNs / 2; + var allowed: usize = 0; + i = 0; + while (i < 20) : (i += 1) { + if (lim.allowAt(halfwayNextWindow)) allowed += 1; + } + try std.testing.expectEqual(@as(usize, 5), allowed); +} + +test "sliding window limiter resets fully after a gap of more than one window" { + const windowNs: u64 = std.time.ns_per_s; + var lim = SlidingWindowLimiter.init(3, windowNs, 0); + try std.testing.expect(lim.allowAt(0)); + try std.testing.expect(lim.allowAt(0)); + try std.testing.expect(lim.allowAt(0)); + try std.testing.expect(!lim.allowAt(0)); + + // Jump 10 windows ahead: previous window count must not leak in. + const farFuture = 10 * windowNs; + try std.testing.expect(lim.allowAt(farFuture)); + try std.testing.expect(lim.allowAt(farFuture)); + try std.testing.expect(lim.allowAt(farFuture)); + try std.testing.expect(!lim.allowAt(farFuture)); +} + +test "registry exports counters gauges and histograms as sorted prometheus text" { + var c = MetricCounter{}; + c.incrBy(7); + var g = Gauge{}; + g.set(2.5); + const bounds = [_]f64{ 5, 10 }; + var h = try Histogram.init(std.testing.allocator, &bounds); + defer h.deinit(); + h.observe(3); + h.observe(8); + + var reg = Registry.init(std.testing.allocator); + defer reg.deinit(); + try reg.registerCounter("requests_total", &c); + try reg.registerGauge("temperature", &g); + try reg.registerHistogram("latency", &h); + try std.testing.expectEqual(@as(usize, 3), reg.count()); + + var buf = std.ArrayList(u8).init(std.testing.allocator); + defer buf.deinit(); + try reg.exportText(std.testing.allocator, buf.writer()); + + const text = buf.items; + // sorted alphabetically: latency < requests_total < temperature + try std.testing.expect(std.mem.indexOf(u8, text, "latency_bucket{le=\"5\"} 1\n") != null); + try std.testing.expect(std.mem.indexOf(u8, text, "latency_bucket{le=\"10\"} 2\n") != null); + try std.testing.expect(std.mem.indexOf(u8, text, "latency_bucket{le=\"+Inf\"} 2\n") != null); + try std.testing.expect(std.mem.indexOf(u8, text, "latency_sum 11") != null); + try std.testing.expect(std.mem.indexOf(u8, text, "latency_count 2\n") != null); + try std.testing.expect(std.mem.indexOf(u8, text, "requests_total 7\n") != null); + try std.testing.expect(std.mem.indexOf(u8, text, "temperature 2.5\n") != null); + + const latencyPos = std.mem.indexOf(u8, text, "latency_bucket").?; + const requestsPos = std.mem.indexOf(u8, text, "requests_total").?; + const temperaturePos = std.mem.indexOf(u8, text, "temperature").?; + try std.testing.expect(latencyPos < requestsPos); + try std.testing.expect(requestsPos < temperaturePos); +} + +test "registry re-registering the same name overwrites rather than duplicating" { + var c1 = MetricCounter{}; + c1.incrBy(1); + var c2 = MetricCounter{}; + c2.incrBy(99); + + var reg = Registry.init(std.testing.allocator); + defer reg.deinit(); + try reg.registerCounter("x", &c1); + try reg.registerCounter("x", &c2); + try std.testing.expectEqual(@as(usize, 1), reg.count()); + + var buf = std.ArrayList(u8).init(std.testing.allocator); + defer buf.deinit(); + try reg.exportText(std.testing.allocator, buf.writer()); + try std.testing.expectEqualStrings("x 99\n", buf.items); +} + +test "formatDbStats renders levels up to the deepest populated one" { + var s = stats_mod.Stats{}; + s.memtableEntries = 3; + s.memtableBytes = 128; + s.sstableCount = 2; + s.cacheHits = 9; + s.cacheMisses = 1; + s.levels[0].tables = 1; + s.levels[0].entries = 10; + s.levels[2].tables = 4; + s.levels[2].entries = 40; + + var buf = std.ArrayList(u8).init(std.testing.allocator); + defer buf.deinit(); + try formatDbStats(buf.writer(), "cobalt", s); + + const text = buf.items; + try std.testing.expect(std.mem.indexOf(u8, text, "cobalt_memtable_entries 3\n") != null); + try std.testing.expect(std.mem.indexOf(u8, text, "cobalt_cache_hit_rate 0.900000\n") != null); + try std.testing.expect(std.mem.indexOf(u8, text, "cobalt_level_tables{level=\"0\"} 1\n") != null); + try std.testing.expect(std.mem.indexOf(u8, text, "cobalt_level_tables{level=\"2\"} 4\n") != null); + // level 1 is empty but must still appear since deepestLevel is 2 + try std.testing.expect(std.mem.indexOf(u8, text, "cobalt_level_tables{level=\"1\"} 0\n") != null); + // level 3+ must not appear at all + try std.testing.expect(std.mem.indexOf(u8, text, "level=\"3\"") == null); +}