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 });
|
||||
}
|
||||
|
||||
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