camel_component_api/
runtime_observability.rs1use std::sync::Arc;
13
14use camel_api::{ComponentMetrics, InFlightGauge, MetricsCollector};
15
16use crate::{ComponentContext, HealthCheckRegistry};
17
18pub trait RuntimeObservability: Send + Sync {
28 fn metrics(&self) -> Arc<dyn MetricsCollector>;
31
32 fn health(&self) -> Arc<dyn HealthCheckRegistry>;
36
37 fn component_metrics(&self, component: &str) -> ComponentMetrics {
55 ComponentMetrics::new(self.metrics(), component, false)
56 }
57
58 fn in_flight_counter(&self) -> Option<Arc<InFlightGauge>> {
62 None
63 }
64}
65
66impl<T: ComponentContext> RuntimeObservability for T {
74 fn metrics(&self) -> Arc<dyn MetricsCollector> {
75 <Self as ComponentContext>::metrics(self)
76 }
77
78 fn health(&self) -> Arc<dyn HealthCheckRegistry> {
79 <Self as ComponentContext>::health(self)
80 }
81
82 fn component_metrics(&self, component: &str) -> ComponentMetrics {
83 let enabled =
84 <Self as ComponentContext>::component_metric_levers(self).enabled_for(component);
85 ComponentMetrics::new(
86 <Self as ComponentContext>::metrics(self),
87 component,
88 enabled,
89 )
90 }
91
92 fn in_flight_counter(&self) -> Option<Arc<InFlightGauge>> {
93 <Self as ComponentContext>::in_flight_counter(self)
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use std::collections::BTreeMap;
100 use std::sync::{Arc, Mutex};
101 use std::time::Duration;
102
103 use camel_api::{ComponentMetricsLevers, MetricsCollector, PlatformService};
104 use camel_language_api::Language;
105
106 use super::*;
107 use crate::{Component, NoOpComponentContext};
108
109 #[derive(Default)]
111 struct RecordingCollector {
112 errors: Mutex<Vec<(String, String)>>,
113 ops: Mutex<Vec<(String, String, String)>>,
114 }
115
116 impl RecordingCollector {
117 fn errors(&self) -> Vec<(String, String)> {
118 self.errors.lock().expect("errors lock").clone()
119 }
120
121 fn ops(&self) -> Vec<(String, String, String)> {
122 self.ops.lock().expect("ops lock").clone()
123 }
124 }
125
126 impl MetricsCollector for RecordingCollector {
127 fn record_exchange_duration(&self, _: &str, _: Duration) {}
128 fn increment_errors(&self, component: &str, error_type: &str) {
129 self.errors
130 .lock()
131 .expect("errors lock")
132 .push((component.to_string(), error_type.to_string()));
133 }
134 fn increment_exchanges(&self, _: &str) {}
135 fn set_queue_depth(&self, _: &str, _: usize) {}
136 fn record_circuit_breaker_change(&self, _: &str, _: &str, _: &str) {}
137 fn record_component_operation(&self, component: &str, operation: &str, outcome: &str) {
138 self.ops.lock().expect("ops lock").push((
139 component.to_string(),
140 operation.to_string(),
141 outcome.to_string(),
142 ));
143 }
144 }
145
146 struct ScopedLeverContext {
148 inner: NoOpComponentContext,
149 collector: Arc<RecordingCollector>,
150 }
151
152 impl ComponentContext for ScopedLeverContext {
153 fn resolve_component(&self, scheme: &str) -> Option<Arc<dyn Component>> {
154 self.inner.resolve_component(scheme)
155 }
156
157 fn resolve_language(&self, name: &str) -> Option<Arc<dyn Language>> {
158 self.inner.resolve_language(name)
159 }
160
161 fn metrics(&self) -> Arc<dyn MetricsCollector> {
162 Arc::clone(&self.collector) as Arc<dyn MetricsCollector>
163 }
164
165 fn component_metric_levers(&self) -> ComponentMetricsLevers {
166 ComponentMetricsLevers::new(true, false, BTreeMap::from([("wasm".to_string(), true)]))
167 }
168
169 fn platform_service(&self) -> Arc<dyn PlatformService> {
170 self.inner.platform_service()
171 }
172
173 fn register_route_health_check(
174 &self,
175 route_id: &str,
176 check: Arc<dyn camel_api::AsyncHealthCheck>,
177 ) {
178 self.inner.register_route_health_check(route_id, check);
179 }
180
181 fn unregister_route_health_check(&self, route_id: &str) {
182 self.inner.unregister_route_health_check(route_id);
183 }
184 }
185
186 #[test]
187 fn runtime_observability_is_send_sync() {
188 fn assert_send_sync<T: Send + Sync + ?Sized>() {}
189 assert_send_sync::<dyn RuntimeObservability>();
190 }
191
192 #[test]
193 fn blanket_impl_uses_context_levers() {
194 let collector = Arc::new(RecordingCollector::default());
195 let ctx = ScopedLeverContext {
196 inner: NoOpComponentContext,
197 collector: Arc::clone(&collector),
198 };
199
200 RuntimeObservability::component_metrics(&ctx, "wasm").observe("invoke", false);
201 assert_eq!(
202 collector.ops(),
203 vec![(
204 "wasm".to_string(),
205 "invoke".to_string(),
206 "success".to_string()
207 )],
208 "the override must enable the wasm component series"
209 );
210
211 RuntimeObservability::component_metrics(&ctx, "kafka").observe("consume", true);
212 assert_eq!(
213 collector.ops().len(),
214 1,
215 "kafka inherits components=false, so no operation is recorded"
216 );
217 assert_eq!(
218 collector.errors(),
219 vec![("kafka".to_string(), "e:kafka:consume".to_string())],
220 "errors must flow regardless of the per-component lever"
221 );
222 }
223}