1use parking_lot::RwLock;
18use std::collections::HashMap;
19use std::sync::Arc;
20
21pub struct Summary {
26 name: String,
27 help: String,
28 quantiles: Vec<f64>,
30 samples: Arc<RwLock<Vec<f64>>>,
32 sum: Arc<RwLock<f64>>,
34 count: Arc<RwLock<u64>>,
36}
37
38impl Summary {
39 pub fn new(name: impl Into<String>, help: impl Into<String>, quantiles: Vec<f64>) -> Self {
46 Self {
47 name: name.into(),
48 help: help.into(),
49 quantiles,
50 samples: Arc::new(RwLock::new(Vec::new())),
51 sum: Arc::new(RwLock::new(0.0)),
52 count: Arc::new(RwLock::new(0)),
53 }
54 }
55
56 pub fn observe(&self, value: f64) {
58 let mut samples = self.samples.write();
59 let pos = samples.partition_point(|&v| v < value);
60 samples.insert(pos, value);
61
62 let mut sum = self.sum.write();
63 *sum += value;
64
65 let mut count = self.count.write();
66 *count += 1;
67 }
68
69 pub fn quantile(&self, q: f64) -> Option<f64> {
74 if !(0.0..=1.0).contains(&q) {
75 return None;
76 }
77 let samples = self.samples.read();
78 if samples.is_empty() {
79 return None;
80 }
81 let n = samples.len();
82 let rank = ((q * n as f64).ceil() as usize).max(1).min(n);
83 Some(samples[rank - 1])
84 }
85
86 pub fn quantiles(&self) -> Vec<(f64, Option<f64>)> {
88 self.quantiles
89 .iter()
90 .map(|&q| (q, self.quantile(q)))
91 .collect()
92 }
93
94 pub fn count(&self) -> u64 {
96 *self.count.read()
97 }
98
99 pub fn sum(&self) -> f64 {
101 *self.sum.read()
102 }
103
104 pub fn name(&self) -> &str {
106 &self.name
107 }
108
109 pub fn render(&self) -> String {
111 let samples = self.samples.read();
112 let sum = *self.sum.read();
113 let count = *self.count.read();
114
115 let mut output = String::new();
116 output.push_str(&format!("# HELP {} {}\n", self.name, self.help));
117 output.push_str(&format!("# TYPE {} summary\n", self.name));
118
119 for &q in &self.quantiles {
120 let value = if samples.is_empty() {
121 0.0
122 } else {
123 let n = samples.len();
124 let rank = ((q * n as f64).ceil() as usize).max(1).min(n);
125 samples[rank - 1]
126 };
127 output.push_str(&format!("{}{{quantile=\"{}\"}} {}\n", self.name, q, value));
128 }
129
130 output.push_str(&format!("{}_sum {}\n", self.name, sum));
131 output.push_str(&format!("{}_count {}\n", self.name, count));
132 output
133 }
134
135 pub fn reset(&self) {
137 let mut samples = self.samples.write();
138 samples.clear();
139 *self.sum.write() = 0.0;
140 *self.count.write() = 0;
141 }
142}
143
144pub struct LabeledHistogram {
149 name: String,
151 help: String,
153 buckets: Vec<f64>,
155 series: RwLock<HashMap<String, LabeledSeries>>,
157}
158
159#[derive(Debug, Clone)]
161struct LabeledSeries {
162 labels: Vec<(String, String)>,
164 counts: Vec<u64>,
166 sum: f64,
168 count: u64,
170}
171
172impl LabeledSeries {
173 fn new(labels: Vec<(String, String)>, bucket_count: usize) -> Self {
174 Self {
175 labels,
176 counts: vec![0; bucket_count],
177 sum: 0.0,
178 count: 0,
179 }
180 }
181
182 fn label_key(labels: &[(String, String)]) -> String {
183 labels
184 .iter()
185 .map(|(k, v)| format!("{}=\"{}\"", k, v.replace('"', "\\\"")))
186 .collect::<Vec<_>>()
187 .join(",")
188 }
189}
190
191impl LabeledHistogram {
192 pub fn new(name: impl Into<String>, help: impl Into<String>, mut buckets: Vec<f64>) -> Self {
199 buckets.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
200 if !buckets.contains(&f64::INFINITY) {
201 buckets.push(f64::INFINITY);
202 }
203 Self {
204 name: name.into(),
205 help: help.into(),
206 buckets,
207 series: RwLock::new(HashMap::new()),
208 }
209 }
210
211 pub fn observe(&self, labels: &HashMap<String, String>, value: f64) {
217 let mut sorted_labels: Vec<(String, String)> =
218 labels.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
219 sorted_labels.sort_by(|a, b| a.0.cmp(&b.0));
220
221 let key = LabeledSeries::label_key(&sorted_labels);
222 let mut series = self.series.write();
223 let entry = series
224 .entry(key)
225 .or_insert_with(|| LabeledSeries::new(sorted_labels.clone(), self.buckets.len()));
226
227 for (i, bucket) in self.buckets.iter().enumerate() {
228 if value <= *bucket {
229 entry.counts[i] += 1;
230 }
231 }
232 entry.sum += value;
233 entry.count += 1;
234 }
235
236 pub fn count(&self, labels: &HashMap<String, String>) -> u64 {
238 let mut sorted_labels: Vec<(String, String)> =
239 labels.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
240 sorted_labels.sort_by(|a, b| a.0.cmp(&b.0));
241 let key = LabeledSeries::label_key(&sorted_labels);
242 self.series.read().get(&key).map(|s| s.count).unwrap_or(0)
243 }
244
245 pub fn label_combination_count(&self) -> usize {
247 self.series.read().len()
248 }
249
250 pub fn render(&self) -> String {
252 let series = self.series.read();
253 let mut output = String::new();
254 output.push_str(&format!("# HELP {} {}\n", self.name, self.help));
255 output.push_str(&format!("# TYPE {} histogram\n", self.name));
256
257 for s in series.values() {
258 let label_str = LabeledSeries::label_key(&s.labels);
259 for (i, bucket) in self.buckets.iter().enumerate() {
260 if *bucket == f64::INFINITY {
261 output.push_str(&format!(
262 "{}_bucket{{{},le=\"+Inf\"}} {}\n",
263 self.name, label_str, s.counts[i]
264 ));
265 } else {
266 output.push_str(&format!(
267 "{}_bucket{{{},le=\"{}\"}} {}\n",
268 self.name, label_str, bucket, s.counts[i]
269 ));
270 }
271 }
272 output.push_str(&format!("{}_sum{{{}}} {}\n", self.name, label_str, s.sum));
273 output.push_str(&format!(
274 "{}_count{{{}}} {}\n",
275 self.name, label_str, s.count
276 ));
277 }
278
279 output
280 }
281}
282
283#[derive(Debug, Clone)]
285pub struct PushgatewayConfig {
286 pub endpoint: String,
288 pub job: String,
290 pub instance: Option<String>,
292}
293
294impl Default for PushgatewayConfig {
295 fn default() -> Self {
296 Self {
297 endpoint: "http://localhost:9091".to_string(),
298 job: "sz-orm".to_string(),
299 instance: None,
300 }
301 }
302}
303
304pub struct PushgatewayExporter {
309 config: PushgatewayConfig,
310 pushed: RwLock<Vec<PushSnapshot>>,
312}
313
314#[derive(Debug, Clone)]
316pub struct PushSnapshot {
317 pub timestamp_ms: i64,
319 pub metrics_text: String,
321 pub job: String,
323 pub instance: Option<String>,
325}
326
327impl PushgatewayExporter {
328 pub fn new(config: PushgatewayConfig) -> Self {
330 Self {
331 config,
332 pushed: RwLock::new(Vec::new()),
333 }
334 }
335
336 pub fn push(&self, metrics_text: impl Into<String>) -> Result<(), String> {
341 let snapshot = PushSnapshot {
342 timestamp_ms: current_timestamp_ms(),
343 metrics_text: metrics_text.into(),
344 job: self.config.job.clone(),
345 instance: self.config.instance.clone(),
346 };
347 let mut pushed = self.pushed.write();
348 pushed.push(snapshot);
349 Ok(())
350 }
351
352 pub fn push_from_registry(&self, registry: &crate::MetricsRegistry) -> Result<(), String> {
354 let text = registry.render();
355 self.push(text)
356 }
357
358 pub fn snapshots(&self) -> Vec<PushSnapshot> {
360 self.pushed.read().clone()
361 }
362
363 pub fn push_count(&self) -> usize {
365 self.pushed.read().len()
366 }
367
368 pub fn clear(&self) {
370 self.pushed.write().clear();
371 }
372
373 pub fn config(&self) -> &PushgatewayConfig {
375 &self.config
376 }
377}
378
379fn current_timestamp_ms() -> i64 {
380 use std::time::{SystemTime, UNIX_EPOCH};
381 SystemTime::now()
382 .duration_since(UNIX_EPOCH)
383 .unwrap_or_default()
384 .as_millis() as i64
385}
386
387#[cfg(test)]
388mod tests {
389 use super::*;
390
391 #[test]
394 fn test_summary_new_empty() {
395 let s = Summary::new("latency", "latency summary", vec![0.5, 0.9, 0.99]);
396 assert_eq!(s.count(), 0);
397 assert_eq!(s.sum(), 0.0);
398 assert!(s.quantile(0.5).is_none());
399 }
400
401 #[test]
402 fn test_summary_observe_single() {
403 let s = Summary::new("latency", "help", vec![0.5]);
404 s.observe(1.5);
405 assert_eq!(s.count(), 1);
406 assert!((s.sum() - 1.5).abs() < 1e-9);
407 assert!((s.quantile(0.5).unwrap() - 1.5).abs() < 1e-9);
408 }
409
410 #[test]
411 fn test_summary_observe_multiple_p50() {
412 let s = Summary::new("latency", "help", vec![0.5]);
413 for v in [1.0, 2.0, 3.0, 4.0, 5.0] {
414 s.observe(v);
415 }
416 assert!((s.quantile(0.5).unwrap() - 3.0).abs() < 1e-9);
418 }
419
420 #[test]
421 fn test_summary_observe_multiple_p99() {
422 let s = Summary::new("latency", "help", vec![0.99]);
423 for v in [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 100.0] {
424 s.observe(v);
425 }
426 assert!((s.quantile(0.99).unwrap() - 100.0).abs() < 1e-9);
428 }
429
430 #[test]
431 fn test_summary_quantile_out_of_range() {
432 let s = Summary::new("latency", "help", vec![0.5]);
433 s.observe(1.0);
434 assert!(s.quantile(-0.1).is_none());
435 assert!(s.quantile(1.1).is_none());
436 }
437
438 #[test]
439 fn test_summary_quantile_empty() {
440 let s = Summary::new("latency", "help", vec![0.5]);
441 assert!(s.quantile(0.5).is_none());
442 }
443
444 #[test]
445 fn test_summary_quantile_p0_and_p1() {
446 let s = Summary::new("latency", "help", vec![]);
447 for v in [10.0, 20.0, 30.0] {
448 s.observe(v);
449 }
450 assert!((s.quantile(0.0).unwrap() - 10.0).abs() < 1e-9);
452 assert!((s.quantile(1.0).unwrap() - 30.0).abs() < 1e-9);
454 }
455
456 #[test]
457 fn test_summary_quantiles_all() {
458 let s = Summary::new("latency", "help", vec![0.5, 0.9, 0.99]);
459 for v in [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0] {
460 s.observe(v);
461 }
462 let qs = s.quantiles();
463 assert_eq!(qs.len(), 3);
464 assert!(qs.iter().all(|(_, v)| v.is_some()));
465 }
466
467 #[test]
468 fn test_summary_unsorted_input_stays_sorted() {
469 let s = Summary::new("latency", "help", vec![0.5]);
470 s.observe(50.0);
471 s.observe(10.0);
472 s.observe(30.0);
473 assert!((s.quantile(0.5).unwrap() - 30.0).abs() < 1e-9);
475 }
476
477 #[test]
478 fn test_summary_render_contains_type() {
479 let s = Summary::new("latency", "latency help", vec![0.5, 0.99]);
480 s.observe(1.0);
481 let output = s.render();
482 assert!(output.contains("# HELP latency latency help"));
483 assert!(output.contains("# TYPE latency summary"));
484 assert!(output.contains("latency{quantile=\"0.5\"}"));
485 assert!(output.contains("latency{quantile=\"0.99\"}"));
486 assert!(output.contains("latency_sum"));
487 assert!(output.contains("latency_count"));
488 }
489
490 #[test]
491 fn test_summary_render_empty_shows_zero() {
492 let s = Summary::new("latency", "help", vec![0.5]);
493 let output = s.render();
494 assert!(output.contains("latency{quantile=\"0.5\"} 0"));
496 assert!(output.contains("latency_count 0"));
497 }
498
499 #[test]
500 fn test_summary_reset() {
501 let s = Summary::new("latency", "help", vec![0.5]);
502 s.observe(1.0);
503 s.observe(2.0);
504 assert_eq!(s.count(), 2);
505
506 s.reset();
507 assert_eq!(s.count(), 0);
508 assert!((s.sum() - 0.0).abs() < 1e-9);
509 assert!(s.quantile(0.5).is_none());
510 }
511
512 #[test]
513 fn test_summary_name() {
514 let s = Summary::new("my_metric", "help", vec![0.5]);
515 assert_eq!(s.name(), "my_metric");
516 }
517
518 #[test]
521 fn test_labeled_histogram_new() {
522 let h = LabeledHistogram::new("requests", "help", vec![0.1, 0.5, 1.0]);
523 assert_eq!(h.label_combination_count(), 0);
524 }
525
526 #[test]
527 fn test_labeled_histogram_observe_single_label() {
528 let h = LabeledHistogram::new("requests", "help", vec![0.1, 0.5, 1.0]);
529 let mut labels = HashMap::new();
530 labels.insert("method".to_string(), "GET".to_string());
531
532 h.observe(&labels, 0.3);
533 assert_eq!(h.count(&labels), 1);
534 assert_eq!(h.label_combination_count(), 1);
535 }
536
537 #[test]
538 fn test_labeled_histogram_observe_multiple_labels() {
539 let h = LabeledHistogram::new("requests", "help", vec![0.1, 0.5, 1.0]);
540
541 let mut get_labels = HashMap::new();
542 get_labels.insert("method".to_string(), "GET".to_string());
543
544 let mut post_labels = HashMap::new();
545 post_labels.insert("method".to_string(), "POST".to_string());
546
547 h.observe(&get_labels, 0.1);
548 h.observe(&get_labels, 0.2);
549 h.observe(&post_labels, 0.5);
550
551 assert_eq!(h.count(&get_labels), 2);
552 assert_eq!(h.count(&post_labels), 1);
553 assert_eq!(h.label_combination_count(), 2);
554 }
555
556 #[test]
557 fn test_labeled_histogram_label_order_independent() {
558 let h = LabeledHistogram::new("requests", "help", vec![0.1, 1.0]);
559
560 let mut labels1 = HashMap::new();
561 labels1.insert("a".to_string(), "1".to_string());
562 labels1.insert("b".to_string(), "2".to_string());
563
564 let mut labels2 = HashMap::new();
565 labels2.insert("b".to_string(), "2".to_string());
566 labels2.insert("a".to_string(), "1".to_string());
567
568 h.observe(&labels1, 0.5);
569 assert_eq!(h.count(&labels2), 1);
571 assert_eq!(h.label_combination_count(), 1);
572 }
573
574 #[test]
575 fn test_labeled_histogram_count_missing_labels() {
576 let h = LabeledHistogram::new("requests", "help", vec![0.1, 1.0]);
577 let labels = HashMap::new();
578 assert_eq!(h.count(&labels), 0);
579 }
580
581 #[test]
582 fn test_labeled_histogram_render_contains_labels() {
583 let h = LabeledHistogram::new("requests", "request help", vec![0.1, 1.0]);
584 let mut labels = HashMap::new();
585 labels.insert("method".to_string(), "GET".to_string());
586 h.observe(&labels, 0.05);
587
588 let output = h.render();
589 assert!(output.contains("# HELP requests request help"));
590 assert!(output.contains("# TYPE requests histogram"));
591 assert!(output.contains("method=\"GET\""));
592 assert!(output.contains("requests_count"));
593 assert!(output.contains("requests_sum"));
594 }
595
596 #[test]
597 fn test_labeled_histogram_render_inf_bucket() {
598 let h = LabeledHistogram::new("req", "help", vec![0.1]);
599 let labels = HashMap::new();
600 h.observe(&labels, 0.05);
601 h.observe(&labels, 5.0);
602 let output = h.render();
603 assert!(output.contains("le=\"+Inf\""));
604 }
605
606 #[test]
609 fn test_pushgateway_config_default() {
610 let config = PushgatewayConfig::default();
611 assert_eq!(config.endpoint, "http://localhost:9091");
612 assert_eq!(config.job, "sz-orm");
613 assert!(config.instance.is_none());
614 }
615
616 #[test]
617 fn test_pushgateway_exporter_new() {
618 let exporter = PushgatewayExporter::new(PushgatewayConfig::default());
619 assert_eq!(exporter.push_count(), 0);
620 assert!(exporter.snapshots().is_empty());
621 }
622
623 #[test]
624 fn test_pushgateway_push_text() {
625 let exporter = PushgatewayExporter::new(PushgatewayConfig::default());
626 exporter.push("metric1 1\n").unwrap();
627 exporter.push("metric2 2\n").unwrap();
628
629 assert_eq!(exporter.push_count(), 2);
630 let snaps = exporter.snapshots();
631 assert_eq!(snaps.len(), 2);
632 assert_eq!(snaps[0].metrics_text, "metric1 1\n");
633 assert_eq!(snaps[1].metrics_text, "metric2 2\n");
634 }
635
636 #[test]
637 fn test_pushgateway_push_from_registry() {
638 let registry = crate::MetricsRegistry::new();
639 let counter = registry.register_counter("test_total", "test");
640 counter.inc();
641
642 let exporter = PushgatewayExporter::new(PushgatewayConfig::default());
643 exporter.push_from_registry(®istry).unwrap();
644
645 assert_eq!(exporter.push_count(), 1);
646 let snap = &exporter.snapshots()[0];
647 assert!(snap.metrics_text.contains("test_total"));
648 }
649
650 #[test]
651 fn test_pushgateway_snapshot_has_metadata() {
652 let config = PushgatewayConfig {
653 endpoint: "http://push:9091".to_string(),
654 job: "myjob".to_string(),
655 instance: Some("inst1".to_string()),
656 };
657 let exporter = PushgatewayExporter::new(config);
658 exporter.push("m 1\n").unwrap();
659
660 let snap = &exporter.snapshots()[0];
661 assert_eq!(snap.job, "myjob");
662 assert_eq!(snap.instance, Some("inst1".to_string()));
663 assert!(snap.timestamp_ms > 0);
664 }
665
666 #[test]
667 fn test_pushgateway_clear() {
668 let exporter = PushgatewayExporter::new(PushgatewayConfig::default());
669 exporter.push("m 1\n").unwrap();
670 assert_eq!(exporter.push_count(), 1);
671
672 exporter.clear();
673 assert_eq!(exporter.push_count(), 0);
674 }
675
676 #[test]
677 fn test_pushgateway_config_access() {
678 let config = PushgatewayConfig {
679 job: "custom".to_string(),
680 ..Default::default()
681 };
682 let exporter = PushgatewayExporter::new(config);
683 assert_eq!(exporter.config().job, "custom");
684 }
685}