use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, LazyLock, Mutex, OnceLock, RwLock};
use metrics::{Counter, Gauge, Histogram, Key, KeyName, Metadata, Recorder, SharedString, Unit};
use metrics_util::registry::{Registry, Storage};
const DEFAULT_BOUNDS: &[f64] = &[
0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0, 60.0, 120.0, 300.0,
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MetricKind {
Counter,
Gauge,
Histogram,
}
impl MetricKind {
pub fn as_str(self) -> &'static str {
match self {
Self::Counter => "counter",
Self::Gauge => "gauge",
Self::Histogram => "histogram",
}
}
}
#[derive(Debug, Clone)]
pub struct MetricDescription {
pub name: String,
pub kind: MetricKind,
pub unit: Option<String>,
pub description: String,
}
#[derive(Debug, Clone)]
pub enum MetricValue {
Scalar(f64),
Histogram {
buckets: Vec<(String, u64)>,
count: u64,
sum: f64,
},
}
#[derive(Debug, Clone)]
pub struct MetricPoint {
pub name: String,
pub kind: MetricKind,
pub attributes: HashMap<String, String>,
pub value: MetricValue,
}
static CATALOG: LazyLock<Mutex<HashMap<String, CatalogEntry>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
struct CatalogEntry {
kind: MetricKind,
unit: Option<String>,
description: String,
}
static HISTOGRAM_BOUNDS: LazyLock<RwLock<HashMap<String, Arc<[f64]>>>> =
LazyLock::new(|| RwLock::new(HashMap::new()));
static REGISTRY: OnceLock<Arc<Registry<Key, LanceStorage>>> = OnceLock::new();
fn bounds_for(name: &str) -> Arc<[f64]> {
HISTOGRAM_BOUNDS
.read()
.unwrap()
.get(name)
.cloned()
.unwrap_or_else(|| Arc::from(DEFAULT_BOUNDS))
}
struct BucketedHistogram {
bounds: Arc<[f64]>,
counts: Box<[AtomicU64]>,
count: AtomicU64,
sum_bits: AtomicU64,
}
impl BucketedHistogram {
fn new(bounds: Arc<[f64]>) -> Self {
let counts = (0..bounds.len() + 1)
.map(|_| AtomicU64::new(0))
.collect::<Vec<_>>()
.into_boxed_slice();
Self {
bounds,
counts,
count: AtomicU64::new(0),
sum_bits: AtomicU64::new(0),
}
}
fn add_to_sum(&self, value: f64) {
let mut current = self.sum_bits.load(Ordering::Relaxed);
loop {
let updated = (f64::from_bits(current) + value).to_bits();
match self.sum_bits.compare_exchange_weak(
current,
updated,
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(_) => break,
Err(actual) => current = actual,
}
}
}
fn snapshot(&self) -> MetricValue {
let mut cumulative = 0u64;
let mut buckets = Vec::with_capacity(self.bounds.len() + 1);
for (i, bound) in self.bounds.iter().enumerate() {
cumulative += self.counts[i].load(Ordering::Relaxed);
buckets.push((format!("{}", bound), cumulative));
}
cumulative += self.counts[self.bounds.len()].load(Ordering::Relaxed);
buckets.push(("+Inf".to_string(), cumulative));
MetricValue::Histogram {
buckets,
count: self.count.load(Ordering::Relaxed),
sum: f64::from_bits(self.sum_bits.load(Ordering::Relaxed)),
}
}
}
impl metrics::HistogramFn for BucketedHistogram {
fn record(&self, value: f64) {
let idx = self.bounds.partition_point(|&bound| bound < value);
self.counts[idx].fetch_add(1, Ordering::Relaxed);
self.count.fetch_add(1, Ordering::Relaxed);
self.add_to_sum(value);
}
}
struct LanceStorage;
impl Storage<Key> for LanceStorage {
type Counter = Arc<AtomicU64>;
type Gauge = Arc<AtomicU64>;
type Histogram = Arc<BucketedHistogram>;
fn counter(&self, _key: &Key) -> Self::Counter {
Arc::new(AtomicU64::new(0))
}
fn gauge(&self, _key: &Key) -> Self::Gauge {
Arc::new(AtomicU64::new(0))
}
fn histogram(&self, key: &Key) -> Self::Histogram {
Arc::new(BucketedHistogram::new(bounds_for(key.name())))
}
}
struct LanceRecorder {
registry: Arc<Registry<Key, LanceStorage>>,
}
impl LanceRecorder {
fn describe(
&self,
key: KeyName,
kind: MetricKind,
unit: Option<Unit>,
description: SharedString,
) {
CATALOG.lock().unwrap().insert(
key.as_str().to_string(),
CatalogEntry {
kind,
unit: unit.map(|u| u.as_canonical_label().to_string()),
description: description.into_owned(),
},
);
}
}
impl Recorder for LanceRecorder {
fn describe_counter(&self, key: KeyName, unit: Option<Unit>, description: SharedString) {
self.describe(key, MetricKind::Counter, unit, description);
}
fn describe_gauge(&self, key: KeyName, unit: Option<Unit>, description: SharedString) {
self.describe(key, MetricKind::Gauge, unit, description);
}
fn describe_histogram(&self, key: KeyName, unit: Option<Unit>, description: SharedString) {
self.describe(key, MetricKind::Histogram, unit, description);
}
fn register_counter(&self, key: &Key, _metadata: &Metadata<'_>) -> Counter {
self.registry
.get_or_create_counter(key, |c| Counter::from_arc(c.clone()))
}
fn register_gauge(&self, key: &Key, _metadata: &Metadata<'_>) -> Gauge {
self.registry
.get_or_create_gauge(key, |g| Gauge::from_arc(g.clone()))
}
fn register_histogram(&self, key: &Key, _metadata: &Metadata<'_>) -> Histogram {
self.registry
.get_or_create_histogram(key, |h| Histogram::from_arc(h.clone()))
}
}
fn register_bounds() {
let mut bounds = HISTOGRAM_BOUNDS.write().unwrap();
for (name, values) in lance_io::object_store::metrics::histogram_bounds() {
bounds.insert((*name).to_string(), Arc::from(*values));
}
}
fn describe_all() {
lance_io::object_store::metrics::describe_metrics();
}
fn labels(key: &Key) -> HashMap<String, String> {
key.labels()
.map(|label| (label.key().to_string(), label.value().to_string()))
.collect()
}
fn collect_points(registry: &Registry<Key, LanceStorage>) -> Vec<MetricPoint> {
let mut points = Vec::new();
for (key, handle) in registry.get_counter_handles() {
points.push(MetricPoint {
name: key.name().to_string(),
kind: MetricKind::Counter,
attributes: labels(&key),
value: MetricValue::Scalar(handle.load(Ordering::Relaxed) as f64),
});
}
for (key, handle) in registry.get_gauge_handles() {
points.push(MetricPoint {
name: key.name().to_string(),
kind: MetricKind::Gauge,
attributes: labels(&key),
value: MetricValue::Scalar(f64::from_bits(handle.load(Ordering::Relaxed))),
});
}
for (key, handle) in registry.get_histogram_handles() {
points.push(MetricPoint {
name: key.name().to_string(),
kind: MetricKind::Histogram,
attributes: labels(&key),
value: handle.snapshot(),
});
}
points
}
pub fn register_metrics_recorder() -> bool {
if REGISTRY.get().is_some() {
return true;
}
let registry = Arc::new(Registry::new(LanceStorage));
let recorder = LanceRecorder {
registry: registry.clone(),
};
register_bounds();
match metrics::set_global_recorder(recorder) {
Ok(()) => {
let _ = REGISTRY.set(registry);
describe_all();
true
}
Err(_) => false,
}
}
pub fn metrics_catalog() -> Vec<MetricDescription> {
CATALOG
.lock()
.unwrap()
.iter()
.map(|(name, entry)| MetricDescription {
name: name.clone(),
kind: entry.kind,
unit: entry.unit.clone(),
description: entry.description.clone(),
})
.collect()
}
pub fn snapshot_metrics() -> Vec<MetricPoint> {
let Some(registry) = REGISTRY.get() else {
return Vec::new();
};
collect_points(registry)
}
#[cfg(test)]
mod tests {
use super::*;
use metrics::HistogramFn;
fn bucket_count(buckets: &[(String, u64)], le: &str) -> u64 {
buckets
.iter()
.find(|(b, _)| b == le)
.map(|(_, c)| *c)
.unwrap_or_else(|| panic!("no bucket with le={le}"))
}
#[test]
fn bucketed_histogram_records_cumulative_buckets() {
let hist = BucketedHistogram::new(Arc::from([0.1f64, 1.0, 10.0].as_slice()));
hist.record(0.05); hist.record(0.5); hist.record(0.5); hist.record(50.0);
let MetricValue::Histogram {
buckets,
count,
sum,
} = hist.snapshot()
else {
panic!("expected histogram");
};
assert_eq!(bucket_count(&buckets, "0.1"), 1);
assert_eq!(bucket_count(&buckets, "1"), 3);
assert_eq!(bucket_count(&buckets, "10"), 3);
assert_eq!(bucket_count(&buckets, "+Inf"), 4);
assert_eq!(count, 4);
assert!((sum - 51.05).abs() < 1e-9);
}
#[test]
fn bucketed_histogram_boundary_is_inclusive() {
let hist = BucketedHistogram::new(Arc::from([1.0f64].as_slice()));
hist.record(1.0); let MetricValue::Histogram { buckets, .. } = hist.snapshot() else {
panic!("expected histogram");
};
assert_eq!(bucket_count(&buckets, "1"), 1);
assert_eq!(bucket_count(&buckets, "+Inf"), 1);
}
#[test]
fn bucketed_histogram_boundary_is_inclusive_mid_range() {
let hist = BucketedHistogram::new(Arc::from([0.1f64, 1.0, 10.0].as_slice()));
hist.record(1.0);
let MetricValue::Histogram { buckets, .. } = hist.snapshot() else {
panic!("expected histogram");
};
assert_eq!(bucket_count(&buckets, "0.1"), 0);
assert_eq!(bucket_count(&buckets, "1"), 1);
assert_eq!(bucket_count(&buckets, "10"), 1); assert_eq!(bucket_count(&buckets, "+Inf"), 1);
}
#[test]
fn recorder_aggregates_counters_with_labels() {
let registry = Arc::new(Registry::new(LanceStorage));
let recorder = LanceRecorder {
registry: registry.clone(),
};
metrics::with_local_recorder(&recorder, || {
metrics::counter!("test_requests_total", "operation" => "get", "scheme" => "s3")
.increment(2);
metrics::counter!("test_requests_total", "operation" => "get", "scheme" => "s3")
.increment(3);
metrics::counter!("test_requests_total", "operation" => "put", "scheme" => "gs")
.increment(7);
});
let scalar = |attrs: &[(&str, &str)]| {
let points = collect_points(®istry);
let point = points
.into_iter()
.find(|p| {
p.name == "test_requests_total"
&& attrs
.iter()
.all(|(k, v)| p.attributes.get(*k).map(String::as_str) == Some(*v))
})
.expect("counter recorded for label set");
assert_eq!(point.kind, MetricKind::Counter);
match point.value {
MetricValue::Scalar(v) => v,
_ => panic!("expected scalar"),
}
};
assert!((scalar(&[("operation", "get"), ("scheme", "s3")]) - 5.0).abs() < 1e-9);
assert!((scalar(&[("operation", "put"), ("scheme", "gs")]) - 7.0).abs() < 1e-9);
}
#[test]
fn recorder_records_gauges() {
let registry = Arc::new(Registry::new(LanceStorage));
let recorder = LanceRecorder {
registry: registry.clone(),
};
metrics::with_local_recorder(&recorder, || {
metrics::gauge!("test_gauge", "scheme" => "s3").set(3.5);
});
let points = collect_points(®istry);
let point = points
.iter()
.find(|p| p.name == "test_gauge")
.expect("gauge recorded");
assert_eq!(point.kind, MetricKind::Gauge);
assert!(matches!(point.value, MetricValue::Scalar(v) if (v - 3.5).abs() < 1e-9));
}
#[test]
fn recorder_falls_back_to_default_bounds() {
let name = "test_unregistered_histogram";
assert!(!HISTOGRAM_BOUNDS.read().unwrap().contains_key(name));
let registry = Arc::new(Registry::new(LanceStorage));
let recorder = LanceRecorder {
registry: registry.clone(),
};
metrics::with_local_recorder(&recorder, || {
metrics::histogram!(name).record(0.02);
});
let points = collect_points(®istry);
let point = points.iter().find(|p| p.name == name).expect("recorded");
let MetricValue::Histogram { buckets, count, .. } = &point.value else {
panic!("expected histogram");
};
assert_eq!(*count, 1);
assert_eq!(buckets.len(), DEFAULT_BOUNDS.len() + 1);
assert_eq!(bucket_count(buckets, "0.025"), 1);
assert_eq!(bucket_count(buckets, "0.01"), 0);
assert_eq!(bucket_count(buckets, "+Inf"), 1);
}
#[test]
fn recorder_uses_registered_histogram_bounds() {
let name = "test_recorder_bounds_seconds";
HISTOGRAM_BOUNDS
.write()
.unwrap()
.insert(name.to_string(), Arc::from([0.1f64, 1.0].as_slice()));
let registry = Arc::new(Registry::new(LanceStorage));
let recorder = LanceRecorder {
registry: registry.clone(),
};
metrics::with_local_recorder(&recorder, || {
metrics::histogram!(name).record(0.05);
metrics::histogram!(name).record(5.0);
});
let points = collect_points(®istry);
let point = points.iter().find(|p| p.name == name).expect("recorded");
let MetricValue::Histogram { buckets, count, .. } = &point.value else {
panic!("expected histogram");
};
assert_eq!(*count, 2);
assert_eq!(bucket_count(buckets, "0.1"), 1);
assert_eq!(bucket_count(buckets, "+Inf"), 2);
}
#[test]
fn describe_populates_catalog() {
let name = "test_describe_catalog_total";
let registry = Arc::new(Registry::new(LanceStorage));
let recorder = LanceRecorder { registry };
metrics::with_local_recorder(&recorder, || {
metrics::describe_counter!(name, Unit::Count, "a test counter");
});
let catalog = CATALOG.lock().unwrap();
let entry = catalog.get(name).expect("described");
assert_eq!(entry.kind, MetricKind::Counter);
assert_eq!(entry.description, "a test counter");
}
}