effect-rs 0.1.0

A high-performance, strictly-typed, functional effect system for Rust.
Documentation
use std::collections::HashMap;
use std::sync::{Arc, Mutex};

#[derive(Debug, Clone, Hash, Eq, PartialEq)]
pub struct MetricLabel {
    pub key: String,
    pub value: String,
}

impl MetricLabel {
    pub fn new(key: &str, value: &str) -> Self {
        Self {
            key: key.to_string(),
            value: value.to_string(),
        }
    }
}

#[derive(Debug, Clone, Hash, Eq, PartialEq)]
pub struct MetricKey {
    pub name: String,
    pub labels: Vec<MetricLabel>,
}

impl MetricKey {
    pub fn new(name: &str, labels: Vec<MetricLabel>) -> Self {
        Self {
            name: name.to_string(),
            labels,
        }
    }
}

// Instruments

pub trait Metric: Send + Sync {
    fn as_any(&self) -> &dyn std::any::Any;
}

pub struct Counter {
    value: std::sync::atomic::AtomicU64,
}

impl Default for Counter {
    fn default() -> Self {
        Self::new()
    }
}

impl Counter {
    pub fn new() -> Self {
        Self {
            value: std::sync::atomic::AtomicU64::new(0),
        }
    }

    pub fn increment(&self, amount: u64) {
        self.value
            .fetch_add(amount, std::sync::atomic::Ordering::Relaxed);
    }

    pub fn get(&self) -> u64 {
        self.value.load(std::sync::atomic::Ordering::Relaxed)
    }
}

impl Metric for Counter {
    fn as_any(&self) -> &dyn std::any::Any {
        self
    }
}

pub struct Gauge {
    value: std::sync::atomic::AtomicI64,
}

impl Default for Gauge {
    fn default() -> Self {
        Self::new()
    }
}

impl Gauge {
    pub fn new() -> Self {
        Self {
            value: std::sync::atomic::AtomicI64::new(0),
        }
    }

    pub fn set(&self, val: i64) {
        self.value.store(val, std::sync::atomic::Ordering::Relaxed);
    }

    pub fn get(&self) -> i64 {
        self.value.load(std::sync::atomic::Ordering::Relaxed)
    }
}

impl Metric for Gauge {
    fn as_any(&self) -> &dyn std::any::Any {
        self
    }
}

// Simple Histogram with fixed buckets for now
use std::sync::atomic::{AtomicU64, Ordering};

pub struct Histogram {
    buckets: Vec<f64>,
    counts: Vec<AtomicU64>,
    sum: Mutex<f64>,
    count: AtomicU64,
}

impl Histogram {
    pub fn new(buckets: Vec<f64>) -> Self {
        let len = buckets.len() + 1;
        let mut counts = Vec::with_capacity(len);
        for _ in 0..len {
            counts.push(AtomicU64::new(0));
        }

        Self {
            buckets,
            counts,
            sum: Mutex::new(0.0),
            count: AtomicU64::new(0),
        }
    }

    pub fn record(&self, value: f64) {
        self.count.fetch_add(1, Ordering::Relaxed);

        // Sum still needs lock for float
        if let Ok(mut g) = self.sum.lock() {
            *g += value;
        }

        // Find bucket
        let mut idx = self.buckets.len();
        for (i, boundary) in self.buckets.iter().enumerate() {
            if value <= *boundary {
                idx = i;
                break;
            }
        }

        // Lock-free increment
        self.counts[idx].fetch_add(1, Ordering::Relaxed);
    }
}

impl Metric for Histogram {
    fn as_any(&self) -> &dyn std::any::Any {
        self
    }
}

// Registry
pub struct MetricRegistry {
    metrics: Mutex<HashMap<MetricKey, Arc<dyn Metric>>>,
}

impl Default for MetricRegistry {
    fn default() -> Self {
        Self::new()
    }
}

impl MetricRegistry {
    pub fn new() -> Self {
        Self {
            metrics: Mutex::new(HashMap::new()),
        }
    }

    pub fn counter(&self, name: &str, labels: Vec<MetricLabel>) -> Arc<Counter> {
        let _key = MetricKey::new(name, labels);
        let _map = self.metrics.lock().unwrap();

        // Remove the generic registry for now to avoid complexity.
        todo!()
    }

    // Better inner impl: use entry API fully
}

// Simplified Global Registry
// To avoid strict typing issues in the mock, usage:
// `get_counter` returns new or existing.
// We need to cast `Arc<dyn Metric>` to `Arc<Counter>`.
// Rust `downcast-rs` crate helps, or `std::any::Any` on Arc.
// `Arc<dyn Any + Send + Sync>` works.

pub struct SimpleRegistry {
    counters: Mutex<HashMap<MetricKey, Arc<Counter>>>,
    gauges: Mutex<HashMap<MetricKey, Arc<Gauge>>>,
    histograms: Mutex<HashMap<MetricKey, Arc<Histogram>>>,
}

impl Default for SimpleRegistry {
    fn default() -> Self {
        Self::new()
    }
}

impl SimpleRegistry {
    pub fn new() -> Self {
        Self {
            counters: Mutex::new(HashMap::new()),
            gauges: Mutex::new(HashMap::new()),
            histograms: Mutex::new(HashMap::new()),
        }
    }

    pub fn get_counter(&self, name: &str, labels: Vec<MetricLabel>) -> Arc<Counter> {
        let key = MetricKey::new(name, labels);
        let mut map = self.counters.lock().unwrap();
        map.entry(key)
            .or_insert_with(|| Arc::new(Counter::new()))
            .clone()
    }

    pub fn get_gauge(&self, name: &str, labels: Vec<MetricLabel>) -> Arc<Gauge> {
        let key = MetricKey::new(name, labels);
        let mut map = self.gauges.lock().unwrap();
        map.entry(key)
            .or_insert_with(|| Arc::new(Gauge::new()))
            .clone()
    }

    pub fn get_histogram(
        &self,
        name: &str,
        labels: Vec<MetricLabel>,
        buckets: Vec<f64>,
    ) -> Arc<Histogram> {
        let key = MetricKey::new(name, labels);
        let mut map = self.histograms.lock().unwrap();
        // If exists, ignore buckets arg? Yes.
        map.entry(key)
            .or_insert_with(|| Arc::new(Histogram::new(buckets)))
            .clone()
    }
}

lazy_static::lazy_static! {
    pub static ref REGISTRY: SimpleRegistry = SimpleRegistry::new();
}