1use std::future::Future;
7use std::sync::{Arc, Mutex};
8
9use metrics::{Counter, Gauge, Histogram, Key, KeyName, Metadata, Recorder, SharedString, Unit};
10
11pub fn with_capturing_test_client(f: impl FnOnce()) -> Vec<String> {
25 let recorder = MockRecorder::default();
26 metrics::with_local_recorder(&recorder, f);
27 recorder.consume()
28}
29
30pub async fn with_capturing_test_client_async(future: impl Future<Output = ()>) -> Vec<String> {
47 let recorder = MockRecorder::default();
48 let guard = metrics::set_default_local_recorder(&recorder);
49 future.await;
50 drop(guard);
51 recorder.consume()
52}
53
54#[derive(Clone, Default)]
56struct MockRecorder {
57 inner: Arc<Mutex<Vec<String>>>,
58}
59
60impl MockRecorder {
61 fn consume(self) -> Vec<String> {
63 self.inner
64 .lock()
65 .unwrap_or_else(|e| e.into_inner())
66 .drain(..)
67 .collect()
68 }
69}
70
71impl Recorder for MockRecorder {
72 fn describe_counter(&self, _key: KeyName, _unit: Option<Unit>, _description: SharedString) {}
73 fn describe_gauge(&self, _key: KeyName, _unit: Option<Unit>, _description: SharedString) {}
74 fn describe_histogram(&self, _key: KeyName, _unit: Option<Unit>, _description: SharedString) {}
75
76 fn register_counter(&self, key: &Key, _metadata: &Metadata<'_>) -> Counter {
77 Counter::from_arc(Arc::new(MockFn::new(key.clone(), self.inner.clone())))
78 }
79
80 fn register_gauge(&self, key: &Key, _metadata: &Metadata<'_>) -> Gauge {
81 Gauge::from_arc(Arc::new(MockFn::new(key.clone(), self.inner.clone())))
82 }
83
84 fn register_histogram(&self, key: &Key, _metadata: &Metadata<'_>) -> Histogram {
85 Histogram::from_arc(Arc::new(MockFn::new(key.clone(), self.inner.clone())))
86 }
87}
88
89struct MockFn {
91 key: Key,
92 inner: Arc<Mutex<Vec<String>>>,
93}
94
95impl MockFn {
96 fn new(key: Key, inner: Arc<Mutex<Vec<String>>>) -> Self {
97 Self { key, inner }
98 }
99
100 fn push(&self, value: &str, ty: &str) {
101 let labels = self
102 .key
103 .labels()
104 .map(|l| format!("{}:{}", l.key(), l.value()))
105 .collect::<Vec<_>>()
106 .join(",");
107
108 let entry = if labels.is_empty() {
109 format!("{}:{}|{}", self.key.name(), value, ty)
110 } else {
111 format!("{}:{}|{}|#{}", self.key.name(), value, ty, labels)
112 };
113
114 if let Ok(mut vec) = self.inner.lock() {
115 vec.push(entry);
116 }
117 }
118}
119
120impl metrics::CounterFn for MockFn {
121 fn increment(&self, value: u64) {
122 self.push(&format!("+{value}"), "c");
123 }
124
125 fn absolute(&self, value: u64) {
126 self.push(&format!("={value}"), "c");
127 }
128}
129
130impl metrics::GaugeFn for MockFn {
131 fn increment(&self, value: f64) {
132 self.push(&format!("+{value}"), "g");
133 }
134
135 fn decrement(&self, value: f64) {
136 self.push(&format!("-{value}"), "g");
137 }
138
139 fn set(&self, value: f64) {
140 self.push(&format!("{value}"), "g");
141 }
142}
143
144impl metrics::HistogramFn for MockFn {
145 fn record(&self, value: f64) {
146 self.push(&format!("{value}"), "d");
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn captures_counter() {
156 let captured = with_capturing_test_client(|| {
157 crate::count!("test.counter");
158 });
159 assert_eq!(captured.len(), 1);
160 assert_eq!(captured[0], "test.counter:+1|c");
161 }
162
163 #[test]
164 fn captures_counter_with_tags() {
165 let captured = with_capturing_test_client(|| {
166 crate::count!("test.counter", env = "prod", region = "us");
167 });
168 assert_eq!(captured.len(), 1);
169 assert_eq!(captured[0], "test.counter:+1|c|#env:prod,region:us");
170 }
171
172 #[test]
173 fn captures_gauge() {
174 let captured = with_capturing_test_client(|| {
175 crate::gauge!("test.gauge" = 42usize);
176 });
177 assert_eq!(captured.len(), 1);
178 assert_eq!(captured[0], "test.gauge:42|g");
179 }
180
181 #[test]
182 fn captures_gauge_increment() {
183 let captured = with_capturing_test_client(|| {
184 crate::gauge!("test.gauge" += 5usize);
185 });
186 assert_eq!(captured.len(), 1);
187 assert_eq!(captured[0], "test.gauge:+5|g");
188 }
189
190 #[test]
191 fn captures_gauge_decrement() {
192 let captured = with_capturing_test_client(|| {
193 crate::gauge!("test.gauge" -= 3usize);
194 });
195 assert_eq!(captured.len(), 1);
196 assert_eq!(captured[0], "test.gauge:-3|g");
197 }
198
199 #[test]
200 fn captures_distribution() {
201 let captured = with_capturing_test_client(|| {
202 crate::record!("test.dist" = 2.78f64);
203 });
204 assert_eq!(captured.len(), 1);
205 assert_eq!(captured[0], "test.dist:2.78|d");
206 }
207
208 #[test]
209 fn captures_distribution_seconds() {
210 let captured = with_capturing_test_client(|| {
211 let dur = std::time::Duration::from_millis(1500);
212 crate::record!("test.latency" = dur);
213 });
214 assert_eq!(captured.len(), 1);
215 assert_eq!(captured[0], "test.latency:1.5|d");
216 }
217
218 #[test]
219 fn captures_counter_explicit_increment() {
220 let captured = with_capturing_test_client(|| {
221 crate::count!("test.counter" += 5);
222 });
223 assert_eq!(captured.len(), 1);
224 assert_eq!(captured[0], "test.counter:+5|c");
225 }
226
227 #[test]
228 fn captures_distribution_with_tags() {
229 let captured = with_capturing_test_client(|| {
230 let dur = std::time::Duration::from_secs(2);
231 crate::record!("test.latency" = dur, route = "/v1/test", method = "GET",);
232 });
233 assert_eq!(captured.len(), 1);
234 assert_eq!(captured[0], "test.latency:2|d|#route:/v1/test,method:GET");
235 }
236
237 #[test]
238 fn timer_record_emits_success_true() {
239 let captured = with_capturing_test_client(|| {
240 let guard = crate::timer!("test.timer");
241 guard.record();
242 });
243 assert_eq!(captured.len(), 1);
244 assert!(captured[0].starts_with("test.timer:"));
245 assert!(captured[0].contains("|d|#success:true"));
246 }
247
248 #[test]
249 fn timer_drop_emits_success_false() {
250 let captured = with_capturing_test_client(|| {
251 let _guard = crate::timer!("test.timer");
252 });
253 assert_eq!(captured.len(), 1);
254 assert!(captured[0].starts_with("test.timer:"));
255 assert!(captured[0].contains("|d|#success:false"));
256 }
257
258 #[test]
259 fn timer_drop_with_success_emits_success_true() {
260 let captured = with_capturing_test_client(|| {
261 let _guard = crate::timer!("test.timer").success();
262 });
263 assert_eq!(captured.len(), 1);
264 assert!(captured[0].starts_with("test.timer:"));
265 assert!(captured[0].contains("|d|#success:true"));
266 }
267
268 #[test]
269 fn timer_with_tags() {
270 let captured = with_capturing_test_client(|| {
271 let guard = crate::timer!("test.timer", route = "/v1/test");
272 guard.record();
273 });
274 assert_eq!(captured.len(), 1);
275 assert!(captured[0].starts_with("test.timer:"));
276 assert!(captured[0].contains("route:/v1/test"));
277 assert!(captured[0].contains("success:true"));
278 }
279
280 #[test]
281 fn timer_drop_with_tags() {
282 let captured = with_capturing_test_client(|| {
283 let _guard = crate::timer!("test.timer", op = "put");
284 });
285 assert_eq!(captured.len(), 1);
286 assert!(captured[0].contains("op:put"));
287 assert!(captured[0].contains("success:false"));
288 }
289
290 #[test]
291 fn timer_deferred_tag_on_record() {
292 let captured = with_capturing_test_client(|| {
293 let guard = crate::timer!("test.timer", usecase = "test");
294 guard.tag("backend", "gcs").record();
295 });
296 assert_eq!(captured.len(), 1);
297 assert!(captured[0].contains("usecase:test"));
298 assert!(captured[0].contains("backend:gcs"));
299 assert!(captured[0].contains("success:true"));
300 }
301
302 #[test]
303 fn timer_deferred_tag_on_drop() {
304 let captured = with_capturing_test_client(|| {
305 let _guard = crate::timer!("test.timer", usecase = "test").tag("backend", "gcs");
306 });
307 assert_eq!(captured.len(), 1);
308 assert!(captured[0].contains("usecase:test"));
309 assert!(captured[0].contains("backend:gcs"));
310 assert!(captured[0].contains("success:false"));
311 }
312}