Skip to main content

camel_api/
component_metrics.rs

1//! `ComponentMetrics` — lever-gated facade for the uniform
2//! component-operations family (dashboard-observability Task 4.1).
3//!
4//! Components call [`ComponentMetrics::observe`] at their principal
5//! operation boundary. The facade owns two concerns so individual
6//! components do not:
7//!
8//! - **Lever gating:** the `[observability.metrics].components` lever
9//!   (default off, metrics-configuration Req 3) suppresses only the
10//!   `camel_component_operations_total` family.
11//! - **Unconditional error forwarding:** failures always increment the
12//!   non-disableable error family (`camel_errors_total`) as
13//!   `increment_errors(component, "e:{component}:{operation}")` — never
14//!   lever-gated (metrics-configuration Req 2).
15//!
16//! The lever arrives as a plain `bool` because `camel-api` cannot depend
17//! on `camel-core`, where `MetricsLeversConfig` lives: the construction
18//! site (camel-core, via the `RuntimeObservability` blanket impl)
19//! snapshots the current levers into the facade at build time.
20
21use std::sync::Arc;
22
23use crate::metrics::MetricsCollector;
24
25/// Facade over a [`MetricsCollector`] for uniform component-operation
26/// emission. Construct via `RuntimeObservability::component_metrics()`
27/// or directly in tests.
28pub struct ComponentMetrics {
29    collector: Arc<dyn MetricsCollector>,
30    components_enabled: bool,
31}
32
33impl ComponentMetrics {
34    /// Builds a facade over `collector`; `components_enabled` is the
35    /// snapshot of the `[observability.metrics].components` lever taken
36    /// at construction time.
37    pub fn new(collector: Arc<dyn MetricsCollector>, components_enabled: bool) -> Self {
38        Self {
39            collector,
40            components_enabled,
41        }
42    }
43
44    /// Observes one component operation. `failed` selects the closed-set
45    /// outcome label ("failure"/"success") and, when true, unconditionally
46    /// forwards to the error family with the `e:{component}:{operation}`
47    /// label — error-family emission is never lever-gated.
48    pub fn observe(&self, component: &str, operation: &str, failed: bool) {
49        if failed {
50            self.collector
51                .increment_errors(component, &format!("e:{component}:{operation}"));
52        }
53        if self.components_enabled {
54            // allow-open-label rc-gm6s (component/operation: caller-bounded literals through the facade; outcome is a two-literal if/else)
55            self.collector.record_component_operation(
56                component,
57                operation,
58                if failed { "failure" } else { "success" },
59            );
60        }
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67    use crate::metrics::MetricsCollector;
68    use std::sync::{Arc, Mutex};
69    use std::time::Duration;
70
71    /// Recording double capturing error-family and component-op emissions.
72    struct RecordingComponentMetrics {
73        errors: Mutex<Vec<(String, String)>>,
74        ops: Mutex<Vec<(String, String, String)>>,
75    }
76
77    impl RecordingComponentMetrics {
78        fn new() -> Self {
79            Self {
80                errors: Mutex::new(Vec::new()),
81                ops: Mutex::new(Vec::new()),
82            }
83        }
84    }
85
86    impl MetricsCollector for RecordingComponentMetrics {
87        fn record_exchange_duration(&self, _route_id: &str, _duration: Duration) {}
88        fn increment_errors(&self, route_id: &str, error_type: &str) {
89            self.errors
90                .lock()
91                .expect("errors lock")
92                .push((route_id.to_string(), error_type.to_string()));
93        }
94        fn increment_exchanges(&self, _route_id: &str) {}
95        fn set_queue_depth(&self, _queue: &str, _depth: usize) {}
96        fn record_circuit_breaker_change(&self, _route_id: &str, _from: &str, _to: &str) {}
97        fn record_component_operation(&self, component: &str, operation: &str, outcome: &str) {
98            self.ops.lock().expect("ops lock").push((
99                component.to_string(),
100                operation.to_string(),
101                outcome.to_string(),
102            ));
103        }
104    }
105
106    /// Task 4.1: the components lever gates ONLY the component-operations
107    /// family; error-family forwarding is unconditional.
108    #[test]
109    fn facade_gates_components_not_errors() {
110        // Lever OFF: a failed observe forwards to the error family and
111        // records no component-op.
112        let off_collector = Arc::new(RecordingComponentMetrics::new());
113        let off = ComponentMetrics::new(
114            Arc::clone(&off_collector) as Arc<dyn MetricsCollector>,
115            false,
116        );
117        off.observe("redis", "command", true);
118        assert_eq!(
119            off_collector.errors.lock().expect("errors lock").clone(),
120            vec![("redis".to_string(), "e:redis:command".to_string())],
121            "failure must hit the error family with the lever off"
122        );
123        assert!(
124            off_collector.ops.lock().expect("ops lock").is_empty(),
125            "component ops must be suppressed with the lever off"
126        );
127
128        // Lever ON: both outcomes recorded; the failure ALSO increments
129        // errors (error family is never lever-gated).
130        let on_collector = Arc::new(RecordingComponentMetrics::new());
131        let on =
132            ComponentMetrics::new(Arc::clone(&on_collector) as Arc<dyn MetricsCollector>, true);
133        on.observe("redis", "command", false);
134        on.observe("redis", "command", true);
135        assert_eq!(
136            on_collector.ops.lock().expect("ops lock").clone(),
137            vec![
138                (
139                    "redis".to_string(),
140                    "command".to_string(),
141                    "success".to_string()
142                ),
143                (
144                    "redis".to_string(),
145                    "command".to_string(),
146                    "failure".to_string()
147                ),
148            ],
149            "lever on must record both outcomes"
150        );
151        assert_eq!(
152            on_collector.errors.lock().expect("errors lock").clone(),
153            vec![("redis".to_string(), "e:redis:command".to_string())],
154            "failure must still increment errors with the lever on"
155        );
156    }
157}