Skip to main content

camel_api/
metrics.rs

1use std::sync::{Arc, Mutex};
2use std::time::Duration;
3
4use arc_swap::ArcSwap;
5
6/// Trait for collecting metrics from the Camel runtime.
7/// Implementations can integrate with Prometheus, OpenTelemetry, etc.
8pub trait MetricsCollector: Send + Sync {
9    /// Record exchange processing time
10    fn record_exchange_duration(&self, route_id: &str, duration: Duration);
11
12    /// Increment error counter
13    fn increment_errors(&self, route_id: &str, error_type: &str);
14
15    /// Increment exchange counter
16    fn increment_exchanges(&self, route_id: &str);
17
18    /// Update queue depth
19    fn set_queue_depth(&self, route_id: &str, depth: usize);
20
21    /// Record circuit breaker state change
22    fn record_circuit_breaker_change(&self, route_id: &str, from: &str, to: &str);
23
24    /// Record a histogram observation (e.g., cost, latency distribution).
25    /// Default: no-op (backward-compatible).
26    fn record_histogram(&self, _name: &str, _value: f64, _labels: &[(&str, &str)]) {}
27
28    /// Record a monotonically-increasing counter (e.g. `foo_total`).
29    /// Default: no-op (backward-compatible).
30    fn record_counter(&self, _name: &str, _value: f64, _labels: &[(&str, &str)]) {}
31}
32
33/// No-op metrics collector for default behavior
34pub 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
44/// Sized slot around `Arc<dyn MetricsCollector>`.
45///
46/// `ArcSwap`'s `RefCnt` implementation requires a `Sized` target, so a bare
47/// `ArcSwap<dyn MetricsCollector>` does not compile; this newtype restores
48/// `Sized`-ness without changing the stored pointee.
49struct CollectorSlot(Arc<dyn MetricsCollector>);
50
51/// A late-bound [`MetricsCollector`] cell.
52///
53/// Contract:
54///
55/// - **Late binding:** a `MetricsHandle` can be handed to consumers before any real
56///   collector exists; it seeds itself with [`NoOpMetrics`] so calls before (and
57///   without) registration are safe no-ops.
58/// - **Composition, not replacement:** each [`MetricsHandle::register`] composes the
59///   new collector *over* the currently stored one (see [`CompositeMetricsCollector`]);
60///   previously registered collectors keep observing.
61/// - **Same-Arc idempotence:** registering the same collector `Arc` twice is a no-op
62///   (detected via `Arc::ptr_eq` against the membership list), so a call site that
63///   wires the same collector through two builder paths does not double-count.
64/// - **Delegation cost:** each trait-method call costs one atomic load of the stored
65///   `Arc` (`ArcSwap::load`); the hot path never clones the `Arc`.
66pub struct MetricsHandle {
67    inner: ArcSwap<CollectorSlot>,
68    /// Membership list of every accepted collector, parallel to `inner`.
69    /// Kept because the stored `dyn` composite cannot be introspected for
70    /// `Arc::ptr_eq` dedupe.
71    members: Mutex<Vec<Arc<dyn MetricsCollector>>>,
72}
73
74impl MetricsHandle {
75    /// Creates a handle that delegates to [`NoOpMetrics`] until a collector is
76    /// registered.
77    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    /// Registers `collector`, composing it over whatever is currently stored.
85    ///
86    /// If the exact same `Arc` was already registered, this is a no-op
87    /// (see *same-Arc idempotence* in the type-level docs).
88    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"); // allow-unwrap
93        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            // Store directly — composing over the seeded NoOp would leave a
100            // permanent dead leg in every later composite chain.
101            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
153/// A [`MetricsCollector`] that fans every observation out to a list of collectors,
154/// in registration order.
155///
156/// Built by [`MetricsHandle::register`] — the second registration stores a
157/// composite of `[first, second]`; a third composes over that composite, so
158/// ordering and prior observation are preserved (composition, not replacement).
159pub struct CompositeMetricsCollector {
160    collectors: Vec<Arc<dyn MetricsCollector>>,
161}
162
163impl CompositeMetricsCollector {
164    /// Creates a composite that delegates to `collectors` in order.
165    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    /// Test double that records observations for later inspection.
220    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        // All methods should execute without panicking
269        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                // In a real implementation, this would record the duration
285                println!("Route {} took {}ms", route_id, duration.as_millis());
286            }
287
288            fn increment_errors(&self, route_id: &str, error_type: &str) {
289                // In a real implementation, this would increment an error counter
290                println!("Route {} had error: {}", route_id, error_type);
291            }
292
293            fn increment_exchanges(&self, route_id: &str) {
294                // In a real implementation, this would increment an exchange counter
295                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                // In a real implementation, this would update a gauge
302                println!("Route {} queue depth: {}", route_id, depth);
303            }
304
305            fn record_circuit_breaker_change(&self, route_id: &str, from: &str, to: &str) {
306                // In a real implementation, this would record the state change
307                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        // Test that all methods work
317        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        // Note: We can't easily test the counter value without additional accessors
324        // This is just to verify the trait implementation works
325    }
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}