1use std::sync::Mutex;
4
5use crate::metrics::MetricsSink;
6
7#[derive(Debug, Default)]
9pub struct RecordingMetrics {
10 counters: Mutex<Vec<CounterEvent>>,
11 histograms: Mutex<Vec<HistogramEvent>>,
12 gauges: Mutex<Vec<GaugeEvent>>,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct CounterEvent {
18 pub name: String,
20 pub value: u64,
22 pub labels: Vec<(String, String)>,
24}
25
26#[derive(Debug, Clone, PartialEq)]
28pub struct HistogramEvent {
29 pub name: String,
31 pub value: f64,
33 pub labels: Vec<(String, String)>,
35}
36
37#[derive(Debug, Clone, PartialEq)]
39pub struct GaugeEvent {
40 pub name: String,
42 pub value: f64,
44 pub labels: Vec<(String, String)>,
46}
47
48impl RecordingMetrics {
49 #[must_use]
51 pub fn new() -> Self {
52 Self::default()
53 }
54
55 #[must_use]
57 pub fn counters(&self) -> Vec<CounterEvent> {
58 self.counters
59 .lock()
60 .unwrap_or_else(std::sync::PoisonError::into_inner)
61 .clone()
62 }
63
64 #[must_use]
66 pub fn histograms(&self) -> Vec<HistogramEvent> {
67 self.histograms
68 .lock()
69 .unwrap_or_else(std::sync::PoisonError::into_inner)
70 .clone()
71 }
72
73 #[must_use]
75 pub fn counter_sum(&self, name: &str) -> u64 {
76 self.counters()
77 .into_iter()
78 .filter(|e| e.name == name)
79 .map(|e| e.value)
80 .sum()
81 }
82
83 #[must_use]
85 pub fn saw(&self, name: &str) -> bool {
86 self.counters().iter().any(|e| e.name == name)
87 || self.histograms().iter().any(|e| e.name == name)
88 || self
89 .gauges
90 .lock()
91 .unwrap_or_else(std::sync::PoisonError::into_inner)
92 .iter()
93 .any(|e| e.name == name)
94 }
95}
96
97impl MetricsSink for RecordingMetrics {
98 fn counter(&self, name: &str, value: u64, labels: &[(&str, &str)]) {
99 self.counters
100 .lock()
101 .unwrap_or_else(std::sync::PoisonError::into_inner)
102 .push(CounterEvent {
103 name: name.to_owned(),
104 value,
105 labels: owned_labels(labels),
106 });
107 }
108
109 fn histogram(&self, name: &str, value: f64, labels: &[(&str, &str)]) {
110 self.histograms
111 .lock()
112 .unwrap_or_else(std::sync::PoisonError::into_inner)
113 .push(HistogramEvent {
114 name: name.to_owned(),
115 value,
116 labels: owned_labels(labels),
117 });
118 }
119
120 fn gauge(&self, name: &str, value: f64, labels: &[(&str, &str)]) {
121 self.gauges
122 .lock()
123 .unwrap_or_else(std::sync::PoisonError::into_inner)
124 .push(GaugeEvent {
125 name: name.to_owned(),
126 value,
127 labels: owned_labels(labels),
128 });
129 }
130}
131
132fn owned_labels(labels: &[(&str, &str)]) -> Vec<(String, String)> {
133 labels
134 .iter()
135 .map(|(k, v)| ((*k).to_owned(), (*v).to_owned()))
136 .collect()
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142 use crate::metrics::{METRIC_TURNS_TOTAL, record_turn};
143
144 #[test]
145 fn captures_turn() {
146 let rec = RecordingMetrics::new();
147 record_turn(&rec, "ok", 2, 5.0);
148 assert!(rec.saw(METRIC_TURNS_TOTAL));
149 assert_eq!(rec.counter_sum(METRIC_TURNS_TOTAL), 1);
150 assert!(!rec.histograms().is_empty());
151 }
152}