use super::error::Result;
use std::collections::HashMap;
use std::time::{Duration, Instant};
#[derive(Debug, Clone, PartialEq)]
pub enum MetricValue {
Gauge(f64),
Counter(u64),
Histogram(Vec<f64>),
Text(String),
}
impl MetricValue {
#[must_use]
pub fn as_gauge(&self) -> Option<f64> {
match self {
Self::Gauge(v) => Some(*v),
_ => None,
}
}
#[must_use]
pub fn as_counter(&self) -> Option<u64> {
match self {
Self::Counter(v) => Some(*v),
_ => None,
}
}
#[must_use]
pub fn as_histogram(&self) -> Option<&[f64]> {
match self {
Self::Histogram(v) => Some(v),
_ => None,
}
}
#[must_use]
pub fn as_text(&self) -> Option<&str> {
match self {
Self::Text(v) => Some(v),
_ => None,
}
}
#[must_use]
pub fn is_gauge(&self) -> bool {
matches!(self, Self::Gauge(_))
}
#[must_use]
pub fn is_counter(&self) -> bool {
matches!(self, Self::Counter(_))
}
#[must_use]
pub fn is_histogram(&self) -> bool {
matches!(self, Self::Histogram(_))
}
#[must_use]
pub fn is_text(&self) -> bool {
matches!(self, Self::Text(_))
}
}
impl From<f64> for MetricValue {
fn from(value: f64) -> Self {
Self::Gauge(value)
}
}
impl From<u64> for MetricValue {
fn from(value: u64) -> Self {
Self::Counter(value)
}
}
impl From<String> for MetricValue {
fn from(value: String) -> Self {
Self::Text(value)
}
}
impl From<&str> for MetricValue {
fn from(value: &str) -> Self {
Self::Text(value.to_string())
}
}
impl From<Vec<f64>> for MetricValue {
fn from(value: Vec<f64>) -> Self {
Self::Histogram(value)
}
}
#[derive(Debug, Clone)]
pub struct Metrics {
pub timestamp: Instant,
pub values: HashMap<String, MetricValue>,
}
impl Metrics {
#[must_use]
pub fn new() -> Self {
Self { timestamp: Instant::now(), values: HashMap::new() }
}
#[must_use]
pub fn with_timestamp(timestamp: Instant) -> Self {
Self { timestamp, values: HashMap::new() }
}
pub fn insert(&mut self, key: impl Into<String>, value: impl Into<MetricValue>) {
self.values.insert(key.into(), value.into());
}
#[must_use]
pub fn get(&self, key: &str) -> Option<&MetricValue> {
self.values.get(key)
}
#[must_use]
pub fn get_gauge(&self, key: &str) -> Option<f64> {
self.values.get(key).and_then(MetricValue::as_gauge)
}
#[must_use]
pub fn get_counter(&self, key: &str) -> Option<u64> {
self.values.get(key).and_then(MetricValue::as_counter)
}
#[must_use]
pub fn len(&self) -> usize {
self.values.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.values.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = (&String, &MetricValue)> {
self.values.iter()
}
}
impl Default for Metrics {
fn default() -> Self {
Self::new()
}
}
pub trait Collector: Send + Sync {
fn id(&self) -> &'static str;
fn collect(&mut self) -> Result<Metrics>;
fn is_available(&self) -> bool;
fn interval_hint(&self) -> Duration {
Duration::from_millis(1000)
}
fn display_name(&self) -> &'static str {
self.id()
}
}
pub type BoxedCollector = Box<dyn Collector>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_metric_value_gauge() {
let v = MetricValue::Gauge(42.5);
assert!(v.is_gauge());
assert!(!v.is_counter());
assert_eq!(v.as_gauge(), Some(42.5));
assert_eq!(v.as_counter(), None);
}
#[test]
fn test_metric_value_counter() {
let v = MetricValue::Counter(1000);
assert!(v.is_counter());
assert!(!v.is_gauge());
assert_eq!(v.as_counter(), Some(1000));
assert_eq!(v.as_gauge(), None);
}
#[test]
fn test_metric_value_histogram() {
let v = MetricValue::Histogram(vec![1.0, 2.0, 3.0]);
assert!(v.is_histogram());
assert_eq!(v.as_histogram(), Some(&[1.0, 2.0, 3.0][..]));
}
#[test]
fn test_metric_value_text() {
let v = MetricValue::Text("hello".to_string());
assert!(v.is_text());
assert_eq!(v.as_text(), Some("hello"));
}
#[test]
fn test_metric_value_from_f64() {
let v: MetricValue = 42.0.into();
assert_eq!(v, MetricValue::Gauge(42.0));
}
#[test]
fn test_metric_value_from_u64() {
let v: MetricValue = 100u64.into();
assert_eq!(v, MetricValue::Counter(100));
}
#[test]
fn test_metric_value_from_string() {
let v: MetricValue = "test".into();
assert_eq!(v, MetricValue::Text("test".to_string()));
}
#[test]
fn test_metric_value_from_vec() {
let v: MetricValue = vec![1.0, 2.0].into();
assert_eq!(v, MetricValue::Histogram(vec![1.0, 2.0]));
}
#[test]
fn test_metric_value_clone() {
let v = MetricValue::Gauge(42.0);
let cloned = v.clone();
assert_eq!(v, cloned);
}
#[test]
fn test_metrics_new() {
let m = Metrics::new();
assert!(m.is_empty());
assert_eq!(m.len(), 0);
}
#[test]
fn test_metrics_insert_and_get() {
let mut m = Metrics::new();
m.insert("cpu", 75.5);
m.insert("memory", 1024u64);
assert_eq!(m.len(), 2);
assert_eq!(m.get_gauge("cpu"), Some(75.5));
assert_eq!(m.get_counter("memory"), Some(1024));
assert_eq!(m.get("nonexistent"), None);
}
#[test]
fn test_metrics_iter() {
let mut m = Metrics::new();
m.insert("a", 1.0);
m.insert("b", 2.0);
let keys: Vec<_> = m.iter().map(|(k, _)| k.clone()).collect();
assert_eq!(keys.len(), 2);
assert!(keys.contains(&"a".to_string()));
assert!(keys.contains(&"b".to_string()));
}
#[test]
fn test_metrics_default() {
let m = Metrics::default();
assert!(m.is_empty());
}
struct TestCollector {
available: bool,
value: f64,
}
impl Collector for TestCollector {
fn id(&self) -> &'static str {
"test"
}
fn collect(&mut self) -> Result<Metrics> {
let mut m = Metrics::new();
m.insert("value", self.value);
Ok(m)
}
fn is_available(&self) -> bool {
self.available
}
fn interval_hint(&self) -> Duration {
Duration::from_millis(500)
}
fn display_name(&self) -> &'static str {
"Test Collector"
}
}
#[test]
fn test_collector_trait() {
let mut collector = TestCollector { available: true, value: 42.0 };
assert_eq!(collector.id(), "test");
assert!(collector.is_available());
assert_eq!(collector.interval_hint(), Duration::from_millis(500));
assert_eq!(collector.display_name(), "Test Collector");
let metrics = collector.collect().expect("collect should succeed");
assert_eq!(metrics.get_gauge("value"), Some(42.0));
}
#[test]
fn test_collector_unavailable() {
let collector = TestCollector { available: false, value: 0.0 };
assert!(!collector.is_available());
}
#[test]
fn test_boxed_collector() {
let collector: BoxedCollector = Box::new(TestCollector { available: true, value: 100.0 });
assert_eq!(collector.id(), "test");
assert!(collector.is_available());
}
#[test]
fn test_collector_is_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<TestCollector>();
}
#[test]
fn test_metric_value_partial_eq() {
let v1 = MetricValue::Gauge(1.0);
let v2 = MetricValue::Gauge(1.0);
let v3 = MetricValue::Gauge(2.0);
assert_eq!(v1, v2);
assert_ne!(v1, v3);
}
#[test]
fn test_metrics_with_timestamp() {
let ts = Instant::now();
let m = Metrics::with_timestamp(ts);
assert_eq!(m.timestamp, ts);
}
#[test]
fn test_metric_value_histogram_empty() {
let v = MetricValue::Histogram(vec![]);
assert!(v.is_histogram());
assert_eq!(v.as_histogram(), Some(&[][..]));
}
}