feat: add count and exportText to metrics

This commit is contained in:
dualfroz committed 2026-09-07 18:12:20 +02:00
1 parent 0bc2663e45
commit cdcaaec1f3
1 file changed
+160
+160
View File
@@ -319,3 +319,163 @@ pub const Registry = struct {
try self.put(name, .{ .histogram = h });
}
pub fn count(self: *const Registry) usize {
return self.entries.count();
}
/// Writes every registered metric in Prometheus exposition format.
/// Output is sorted by name so it is deterministic (and diffable).
pub fn exportText(self: *Registry, allocator: std.mem.Allocator, writer: anytype) !void {
var names = try allocator.alloc([]const u8, self.entries.count());
defer allocator.free(names);
var it = self.entries.keyIterator();
var i: usize = 0;
while (it.next()) |key| : (i += 1) names[i] = key.*;
std.mem.sort([]const u8, names, {}, lessThanStr);
for (names) |name| {
const ref = self.entries.get(name).?;
switch (ref) {
.counter => |c| try writer.print("{s} {d}\n", .{ name, c.get() }),
.gauge => |g| try writer.print("{s} {d}\n", .{ name, g.get() }),
.histogram => |h| {
var cumulative: u64 = 0;
for (h.bounds, 0..) |bound, idx| {
cumulative += h.counts[idx];
try writer.print("{s}_bucket{{le=\"{d}\"}} {d}\n", .{ name, bound, cumulative });
}
cumulative += h.counts[h.bounds.len];
try writer.print("{s}_bucket{{le=\"+Inf\"}} {d}\n", .{ name, cumulative });
try writer.print("{s}_sum {d}\n", .{ name, h.sum });
try writer.print("{s}_count {d}\n", .{ name, h.total });
},
}
}
}
};
fn lessThanStr(_: void, a: []const u8, b: []const u8) bool {
return std.mem.order(u8, a, b) == .lt;
}
/// Renders a Db Stats snapshot in the same Prometheus text format used by
/// Registry.exportText, so `stats` output and ad-hoc registry metrics can be
/// scraped through one shared shape.
pub fn formatDbStats(writer: anytype, prefix: []const u8, s: stats_mod.Stats) !void {
try writer.print("{s}_memtable_entries {d}\n", .{ prefix, s.memtableEntries });
try writer.print("{s}_memtable_bytes {d}\n", .{ prefix, s.memtableBytes });
try writer.print("{s}_sstable_count {d}\n", .{ prefix, s.sstableCount });
try writer.print("{s}_live_entries {d}\n", .{ prefix, s.liveEntries });
try writer.print("{s}_physical_bytes {d}\n", .{ prefix, s.physicalBytes });
try writer.print("{s}_cache_hits {d}\n", .{ prefix, s.cacheHits });
try writer.print("{s}_cache_misses {d}\n", .{ prefix, s.cacheMisses });
try writer.print("{s}_cache_hit_rate {d:.6}\n", .{ prefix, s.cacheHitRate() });
try writer.print("{s}_next_seq {d}\n", .{ prefix, s.nextSeq });
const deepest = s.deepestLevel();
var i: usize = 0;
while (i <= deepest) : (i += 1) {
const level = s.levels[i];
try writer.print("{s}_level_tables{{level=\"{d}\"}} {d}\n", .{ prefix, i, level.tables });
try writer.print("{s}_level_entries{{level=\"{d}\"}} {d}\n", .{ prefix, i, level.entries });
}
}
test "counter incr and incrBy accumulate" {
var c = MetricCounter{};
c.incr();
c.incrBy(41);
try std.testing.expectEqual(@as(u64, 42), c.get());
c.reset();
try std.testing.expectEqual(@as(u64, 0), c.get());
}
test "gauge set/add round trip through bit reinterpretation" {
var g = Gauge{};
g.set(3.5);
try std.testing.expectApproxEqAbs(@as(f64, 3.5), g.get(), 1e-12);
g.add(-1.25);
try std.testing.expectApproxEqAbs(@as(f64, 2.25), g.get(), 1e-12);
}
test "histogram observe buckets and mean" {
const bounds = [_]f64{ 10, 50, 100 };
var h = try Histogram.init(std.testing.allocator, &bounds);
defer h.deinit();
h.observe(5);
h.observe(20);
h.observe(20);
h.observe(75);
h.observe(1000);
try std.testing.expectEqual(@as(u64, 1), h.bucketCount(0));
try std.testing.expectEqual(@as(u64, 2), h.bucketCount(1));
try std.testing.expectEqual(@as(u64, 1), h.bucketCount(2));
try std.testing.expectEqual(@as(u64, 1), h.bucketCount(3));
try std.testing.expectEqual(@as(u64, 5), h.count());
try std.testing.expectApproxEqAbs(@as(f64, (5 + 20 + 20 + 75 + 1000) / 5.0), h.mean(), 1e-9);
}
test "histogram quantile interpolates within a bucket" {
const bounds = [_]f64{ 100, 200 };
var h = try Histogram.init(std.testing.allocator, &bounds);
defer h.deinit();
// 4 values land in the (0,100] bucket, evenly spread by construction;
// linear interpolation only sees prev/cur cumulative counts, so the
// median of an all-in-one-bucket histogram is the bucket's own bound.
var i: usize = 0;
while (i < 4) : (i += 1) h.observe(50);
try std.testing.expectApproxEqAbs(@as(f64, 100), h.quantile(0.5), 1e-9);
// Now spread evenly across both finite buckets: p50 should land exactly
// on the boundary between them.
h.observe(150);
h.observe(150);
h.observe(150);
h.observe(150);
// counts: bucket0=4 (<=100), bucket1=4 (<=200) => cumulative [4,8], total 8
const q50 = h.quantile(0.5);
try std.testing.expectApproxEqAbs(@as(f64, 100), q50, 1e-9);
}
test "histogram quantile falls back to last bound for the +Inf bucket" {
const bounds = [_]f64{ 10, 20 };
var h = try Histogram.init(std.testing.allocator, &bounds);
defer h.deinit();
h.observe(5);
h.observe(9999);
try std.testing.expectApproxEqAbs(@as(f64, 20), h.quantile(1.0), 1e-9);
}
test "linearBuckets and exponentialBuckets produce expected boundaries" {
const lin = try linearBuckets(std.testing.allocator, 0, 10, 5);
defer std.testing.allocator.free(lin);
try std.testing.expectEqualSlices(f64, &[_]f64{ 0, 10, 20, 30, 40 }, lin);
const exp = try exponentialBuckets(std.testing.allocator, 1, 2, 5);
defer std.testing.allocator.free(exp);
try std.testing.expectEqualSlices(f64, &[_]f64{ 1, 2, 4, 8, 16 }, exp);
}
test "token bucket refills over time and denies when empty" {
var b = TokenBucket.init(10, 5, 0); // 10 capacity, 5 tokens/sec
try std.testing.expect(b.tryAcquireAt(10, 0));
try std.testing.expect(!b.tryAcquireAt(1, 0));
// half a second later: +2.5 tokens
try std.testing.expect(!b.tryAcquireAt(3, std.time.ns_per_s / 2));
try std.testing.expectApproxEqAbs(@as(f64, 2.5), b.availableAt(std.time.ns_per_s / 2), 1e-9);
// two full seconds later than that: refill caps at capacity
const later = std.time.ns_per_s / 2 + 2 * std.time.ns_per_s;
try std.testing.expect(b.tryAcquireAt(10, later));
try std.testing.expect(!b.tryAcquireAt(1, later));
}
test "token bucket never partially consumes on a failed acquire" {
var b = TokenBucket.init(5, 1, 0);
try std.testing.expect(!b.tryAcquireAt(6, 0));
try std.testing.expectApproxEqAbs(@as(f64, 5), b.availableAt(0), 1e-9);
}