Skip to main content

textfile_metrics/
metric.rs

1// Copyright (c) Ted Kaplan. All Rights Reserved.
2// SPDX-License-Identifier: MIT
3
4//! Metric types: Counter and Gauge.
5
6use std::{cmp::Ordering, fmt};
7
8use crate::labels::Labels;
9
10/// Metric type enumeration.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum MetricType {
13    /// Counter (monotonically increasing).
14    Counter,
15    /// Gauge (arbitrary value).
16    Gauge,
17}
18
19impl fmt::Display for MetricType {
20    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21        match self {
22            MetricType::Counter => write!(f, "counter"),
23            MetricType::Gauge => write!(f, "gauge"),
24        }
25    }
26}
27
28/// A Counter metric - monotonically increasing value.
29#[derive(Debug, Clone)]
30pub struct Counter {
31    /// Metric name (e.g., "requests_total").
32    pub name: String,
33    /// Labels for the metric.
34    pub labels: Labels,
35    /// Current counter value.
36    pub value: f64,
37}
38
39impl Counter {
40    /// Create a new counter.
41    pub fn new(name: impl Into<String>, labels: Labels, value: f64) -> Self {
42        Self {
43            name: name.into(),
44            labels,
45            value,
46        }
47    }
48
49    /// Increment the counter.
50    pub fn increment(&mut self, delta: f64) {
51        self.value += delta;
52    }
53
54    /// Get the counter value.
55    pub fn value(&self) -> f64 {
56        self.value
57    }
58}
59
60/// A Gauge metric - arbitrary numeric value.
61#[derive(Debug, Clone)]
62pub struct Gauge {
63    /// Metric name (e.g., "temperature_celsius").
64    pub name: String,
65    /// Labels for the metric.
66    pub labels: Labels,
67    /// Current gauge value.
68    pub value: f64,
69}
70
71impl Gauge {
72    /// Create a new gauge.
73    pub fn new(name: impl Into<String>, labels: Labels, value: f64) -> Self {
74        Self {
75            name: name.into(),
76            labels,
77            value,
78        }
79    }
80
81    /// Set the gauge value.
82    pub fn set(&mut self, value: f64) {
83        self.value = value;
84    }
85
86    /// Get the gauge value.
87    pub fn value(&self) -> f64 {
88        self.value
89    }
90}
91
92/// A Prometheus metric with type information.
93#[derive(Debug, Clone)]
94pub(crate) struct PrometheusMetric {
95    /// Metric name.
96    pub name: String,
97    /// Metric type (COUNTER or GAUGE).
98    pub metric_type: MetricType,
99    /// Labels.
100    pub labels: Labels,
101    /// Metric value.
102    pub value: f64,
103    /// Optional timestamp (Unix milliseconds).
104    pub timestamp: Option<i64>,
105}
106
107impl PrometheusMetric {
108    /// Create a new Prometheus metric.
109    pub(crate) fn new(
110        name: impl Into<String>,
111        metric_type: MetricType,
112        labels: Labels,
113        value: f64,
114    ) -> Self {
115        Self {
116            name: name.into(),
117            metric_type,
118            labels,
119            value,
120            timestamp: None,
121        }
122    }
123
124    /// Set an optional timestamp (Unix milliseconds).
125    #[allow(dead_code)]
126    pub(crate) fn with_timestamp(mut self, timestamp: i64) -> Self {
127        self.timestamp = Some(timestamp);
128        self
129    }
130
131    /// Format as Prometheus textfile format line.
132    ///
133    /// Format: `metric_name{labels} value [timestamp]`
134    pub(crate) fn to_prometheus_line(&self) -> String {
135        let labels_str = self.labels.to_string();
136        let full_name = if labels_str.is_empty() {
137            self.name.clone()
138        } else {
139            format!("{}{}", self.name, labels_str)
140        };
141
142        match self.timestamp {
143            Some(ts) => format!("{} {} {}", full_name, self.value, ts),
144            None => format!("{} {}", full_name, self.value),
145        }
146    }
147
148    /// Format as Prometheus TYPE declaration.
149    ///
150    /// Format: `# TYPE metric_name counter|gauge`
151    #[allow(dead_code)]
152    pub(crate) fn type_declaration(&self) -> String {
153        format!("# TYPE {} {}", self.name, self.metric_type)
154    }
155
156    /// Check if this metric has valid value (not NaN or Inf).
157    pub(crate) fn is_valid(&self) -> bool {
158        self.value.is_finite()
159    }
160}
161
162impl PartialEq for PrometheusMetric {
163    fn eq(&self, other: &Self) -> bool {
164        self.name == other.name
165            && self.metric_type == other.metric_type
166            && self.labels == other.labels
167            && (self.value - other.value).abs() < 1e-10
168    }
169}
170
171impl Eq for PrometheusMetric {}
172
173impl PartialOrd for PrometheusMetric {
174    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
175        Some(self.cmp(other))
176    }
177}
178
179impl Ord for PrometheusMetric {
180    fn cmp(&self, other: &Self) -> Ordering {
181        self.name.cmp(&other.name).then_with(|| {
182            self.metric_type
183                .to_string()
184                .cmp(&other.metric_type.to_string())
185        })
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    #[test]
194    fn test_counter_new() {
195        let counter = Counter::new("test_counter", Labels::new(), 10.0);
196        assert_eq!(counter.value(), 10.0);
197    }
198
199    #[test]
200    fn test_counter_increment() {
201        let mut counter = Counter::new("test_counter", Labels::new(), 10.0);
202        counter.increment(5.0);
203        assert_eq!(counter.value(), 15.0);
204    }
205
206    #[test]
207    fn test_gauge_new() {
208        let gauge = Gauge::new("test_gauge", Labels::new(), 42.5);
209        assert_eq!(gauge.value(), 42.5);
210    }
211
212    #[test]
213    fn test_gauge_set() {
214        let mut gauge = Gauge::new("test_gauge", Labels::new(), 42.5);
215        gauge.set(99.9);
216        assert_eq!(gauge.value(), 99.9);
217    }
218
219    #[test]
220    fn test_prometheus_metric_line_no_labels() {
221        let metric = PrometheusMetric::new("test_metric", MetricType::Gauge, Labels::new(), 42.0);
222        assert_eq!(metric.to_prometheus_line(), "test_metric 42");
223    }
224
225    #[test]
226    fn test_prometheus_metric_line_with_labels() {
227        let labels = Labels::from(vec![("method".to_string(), "GET".to_string())]);
228        let metric = PrometheusMetric::new("requests_total", MetricType::Counter, labels, 100.0);
229        let line = metric.to_prometheus_line();
230
231        assert!(line.contains("requests_total{"));
232        assert!(line.contains("method=\"GET\""));
233        assert!(line.contains("100"));
234    }
235
236    #[test]
237    fn test_prometheus_metric_with_timestamp() {
238        let metric = PrometheusMetric::new("test_metric", MetricType::Gauge, Labels::new(), 42.0)
239            .with_timestamp(1699527600000);
240
241        let line = metric.to_prometheus_line();
242        assert!(line.contains("1699527600000"));
243    }
244
245    #[test]
246    fn test_type_declaration() {
247        let counter =
248            PrometheusMetric::new("requests_total", MetricType::Counter, Labels::new(), 0.0);
249        assert_eq!(counter.type_declaration(), "# TYPE requests_total counter");
250
251        let gauge = PrometheusMetric::new("temperature", MetricType::Gauge, Labels::new(), 0.0);
252        assert_eq!(gauge.type_declaration(), "# TYPE temperature gauge");
253    }
254
255    #[test]
256    fn test_is_valid() {
257        let valid = PrometheusMetric::new("test", MetricType::Gauge, Labels::new(), 42.0);
258        assert!(valid.is_valid());
259
260        let invalid_nan = PrometheusMetric::new("test", MetricType::Gauge, Labels::new(), f64::NAN);
261        assert!(!invalid_nan.is_valid());
262
263        let invalid_inf =
264            PrometheusMetric::new("test", MetricType::Gauge, Labels::new(), f64::INFINITY);
265        assert!(!invalid_inf.is_valid());
266    }
267
268    #[test]
269    fn test_metric_ordering() {
270        let metric1 = PrometheusMetric::new("aaa_metric", MetricType::Counter, Labels::new(), 1.0);
271        let metric2 = PrometheusMetric::new("zzz_metric", MetricType::Counter, Labels::new(), 1.0);
272
273        assert!(metric1 < metric2);
274    }
275}