objectstore_metrics/
mock.rs

1//! Mock metrics recorder for tests.
2//!
3//! Provides [`with_capturing_test_client`], which installs a thread-local
4//! recorder that captures all emitted metrics as DogStatsD-format strings.
5
6use std::sync::{Arc, Mutex};
7
8use metrics::{Counter, Gauge, Histogram, Key, KeyName, Metadata, Recorder, SharedString, Unit};
9
10/// Runs `f` with a thread-local mock recorder installed, then returns all
11/// captured metrics as `"name:value|type|#key:value,key:value"` strings.
12///
13/// Only affects the calling thread — safe for use in parallel tests.
14///
15/// # Example
16///
17/// ```ignore
18/// let captured = objectstore_metrics::with_capturing_test_client(|| {
19///     objectstore_metrics::counter!("test.counter": 1, "tag" => "val");
20/// });
21/// assert!(captured.iter().any(|m| m.starts_with("test.counter:")));
22/// ```
23pub fn with_capturing_test_client(f: impl FnOnce()) -> Vec<String> {
24    let recorder = MockRecorder::default();
25    metrics::with_local_recorder(&recorder, f);
26    recorder.consume()
27}
28
29/// A metrics recorder that formats and stores every operation as a string.
30#[derive(Clone, Default)]
31struct MockRecorder {
32    inner: Arc<Mutex<Vec<String>>>,
33}
34
35impl MockRecorder {
36    /// Drains and returns all captured metric strings.
37    fn consume(self) -> Vec<String> {
38        self.inner
39            .lock()
40            .unwrap_or_else(|e| e.into_inner())
41            .drain(..)
42            .collect()
43    }
44}
45
46impl Recorder for MockRecorder {
47    fn describe_counter(&self, _key: KeyName, _unit: Option<Unit>, _description: SharedString) {}
48    fn describe_gauge(&self, _key: KeyName, _unit: Option<Unit>, _description: SharedString) {}
49    fn describe_histogram(&self, _key: KeyName, _unit: Option<Unit>, _description: SharedString) {}
50
51    fn register_counter(&self, key: &Key, _metadata: &Metadata<'_>) -> Counter {
52        Counter::from_arc(Arc::new(MockFn::new(key.clone(), self.inner.clone())))
53    }
54
55    fn register_gauge(&self, key: &Key, _metadata: &Metadata<'_>) -> Gauge {
56        Gauge::from_arc(Arc::new(MockFn::new(key.clone(), self.inner.clone())))
57    }
58
59    fn register_histogram(&self, key: &Key, _metadata: &Metadata<'_>) -> Histogram {
60        Histogram::from_arc(Arc::new(MockFn::new(key.clone(), self.inner.clone())))
61    }
62}
63
64/// Shared implementation for all metric types that formats and records operations.
65struct MockFn {
66    key: Key,
67    inner: Arc<Mutex<Vec<String>>>,
68}
69
70impl MockFn {
71    fn new(key: Key, inner: Arc<Mutex<Vec<String>>>) -> Self {
72        Self { key, inner }
73    }
74
75    fn push(&self, value: &str, ty: &str) {
76        let labels = self
77            .key
78            .labels()
79            .map(|l| format!("{}:{}", l.key(), l.value()))
80            .collect::<Vec<_>>()
81            .join(",");
82
83        let entry = if labels.is_empty() {
84            format!("{}:{}|{}", self.key.name(), value, ty)
85        } else {
86            format!("{}:{}|{}|#{}", self.key.name(), value, ty, labels)
87        };
88
89        if let Ok(mut vec) = self.inner.lock() {
90            vec.push(entry);
91        }
92    }
93}
94
95impl metrics::CounterFn for MockFn {
96    fn increment(&self, value: u64) {
97        self.push(&format!("+{value}"), "c");
98    }
99
100    fn absolute(&self, value: u64) {
101        self.push(&format!("={value}"), "c");
102    }
103}
104
105impl metrics::GaugeFn for MockFn {
106    fn increment(&self, value: f64) {
107        self.push(&format!("+{value}"), "g");
108    }
109
110    fn decrement(&self, value: f64) {
111        self.push(&format!("-{value}"), "g");
112    }
113
114    fn set(&self, value: f64) {
115        self.push(&format!("{value}"), "g");
116    }
117}
118
119impl metrics::HistogramFn for MockFn {
120    fn record(&self, value: f64) {
121        self.push(&format!("{value}"), "d");
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[test]
130    fn captures_counter() {
131        let captured = with_capturing_test_client(|| {
132            crate::counter!("test.counter": 1);
133        });
134        assert_eq!(captured.len(), 1);
135        assert_eq!(captured[0], "test.counter:+1|c");
136    }
137
138    #[test]
139    fn captures_counter_with_tags() {
140        let captured = with_capturing_test_client(|| {
141            crate::counter!("test.counter": 1, "env" => "prod", "region" => "us");
142        });
143        assert_eq!(captured.len(), 1);
144        assert_eq!(captured[0], "test.counter:+1|c|#env:prod,region:us");
145    }
146
147    #[test]
148    fn captures_gauge() {
149        let captured = with_capturing_test_client(|| {
150            crate::gauge!("test.gauge": 42usize);
151        });
152        assert_eq!(captured.len(), 1);
153        assert_eq!(captured[0], "test.gauge:42|g");
154    }
155
156    #[test]
157    fn captures_gauge_bytes() {
158        let captured = with_capturing_test_client(|| {
159            crate::gauge!("test.gauge"@b: 1024u64);
160        });
161        assert_eq!(captured.len(), 1);
162        assert_eq!(captured[0], "test.gauge:1024|g");
163    }
164
165    #[test]
166    fn captures_distribution() {
167        let captured = with_capturing_test_client(|| {
168            crate::distribution!("test.dist": 2.78f64);
169        });
170        assert_eq!(captured.len(), 1);
171        assert_eq!(captured[0], "test.dist:2.78|d");
172    }
173
174    #[test]
175    fn captures_distribution_seconds() {
176        let captured = with_capturing_test_client(|| {
177            let dur = std::time::Duration::from_millis(1500);
178            crate::distribution!("test.latency"@s: dur);
179        });
180        assert_eq!(captured.len(), 1);
181        assert_eq!(captured[0], "test.latency:1.5|d");
182    }
183
184    #[test]
185    fn captures_distribution_bytes() {
186        let captured = with_capturing_test_client(|| {
187            crate::distribution!("test.size"@b: 4096u64);
188        });
189        assert_eq!(captured.len(), 1);
190        assert_eq!(captured[0], "test.size:4096|d");
191    }
192
193    #[test]
194    fn captures_distribution_with_tags() {
195        let captured = with_capturing_test_client(|| {
196            let dur = std::time::Duration::from_secs(2);
197            crate::distribution!(
198                "test.latency"@s: dur,
199                "route" => "/v1/test",
200                "method" => "GET"
201            );
202        });
203        assert_eq!(captured.len(), 1);
204        assert_eq!(captured[0], "test.latency:2|d|#route:/v1/test,method:GET");
205    }
206
207    #[test]
208    fn integer_tag_values() {
209        let captured = with_capturing_test_client(|| {
210            crate::counter!("test.status": 1, "status" => 200u16);
211        });
212        assert_eq!(captured.len(), 1);
213        assert_eq!(captured[0], "test.status:+1|c|#status:200");
214    }
215}