1use std::sync::{Arc, Mutex};
2use std::time::Duration;
3
4use arc_swap::ArcSwap;
5
6pub trait MetricsCollector: Send + Sync {
9 fn record_exchange_duration(&self, route_id: &str, duration: Duration);
11
12 fn increment_errors(&self, route_id: &str, error_type: &str);
14
15 fn increment_exchanges(&self, route_id: &str);
17
18 fn set_queue_depth(&self, route_id: &str, depth: usize);
20
21 fn record_circuit_breaker_change(&self, route_id: &str, from: &str, to: &str);
23
24 fn record_histogram(&self, _name: &str, _value: f64, _labels: &[(&str, &str)]) {}
27
28 fn record_counter(&self, _name: &str, _value: f64, _labels: &[(&str, &str)]) {}
31}
32
33pub struct NoOpMetrics;
35
36impl MetricsCollector for NoOpMetrics {
37 fn record_exchange_duration(&self, _route_id: &str, _duration: Duration) {}
38 fn increment_errors(&self, _route_id: &str, _error_type: &str) {}
39 fn increment_exchanges(&self, _route_id: &str) {}
40 fn set_queue_depth(&self, _route_id: &str, _depth: usize) {}
41 fn record_circuit_breaker_change(&self, _route_id: &str, _from: &str, _to: &str) {}
42}
43
44struct CollectorSlot(Arc<dyn MetricsCollector>);
50
51pub struct MetricsHandle {
67 inner: ArcSwap<CollectorSlot>,
68 members: Mutex<Vec<Arc<dyn MetricsCollector>>>,
72}
73
74impl MetricsHandle {
75 pub fn new() -> Self {
78 Self {
79 inner: ArcSwap::from_pointee(CollectorSlot(Arc::new(NoOpMetrics))),
80 members: Mutex::new(Vec::new()),
81 }
82 }
83
84 pub fn register(&self, collector: Arc<dyn MetricsCollector>) {
89 let mut members = self
90 .members
91 .lock()
92 .expect("metrics members lock poisoned by a panicked register"); if members.iter().any(|m| Arc::ptr_eq(m, &collector)) {
94 return;
95 }
96 let first = members.is_empty();
97 members.push(Arc::clone(&collector));
98 if first {
99 self.inner.store(Arc::new(CollectorSlot(collector)));
102 return;
103 }
104 let prev = Arc::clone(&self.inner.load().0);
105 self.inner.store(Arc::new(CollectorSlot(Arc::new(
106 CompositeMetricsCollector::new(vec![prev, collector]),
107 ))));
108 }
109}
110
111impl Default for MetricsHandle {
112 fn default() -> Self {
113 Self::new()
114 }
115}
116
117impl MetricsCollector for MetricsHandle {
118 fn record_exchange_duration(&self, route_id: &str, duration: Duration) {
119 self.inner
120 .load()
121 .0
122 .record_exchange_duration(route_id, duration)
123 }
124
125 fn increment_errors(&self, route_id: &str, error_type: &str) {
126 self.inner.load().0.increment_errors(route_id, error_type)
127 }
128
129 fn increment_exchanges(&self, route_id: &str) {
130 self.inner.load().0.increment_exchanges(route_id)
131 }
132
133 fn set_queue_depth(&self, route_id: &str, depth: usize) {
134 self.inner.load().0.set_queue_depth(route_id, depth)
135 }
136
137 fn record_circuit_breaker_change(&self, route_id: &str, from: &str, to: &str) {
138 self.inner
139 .load()
140 .0
141 .record_circuit_breaker_change(route_id, from, to)
142 }
143
144 fn record_histogram(&self, name: &str, value: f64, labels: &[(&str, &str)]) {
145 self.inner.load().0.record_histogram(name, value, labels)
146 }
147
148 fn record_counter(&self, name: &str, value: f64, labels: &[(&str, &str)]) {
149 self.inner.load().0.record_counter(name, value, labels)
150 }
151}
152
153pub struct CompositeMetricsCollector {
160 collectors: Vec<Arc<dyn MetricsCollector>>,
161}
162
163impl CompositeMetricsCollector {
164 pub fn new(collectors: Vec<Arc<dyn MetricsCollector>>) -> Self {
166 Self { collectors }
167 }
168}
169
170impl MetricsCollector for CompositeMetricsCollector {
171 fn record_exchange_duration(&self, route_id: &str, duration: Duration) {
172 for collector in &self.collectors {
173 collector.record_exchange_duration(route_id, duration);
174 }
175 }
176
177 fn increment_errors(&self, route_id: &str, error_type: &str) {
178 for collector in &self.collectors {
179 collector.increment_errors(route_id, error_type);
180 }
181 }
182
183 fn increment_exchanges(&self, route_id: &str) {
184 for collector in &self.collectors {
185 collector.increment_exchanges(route_id);
186 }
187 }
188
189 fn set_queue_depth(&self, route_id: &str, depth: usize) {
190 for collector in &self.collectors {
191 collector.set_queue_depth(route_id, depth);
192 }
193 }
194
195 fn record_circuit_breaker_change(&self, route_id: &str, from: &str, to: &str) {
196 for collector in &self.collectors {
197 collector.record_circuit_breaker_change(route_id, from, to);
198 }
199 }
200
201 fn record_histogram(&self, name: &str, value: f64, labels: &[(&str, &str)]) {
202 for collector in &self.collectors {
203 collector.record_histogram(name, value, labels);
204 }
205 }
206
207 fn record_counter(&self, name: &str, value: f64, labels: &[(&str, &str)]) {
208 for collector in &self.collectors {
209 collector.record_counter(name, value, labels);
210 }
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217 use std::sync::{Arc, Mutex};
218
219 struct RecordingMetrics {
221 durations: Mutex<Vec<(String, Duration)>>,
222 errors: Mutex<Vec<(String, String)>>,
223 exchanges: Mutex<Vec<String>>,
224 }
225
226 impl RecordingMetrics {
227 fn new() -> Self {
228 Self {
229 durations: Mutex::new(Vec::new()),
230 errors: Mutex::new(Vec::new()),
231 exchanges: Mutex::new(Vec::new()),
232 }
233 }
234 }
235
236 impl MetricsCollector for RecordingMetrics {
237 fn record_exchange_duration(&self, route_id: &str, duration: Duration) {
238 self.durations
239 .lock()
240 .expect("durations lock")
241 .push((route_id.to_string(), duration));
242 }
243
244 fn increment_errors(&self, route_id: &str, error_type: &str) {
245 self.errors
246 .lock()
247 .expect("errors lock")
248 .push((route_id.to_string(), error_type.to_string()));
249 }
250
251 fn increment_exchanges(&self, route_id: &str) {
252 self.exchanges
253 .lock()
254 .expect("exchanges lock")
255 .push(route_id.to_string());
256 }
257
258 fn set_queue_depth(&self, _route_id: &str, _depth: usize) {}
259
260 fn record_circuit_breaker_change(&self, _route_id: &str, _from: &str, _to: &str) {}
261 }
262
263 #[test]
264 fn test_noop_metrics_implements_trait() {
265 let metrics = NoOpMetrics;
266 let metrics_arc: Arc<dyn MetricsCollector> = Arc::new(metrics);
267
268 metrics_arc.record_exchange_duration("test-route", Duration::from_millis(100));
270 metrics_arc.increment_errors("test-route", "test-error");
271 metrics_arc.increment_exchanges("test-route");
272 metrics_arc.set_queue_depth("test-route", 5);
273 metrics_arc.record_circuit_breaker_change("test-route", "closed", "open");
274 }
275
276 #[test]
277 fn test_custom_metrics_collector() {
278 struct TestMetrics {
279 exchange_count: std::sync::atomic::AtomicU64,
280 }
281
282 impl MetricsCollector for TestMetrics {
283 fn record_exchange_duration(&self, route_id: &str, duration: Duration) {
284 println!("Route {} took {}ms", route_id, duration.as_millis());
286 }
287
288 fn increment_errors(&self, route_id: &str, error_type: &str) {
289 println!("Route {} had error: {}", route_id, error_type);
291 }
292
293 fn increment_exchanges(&self, route_id: &str) {
294 self.exchange_count
296 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
297 println!("Route {} processed exchange", route_id);
298 }
299
300 fn set_queue_depth(&self, route_id: &str, depth: usize) {
301 println!("Route {} queue depth: {}", route_id, depth);
303 }
304
305 fn record_circuit_breaker_change(&self, route_id: &str, from: &str, to: &str) {
306 println!("Route {} circuit breaker: {} -> {}", route_id, from, to);
308 }
309 }
310
311 let test_metrics = TestMetrics {
312 exchange_count: std::sync::atomic::AtomicU64::new(0),
313 };
314 let metrics_arc: Arc<dyn MetricsCollector> = Arc::new(test_metrics);
315
316 metrics_arc.record_exchange_duration("test-route", Duration::from_millis(100));
318 metrics_arc.increment_errors("test-route", "test-error");
319 metrics_arc.increment_exchanges("test-route");
320 metrics_arc.set_queue_depth("test-route", 5);
321 metrics_arc.record_circuit_breaker_change("test-route", "closed", "open");
322
323 }
326
327 #[test]
328 fn handle_delegates_to_stored_collector() {
329 let collector = Arc::new(RecordingMetrics::new());
330 let handle = MetricsHandle::new();
331 handle.register(collector.clone());
332
333 handle.record_exchange_duration("r", Duration::from_millis(1));
334
335 let recorded = collector.durations.lock().expect("durations lock").clone();
336 assert_eq!(recorded, vec![("r".to_string(), Duration::from_millis(1))]);
337 }
338
339 #[test]
340 fn second_registration_composes_both_observe() {
341 let a = Arc::new(RecordingMetrics::new());
342 let b = Arc::new(RecordingMetrics::new());
343 let handle = MetricsHandle::new();
344 handle.register(a.clone());
345 handle.register(b.clone());
346
347 handle.increment_errors("r", "x");
348
349 let a_errors = a.errors.lock().expect("errors lock").clone();
350 let b_errors = b.errors.lock().expect("errors lock").clone();
351 assert_eq!(a_errors, vec![("r".to_string(), "x".to_string())]);
352 assert_eq!(b_errors, vec![("r".to_string(), "x".to_string())]);
353 }
354
355 #[test]
356 fn register_same_arc_is_idempotent() {
357 let a = Arc::new(RecordingMetrics::new());
358 let handle = MetricsHandle::new();
359 handle.register(a.clone());
360 handle.register(a.clone());
361
362 handle.increment_exchanges("r");
363
364 let recorded = a.exchanges.lock().expect("exchanges lock").clone();
365 assert_eq!(recorded, vec!["r".to_string()]);
366 }
367
368 #[test]
369 fn handle_defaults_to_noop() {
370 let handle = MetricsHandle::new();
371 handle.record_exchange_duration("r", Duration::from_millis(1));
372 handle.increment_errors("r", "x");
373 handle.increment_exchanges("r");
374 handle.set_queue_depth("r", 5);
375 handle.record_circuit_breaker_change("r", "closed", "open");
376 handle.record_histogram("h", 1.0, &[("k", "v")]);
377 handle.record_counter("c", 1.0, &[("k", "v")]);
378 }
379}