use std::sync::OnceLock;
use opentelemetry::metrics::{Counter, Gauge, Histogram, Meter};
use opentelemetry::{KeyValue, global};
const SCOPE: &str = "meterstore";
pub struct Metrics {
pub archival_rows: Counter<u64>,
pub archival_duration: Histogram<f64>,
pub archival_failures: Counter<u64>,
pub archival_deferred: Counter<u64>,
pub partitions_dropped: Counter<u64>,
pub orphans_reclaimed: Counter<u64>,
pub rows_written: Counter<u64>,
pub rows_deduplicated: Counter<u64>,
pub late_corrections: Counter<u64>,
pub subjects_anonymised: Counter<u64>,
pub rows_scanned: Counter<u64>,
pub plan_duration: Histogram<f64>,
pub scan_duration: Histogram<f64>,
pub merge_elision_decisions: Counter<u64>,
pub merge_elided: Counter<u64>,
pub watermark_lag: Gauge<u64>,
pub hot_partitions_ahead: Gauge<u64>,
pub invariant_violations: Gauge<u64>,
}
impl Metrics {
pub fn new(meter: &Meter) -> Self {
Self {
archival_rows: meter
.u64_counter("meterstore.archival.rows")
.with_description("Rows moved from the hot tier to the cold tier")
.build(),
archival_duration: meter
.f64_histogram("meterstore.archival.duration")
.with_unit("s")
.with_description("Time to archive one window")
.build(),
archival_failures: meter
.u64_counter("meterstore.archival.failures")
.with_description("Archival runs that did not complete")
.build(),
archival_deferred: meter
.u64_counter("meterstore.archival.deferred")
.with_description(
"Archival runs that stopped because a lock was not available. \
Not a failure: nothing was changed and the next cycle retries",
)
.build(),
subjects_anonymised: meter
.u64_counter("meterstore.retention.subjects_anonymised")
.with_description(
"Subjects whose linkage a § 60 Abs. 6 retention sweep destroyed. \
Irreversible: a rise is worth a look, and a flat zero over a \
year is a sweep that is not running",
)
.build(),
partitions_dropped: meter
.u64_counter("meterstore.partitions.dropped")
.with_description("Hot partitions dropped after a durable cold commit")
.build(),
orphans_reclaimed: meter
.u64_counter("meterstore.partitions.orphans_reclaimed")
.with_description("Detached partitions reclaimed from an interrupted run")
.build(),
rows_written: meter
.u64_counter("meterstore.write.rows")
.with_description("Rows written to the hot tier")
.build(),
rows_deduplicated: meter
.u64_counter("meterstore.write.rows_deduplicated")
.with_description("Rows skipped as already present — the redelivery rate")
.build(),
late_corrections: meter
.u64_counter("meterstore.write.late_corrections")
.with_description("Corrections routed to the cold tier")
.build(),
rows_scanned: meter
.u64_counter("meterstore.query.rows_scanned")
.with_description("Rows read, by tier")
.build(),
plan_duration: meter
.f64_histogram("meterstore.query.plan_duration")
.with_unit("s")
.with_description("Time to plan a tiered scan — catalog and manifest reads")
.build(),
scan_duration: meter
.f64_histogram("meterstore.query.scan_duration")
.with_unit("s")
.with_description("Time to drain one tier's scan")
.build(),
merge_elision_decisions: meter
.u64_counter("meterstore.query.merge_elision_decisions")
.with_description("Scans that evaluated whether resolution could be skipped")
.build(),
merge_elided: meter
.u64_counter("meterstore.query.merge_elided")
.with_description(
"Scans that skipped version resolution — over decisions, the \
elided ratio",
)
.build(),
watermark_lag: meter
.u64_gauge("meterstore.tiering.watermark_lag")
.with_unit("s")
.with_description("Seconds between the tiering watermark and wall clock")
.build(),
hot_partitions_ahead: meter
.u64_gauge("meterstore.tiering.hot_partitions_ahead")
.with_description("Partitions remaining before inserts fail")
.build(),
invariant_violations: meter
.u64_gauge("meterstore.tiering.invariant_violations")
.with_description(
"Rows below the watermark still in the hot tier — non-zero means \
query results may be wrong",
)
.build(),
}
}
}
pub fn metrics() -> &'static Metrics {
static METRICS: OnceLock<Metrics> = OnceLock::new();
METRICS.get_or_init(|| Metrics::new(&global::meter(SCOPE)))
}
pub fn table(name: &str) -> [KeyValue; 1] {
[KeyValue::new("table", name.to_string())]
}
pub fn table_tier(name: &str, tier: &'static str) -> [KeyValue; 2] {
[
KeyValue::new("table", name.to_string()),
KeyValue::new("tier", tier),
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn instruments_build_without_an_sdk() {
let m = metrics();
m.archival_rows.add(1, &table("readings"));
m.watermark_lag.record(42, &table("readings"));
m.plan_duration.record(0.01, &table("readings"));
m.scan_duration
.record(0.01, &table_tier("readings", "cold"));
m.subjects_anonymised.add(1, &[]);
}
#[test]
fn the_instrument_set_is_built_once() {
assert!(std::ptr::eq(metrics(), metrics()));
}
#[test]
fn attributes_name_the_table() {
let attrs = table("readings_versions");
assert_eq!(attrs[0].key.as_str(), "table");
assert_eq!(attrs[0].value.as_str(), "readings_versions");
}
#[test]
fn tier_attributes_distinguish_the_halves() {
let cold = table_tier("readings", "cold");
let hot = table_tier("readings", "hot");
assert_ne!(cold[1].value.as_str(), hot[1].value.as_str());
}
}