use std::collections::HashMap;
use std::hash::{BuildHasherDefault, Hasher};
use std::sync::atomic::AtomicBool;
use std::sync::{Arc, Mutex, OnceLock, RwLock, RwLockReadGuard, RwLockWriteGuard};
fn read_lock<T>(lock: &RwLock<T>) -> RwLockReadGuard<'_, T> {
lock.read().unwrap_or_else(|p| p.into_inner())
}
fn write_lock<T>(lock: &RwLock<T>) -> RwLockWriteGuard<'_, T> {
lock.write().unwrap_or_else(|p| p.into_inner())
}
const DEFAULT_MAX_CARDINALITY: usize = 2000;
#[derive(Default)]
struct IdentityHasher(u64);
impl Hasher for IdentityHasher {
fn finish(&self) -> u64 {
self.0
}
fn write(&mut self, _bytes: &[u8]) {
unreachable!("IdentityHasher only supports write_u64");
}
fn write_u64(&mut self, n: u64) {
self.0 = n;
}
}
type IdentityBuildHasher = BuildHasherDefault<IdentityHasher>;
static GLOBAL_REGISTRY: OnceLock<MetricsRegistry> = OnceLock::new();
pub fn global_registry() -> &'static MetricsRegistry {
GLOBAL_REGISTRY.get_or_init(MetricsRegistry::new)
}
pub struct HistogramDataPoint {
pub attrs: Arc<Vec<(String, String)>>,
pub bucket_counts: Vec<u64>,
pub sum: f64,
pub count: u64,
pub min: f64,
pub max: f64,
pub exemplar: Option<Exemplar>,
}
#[derive(Clone, Debug)]
pub struct Exemplar {
pub trace_id: [u8; 16],
pub span_id: [u8; 8],
pub time_unix_nano: u64,
pub value: ExemplarValue,
}
#[derive(Clone, Debug)]
pub enum ExemplarValue {
Int(i64),
Double(f64),
}
pub type Attrs = Arc<Vec<(String, String)>>;
pub type CounterDataPoint = (Attrs, i64, Option<Exemplar>);
pub type GaugeDataPoint = (Attrs, f64, Option<Exemplar>);
pub enum MetricSnapshot {
Counter {
name: String,
description: String,
data_points: Vec<CounterDataPoint>,
},
Gauge {
name: String,
description: String,
data_points: Vec<GaugeDataPoint>,
},
Histogram {
name: String,
description: String,
boundaries: Vec<f64>,
data_points: Vec<HistogramDataPoint>,
},
}
#[derive(Debug, Clone, Copy)]
enum MetricKind {
Counter,
Gauge,
Histogram,
}
impl MetricKind {
fn as_str(self) -> &'static str {
match self {
MetricKind::Counter => "counter",
MetricKind::Gauge => "gauge",
MetricKind::Histogram => "histogram",
}
}
}
pub struct MetricsRegistry {
counters: RwLock<HashMap<String, Counter>>,
gauges: RwLock<HashMap<String, Gauge>>,
histograms: RwLock<HashMap<String, Histogram>>,
default_max_cardinality: usize,
}
impl Default for MetricsRegistry {
fn default() -> Self {
Self::new()
}
}
impl MetricsRegistry {
pub fn new() -> Self {
Self {
counters: RwLock::new(HashMap::new()),
gauges: RwLock::new(HashMap::new()),
histograms: RwLock::new(HashMap::new()),
default_max_cardinality: DEFAULT_MAX_CARDINALITY,
}
}
pub fn with_max_cardinality(max_cardinality: usize) -> Self {
Self {
counters: RwLock::new(HashMap::new()),
gauges: RwLock::new(HashMap::new()),
histograms: RwLock::new(HashMap::new()),
default_max_cardinality: max_cardinality,
}
}
fn warn_cross_type_conflict(&self, name: &str, kind: MetricKind) {
let conflict = match kind {
MetricKind::Counter => {
read_lock(&self.gauges).contains_key(name)
|| read_lock(&self.histograms).contains_key(name)
}
MetricKind::Gauge => {
read_lock(&self.counters).contains_key(name)
|| read_lock(&self.histograms).contains_key(name)
}
MetricKind::Histogram => {
read_lock(&self.counters).contains_key(name)
|| read_lock(&self.gauges).contains_key(name)
}
};
if conflict {
tracing::warn!(
metric = name,
requested = kind.as_str(),
"metric name already registered as a different instrument type"
);
}
}
pub fn counter(&self, name: &str, description: &str) -> Counter {
self.counter_with_max_cardinality(name, description, self.default_max_cardinality)
}
pub fn counter_with_max_cardinality(
&self,
name: &str,
description: &str,
max_cardinality: usize,
) -> Counter {
{
let counters = read_lock(&self.counters);
if let Some(c) = counters.get(name) {
if c.inner.description != description || c.inner.max_cardinality != max_cardinality
{
tracing::warn!(
metric = name,
"counter re-registered with different metadata; using original"
);
}
return c.clone();
}
}
self.warn_cross_type_conflict(name, MetricKind::Counter);
let mut counters = write_lock(&self.counters);
counters
.entry(name.to_string())
.or_insert_with(|| Counter {
inner: Arc::new(CounterInner {
name: name.to_string(),
description: description.to_string(),
max_cardinality,
overflow_warned: AtomicBool::new(false),
data: Mutex::new(HashMap::with_hasher(IdentityBuildHasher::default())),
}),
})
.clone()
}
pub fn gauge(&self, name: &str, description: &str) -> Gauge {
self.gauge_with_max_cardinality(name, description, self.default_max_cardinality)
}
pub fn gauge_with_max_cardinality(
&self,
name: &str,
description: &str,
max_cardinality: usize,
) -> Gauge {
{
let gauges = read_lock(&self.gauges);
if let Some(g) = gauges.get(name) {
if g.inner.description != description || g.inner.max_cardinality != max_cardinality
{
tracing::warn!(
metric = name,
"gauge re-registered with different metadata; using original"
);
}
return g.clone();
}
}
self.warn_cross_type_conflict(name, MetricKind::Gauge);
let mut gauges = write_lock(&self.gauges);
gauges
.entry(name.to_string())
.or_insert_with(|| Gauge {
inner: Arc::new(GaugeInner {
name: name.to_string(),
description: description.to_string(),
max_cardinality,
overflow_warned: AtomicBool::new(false),
data: Mutex::new(HashMap::with_hasher(IdentityBuildHasher::default())),
}),
})
.clone()
}
pub fn histogram(&self, name: &str, description: &str, boundaries: &[f64]) -> Histogram {
self.histogram_with_max_cardinality(
name,
description,
boundaries,
self.default_max_cardinality,
)
}
pub fn histogram_with_max_cardinality(
&self,
name: &str,
description: &str,
boundaries: &[f64],
max_cardinality: usize,
) -> Histogram {
{
let histograms = read_lock(&self.histograms);
if let Some(h) = histograms.get(name) {
if h.inner.description != description || h.inner.max_cardinality != max_cardinality
{
tracing::warn!(
metric = name,
"histogram re-registered with different metadata; using original"
);
} else {
let mut incoming: Vec<f64> = boundaries
.iter()
.copied()
.filter(|b| b.is_finite())
.collect();
incoming.sort_by(|a, b| a.partial_cmp(b).unwrap());
incoming.dedup();
if h.inner.boundaries != incoming {
tracing::warn!(
metric = name,
"histogram re-registered with different boundaries; using original"
);
}
}
return h.clone();
}
}
self.warn_cross_type_conflict(name, MetricKind::Histogram);
let mut sorted: Vec<f64> = boundaries
.iter()
.copied()
.filter(|b| b.is_finite())
.collect();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
sorted.dedup();
let mut histograms = write_lock(&self.histograms);
histograms
.entry(name.to_string())
.or_insert_with(|| Histogram {
inner: Arc::new(HistogramInner {
name: name.to_string(),
description: description.to_string(),
boundaries: sorted,
max_cardinality,
overflow_warned: AtomicBool::new(false),
data: Mutex::new(HashMap::with_hasher(IdentityBuildHasher::default())),
}),
})
.clone()
}
pub fn collect(&self) -> Vec<MetricSnapshot> {
let mut snapshots = Vec::new();
{
let counters = read_lock(&self.counters);
for counter in counters.values() {
let mut data = counter.inner.data.lock().unwrap_or_else(|p| p.into_inner());
if data.is_empty() {
continue;
}
let data_points: Vec<_> = data
.values_mut()
.map(|(attrs, val, exemplar)| (Arc::clone(attrs), *val, exemplar.take()))
.collect();
snapshots.push(MetricSnapshot::Counter {
name: counter.inner.name.clone(),
description: counter.inner.description.clone(),
data_points,
});
}
}
{
let gauges = read_lock(&self.gauges);
for gauge in gauges.values() {
let mut data = gauge.inner.data.lock().unwrap_or_else(|p| p.into_inner());
if data.is_empty() {
continue;
}
let data_points: Vec<_> = data
.values_mut()
.map(|(attrs, val, exemplar)| (Arc::clone(attrs), *val, exemplar.take()))
.collect();
snapshots.push(MetricSnapshot::Gauge {
name: gauge.inner.name.clone(),
description: gauge.inner.description.clone(),
data_points,
});
}
}
{
let histograms = read_lock(&self.histograms);
for histogram in histograms.values() {
let mut data = histogram
.inner
.data
.lock()
.unwrap_or_else(|p| p.into_inner());
if data.is_empty() {
continue;
}
let data_points: Vec<_> = data
.values_mut()
.map(|(attrs, state, exemplar)| HistogramDataPoint {
attrs: Arc::clone(attrs),
bucket_counts: state.bucket_counts.clone(),
sum: state.sum,
count: state.count,
min: state.min,
max: state.max,
exemplar: exemplar.take(),
})
.collect();
snapshots.push(MetricSnapshot::Histogram {
name: histogram.inner.name.clone(),
description: histogram.inner.description.clone(),
boundaries: histogram.inner.boundaries.clone(),
data_points,
});
}
}
snapshots
}
}
fn attrs_hash_unordered(attrs: &[(&str, &str)]) -> u64 {
const FNV_OFFSET: u64 = 0xcbf29ce484222325;
const FNV_PRIME: u64 = 0x100000001b3;
let mut combined: u64 = 0;
for &(k, v) in attrs {
let mut h: u64 = FNV_OFFSET;
for byte in k.as_bytes() {
h ^= *byte as u64;
h = h.wrapping_mul(FNV_PRIME);
}
h ^= 0xff;
h = h.wrapping_mul(FNV_PRIME);
for byte in v.as_bytes() {
h ^= *byte as u64;
h = h.wrapping_mul(FNV_PRIME);
}
combined = combined.wrapping_add(h);
}
combined
}
fn owned_attrs(attrs: &[(&str, &str)]) -> Arc<Vec<(String, String)>> {
let mut owned: Vec<(String, String)> = attrs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
owned.sort();
Arc::new(owned)
}
fn attrs_match(stored: &[(String, String)], incoming: &[(&str, &str)]) -> bool {
if stored.len() != incoming.len() {
return false;
}
let direct_match = stored
.iter()
.zip(incoming.iter())
.all(|((sk, sv), (ik, iv))| sk.as_str() == *ik && sv.as_str() == *iv);
if direct_match {
return true;
}
let mut incoming_sorted: Vec<(&str, &str)> = incoming.to_vec();
incoming_sorted.sort();
stored
.iter()
.zip(incoming_sorted.iter())
.all(|((sk, sv), (ik, iv))| sk.as_str() == *ik && sv.as_str() == *iv)
}
fn current_trace_context() -> Option<([u8; 16], [u8; 8])> {
let mut result = None;
tracing::Span::current().with_subscriber(|(id, dispatch)| {
use tracing_subscriber::registry::LookupSpan;
if let Some(registry) = dispatch.downcast_ref::<tracing_subscriber::Registry>() {
if let Some(span_ref) = registry.span(id) {
let ext = span_ref.extensions();
if let Some(fields) = ext.get::<crate::otlp_layer::SpanFields>() {
result = Some((fields.trace_id, fields.span_id));
} else {
for ancestor in span_ref.scope().skip(1) {
let ext = ancestor.extensions();
if let Some(fields) = ext.get::<crate::otlp_layer::SpanFields>() {
result = Some((fields.trace_id, fields.span_id));
break;
}
}
}
}
}
});
result
}
fn capture_exemplar(value: ExemplarValue) -> Option<Exemplar> {
if !tracing::dispatcher::has_been_set() {
return None;
}
let (trace_id, span_id) = current_trace_context()?;
if trace_id == [0u8; 16] {
return None;
}
let time_unix_nano = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos() as u64;
Some(Exemplar {
trace_id,
span_id,
time_unix_nano,
value,
})
}
struct CounterInner {
name: String,
description: String,
max_cardinality: usize,
overflow_warned: AtomicBool,
data: Mutex<HashMap<u64, CounterDataPoint, IdentityBuildHasher>>,
}
#[derive(Clone)]
pub struct Counter {
inner: Arc<CounterInner>,
}
impl Counter {
pub fn add(&self, value: u64, attrs: &[(&str, &str)]) {
let clamped = value.min(i64::MAX as u64) as i64;
let exemplar = capture_exemplar(ExemplarValue::Int(clamped));
let key = if attrs.is_empty() {
0
} else {
attrs_hash_unordered(attrs)
};
let mut data = self.inner.data.lock().unwrap_or_else(|p| p.into_inner());
if !data.contains_key(&key) && data.len() >= self.inner.max_cardinality {
if !self
.inner
.overflow_warned
.swap(true, std::sync::atomic::Ordering::Relaxed)
{
tracing::warn!(
metric = self.inner.name,
limit = self.inner.max_cardinality,
"metric cardinality limit reached, dropping new attribute sets"
);
}
return;
}
if let Some(existing) = data.get_mut(&key) {
if !attrs_match(&existing.0, attrs) {
return;
}
existing.1 = existing.1.saturating_add(clamped);
if exemplar.is_some() {
existing.2 = exemplar;
}
} else {
data.insert(key, (owned_attrs(attrs), clamped, exemplar));
}
}
}
struct GaugeInner {
name: String,
description: String,
max_cardinality: usize,
overflow_warned: AtomicBool,
data: Mutex<HashMap<u64, GaugeDataPoint, IdentityBuildHasher>>,
}
#[derive(Clone)]
pub struct Gauge {
inner: Arc<GaugeInner>,
}
impl Gauge {
pub fn set(&self, value: f64, attrs: &[(&str, &str)]) {
if !value.is_finite() {
return;
}
let exemplar = capture_exemplar(ExemplarValue::Double(value));
let key = if attrs.is_empty() {
0
} else {
attrs_hash_unordered(attrs)
};
let mut data = self.inner.data.lock().unwrap_or_else(|p| p.into_inner());
if !data.contains_key(&key) && data.len() >= self.inner.max_cardinality {
if !self
.inner
.overflow_warned
.swap(true, std::sync::atomic::Ordering::Relaxed)
{
tracing::warn!(
metric = self.inner.name,
limit = self.inner.max_cardinality,
"metric cardinality limit reached, dropping new attribute sets"
);
}
return;
}
if let Some(existing) = data.get_mut(&key) {
if !attrs_match(&existing.0, attrs) {
return;
}
existing.1 = value;
if exemplar.is_some() {
existing.2 = exemplar;
}
} else {
data.insert(key, (owned_attrs(attrs), value, exemplar));
}
}
}
struct HistogramState {
bucket_counts: Vec<u64>,
sum: f64,
count: u64,
min: f64,
max: f64,
}
type HistogramEntry = (Attrs, HistogramState, Option<Exemplar>);
struct HistogramInner {
name: String,
description: String,
boundaries: Vec<f64>,
max_cardinality: usize,
overflow_warned: AtomicBool,
data: Mutex<HashMap<u64, HistogramEntry, IdentityBuildHasher>>,
}
#[derive(Clone)]
pub struct Histogram {
inner: Arc<HistogramInner>,
}
impl Histogram {
pub fn observe(&self, value: f64, attrs: &[(&str, &str)]) {
if !value.is_finite() {
return;
}
let exemplar = capture_exemplar(ExemplarValue::Double(value));
let bucket_idx = self.inner.boundaries.partition_point(|&b| b < value);
let key = if attrs.is_empty() {
0
} else {
attrs_hash_unordered(attrs)
};
let mut data = self.inner.data.lock().unwrap_or_else(|p| p.into_inner());
if !data.contains_key(&key) && data.len() >= self.inner.max_cardinality {
if !self
.inner
.overflow_warned
.swap(true, std::sync::atomic::Ordering::Relaxed)
{
tracing::warn!(
metric = self.inner.name,
limit = self.inner.max_cardinality,
"metric cardinality limit reached, dropping new attribute sets"
);
}
return;
}
if let Some(existing) = data.get_mut(&key) {
if !attrs_match(&existing.0, attrs) {
return;
}
existing.1.bucket_counts[bucket_idx] += 1;
existing.1.sum += value;
existing.1.count += 1;
if value < existing.1.min {
existing.1.min = value;
}
if value > existing.1.max {
existing.1.max = value;
}
if exemplar.is_some() {
existing.2 = exemplar;
}
} else {
let num_buckets = self.inner.boundaries.len() + 1;
let mut bucket_counts = vec![0u64; num_buckets];
bucket_counts[bucket_idx] = 1;
data.insert(
key,
(
owned_attrs(attrs),
HistogramState {
bucket_counts,
sum: value,
count: 1,
min: value,
max: value,
},
exemplar,
),
);
}
}
}
pub fn counter(name: &str, description: &str) -> Counter {
global_registry().counter(name, description)
}
pub fn gauge(name: &str, description: &str) -> Gauge {
global_registry().gauge(name, description)
}
pub fn histogram(name: &str, description: &str, boundaries: &[f64]) -> Histogram {
global_registry().histogram(name, description, boundaries)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn counter_add_accumulates() {
let registry = MetricsRegistry::new();
let c = registry.counter("req_total", "Total requests");
c.add(1, &[("method", "GET")]);
c.add(3, &[("method", "GET")]);
c.add(1, &[("method", "POST")]);
let snapshots = registry.collect();
assert_eq!(snapshots.len(), 1);
match &snapshots[0] {
MetricSnapshot::Counter {
name, data_points, ..
} => {
assert_eq!(name, "req_total");
assert_eq!(data_points.len(), 2);
let get_val = data_points
.iter()
.find(|(a, _, _)| a[0].1 == "GET")
.unwrap()
.1;
assert_eq!(get_val, 4);
let post_val = data_points
.iter()
.find(|(a, _, _)| a[0].1 == "POST")
.unwrap()
.1;
assert_eq!(post_val, 1);
}
_ => panic!("expected Counter snapshot"),
}
}
#[test]
fn gauge_set_overwrites() {
let registry = MetricsRegistry::new();
let g = registry.gauge("cpu_usage", "CPU usage");
g.set(50.0, &[("core", "0")]);
g.set(75.5, &[("core", "0")]);
let snapshots = registry.collect();
assert_eq!(snapshots.len(), 1);
match &snapshots[0] {
MetricSnapshot::Gauge {
name, data_points, ..
} => {
assert_eq!(name, "cpu_usage");
assert_eq!(data_points.len(), 1);
assert!((data_points[0].1 - 75.5).abs() < f64::EPSILON);
}
_ => panic!("expected Gauge snapshot"),
}
}
#[test]
fn counter_no_attrs() {
let registry = MetricsRegistry::new();
let c = registry.counter("simple", "simple counter");
c.add(10, &[]);
let snapshots = registry.collect();
assert_eq!(snapshots.len(), 1);
match &snapshots[0] {
MetricSnapshot::Counter { data_points, .. } => {
assert_eq!(data_points.len(), 1);
assert_eq!(data_points[0].1, 10);
assert!(data_points[0].0.is_empty());
}
_ => panic!("expected Counter"),
}
}
#[test]
fn empty_registry_collects_nothing() {
let registry = MetricsRegistry::new();
let _ = registry.counter("unused", "never incremented");
assert!(registry.collect().is_empty());
}
#[test]
fn counter_clone_shares_state() {
let registry = MetricsRegistry::new();
let c1 = registry.counter("shared", "shared counter");
let c2 = c1.clone();
c1.add(5, &[]);
c2.add(3, &[]);
let snapshots = registry.collect();
match &snapshots[0] {
MetricSnapshot::Counter { data_points, .. } => {
assert_eq!(data_points[0].1, 8);
}
_ => panic!("expected Counter"),
}
}
#[test]
fn attrs_order_does_not_matter() {
let registry = MetricsRegistry::new();
let c = registry.counter("order_test", "test");
c.add(1, &[("a", "1"), ("b", "2")]);
c.add(1, &[("b", "2"), ("a", "1")]);
let snapshots = registry.collect();
match &snapshots[0] {
MetricSnapshot::Counter { data_points, .. } => {
assert_eq!(data_points.len(), 1);
assert_eq!(data_points[0].1, 2);
}
_ => panic!("expected Counter"),
}
}
#[test]
fn histogram_observe_accumulates() {
let registry = MetricsRegistry::new();
let h = registry.histogram("latency", "request latency", &[10.0, 50.0, 100.0]);
h.observe(5.0, &[("method", "GET")]);
h.observe(25.0, &[("method", "GET")]);
h.observe(75.0, &[("method", "GET")]);
h.observe(200.0, &[("method", "GET")]);
let snapshots = registry.collect();
assert_eq!(snapshots.len(), 1);
match &snapshots[0] {
MetricSnapshot::Histogram {
name,
boundaries,
data_points,
..
} => {
assert_eq!(name, "latency");
assert_eq!(boundaries, &[10.0, 50.0, 100.0]);
assert_eq!(data_points.len(), 1);
let dp = &data_points[0];
assert_eq!(dp.bucket_counts, vec![1, 1, 1, 1]);
assert_eq!(dp.count, 4);
assert!((dp.sum - 305.0).abs() < f64::EPSILON);
assert!((dp.min - 5.0).abs() < f64::EPSILON);
assert!((dp.max - 200.0).abs() < f64::EPSILON);
}
_ => panic!("expected Histogram snapshot"),
}
}
#[test]
fn histogram_boundary_placement() {
let registry = MetricsRegistry::new();
let h = registry.histogram("bp", "test", &[10.0, 20.0]);
h.observe(10.0, &[]); h.observe(20.0, &[]); h.observe(0.0, &[]);
let snapshots = registry.collect();
match &snapshots[0] {
MetricSnapshot::Histogram { data_points, .. } => {
let dp = &data_points[0];
assert_eq!(dp.bucket_counts, vec![2, 1, 0]);
}
_ => panic!("expected Histogram"),
}
}
#[test]
fn histogram_multiple_attr_sets() {
let registry = MetricsRegistry::new();
let h = registry.histogram("multi", "test", &[50.0]);
h.observe(10.0, &[("method", "GET")]);
h.observe(60.0, &[("method", "POST")]);
let snapshots = registry.collect();
match &snapshots[0] {
MetricSnapshot::Histogram { data_points, .. } => {
assert_eq!(data_points.len(), 2);
}
_ => panic!("expected Histogram"),
}
}
#[test]
fn histogram_clone_shares_state() {
let registry = MetricsRegistry::new();
let h1 = registry.histogram("shared_h", "test", &[10.0]);
let h2 = h1.clone();
h1.observe(5.0, &[]);
h2.observe(15.0, &[]);
let snapshots = registry.collect();
match &snapshots[0] {
MetricSnapshot::Histogram { data_points, .. } => {
let dp = &data_points[0];
assert_eq!(dp.count, 2);
}
_ => panic!("expected Histogram"),
}
}
#[test]
fn histogram_empty_not_collected() {
let registry = MetricsRegistry::new();
let _ = registry.histogram("unused_h", "test", &[10.0]);
assert!(registry.collect().is_empty());
}
#[test]
fn histogram_no_attrs() {
let registry = MetricsRegistry::new();
let h = registry.histogram("no_attrs_h", "test", &[5.0]);
h.observe(1.0, &[]);
let snapshots = registry.collect();
match &snapshots[0] {
MetricSnapshot::Histogram { data_points, .. } => {
assert_eq!(data_points.len(), 1);
assert!(data_points[0].attrs.is_empty());
}
_ => panic!("expected Histogram"),
}
}
#[test]
fn counter_no_exemplar_without_span() {
let registry = MetricsRegistry::new();
let c = registry.counter("no_span", "test");
c.add(1, &[]);
let snapshots = registry.collect();
match &snapshots[0] {
MetricSnapshot::Counter { data_points, .. } => {
assert!(data_points[0].2.is_none());
}
_ => panic!("expected Counter"),
}
}
#[test]
fn exemplar_resets_on_collect() {
let registry = MetricsRegistry::new();
let c = registry.counter("reset_test", "test");
c.add(1, &[]);
{
let counters = read_lock(®istry.counters);
let counter = counters.get("reset_test").unwrap();
let mut data = counter.inner.data.lock().unwrap_or_else(|p| p.into_inner());
for entry in data.values_mut() {
entry.2 = Some(Exemplar {
trace_id: [0xAA; 16],
span_id: [0xBB; 8],
time_unix_nano: 123_456,
value: ExemplarValue::Int(1),
});
}
}
let snap1 = registry.collect();
match &snap1[0] {
MetricSnapshot::Counter { data_points, .. } => {
assert!(data_points[0].2.is_some());
}
_ => panic!("expected Counter"),
}
let snap2 = registry.collect();
match &snap2[0] {
MetricSnapshot::Counter { data_points, .. } => {
assert!(data_points[0].2.is_none());
}
_ => panic!("expected Counter"),
}
}
#[test]
fn cardinality_limit_drops_excess() {
let registry = MetricsRegistry::new();
let c = registry.counter_with_max_cardinality("limited", "test", 3);
c.add(1, &[("k", "a")]);
c.add(1, &[("k", "b")]);
c.add(1, &[("k", "c")]);
c.add(1, &[("k", "d")]);
let snapshots = registry.collect();
match &snapshots[0] {
MetricSnapshot::Counter { data_points, .. } => {
assert_eq!(data_points.len(), 3);
}
_ => panic!("expected Counter"),
}
}
#[test]
fn cardinality_limit_allows_existing_keys() {
let registry = MetricsRegistry::new();
let c = registry.counter_with_max_cardinality("limited2", "test", 2);
c.add(1, &[("k", "a")]);
c.add(1, &[("k", "b")]);
c.add(1, &[("k", "c")]); c.add(5, &[("k", "a")]); c.add(3, &[("k", "b")]);
let snapshots = registry.collect();
match &snapshots[0] {
MetricSnapshot::Counter { data_points, .. } => {
assert_eq!(data_points.len(), 2);
let total: i64 = data_points.iter().map(|(_, v, _)| v).sum();
assert_eq!(total, 10); }
_ => panic!("expected Counter"),
}
}
#[test]
fn per_metric_overrides_global() {
let registry = MetricsRegistry::with_max_cardinality(100);
let c = registry.counter_with_max_cardinality("override", "test", 2);
c.add(1, &[("k", "a")]);
c.add(1, &[("k", "b")]);
c.add(1, &[("k", "c")]);
let snapshots = registry.collect();
match &snapshots[0] {
MetricSnapshot::Counter { data_points, .. } => {
assert_eq!(data_points.len(), 2);
}
_ => panic!("expected Counter"),
}
}
#[test]
fn default_cardinality_is_2000() {
let registry = MetricsRegistry::new();
let c = registry.counter("big", "test");
for i in 0..2000 {
c.add(1, &[("k", &i.to_string())]);
}
let snapshots = registry.collect();
match &snapshots[0] {
MetricSnapshot::Counter { data_points, .. } => {
assert_eq!(data_points.len(), 2000);
}
_ => panic!("expected Counter"),
}
}
#[test]
fn gauge_cardinality_limit() {
let registry = MetricsRegistry::new();
let g = registry.gauge_with_max_cardinality("g_limited", "test", 2);
g.set(1.0, &[("k", "a")]);
g.set(2.0, &[("k", "b")]);
g.set(3.0, &[("k", "c")]);
let snapshots = registry.collect();
match &snapshots[0] {
MetricSnapshot::Gauge { data_points, .. } => {
assert_eq!(data_points.len(), 2);
}
_ => panic!("expected Gauge"),
}
}
#[test]
fn histogram_cardinality_limit() {
let registry = MetricsRegistry::new();
let h = registry.histogram_with_max_cardinality("h_limited", "test", &[10.0], 2);
h.observe(1.0, &[("k", "a")]);
h.observe(2.0, &[("k", "b")]);
h.observe(3.0, &[("k", "c")]);
let snapshots = registry.collect();
match &snapshots[0] {
MetricSnapshot::Histogram { data_points, .. } => {
assert_eq!(data_points.len(), 2);
}
_ => panic!("expected Histogram"),
}
}
}
#[cfg(kani)]
mod kani_proofs {
use super::*;
#[kani::proof]
#[kani::unwind(5)]
fn attrs_hash_no_panic() {
let count: usize = kani::any();
kani::assume(count <= 3);
let k0: [u8; 3] = kani::any();
let v0: [u8; 3] = kani::any();
let k1: [u8; 3] = kani::any();
let v1: [u8; 3] = kani::any();
let k2: [u8; 3] = kani::any();
let v2: [u8; 3] = kani::any();
let kl0: usize = kani::any();
kani::assume(kl0 <= 3);
let vl0: usize = kani::any();
kani::assume(vl0 <= 3);
let kl1: usize = kani::any();
kani::assume(kl1 <= 3);
let vl1: usize = kani::any();
kani::assume(vl1 <= 3);
let kl2: usize = kani::any();
kani::assume(kl2 <= 3);
let vl2: usize = kani::any();
kani::assume(vl2 <= 3);
for b in k0
.iter()
.chain(v0.iter())
.chain(k1.iter())
.chain(v1.iter())
.chain(k2.iter())
.chain(v2.iter())
{
kani::assume(*b < 128);
}
let s_k0 = core::str::from_utf8(&k0[..kl0]).unwrap();
let s_v0 = core::str::from_utf8(&v0[..vl0]).unwrap();
let s_k1 = core::str::from_utf8(&k1[..kl1]).unwrap();
let s_v1 = core::str::from_utf8(&v1[..vl1]).unwrap();
let s_k2 = core::str::from_utf8(&k2[..kl2]).unwrap();
let s_v2 = core::str::from_utf8(&v2[..vl2]).unwrap();
let all = [(s_k0, s_v0), (s_k1, s_v1), (s_k2, s_v2)];
let _ = attrs_hash_unordered(&all[..count]);
}
#[kani::proof]
#[kani::unwind(5)]
fn attrs_hash_order_independent() {
let k0: [u8; 2] = kani::any();
let v0: [u8; 2] = kani::any();
let k1: [u8; 2] = kani::any();
let v1: [u8; 2] = kani::any();
for b in k0.iter().chain(v0.iter()).chain(k1.iter()).chain(v1.iter()) {
kani::assume(*b < 128);
}
let sk0 = core::str::from_utf8(&k0).unwrap();
let sv0 = core::str::from_utf8(&v0).unwrap();
let sk1 = core::str::from_utf8(&k1).unwrap();
let sv1 = core::str::from_utf8(&v1).unwrap();
let ab = [(sk0, sv0), (sk1, sv1)];
let ba = [(sk1, sv1), (sk0, sv0)];
assert!(attrs_hash_unordered(&ab) == attrs_hash_unordered(&ba));
}
#[kani::proof]
fn attrs_hash_empty_is_zero() {
let empty: &[(&str, &str)] = &[];
assert!(attrs_hash_unordered(empty) == 0);
}
#[kani::proof]
#[kani::unwind(5)]
fn partition_point_valid_bucket() {
let count: usize = kani::any();
kani::assume(count <= 3);
let b0: f64 = kani::any();
let b1: f64 = kani::any();
let b2: f64 = kani::any();
kani::assume(b0.is_finite() && b1.is_finite() && b2.is_finite());
kani::assume(b0 <= b1 && b1 <= b2);
let boundaries = [b0, b1, b2];
let bounds = &boundaries[..count];
let value: f64 = kani::any();
kani::assume(value.is_finite());
let idx = bounds.partition_point(|&b| b <= value);
assert!(idx <= count);
}
}