use std::fmt::Write;
use crate::api::stats::TreeStats;
#[allow(clippy::too_many_lines)] #[must_use]
pub fn render_prometheus(stats: &TreeStats) -> String {
let mut out = String::with_capacity(1536);
metric(
&mut out,
"holt_blob_count",
"Number of distinct blobs reachable from the tree root.",
"gauge",
u64::from(stats.blob_count),
);
metric(
&mut out,
"holt_space_used_bytes",
"Sum of `space_used` across every blob (live extent bytes).",
"gauge",
stats.total_space_used,
);
metric(
&mut out,
"holt_gap_space_bytes",
"Sum of `gap_space` across every blob (reclaimable on compact).",
"gauge",
stats.total_gap_space,
);
metric(
&mut out,
"holt_slots",
"Sum of `num_slots` across every reachable blob.",
"gauge",
stats.total_slots,
);
metric(
&mut out,
"holt_compactions",
"Sum of `compact_times` across currently-reachable blobs. \
A gauge, not a counter — a blob that merges into its \
parent (or is deleted) takes its `compact_times` with \
it, so this can go down.",
"gauge",
stats.total_compactions,
);
metric(
&mut out,
"holt_tombstones",
"Sum of `tombstone_leaf_cnt` across every reachable blob.",
"gauge",
stats.total_tombstones,
);
metric(
&mut out,
"holt_bm_dirty_count",
"Number of blobs in the buffer manager dirty set.",
"gauge",
stats.bm_dirty_count as u64,
);
metric(
&mut out,
"holt_bm_pending_delete_count",
"Number of blobs queued for deferred backend deletion.",
"gauge",
stats.bm_pending_delete_count as u64,
);
metric(
&mut out,
"holt_bm_cache_hits_total",
"Cumulative buffer-manager cache hits.",
"counter",
stats.bm_cache_hits,
);
metric(
&mut out,
"holt_bm_cache_misses_total",
"Cumulative buffer-manager cache misses (fell through to backend).",
"counter",
stats.bm_cache_misses,
);
metric(
&mut out,
"holt_bm_optimistic_restarts_total",
"Cumulative wait-free read restarts (concurrent writer lapped snapshot).",
"counter",
stats.bm_optimistic_restarts,
);
if let Some(ck) = &stats.checkpointer {
metric(
&mut out,
"holt_checkpoint_rounds_attempted_total",
"Background checkpoint rounds the planner started.",
"counter",
ck.rounds_attempted,
);
metric(
&mut out,
"holt_checkpoint_rounds_succeeded_total",
"Background checkpoint rounds that completed without error.",
"counter",
ck.rounds_succeeded,
);
metric(
&mut out,
"holt_checkpoint_blobs_flushed_total",
"Blobs the checkpointer's I/O worker wrote through to backend.",
"counter",
ck.blobs_flushed,
);
metric(
&mut out,
"holt_checkpoint_merges_total",
"Cross-blob `BlobNode` crossings folded back into a parent.",
"counter",
ck.merges_total,
);
metric(
&mut out,
"holt_checkpoint_truncates_total",
"WAL truncations performed at the round's truncate gate.",
"counter",
ck.truncates,
);
metric(
&mut out,
"holt_checkpoint_evictions_total",
"Cache entries the eviction thread dropped.",
"counter",
ck.evictions,
);
}
out
}
#[inline]
fn metric(out: &mut String, name: &str, help: &str, ty: &str, value: u64) {
let _ = writeln!(out, "# HELP {name} {help}");
let _ = writeln!(out, "# TYPE {name} {ty}");
let _ = writeln!(out, "{name} {value}");
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::stats::{CheckpointerStats, TreeStats};
fn stats_fixture(with_checkpointer: bool) -> TreeStats {
TreeStats {
blob_count: 3,
total_space_used: 1024,
total_gap_space: 256,
total_slots: 42,
total_compactions: 7,
total_tombstones: 5,
blobs: Vec::new(),
bm_dirty_count: 2,
bm_pending_delete_count: 1,
bm_cache_hits: 1_000,
bm_cache_misses: 25,
bm_optimistic_restarts: 3,
checkpointer: with_checkpointer.then_some(CheckpointerStats {
rounds_attempted: 11,
rounds_succeeded: 10,
blobs_flushed: 30,
merges_total: 4,
truncates: 8,
evictions: 17,
}),
}
}
#[test]
fn renders_core_metrics_for_stats_without_checkpointer() {
let out = render_prometheus(&stats_fixture(false));
assert!(out.contains("# HELP holt_blob_count "));
assert!(out.contains("# TYPE holt_blob_count gauge\n"));
assert!(out.contains("holt_blob_count 3\n"));
assert!(out.contains("holt_bm_cache_hits_total 1000\n"));
assert!(out.contains("holt_bm_optimistic_restarts_total 3\n"));
assert!(out.contains("# TYPE holt_slots gauge\n"));
assert!(out.contains("holt_slots 42\n"));
assert!(out.contains("# TYPE holt_compactions gauge\n"));
assert!(out.contains("holt_compactions 7\n"));
assert!(out.contains("# TYPE holt_tombstones gauge\n"));
assert!(out.contains("holt_tombstones 5\n"));
assert!(!out.contains("holt_slots_total"));
assert!(!out.contains("holt_compactions_total"));
assert!(!out.contains("holt_tombstones_total"));
assert!(!out.contains("holt_checkpoint_"));
}
#[test]
fn renders_checkpoint_block_when_present() {
let out = render_prometheus(&stats_fixture(true));
assert!(out.contains("holt_checkpoint_rounds_attempted_total 11\n"));
assert!(out.contains("holt_checkpoint_blobs_flushed_total 30\n"));
assert!(out.contains("holt_checkpoint_evictions_total 17\n"));
}
#[test]
fn output_ends_with_newline() {
let out = render_prometheus(&stats_fixture(false));
assert!(out.ends_with('\n'), "Prometheus expects a trailing newline");
}
}