feat: add count and exportText to metrics
This commit is contained in:
1 parent
0bc2663e45
commit
cdcaaec1f3
1 file changed
+160
+160
@@ -319,3 +319,163 @@ pub const Registry = struct {
|
|||||||
try self.put(name, .{ .histogram = h });
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
Reference in new issue
Block a user