1use std::collections::BTreeMap;
23use std::sync::Arc;
24
25use crate::metrics::MetricsCollector;
26
27#[derive(Clone)]
35pub struct ComponentMetrics {
36 collector: Arc<dyn MetricsCollector>,
37 component: Arc<str>,
38 enabled: bool,
39}
40
41impl ComponentMetrics {
42 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 pub fn component(&self) -> &str {
60 &self.component
61 }
62
63 pub fn observe(&self, operation: &str, failed: bool) {
69 if failed {
70 self.collector.increment_errors(
72 &self.component,
73 &format!("e:{}:{operation}", self.component),
74 );
75 }
76 if self.enabled {
77 self.collector.record_component_operation(
79 &self.component,
80 operation,
81 if failed { "failure" } else { "success" },
82 );
83 }
84 }
85}
86
87#[derive(Clone, Debug)]
100pub struct ComponentMetricsLevers {
101 enabled: bool,
102 components: bool,
103 overrides: Arc<BTreeMap<String, bool>>,
104}
105
106impl ComponentMetricsLevers {
107 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 pub fn uniform(components: bool) -> Self {
121 Self {
122 enabled: components,
123 components,
124 overrides: Arc::new(BTreeMap::new()),
125 }
126 }
127
128 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 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}