use std::fmt;
use std::sync::{Arc, Mutex};
#[derive(Debug, Clone)]
pub struct MetricEvent {
pub name: &'static str,
pub value: MetricValue,
pub labels: Vec<(&'static str, String)>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum MetricValue {
Counter(u64),
Gauge(f64),
Histogram(f64),
}
impl fmt::Display for MetricValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MetricValue::Counter(v) => write!(f, "counter({v})"),
MetricValue::Gauge(v) => write!(f, "gauge({v})"),
MetricValue::Histogram(v) => write!(f, "histogram({v})"),
}
}
}
pub trait MetricsSink: Send + Sync + fmt::Debug {
fn record(&self, event: MetricEvent);
}
#[derive(Debug, Clone, Copy, Default)]
pub struct NoopSink;
impl MetricsSink for NoopSink {
#[inline]
fn record(&self, _event: MetricEvent) {
}
}
#[derive(Debug, Default)]
pub struct RecordingSink {
events: Mutex<Vec<MetricEvent>>,
}
impl RecordingSink {
pub fn new() -> Self {
Self {
events: Mutex::new(Vec::new()),
}
}
pub fn events(&self) -> Vec<MetricEvent> {
self.events
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.clone()
}
pub fn clear(&self) {
if let Ok(mut guard) = self.events.lock() {
guard.clear();
}
}
pub fn len(&self) -> usize {
self.events
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
impl MetricsSink for RecordingSink {
fn record(&self, event: MetricEvent) {
if let Ok(mut guard) = self.events.lock() {
guard.push(event);
}
}
}
pub mod names {
pub const HNSW_SEARCH_DURATION_MS: &str = "hnsw.search.duration_ms";
pub const HNSW_SEARCH_COUNT: &str = "hnsw.search.count";
pub const HNSW_SEARCH_RESULTS: &str = "hnsw.search.results";
pub const HNSW_INSERT_DURATION_MS: &str = "hnsw.insert.duration_ms";
pub const HNSW_INSERT_COUNT: &str = "hnsw.insert.count";
pub const HNSW_REBUILD_DURATION_MS: &str = "hnsw.rebuild.duration_ms";
pub const HNSW_REBUILD_COUNT: &str = "hnsw.rebuild.count";
pub const HNSW_REBUILD_NODES_REMOVED: &str = "hnsw.rebuild.nodes_removed";
pub const HNSW_INDEX_SIZE: &str = "hnsw.index.size";
pub const BM25_SEARCH_DURATION_MS: &str = "bm25.search.duration_ms";
pub const BM25_SEARCH_COUNT: &str = "bm25.search.count";
pub const BM25_SEARCH_RESULTS: &str = "bm25.search.results";
pub const BM25_INDEX_DURATION_MS: &str = "bm25.index_document.duration_ms";
pub const BM25_INDEX_COUNT: &str = "bm25.index_document.count";
pub const BM25_INDEX_SIZE: &str = "bm25.index.size";
}
#[inline]
pub fn emit(sink: &Option<Arc<dyn MetricsSink>>, event: MetricEvent) {
if let Some(s) = sink {
s.record(event);
}
}
#[cfg(test)]
#[allow(clippy::approx_constant)]
mod tests {
use super::*;
#[test]
fn noop_sink_does_not_panic() {
let sink = NoopSink;
sink.record(MetricEvent {
name: names::HNSW_SEARCH_COUNT,
value: MetricValue::Counter(1),
labels: vec![],
});
}
#[test]
fn recording_sink_captures_events() {
let sink = RecordingSink::new();
assert!(sink.is_empty());
sink.record(MetricEvent {
name: names::HNSW_SEARCH_DURATION_MS,
value: MetricValue::Histogram(1.5),
labels: vec![("k", "10".to_string())],
});
sink.record(MetricEvent {
name: names::HNSW_SEARCH_COUNT,
value: MetricValue::Counter(1),
labels: vec![],
});
assert_eq!(sink.len(), 2);
assert!(!sink.is_empty());
let events = sink.events();
assert_eq!(events.len(), 2);
assert_eq!(events[0].name, names::HNSW_SEARCH_DURATION_MS);
assert_eq!(events[1].name, names::HNSW_SEARCH_COUNT);
}
#[test]
fn recording_sink_clear() {
let sink = RecordingSink::new();
sink.record(MetricEvent {
name: names::HNSW_INSERT_COUNT,
value: MetricValue::Counter(1),
labels: vec![],
});
assert_eq!(sink.len(), 1);
sink.clear();
assert!(sink.is_empty());
}
#[test]
fn metric_value_display() {
assert_eq!(MetricValue::Counter(42).to_string(), "counter(42)");
assert_eq!(MetricValue::Gauge(3.14).to_string(), "gauge(3.14)");
assert_eq!(MetricValue::Histogram(1.5).to_string(), "histogram(1.5)");
}
#[test]
fn emit_helper_with_none() {
emit(
&None,
MetricEvent {
name: names::HNSW_SEARCH_COUNT,
value: MetricValue::Counter(1),
labels: vec![],
},
);
}
#[test]
fn emit_helper_with_some() {
let sink = Arc::new(RecordingSink::new());
let opt: Option<Arc<dyn MetricsSink>> = Some(sink.clone());
emit(
&opt,
MetricEvent {
name: names::BM25_SEARCH_COUNT,
value: MetricValue::Counter(1),
labels: vec![],
},
);
assert_eq!(sink.len(), 1);
}
#[test]
fn recording_sink_is_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<RecordingSink>();
}
#[test]
fn metrics_sink_is_object_safe() {
let _: Arc<dyn MetricsSink> = Arc::new(NoopSink);
let _: Arc<dyn MetricsSink> = Arc::new(RecordingSink::new());
}
}