use std::{cmp::Ordering, fmt};
use crate::labels::Labels;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MetricType {
Counter,
Gauge,
}
impl fmt::Display for MetricType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MetricType::Counter => write!(f, "counter"),
MetricType::Gauge => write!(f, "gauge"),
}
}
}
#[derive(Debug, Clone)]
pub struct Counter {
pub name: String,
pub labels: Labels,
pub value: f64,
}
impl Counter {
pub fn new(name: impl Into<String>, labels: Labels, value: f64) -> Self {
Self {
name: name.into(),
labels,
value,
}
}
pub fn increment(&mut self, delta: f64) {
self.value += delta;
}
pub fn value(&self) -> f64 {
self.value
}
}
#[derive(Debug, Clone)]
pub struct Gauge {
pub name: String,
pub labels: Labels,
pub value: f64,
}
impl Gauge {
pub fn new(name: impl Into<String>, labels: Labels, value: f64) -> Self {
Self {
name: name.into(),
labels,
value,
}
}
pub fn set(&mut self, value: f64) {
self.value = value;
}
pub fn value(&self) -> f64 {
self.value
}
}
#[derive(Debug, Clone)]
pub(crate) struct PrometheusMetric {
pub name: String,
pub metric_type: MetricType,
pub labels: Labels,
pub value: f64,
pub timestamp: Option<i64>,
}
impl PrometheusMetric {
pub(crate) fn new(
name: impl Into<String>,
metric_type: MetricType,
labels: Labels,
value: f64,
) -> Self {
Self {
name: name.into(),
metric_type,
labels,
value,
timestamp: None,
}
}
#[allow(dead_code)]
pub(crate) fn with_timestamp(mut self, timestamp: i64) -> Self {
self.timestamp = Some(timestamp);
self
}
pub(crate) fn to_prometheus_line(&self) -> String {
let labels_str = self.labels.to_string();
let full_name = if labels_str.is_empty() {
self.name.clone()
} else {
format!("{}{}", self.name, labels_str)
};
match self.timestamp {
Some(ts) => format!("{} {} {}", full_name, self.value, ts),
None => format!("{} {}", full_name, self.value),
}
}
#[allow(dead_code)]
pub(crate) fn type_declaration(&self) -> String {
format!("# TYPE {} {}", self.name, self.metric_type)
}
pub(crate) fn is_valid(&self) -> bool {
self.value.is_finite()
}
}
impl PartialEq for PrometheusMetric {
fn eq(&self, other: &Self) -> bool {
self.name == other.name
&& self.metric_type == other.metric_type
&& self.labels == other.labels
&& (self.value - other.value).abs() < 1e-10
}
}
impl Eq for PrometheusMetric {}
impl PartialOrd for PrometheusMetric {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for PrometheusMetric {
fn cmp(&self, other: &Self) -> Ordering {
self.name.cmp(&other.name).then_with(|| {
self.metric_type
.to_string()
.cmp(&other.metric_type.to_string())
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_counter_new() {
let counter = Counter::new("test_counter", Labels::new(), 10.0);
assert_eq!(counter.value(), 10.0);
}
#[test]
fn test_counter_increment() {
let mut counter = Counter::new("test_counter", Labels::new(), 10.0);
counter.increment(5.0);
assert_eq!(counter.value(), 15.0);
}
#[test]
fn test_gauge_new() {
let gauge = Gauge::new("test_gauge", Labels::new(), 42.5);
assert_eq!(gauge.value(), 42.5);
}
#[test]
fn test_gauge_set() {
let mut gauge = Gauge::new("test_gauge", Labels::new(), 42.5);
gauge.set(99.9);
assert_eq!(gauge.value(), 99.9);
}
#[test]
fn test_prometheus_metric_line_no_labels() {
let metric = PrometheusMetric::new("test_metric", MetricType::Gauge, Labels::new(), 42.0);
assert_eq!(metric.to_prometheus_line(), "test_metric 42");
}
#[test]
fn test_prometheus_metric_line_with_labels() {
let labels = Labels::from(vec![("method".to_string(), "GET".to_string())]);
let metric = PrometheusMetric::new("requests_total", MetricType::Counter, labels, 100.0);
let line = metric.to_prometheus_line();
assert!(line.contains("requests_total{"));
assert!(line.contains("method=\"GET\""));
assert!(line.contains("100"));
}
#[test]
fn test_prometheus_metric_with_timestamp() {
let metric = PrometheusMetric::new("test_metric", MetricType::Gauge, Labels::new(), 42.0)
.with_timestamp(1699527600000);
let line = metric.to_prometheus_line();
assert!(line.contains("1699527600000"));
}
#[test]
fn test_type_declaration() {
let counter =
PrometheusMetric::new("requests_total", MetricType::Counter, Labels::new(), 0.0);
assert_eq!(counter.type_declaration(), "# TYPE requests_total counter");
let gauge = PrometheusMetric::new("temperature", MetricType::Gauge, Labels::new(), 0.0);
assert_eq!(gauge.type_declaration(), "# TYPE temperature gauge");
}
#[test]
fn test_is_valid() {
let valid = PrometheusMetric::new("test", MetricType::Gauge, Labels::new(), 42.0);
assert!(valid.is_valid());
let invalid_nan = PrometheusMetric::new("test", MetricType::Gauge, Labels::new(), f64::NAN);
assert!(!invalid_nan.is_valid());
let invalid_inf =
PrometheusMetric::new("test", MetricType::Gauge, Labels::new(), f64::INFINITY);
assert!(!invalid_inf.is_valid());
}
#[test]
fn test_metric_ordering() {
let metric1 = PrometheusMetric::new("aaa_metric", MetricType::Counter, Labels::new(), 1.0);
let metric2 = PrometheusMetric::new("zzz_metric", MetricType::Counter, Labels::new(), 1.0);
assert!(metric1 < metric2);
}
}