Skip to main content

camel_api/
component_metrics.rs

1//! `ComponentMetrics` — component-scoped facade for the uniform
2//! component-operations family.
3//!
4//! Each handle fixes its component identity at construction; call sites
5//! only supply the operation and outcome. The facade owns two concerns so
6//! individual components do not:
7//!
8//! - **Lever gating:** the effective per-component switch
9//!   ([`ComponentMetricsLevers::enabled_for`]) 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//! [`ComponentMetricsLevers`] is the effective per-component switch
17//! snapshot. It arrives pre-resolved because `camel-api` cannot depend on
18//! `camel-core`, where `MetricsLeversConfig` lives: the construction site
19//! (camel-core, via the `RuntimeObservability` blanket impl) snapshots the
20//! current levers into the handle at build time.
21
22use std::collections::BTreeMap;
23use std::sync::Arc;
24
25use crate::metrics::MetricsCollector;
26
27/// Facade over a [`MetricsCollector`] for uniform component-operation
28/// emission. Construct via `RuntimeObservability::component_metrics(component)`
29/// or directly in tests.
30///
31/// Cheap to clone: the collector sits behind an `Arc`, so one facade can
32/// be shared across objects built from the same connection (e.g. a
33/// repository and its payload store).
34#[derive(Clone)]
35pub struct ComponentMetrics {
36    collector: Arc<dyn MetricsCollector>,
37    component: Arc<str>,
38    enabled: bool,
39}
40
41impl ComponentMetrics {
42    /// Builds a facade over `collector` with the component identity fixed
43    /// to `component`; `enabled` is the effective per-component switch
44    /// ([`ComponentMetricsLevers::enabled_for`]) snapshot taken at
45    /// construction time.
46    pub fn new(
47        collector: Arc<dyn MetricsCollector>,
48        component: impl Into<Arc<str>>,
49        enabled: bool,
50    ) -> Self {
51        Self {
52            collector,
53            component: component.into(),
54            enabled,
55        }
56    }
57
58    /// Returns the component identity fixed at construction.
59    pub fn component(&self) -> &str {
60        &self.component
61    }
62
63    /// Observes one component operation for the fixed identity. `failed`
64    /// selects the closed-set outcome label ("failure"/"success") and,
65    /// when true, unconditionally forwards to the error family with the
66    /// `e:{component}:{operation}` label — error-family emission is never
67    /// lever-gated.
68    pub fn observe(&self, operation: &str, failed: bool) {
69        if failed {
70            // allow-open-label rc-otxh (facade builds the e:{component}:{operation} label per ADR-0012; names bounded at observe() call sites)
71            self.collector.increment_errors(
72                &self.component,
73                &format!("e:{}:{operation}", self.component),
74            );
75        }
76        if self.enabled {
77            // allow-open-label rc-gm6s (component/operation: identity is fixed at construction and operation is a caller-bounded literal; outcome is a two-literal if/else)
78            self.collector.record_component_operation(
79                &self.component,
80                operation,
81                if failed { "failure" } else { "success" },
82            );
83        }
84    }
85}
86
87/// Effective per-component switch snapshot for the uniform
88/// component-operations family.
89///
90/// `enabled` is the master switch and `components` is the coarse default;
91/// an override wins over `components` in both directions but can never
92/// resurrect the family under the master switch:
93///
94/// ```text
95/// enabled_for(C) = enabled && overrides.get(C).unwrap_or(components)
96/// ```
97///
98/// Cloning is cheap: the overrides map is shared behind an `Arc`.
99#[derive(Clone, Debug)]
100pub struct ComponentMetricsLevers {
101    enabled: bool,
102    components: bool,
103    overrides: Arc<BTreeMap<String, bool>>,
104}
105
106impl ComponentMetricsLevers {
107    /// Builds a snapshot from the resolved master switch, the component
108    /// default, and the per-component overrides.
109    pub fn new(enabled: bool, components: bool, overrides: BTreeMap<String, bool>) -> Self {
110        Self {
111            enabled,
112            components,
113            overrides: Arc::new(overrides),
114        }
115    }
116
117    /// Builds a snapshot with no overrides: `components=true` enables the
118    /// family for every component, `components=false` disables it
119    /// everywhere (including the master switch).
120    pub fn uniform(components: bool) -> Self {
121        Self {
122            enabled: components,
123            components,
124            overrides: Arc::new(BTreeMap::new()),
125        }
126    }
127
128    /// Resolves the effective switch for `component`.
129    pub fn enabled_for(&self, component: &str) -> bool {
130        self.enabled
131            && self
132                .overrides
133                .get(component)
134                .copied()
135                .unwrap_or(self.components)
136    }
137}
138
139impl Default for ComponentMetricsLevers {
140    fn default() -> Self {
141        Self::new(false, false, BTreeMap::new())
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148    use crate::metrics::MetricsCollector;
149    use crate::metrics::MetricsHandle;
150    use crate::metrics::NoOpMetrics;
151    use std::sync::Arc;
152    use std::sync::Mutex;
153    use std::time::Duration;
154
155    /// Recording double capturing error-family and component-op emissions.
156    struct RecordingComponentMetrics {
157        errors: Mutex<Vec<(String, String)>>,
158        ops: Mutex<Vec<(String, String, String)>>,
159    }
160
161    impl RecordingComponentMetrics {
162        fn new() -> Self {
163            Self {
164                errors: Mutex::new(Vec::new()),
165                ops: Mutex::new(Vec::new()),
166            }
167        }
168    }
169
170    impl MetricsCollector for RecordingComponentMetrics {
171        fn record_exchange_duration(&self, _route_id: &str, _duration: Duration) {}
172        fn increment_errors(&self, route_id: &str, error_type: &str) {
173            self.errors
174                .lock()
175                .expect("errors lock")
176                .push((route_id.to_string(), error_type.to_string()));
177        }
178        fn increment_exchanges(&self, _route_id: &str) {}
179        fn set_queue_depth(&self, _queue: &str, _depth: usize) {}
180        fn record_circuit_breaker_change(&self, _route_id: &str, _from: &str, _to: &str) {}
181        fn record_component_operation(&self, component: &str, operation: &str, outcome: &str) {
182            self.ops.lock().expect("ops lock").push((
183                component.to_string(),
184                operation.to_string(),
185                outcome.to_string(),
186            ));
187        }
188    }
189
190    impl RecordingComponentMetrics {
191        fn errors(&self) -> Vec<(String, String)> {
192            self.errors.lock().expect("errors lock").clone()
193        }
194
195        fn ops(&self) -> Vec<(String, String, String)> {
196            self.ops.lock().expect("ops lock").clone()
197        }
198    }
199
200    fn recording() -> (Arc<RecordingComponentMetrics>, Arc<dyn MetricsCollector>) {
201        let collector = Arc::new(RecordingComponentMetrics::new());
202        let erased = Arc::clone(&collector) as Arc<dyn MetricsCollector>;
203        (collector, erased)
204    }
205
206    #[test]
207    fn scoped_handle_records_fixed_component() {
208        let (collector, erased) = recording();
209        let metrics = ComponentMetrics::new(erased, "redis", true);
210
211        assert_eq!(metrics.component(), "redis");
212        metrics.observe("command", false);
213        metrics.observe("command", false);
214
215        assert_eq!(
216            collector.ops(),
217            vec![
218                (
219                    "redis".to_string(),
220                    "command".to_string(),
221                    "success".to_string()
222                );
223                2
224            ],
225            "both observations must carry the fixed component identity"
226        );
227        assert!(
228            collector.errors().is_empty(),
229            "successful observations must not touch the error family"
230        );
231    }
232
233    #[test]
234    fn lever_off_gates_operations_not_errors() {
235        let (collector, erased) = recording();
236        let metrics = ComponentMetrics::new(erased, "redis", false);
237
238        metrics.observe("command", true);
239
240        assert_eq!(
241            collector.errors(),
242            vec![("redis".to_string(), "e:redis:command".to_string())],
243            "failure must hit the error family with the lever off"
244        );
245        assert!(
246            collector.ops().is_empty(),
247            "component ops must be suppressed with the lever off"
248        );
249    }
250
251    #[test]
252    fn lever_on_records_both_outcomes() {
253        let (collector, erased) = recording();
254        let metrics = ComponentMetrics::new(erased, "redis", true);
255
256        metrics.observe("command", false);
257        metrics.observe("command", true);
258
259        assert_eq!(
260            collector.ops(),
261            vec![
262                (
263                    "redis".to_string(),
264                    "command".to_string(),
265                    "success".to_string()
266                ),
267                (
268                    "redis".to_string(),
269                    "command".to_string(),
270                    "failure".to_string()
271                ),
272            ],
273            "lever on must record both outcomes in order"
274        );
275        assert_eq!(
276            collector.errors(),
277            vec![("redis".to_string(), "e:redis:command".to_string())],
278            "failure must still increment errors with the lever on"
279        );
280    }
281
282    #[test]
283    fn levers_enabled_for_precedence() {
284        let levers =
285            ComponentMetricsLevers::new(true, false, BTreeMap::from([("http".to_string(), true)]));
286        assert!(
287            levers.enabled_for("http"),
288            "override must win over components=false"
289        );
290        assert!(
291            !levers.enabled_for("kafka"),
292            "absent override inherits components=false"
293        );
294
295        let levers =
296            ComponentMetricsLevers::new(false, true, BTreeMap::from([("http".to_string(), true)]));
297        assert!(
298            !levers.enabled_for("http"),
299            "master switch off must beat an enabled override"
300        );
301        assert!(
302            !levers.enabled_for("kafka"),
303            "master switch off must suppress everything"
304        );
305    }
306
307    #[test]
308    fn levers_uniform_off() {
309        let levers = ComponentMetricsLevers::uniform(false);
310        assert!(
311            !levers.enabled_for("http"),
312            "uniform(false) must disable the family for every component"
313        );
314    }
315
316    #[test]
317    fn levers_uniform_on() {
318        let levers = ComponentMetricsLevers::uniform(true);
319        assert!(levers.enabled_for("http"), "uniform(true) must enable http");
320        assert!(
321            ComponentMetricsLevers::uniform(true).enabled_for("unlisted"),
322            "uniform(true) must inherit components=true for unlisted components"
323        );
324    }
325
326    #[test]
327    fn composite_collector_receives_once() {
328        let (first, first_erased) = recording();
329        let (second, second_erased) = recording();
330        let cell = MetricsHandle::new();
331        cell.register(first_erased);
332        cell.register(second_erased);
333        let metrics =
334            ComponentMetrics::new(Arc::new(cell) as Arc<dyn MetricsCollector>, "http", true);
335
336        metrics.observe("consume", true);
337
338        for collector in [&first, &second] {
339            assert_eq!(
340                collector.ops(),
341                vec![(
342                    "http".to_string(),
343                    "consume".to_string(),
344                    "failure".to_string()
345                )],
346                "each collector must receive exactly one scoped operation"
347            );
348            assert_eq!(
349                collector.errors(),
350                vec![("http".to_string(), "e:http:consume".to_string())],
351                "each collector must receive exactly one scoped error"
352            );
353        }
354    }
355
356    #[test]
357    fn late_binding_reaches_collector_registered_after_first_observe() {
358        let cell = Arc::new(MetricsHandle::new());
359        let metrics =
360            ComponentMetrics::new(Arc::clone(&cell) as Arc<dyn MetricsCollector>, "http", true);
361
362        metrics.observe("consume", false);
363
364        let (collector, erased) = recording();
365        cell.register(erased);
366        metrics.observe("consume", false);
367
368        assert_eq!(
369            collector.ops(),
370            vec![(
371                "http".to_string(),
372                "consume".to_string(),
373                "success".to_string()
374            )],
375            "only the post-registration observation must reach the collector"
376        );
377        assert!(
378            collector.errors().is_empty(),
379            "successful observations must not touch the error family"
380        );
381    }
382
383    #[test]
384    fn noop_collector_is_safe() {
385        let metrics = ComponentMetrics::new(
386            Arc::new(NoOpMetrics) as Arc<dyn MetricsCollector>,
387            "redis",
388            true,
389        );
390
391        metrics.observe("command", false);
392        metrics.observe("command", true);
393    }
394}