use dashmap::DashMap;
use std::sync::Arc;
use thiserror::Error;
use super::counter::Counter;
use super::gauge::Gauge;
use super::histogram::{Histogram, DEFAULT_BUCKETS};
use super::labels::{LabelValidator, Labels};
use super::MetricType;
#[derive(Debug, Error)]
pub enum RegistryError {
#[error("metric already registered: {name}")]
AlreadyRegistered { name: String },
#[error("metric type mismatch: {name} is {existing}, not {requested}")]
TypeMismatch { name: String, existing: MetricType, requested: MetricType },
#[error("label validation failed: {0}")]
LabelError(#[from] super::labels::LabelError),
#[error("invalid metric name: {0} (must match [a-zA-Z_:][a-zA-Z0-9_:]*)")]
InvalidName(String),
#[error("cardinality limit exceeded: {count} series (max {max})")]
CardinalityLimit { count: usize, max: usize },
}
#[derive(Clone, Debug)]
pub enum RegisteredMetric {
Counter(Arc<Counter>),
Gauge(Arc<Gauge>),
Histogram(Arc<Histogram>),
}
impl RegisteredMetric {
pub fn metric_type(&self) -> MetricType {
match self {
RegisteredMetric::Counter(_) => MetricType::Counter,
RegisteredMetric::Gauge(_) => MetricType::Gauge,
RegisteredMetric::Histogram(_) => MetricType::Histogram,
}
}
pub fn name(&self) -> &str {
match self {
RegisteredMetric::Counter(c) => c.name(),
RegisteredMetric::Gauge(g) => g.name(),
RegisteredMetric::Histogram(h) => h.name(),
}
}
}
#[derive(Debug)]
pub struct Registry {
metrics: DashMap<String, RegisteredMetric>,
validator: LabelValidator,
max_series: usize,
}
impl Default for Registry {
fn default() -> Self {
Self::new()
}
}
impl Registry {
pub fn new() -> Self {
Self { metrics: DashMap::new(), validator: LabelValidator::new(), max_series: 100_000 }
}
pub fn with_validator(validator: LabelValidator) -> Self {
Self { metrics: DashMap::new(), validator, max_series: 100_000 }
}
pub fn max_series(mut self, max: usize) -> Self {
self.max_series = max;
self
}
pub fn counter(
&self,
name: &str,
labels: &[(&str, &str)],
) -> Result<Arc<Counter>, RegistryError> {
self.validate_name(name)?;
let labels_map = self.labels_from_pairs(labels)?;
let key = metric_key(name, &labels_map);
if let Some(entry) = self.metrics.get(&key) {
match entry.value() {
RegisteredMetric::Counter(c) => return Ok(c.clone()),
other => {
return Err(RegistryError::TypeMismatch {
name: name.to_string(),
existing: other.metric_type(),
requested: MetricType::Counter,
})
}
}
}
self.check_cardinality()?;
let counter = Counter::new_arc(name, labels_map);
self.metrics.insert(key, RegisteredMetric::Counter(counter.clone()));
Ok(counter)
}
pub fn gauge(&self, name: &str, labels: &[(&str, &str)]) -> Result<Arc<Gauge>, RegistryError> {
self.validate_name(name)?;
let labels_map = self.labels_from_pairs(labels)?;
let key = metric_key(name, &labels_map);
if let Some(entry) = self.metrics.get(&key) {
match entry.value() {
RegisteredMetric::Gauge(g) => return Ok(g.clone()),
other => {
return Err(RegistryError::TypeMismatch {
name: name.to_string(),
existing: other.metric_type(),
requested: MetricType::Gauge,
})
}
}
}
self.check_cardinality()?;
let gauge = Gauge::new_arc(name, labels_map);
self.metrics.insert(key, RegisteredMetric::Gauge(gauge.clone()));
Ok(gauge)
}
pub fn histogram(
&self,
name: &str,
labels: &[(&str, &str)],
) -> Result<Arc<Histogram>, RegistryError> {
self.histogram_with_buckets(name, labels, DEFAULT_BUCKETS)
}
pub fn histogram_with_buckets(
&self,
name: &str,
labels: &[(&str, &str)],
buckets: &[f64],
) -> Result<Arc<Histogram>, RegistryError> {
self.validate_name(name)?;
let labels_map = self.labels_from_pairs(labels)?;
let key = metric_key(name, &labels_map);
if let Some(entry) = self.metrics.get(&key) {
match entry.value() {
RegisteredMetric::Histogram(h) => return Ok(h.clone()),
other => {
return Err(RegistryError::TypeMismatch {
name: name.to_string(),
existing: other.metric_type(),
requested: MetricType::Histogram,
})
}
}
}
self.check_cardinality()?;
let histogram = Histogram::new_arc(name, labels_map, buckets);
self.metrics.insert(key, RegisteredMetric::Histogram(histogram.clone()));
Ok(histogram)
}
pub fn len(&self) -> usize {
self.metrics.len()
}
pub fn is_empty(&self) -> bool {
self.metrics.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = RegisteredMetric> + '_ {
self.metrics.iter().map(|entry| entry.value().clone())
}
pub fn counters(&self) -> Vec<Arc<Counter>> {
self.metrics
.iter()
.filter_map(|entry| match entry.value() {
RegisteredMetric::Counter(c) => Some(c.clone()),
_ => None,
})
.collect()
}
pub fn gauges(&self) -> Vec<Arc<Gauge>> {
self.metrics
.iter()
.filter_map(|entry| match entry.value() {
RegisteredMetric::Gauge(g) => Some(g.clone()),
_ => None,
})
.collect()
}
pub fn histograms(&self) -> Vec<Arc<Histogram>> {
self.metrics
.iter()
.filter_map(|entry| match entry.value() {
RegisteredMetric::Histogram(h) => Some(h.clone()),
_ => None,
})
.collect()
}
pub fn unregister(&self, name: &str, labels: &[(&str, &str)]) -> bool {
if let Ok(labels_map) = self.labels_from_pairs(labels) {
let key = metric_key(name, &labels_map);
self.metrics.remove(&key).is_some()
} else {
false
}
}
pub fn clear(&self) {
self.metrics.clear();
}
fn validate_name(&self, name: &str) -> Result<(), RegistryError> {
if name.is_empty() {
return Err(RegistryError::InvalidName(name.to_string()));
}
let mut chars = name.chars();
match chars.next() {
Some(c) if c.is_ascii_alphabetic() || c == '_' || c == ':' => {}
_ => return Err(RegistryError::InvalidName(name.to_string())),
}
for c in chars {
if !c.is_ascii_alphanumeric() && c != '_' && c != ':' {
return Err(RegistryError::InvalidName(name.to_string()));
}
}
Ok(())
}
fn labels_from_pairs(&self, pairs: &[(&str, &str)]) -> Result<Labels, RegistryError> {
let labels: Labels =
pairs.iter().map(|(k, v)| ((*k).to_string(), (*v).to_string())).collect();
self.validator.validate(&labels)?;
Ok(labels)
}
fn check_cardinality(&self) -> Result<(), RegistryError> {
if self.metrics.len() >= self.max_series {
return Err(RegistryError::CardinalityLimit {
count: self.metrics.len(),
max: self.max_series,
});
}
Ok(())
}
}
fn metric_key(name: &str, labels: &Labels) -> String {
let mut key = name.to_string();
let mut sorted_labels: Vec<_> = labels.iter().collect();
sorted_labels.sort_by_key(|(k, _)| *k);
for (k, v) in sorted_labels {
key.push_str(&format!("|{}={}", k, v));
}
key
}
#[cfg(test)]
mod tests {
use super::*;
fn default_registry() -> &'static Registry {
use std::sync::OnceLock;
static REGISTRY: OnceLock<Registry> = OnceLock::new();
REGISTRY.get_or_init(Registry::new)
}
#[test]
fn test_registry_counter() {
let reg = Registry::new();
let c1 = reg.counter("requests_total", &[("method", "GET")]).unwrap();
c1.inc();
let c2 = reg.counter("requests_total", &[("method", "GET")]).unwrap();
assert_eq!(c2.get(), 1);
let c3 = reg.counter("requests_total", &[("method", "POST")]).unwrap();
c3.inc();
assert_eq!(c3.get(), 1); }
#[test]
fn test_registry_gauge() {
let reg = Registry::new();
let g = reg.gauge("temperature", &[("location", "kitchen")]).unwrap();
g.set(22);
let g2 = reg.gauge("temperature", &[("location", "kitchen")]).unwrap();
assert_eq!(g2.get(), 22);
}
#[test]
fn test_registry_histogram() {
let reg = Registry::new();
let h = reg.histogram("latency", &[]).unwrap();
h.observe(0.1);
let h2 = reg.histogram("latency", &[]).unwrap();
assert_eq!(h2.get_count(), 1);
}
#[test]
fn test_registry_type_mismatch() {
let reg = Registry::new();
reg.counter("metric", &[]).unwrap();
let result = reg.gauge("metric", &[]);
assert!(matches!(result, Err(RegistryError::TypeMismatch { .. })));
}
#[test]
fn test_registry_invalid_name() {
let reg = Registry::new();
let result = reg.counter("123invalid", &[]);
assert!(matches!(result, Err(RegistryError::InvalidName(_))));
}
#[test]
fn test_registry_cardinality_limit() {
let reg = Registry::new().max_series(2);
reg.counter("metric1", &[]).unwrap();
reg.counter("metric2", &[]).unwrap();
let result = reg.counter("metric3", &[]);
assert!(matches!(result, Err(RegistryError::CardinalityLimit { .. })));
}
#[test]
fn test_registry_unregister() {
let reg = Registry::new();
reg.counter("test", &[("a", "b")]).unwrap();
assert_eq!(reg.len(), 1);
let removed = reg.unregister("test", &[("a", "b")]);
assert!(removed);
assert_eq!(reg.len(), 0);
}
#[test]
fn test_registry_iteration() {
let reg = Registry::new();
reg.counter("counter1", &[]).unwrap();
reg.gauge("gauge1", &[]).unwrap();
reg.histogram("histogram1", &[]).unwrap();
assert_eq!(reg.len(), 3);
assert_eq!(reg.counters().len(), 1);
assert_eq!(reg.gauges().len(), 1);
assert_eq!(reg.histograms().len(), 1);
}
#[test]
fn test_default_registry() {
let reg = default_registry();
let _ = reg.len();
}
#[test]
fn test_metric_key_generation() {
let labels: Labels = [("b", "2"), ("a", "1")]
.into_iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
let key = metric_key("test", &labels);
assert_eq!(key, "test|a=1|b=2");
}
#[test]
fn test_registry_default() {
let reg = Registry::default();
assert_eq!(reg.len(), 0);
assert!(reg.is_empty());
}
#[test]
fn test_registry_is_empty() {
let reg = Registry::new();
assert!(reg.is_empty());
reg.counter("test", &[]).unwrap();
assert!(!reg.is_empty());
}
#[test]
fn test_registry_clear() {
let reg = Registry::new();
reg.counter("counter1", &[]).unwrap();
reg.gauge("gauge1", &[]).unwrap();
reg.histogram("histogram1", &[]).unwrap();
assert_eq!(reg.len(), 3);
reg.clear();
assert_eq!(reg.len(), 0);
assert!(reg.is_empty());
}
#[test]
fn test_registry_iter() {
let reg = Registry::new();
reg.counter("c", &[]).unwrap();
reg.gauge("g", &[]).unwrap();
reg.histogram("h", &[]).unwrap();
let metrics: Vec<_> = reg.iter().collect();
assert_eq!(metrics.len(), 3);
}
#[test]
fn test_registered_metric_type() {
let counter = Counter::new_arc("test", Labels::new());
let gauge = Gauge::new_arc("test", Labels::new());
let histogram = Histogram::new_arc("test", Labels::new(), DEFAULT_BUCKETS);
let rm_counter = RegisteredMetric::Counter(counter);
let rm_gauge = RegisteredMetric::Gauge(gauge);
let rm_histogram = RegisteredMetric::Histogram(histogram);
assert_eq!(rm_counter.metric_type(), MetricType::Counter);
assert_eq!(rm_gauge.metric_type(), MetricType::Gauge);
assert_eq!(rm_histogram.metric_type(), MetricType::Histogram);
}
#[test]
fn test_registered_metric_name() {
let counter = Counter::new_arc("my_counter", Labels::new());
let gauge = Gauge::new_arc("my_gauge", Labels::new());
let histogram = Histogram::new_arc("my_histogram", Labels::new(), DEFAULT_BUCKETS);
let rm_counter = RegisteredMetric::Counter(counter);
let rm_gauge = RegisteredMetric::Gauge(gauge);
let rm_histogram = RegisteredMetric::Histogram(histogram);
assert_eq!(rm_counter.name(), "my_counter");
assert_eq!(rm_gauge.name(), "my_gauge");
assert_eq!(rm_histogram.name(), "my_histogram");
}
#[test]
fn test_registered_metric_clone() {
let counter = Counter::new_arc("test", Labels::new());
let rm = RegisteredMetric::Counter(counter);
let cloned = rm.clone();
assert_eq!(rm.name(), cloned.name());
}
#[test]
fn test_registry_with_validator() {
let validator = LabelValidator::new();
let reg = Registry::with_validator(validator);
let result = reg.counter("test", &[("valid_key", "value")]);
assert!(result.is_ok());
}
#[test]
fn test_registry_histogram_with_custom_buckets() {
let reg = Registry::new();
let buckets = &[0.001, 0.01, 0.1, 1.0, 10.0];
let h = reg.histogram_with_buckets("latency", &[], buckets).unwrap();
h.observe(0.05);
h.observe(5.0);
assert_eq!(h.get_count(), 2);
}
#[test]
fn test_registry_unregister_non_existent() {
let reg = Registry::new();
let removed = reg.unregister("nonexistent", &[]);
assert!(!removed);
}
#[test]
fn test_registry_valid_names() {
let reg = Registry::new();
assert!(reg.counter("valid_name", &[]).is_ok());
assert!(reg.counter("_underscore_start", &[]).is_ok());
assert!(reg.counter(":colon_start", &[]).is_ok());
assert!(reg.counter("name123", &[]).is_ok());
assert!(reg.counter("name:with:colons", &[]).is_ok());
assert!(reg.counter("name_with_underscores", &[]).is_ok());
}
#[test]
fn test_registry_invalid_names() {
let reg = Registry::new();
assert!(matches!(reg.counter("", &[]), Err(RegistryError::InvalidName(_))));
assert!(matches!(reg.counter("123start", &[]), Err(RegistryError::InvalidName(_))));
assert!(matches!(reg.counter("has-dash", &[]), Err(RegistryError::InvalidName(_))));
assert!(matches!(reg.counter("has.dot", &[]), Err(RegistryError::InvalidName(_))));
assert!(matches!(reg.counter("has space", &[]), Err(RegistryError::InvalidName(_))));
}
#[test]
fn test_registry_type_mismatch_gauge_to_counter() {
let reg = Registry::new();
reg.gauge("metric", &[]).unwrap();
let result = reg.counter("metric", &[]);
assert!(matches!(result, Err(RegistryError::TypeMismatch { .. })));
}
#[test]
fn test_registry_type_mismatch_histogram_to_gauge() {
let reg = Registry::new();
reg.histogram("metric", &[]).unwrap();
let result = reg.gauge("metric", &[]);
assert!(matches!(result, Err(RegistryError::TypeMismatch { .. })));
}
#[test]
fn test_registry_type_mismatch_counter_to_histogram() {
let reg = Registry::new();
reg.counter("metric", &[]).unwrap();
let result = reg.histogram("metric", &[]);
assert!(matches!(result, Err(RegistryError::TypeMismatch { .. })));
}
#[test]
fn test_registry_cardinality_limit_gauge() {
let reg = Registry::new().max_series(1);
reg.gauge("metric1", &[]).unwrap();
let result = reg.gauge("metric2", &[]);
assert!(matches!(result, Err(RegistryError::CardinalityLimit { .. })));
}
#[test]
fn test_registry_cardinality_limit_histogram() {
let reg = Registry::new().max_series(1);
reg.histogram("metric1", &[]).unwrap();
let result = reg.histogram("metric2", &[]);
assert!(matches!(result, Err(RegistryError::CardinalityLimit { .. })));
}
#[test]
fn test_registry_error_display() {
let err = RegistryError::AlreadyRegistered { name: "test".to_string() };
assert!(err.to_string().contains("test"));
let err = RegistryError::TypeMismatch {
name: "metric".to_string(),
existing: MetricType::Counter,
requested: MetricType::Gauge,
};
assert!(err.to_string().contains("metric"));
let err = RegistryError::InvalidName("bad!name".to_string());
assert!(err.to_string().contains("bad!name"));
let err = RegistryError::CardinalityLimit { count: 100, max: 50 };
assert!(err.to_string().contains("100"));
}
#[test]
fn test_metric_key_empty_labels() {
let labels: Labels = Labels::new();
let key = metric_key("test", &labels);
assert_eq!(key, "test");
}
#[test]
fn test_registry_debug() {
let reg = Registry::new();
let debug_str = format!("{:?}", reg);
assert!(debug_str.contains("Registry"));
}
}