1#![warn(missing_docs)]
54
55use parking_lot::RwLock;
56use std::collections::HashMap;
57use std::sync::Arc;
58
59pub mod slo;
60
61pub use slo::{SloBurnRate, SloConfig, SloMonitor};
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum MetricKind {
66 Counter,
68 Gauge,
70 Histogram,
72}
73
74#[derive(Debug, Clone)]
76pub struct MetricMeta {
77 pub name: String,
79 pub help: String,
81 pub kind: MetricKind,
83}
84
85pub struct Counter {
87 name: String,
88 value: Arc<RwLock<f64>>,
89 labels: HashMap<String, String>,
90}
91
92impl Counter {
93 pub fn inc(&self) {
95 self.inc_by(1.0);
96 }
97
98 pub fn inc_by(&self, delta: f64) {
100 let mut v = self.value.write();
101 *v += delta;
102 }
103
104 pub fn value(&self) -> f64 {
106 *self.value.read()
107 }
108
109 pub fn name(&self) -> &str {
111 &self.name
112 }
113
114 pub fn render(&self) -> String {
116 let v = self.value.read();
117 if self.labels.is_empty() {
118 format!("{} {}\n", self.name, v)
119 } else {
120 let labels: Vec<String> = self
121 .labels
122 .iter()
123 .map(|(k, val)| format!("{}=\"{}\"", k, val.replace('"', "\\\"")))
124 .collect();
125 format!("{}{{{}}} {}\n", self.name, labels.join(","), v)
126 }
127 }
128}
129
130pub struct Gauge {
132 name: String,
133 value: Arc<RwLock<f64>>,
134 labels: HashMap<String, String>,
135}
136
137impl Gauge {
138 pub fn set(&self, value: f64) {
140 *self.value.write() = value;
141 }
142
143 pub fn inc(&self) {
145 self.inc_by(1.0);
146 }
147
148 pub fn inc_by(&self, delta: f64) {
150 let mut v = self.value.write();
151 *v += delta;
152 }
153
154 pub fn dec_by(&self, delta: f64) {
156 let mut v = self.value.write();
157 *v -= delta;
158 }
159
160 pub fn value(&self) -> f64 {
162 *self.value.read()
163 }
164
165 pub fn name(&self) -> &str {
167 &self.name
168 }
169
170 pub fn render(&self) -> String {
172 let v = self.value.read();
173 if self.labels.is_empty() {
174 format!("{} {}\n", self.name, v)
175 } else {
176 let labels: Vec<String> = self
177 .labels
178 .iter()
179 .map(|(k, val)| format!("{}=\"{}\"", k, val.replace('"', "\\\"")))
180 .collect();
181 format!("{}{{{}}} {}\n", self.name, labels.join(","), v)
182 }
183 }
184}
185
186pub struct Histogram {
188 name: String,
189 buckets: Vec<f64>,
190 counts: Arc<RwLock<Vec<u64>>>,
191 sum: Arc<RwLock<f64>>,
192 count: Arc<RwLock<u64>>,
193}
194
195impl Histogram {
196 pub fn observe(&self, value: f64) {
198 let mut counts = self.counts.write();
199 for (i, bucket) in self.buckets.iter().enumerate() {
200 if value <= *bucket {
201 counts[i] += 1;
202 }
203 }
204 let last = counts.len() - 1;
206 counts[last] += 1;
207
208 let mut sum = self.sum.write();
209 *sum += value;
210 let mut count = self.count.write();
211 *count += 1;
212 }
213
214 pub fn count(&self) -> u64 {
216 *self.count.read()
217 }
218
219 pub fn sum(&self) -> f64 {
221 *self.sum.read()
222 }
223
224 pub fn name(&self) -> &str {
226 &self.name
227 }
228
229 pub fn render(&self) -> String {
231 let counts = self.counts.read();
232 let sum = self.sum.read();
233 let count = self.count.read();
234
235 let mut output = String::new();
236 for (i, bucket) in self.buckets.iter().enumerate() {
237 output.push_str(&format!(
238 "{}_bucket{{le=\"{}\"}} {}\n",
239 self.name, bucket, counts[i]
240 ));
241 }
242 output.push_str(&format!("{}_sum {}\n", self.name, sum));
243 output.push_str(&format!("{}_count {}\n", self.name, count));
244 output
245 }
246}
247
248pub struct MetricsRegistry {
250 counters: RwLock<HashMap<String, Arc<Counter>>>,
251 gauges: RwLock<HashMap<String, Arc<Gauge>>>,
252 histograms: RwLock<HashMap<String, Arc<Histogram>>>,
253 metas: RwLock<Vec<MetricMeta>>,
254}
255
256impl Default for MetricsRegistry {
257 fn default() -> Self {
258 Self::new()
259 }
260}
261
262impl MetricsRegistry {
263 pub fn new() -> Self {
265 Self {
266 counters: RwLock::new(HashMap::new()),
267 gauges: RwLock::new(HashMap::new()),
268 histograms: RwLock::new(HashMap::new()),
269 metas: RwLock::new(Vec::new()),
270 }
271 }
272
273 pub fn register_counter(&self, name: &str, help: &str) -> Arc<Counter> {
275 self.register_counter_with_labels(name, help, HashMap::new())
276 }
277
278 pub fn register_counter_with_labels(
280 &self,
281 name: &str,
282 help: &str,
283 labels: HashMap<String, String>,
284 ) -> Arc<Counter> {
285 let mut counters = self.counters.write();
286 let key = format!("{}_{:?}", name, labels);
287 if let Some(c) = counters.get(&key) {
288 return c.clone();
289 }
290 let counter = Arc::new(Counter {
291 name: name.to_string(),
292 value: Arc::new(RwLock::new(0.0)),
293 labels,
294 });
295 counters.insert(key, counter.clone());
296
297 let mut metas = self.metas.write();
298 metas.push(MetricMeta {
299 name: name.to_string(),
300 help: help.to_string(),
301 kind: MetricKind::Counter,
302 });
303 counter
304 }
305
306 pub fn register_gauge(&self, name: &str, help: &str) -> Arc<Gauge> {
308 self.register_gauge_with_labels(name, help, HashMap::new())
309 }
310
311 pub fn register_gauge_with_labels(
313 &self,
314 name: &str,
315 help: &str,
316 labels: HashMap<String, String>,
317 ) -> Arc<Gauge> {
318 let mut gauges = self.gauges.write();
319 let key = format!("{}_{:?}", name, labels);
320 if let Some(g) = gauges.get(&key) {
321 return g.clone();
322 }
323 let gauge = Arc::new(Gauge {
324 name: name.to_string(),
325 value: Arc::new(RwLock::new(0.0)),
326 labels,
327 });
328 gauges.insert(key, gauge.clone());
329
330 let mut metas = self.metas.write();
331 metas.push(MetricMeta {
332 name: name.to_string(),
333 help: help.to_string(),
334 kind: MetricKind::Gauge,
335 });
336 gauge
337 }
338
339 pub fn register_histogram(&self, name: &str, help: &str, buckets: Vec<f64>) -> Arc<Histogram> {
341 let mut histograms = self.histograms.write();
342 if let Some(h) = histograms.get(name) {
343 return h.clone();
344 }
345 let mut all_buckets = buckets;
347 all_buckets.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
348 if !all_buckets.contains(&f64::INFINITY) {
349 all_buckets.push(f64::INFINITY);
350 }
351 let count = all_buckets.len();
352 let histogram = Arc::new(Histogram {
353 name: name.to_string(),
354 buckets: all_buckets,
355 counts: Arc::new(RwLock::new(vec![0; count])),
356 sum: Arc::new(RwLock::new(0.0)),
357 count: Arc::new(RwLock::new(0)),
358 });
359 histograms.insert(name.to_string(), histogram.clone());
360
361 let mut metas = self.metas.write();
362 metas.push(MetricMeta {
363 name: name.to_string(),
364 help: help.to_string(),
365 kind: MetricKind::Histogram,
366 });
367 histogram
368 }
369
370 pub fn render(&self) -> String {
372 let mut output = String::new();
373
374 let metas = self.metas.read();
376 let mut seen = std::collections::HashSet::new();
377 for meta in metas.iter() {
378 if seen.contains(&meta.name) {
379 continue;
380 }
381 seen.insert(meta.name.clone());
382 output.push_str(&format!("# HELP {} {}\n", meta.name, meta.help));
383 let type_str = match meta.kind {
384 MetricKind::Counter => "counter",
385 MetricKind::Gauge => "gauge",
386 MetricKind::Histogram => "histogram",
387 };
388 output.push_str(&format!("# TYPE {} {}\n", meta.name, type_str));
389 }
390
391 let counters = self.counters.read();
393 for c in counters.values() {
394 output.push_str(&c.render());
395 }
396
397 let gauges = self.gauges.read();
399 for g in gauges.values() {
400 output.push_str(&g.render());
401 }
402
403 let histograms = self.histograms.read();
405 for h in histograms.values() {
406 output.push_str(&h.render());
407 }
408
409 output
410 }
411}
412
413#[cfg(test)]
414mod tests {
415 use super::*;
416
417 #[test]
418 fn test_counter_basic() {
419 let registry = MetricsRegistry::new();
420 let counter = registry.register_counter("test_counter", "Test counter");
421 counter.inc();
422 counter.inc_by(2.5);
423 assert_eq!(counter.value(), 3.5);
424 }
425
426 #[test]
427 fn test_gauge_basic() {
428 let registry = MetricsRegistry::new();
429 let gauge = registry.register_gauge("test_gauge", "Test gauge");
430 gauge.set(10.0);
431 gauge.inc();
432 gauge.dec_by(3.0);
433 assert_eq!(gauge.value(), 8.0);
434 }
435
436 #[test]
437 fn test_histogram_basic() {
438 let registry = MetricsRegistry::new();
439 let histogram =
440 registry.register_histogram("test_histogram", "Test histogram", vec![0.1, 0.5, 1.0]);
441 histogram.observe(0.05);
442 histogram.observe(0.2);
443 histogram.observe(0.6);
444 histogram.observe(1.5);
445
446 assert_eq!(histogram.count(), 4);
447 assert!((histogram.sum() - 2.35).abs() < 1e-9);
448 }
449
450 #[test]
451 fn test_render_prometheus_format() {
452 let registry = MetricsRegistry::new();
453 let counter = registry.register_counter("ops_total", "Total operations");
454 let gauge = registry.register_gauge("conn_active", "Active connections");
455 let histogram =
456 registry.register_histogram("latency_seconds", "Latency in seconds", vec![0.01, 0.1]);
457
458 counter.inc_by(10.0);
459 gauge.set(5.0);
460 histogram.observe(0.005);
461 histogram.observe(0.05);
462 histogram.observe(0.5);
463
464 let output = registry.render();
465 assert!(output.contains("# HELP ops_total Total operations"));
466 assert!(output.contains("# TYPE ops_total counter"));
467 assert!(output.contains("ops_total 10"));
468 assert!(output.contains("conn_active 5"));
469 assert!(output.contains("latency_seconds_bucket{le=\"0.01\"} 1"));
470 assert!(output.contains("latency_seconds_bucket{le=\"0.1\"} 2"));
471 assert!(output.contains("latency_seconds_sum 0.555"));
472 assert!(output.contains("latency_seconds_count 3"));
473 }
474
475 #[test]
476 fn test_counter_with_labels() {
477 let registry = MetricsRegistry::new();
478 let mut labels = HashMap::new();
479 labels.insert("method".to_string(), "GET".to_string());
480 labels.insert("status".to_string(), "200".to_string());
481
482 let counter =
483 registry.register_counter_with_labels("http_requests_total", "HTTP requests", labels);
484 counter.inc();
485 let output = registry.render();
486 assert!(output.contains("http_requests_total{"));
488 assert!(output.contains("method=\"GET\""));
489 assert!(output.contains("status=\"200\""));
490 assert!(output.contains("} 1"));
491 }
492}