1use std::{cmp::Ordering, fmt};
7
8use crate::labels::Labels;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum MetricType {
13 Counter,
15 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#[derive(Debug, Clone)]
30pub struct Counter {
31 pub name: String,
33 pub labels: Labels,
35 pub value: f64,
37}
38
39impl Counter {
40 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 pub fn increment(&mut self, delta: f64) {
51 self.value += delta;
52 }
53
54 pub fn value(&self) -> f64 {
56 self.value
57 }
58}
59
60#[derive(Debug, Clone)]
62pub struct Gauge {
63 pub name: String,
65 pub labels: Labels,
67 pub value: f64,
69}
70
71impl Gauge {
72 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 pub fn set(&mut self, value: f64) {
83 self.value = value;
84 }
85
86 pub fn value(&self) -> f64 {
88 self.value
89 }
90}
91
92#[derive(Debug, Clone)]
94pub(crate) struct PrometheusMetric {
95 pub name: String,
97 pub metric_type: MetricType,
99 pub labels: Labels,
101 pub value: f64,
103 pub timestamp: Option<i64>,
105}
106
107impl PrometheusMetric {
108 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 #[allow(dead_code)]
126 pub(crate) fn with_timestamp(mut self, timestamp: i64) -> Self {
127 self.timestamp = Some(timestamp);
128 self
129 }
130
131 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 #[allow(dead_code)]
152 pub(crate) fn type_declaration(&self) -> String {
153 format!("# TYPE {} {}", self.name, self.metric_type)
154 }
155
156 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}