1use std::sync::{Arc, Mutex};
2use std::time::Duration;
3
4use arc_swap::ArcSwap;
5
6#[derive(Clone, Copy, PartialEq, Eq, Debug)]
17pub enum AllocatorStat {
18 Allocated,
20 Resident,
22 Active,
24 Mapped,
26}
27
28impl AllocatorStat {
29 pub fn as_str(&self) -> &'static str {
31 match self {
32 AllocatorStat::Allocated => "allocated",
33 AllocatorStat::Resident => "resident",
34 AllocatorStat::Active => "active",
35 AllocatorStat::Mapped => "mapped",
36 }
37 }
38}
39
40pub trait MetricsCollector: Send + Sync {
43 fn record_exchange_duration(&self, route_id: &str, duration: Duration);
45
46 fn increment_errors(&self, route_id: &str, error_type: &str);
48
49 fn increment_exchanges(&self, route_id: &str);
51
52 fn set_queue_depth(&self, queue: &str, depth: usize);
57
58 fn record_circuit_breaker_change(&self, route_id: &str, from: &str, to: &str);
60
61 fn record_histogram(&self, _name: &str, _value: f64, _labels: &[(&str, &str)]) {}
64
65 fn record_counter(&self, _name: &str, _value: f64, _labels: &[(&str, &str)]) {}
68
69 fn increment_retry_attempt(&self, _scheme: &str, _operation: &str) {}
73
74 fn increment_circuit_breaker_rejection(&self, _route: &str) {}
79
80 fn set_route_state(&self, _route: &str, _state: &str) {}
88
89 fn clear_route_state(&self, _route: &str) {}
92
93 fn record_build_info(&self, _version: &str, _git_sha: &str) {}
97
98 fn record_uptime(&self, _seconds: f64) {}
102
103 fn record_component_operation(&self, _component: &str, _operation: &str, _outcome: &str) {}
109
110 fn set_pinned_client_cache_size(&self, _component: &str, _entries: u64) {}
116
117 fn increment_pinned_client_cache_hit(&self, _component: &str) {}
122
123 fn increment_pinned_client_cache_miss(&self, _component: &str) {}
128
129 fn set_allocator_memory(&self, _stat: AllocatorStat, _bytes: u64) {}
134}
135
136pub struct NoOpMetrics;
138
139impl MetricsCollector for NoOpMetrics {
140 fn record_exchange_duration(&self, _route_id: &str, _duration: Duration) {}
141 fn increment_errors(&self, _route_id: &str, _error_type: &str) {}
142 fn increment_exchanges(&self, _route_id: &str) {}
143 fn set_queue_depth(&self, _queue: &str, _depth: usize) {}
144 fn record_circuit_breaker_change(&self, _route_id: &str, _from: &str, _to: &str) {}
145}
146
147struct CollectorSlot(Arc<dyn MetricsCollector>);
153
154pub struct MetricsHandle {
170 inner: ArcSwap<CollectorSlot>,
171 members: Mutex<Vec<Arc<dyn MetricsCollector>>>,
175}
176
177impl MetricsHandle {
178 pub fn new() -> Self {
181 Self {
182 inner: ArcSwap::from_pointee(CollectorSlot(Arc::new(NoOpMetrics))),
183 members: Mutex::new(Vec::new()),
184 }
185 }
186
187 pub fn register(&self, collector: Arc<dyn MetricsCollector>) {
192 let mut members = self
193 .members
194 .lock()
195 .expect("metrics members lock poisoned by a panicked register"); if members.iter().any(|m| Arc::ptr_eq(m, &collector)) {
197 return;
198 }
199 let first = members.is_empty();
200 members.push(Arc::clone(&collector));
201 if first {
202 self.inner.store(Arc::new(CollectorSlot(collector)));
205 return;
206 }
207 let prev = Arc::clone(&self.inner.load().0);
208 self.inner.store(Arc::new(CollectorSlot(Arc::new(
209 CompositeMetricsCollector::new(vec![prev, collector]),
210 ))));
211 }
212}
213
214impl Default for MetricsHandle {
215 fn default() -> Self {
216 Self::new()
217 }
218}
219
220impl MetricsCollector for MetricsHandle {
221 fn record_exchange_duration(&self, route_id: &str, duration: Duration) {
222 self.inner
223 .load()
224 .0
225 .record_exchange_duration(route_id, duration)
226 }
227
228 fn increment_errors(&self, route_id: &str, error_type: &str) {
229 self.inner.load().0.increment_errors(route_id, error_type)
230 }
231
232 fn increment_exchanges(&self, route_id: &str) {
233 self.inner.load().0.increment_exchanges(route_id)
234 }
235
236 fn set_queue_depth(&self, queue: &str, depth: usize) {
237 self.inner.load().0.set_queue_depth(queue, depth)
238 }
239
240 fn record_circuit_breaker_change(&self, route_id: &str, from: &str, to: &str) {
241 self.inner
242 .load()
243 .0
244 .record_circuit_breaker_change(route_id, from, to)
245 }
246
247 fn record_histogram(&self, name: &str, value: f64, labels: &[(&str, &str)]) {
248 self.inner.load().0.record_histogram(name, value, labels)
249 }
250
251 fn record_counter(&self, name: &str, value: f64, labels: &[(&str, &str)]) {
252 self.inner.load().0.record_counter(name, value, labels)
253 }
254
255 fn increment_retry_attempt(&self, scheme: &str, operation: &str) {
256 self.inner
257 .load()
258 .0
259 .increment_retry_attempt(scheme, operation)
260 }
261
262 fn increment_circuit_breaker_rejection(&self, route: &str) {
263 self.inner
264 .load()
265 .0
266 .increment_circuit_breaker_rejection(route)
267 }
268
269 fn set_route_state(&self, route: &str, state: &str) {
270 self.inner.load().0.set_route_state(route, state)
271 }
272
273 fn clear_route_state(&self, route: &str) {
274 self.inner.load().0.clear_route_state(route)
275 }
276
277 fn record_build_info(&self, version: &str, git_sha: &str) {
278 self.inner.load().0.record_build_info(version, git_sha)
279 }
280
281 fn record_uptime(&self, seconds: f64) {
282 self.inner.load().0.record_uptime(seconds)
283 }
284
285 fn record_component_operation(&self, component: &str, operation: &str, outcome: &str) {
286 self.inner
287 .load()
288 .0
289 .record_component_operation(component, operation, outcome)
290 }
291
292 fn set_pinned_client_cache_size(&self, component: &str, entries: u64) {
293 self.inner
294 .load()
295 .0
296 .set_pinned_client_cache_size(component, entries)
297 }
298
299 fn increment_pinned_client_cache_hit(&self, component: &str) {
300 self.inner
301 .load()
302 .0
303 .increment_pinned_client_cache_hit(component)
304 }
305
306 fn increment_pinned_client_cache_miss(&self, component: &str) {
307 self.inner
308 .load()
309 .0
310 .increment_pinned_client_cache_miss(component)
311 }
312
313 fn set_allocator_memory(&self, stat: AllocatorStat, bytes: u64) {
314 self.inner.load().0.set_allocator_memory(stat, bytes)
315 }
316}
317
318#[doc(hidden)]
331pub struct CompositeMetricsCollector {
332 collectors: Vec<Arc<dyn MetricsCollector>>,
333}
334
335impl CompositeMetricsCollector {
336 #[doc(hidden)]
343 pub fn new(collectors: Vec<Arc<dyn MetricsCollector>>) -> Self {
344 Self { collectors }
345 }
346}
347
348impl MetricsCollector for CompositeMetricsCollector {
349 fn record_exchange_duration(&self, route_id: &str, duration: Duration) {
350 for collector in &self.collectors {
351 collector.record_exchange_duration(route_id, duration);
352 }
353 }
354
355 fn increment_errors(&self, route_id: &str, error_type: &str) {
356 for collector in &self.collectors {
357 collector.increment_errors(route_id, error_type);
358 }
359 }
360
361 fn increment_exchanges(&self, route_id: &str) {
362 for collector in &self.collectors {
363 collector.increment_exchanges(route_id);
364 }
365 }
366
367 fn set_queue_depth(&self, queue: &str, depth: usize) {
368 for collector in &self.collectors {
369 collector.set_queue_depth(queue, depth);
370 }
371 }
372
373 fn record_circuit_breaker_change(&self, route_id: &str, from: &str, to: &str) {
374 for collector in &self.collectors {
375 collector.record_circuit_breaker_change(route_id, from, to);
376 }
377 }
378
379 fn record_histogram(&self, name: &str, value: f64, labels: &[(&str, &str)]) {
380 for collector in &self.collectors {
381 collector.record_histogram(name, value, labels);
382 }
383 }
384
385 fn record_counter(&self, name: &str, value: f64, labels: &[(&str, &str)]) {
386 for collector in &self.collectors {
387 collector.record_counter(name, value, labels);
388 }
389 }
390
391 fn increment_retry_attempt(&self, scheme: &str, operation: &str) {
392 for collector in &self.collectors {
393 collector.increment_retry_attempt(scheme, operation);
394 }
395 }
396
397 fn increment_circuit_breaker_rejection(&self, route: &str) {
398 for collector in &self.collectors {
399 collector.increment_circuit_breaker_rejection(route);
400 }
401 }
402
403 fn set_route_state(&self, route: &str, state: &str) {
404 for collector in &self.collectors {
405 collector.set_route_state(route, state);
406 }
407 }
408
409 fn clear_route_state(&self, route: &str) {
410 for collector in &self.collectors {
411 collector.clear_route_state(route);
412 }
413 }
414
415 fn record_build_info(&self, version: &str, git_sha: &str) {
416 for collector in &self.collectors {
417 collector.record_build_info(version, git_sha);
418 }
419 }
420
421 fn record_uptime(&self, seconds: f64) {
422 for collector in &self.collectors {
423 collector.record_uptime(seconds);
424 }
425 }
426
427 fn record_component_operation(&self, component: &str, operation: &str, outcome: &str) {
428 for collector in &self.collectors {
429 collector.record_component_operation(component, operation, outcome);
430 }
431 }
432
433 fn set_pinned_client_cache_size(&self, component: &str, entries: u64) {
434 for collector in &self.collectors {
435 collector.set_pinned_client_cache_size(component, entries);
436 }
437 }
438
439 fn increment_pinned_client_cache_hit(&self, component: &str) {
440 for collector in &self.collectors {
441 collector.increment_pinned_client_cache_hit(component);
442 }
443 }
444
445 fn increment_pinned_client_cache_miss(&self, component: &str) {
446 for collector in &self.collectors {
447 collector.increment_pinned_client_cache_miss(component);
448 }
449 }
450
451 fn set_allocator_memory(&self, stat: AllocatorStat, bytes: u64) {
452 for collector in &self.collectors {
453 collector.set_allocator_memory(stat, bytes);
454 }
455 }
456}
457
458#[cfg(test)]
459mod tests {
460 use super::*;
461 use std::sync::{Arc, Mutex};
462
463 struct RecordingMetrics {
465 durations: Mutex<Vec<(String, Duration)>>,
466 errors: Mutex<Vec<(String, String)>>,
467 exchanges: Mutex<Vec<String>>,
468 retries: Mutex<Vec<(String, String)>>,
469 rejections: Mutex<Vec<String>>,
470 pinned: Mutex<Vec<(&'static str, String, u64)>>,
471 allocator: Mutex<Vec<(AllocatorStat, u64)>>,
472 }
473
474 impl RecordingMetrics {
475 fn new() -> Self {
476 Self {
477 durations: Mutex::new(Vec::new()),
478 errors: Mutex::new(Vec::new()),
479 exchanges: Mutex::new(Vec::new()),
480 retries: Mutex::new(Vec::new()),
481 rejections: Mutex::new(Vec::new()),
482 pinned: Mutex::new(Vec::new()),
483 allocator: Mutex::new(Vec::new()),
484 }
485 }
486 }
487
488 impl MetricsCollector for RecordingMetrics {
489 fn record_exchange_duration(&self, route_id: &str, duration: Duration) {
490 self.durations
491 .lock()
492 .expect("durations lock")
493 .push((route_id.to_string(), duration));
494 }
495
496 fn increment_errors(&self, route_id: &str, error_type: &str) {
497 self.errors
498 .lock()
499 .expect("errors lock")
500 .push((route_id.to_string(), error_type.to_string()));
501 }
502
503 fn increment_exchanges(&self, route_id: &str) {
504 self.exchanges
505 .lock()
506 .expect("exchanges lock")
507 .push(route_id.to_string());
508 }
509
510 fn set_queue_depth(&self, _queue: &str, _depth: usize) {}
511
512 fn record_circuit_breaker_change(&self, _route_id: &str, _from: &str, _to: &str) {}
513
514 fn increment_retry_attempt(&self, scheme: &str, operation: &str) {
515 self.retries
516 .lock()
517 .expect("retries lock")
518 .push((scheme.to_string(), operation.to_string()));
519 }
520
521 fn increment_circuit_breaker_rejection(&self, route: &str) {
522 self.rejections
523 .lock()
524 .expect("rejections lock")
525 .push(route.to_string());
526 }
527
528 fn set_pinned_client_cache_size(&self, component: &str, entries: u64) {
529 self.pinned.lock().expect("pinned lock").push((
530 "set_pinned_client_cache_size",
531 component.to_string(),
532 entries,
533 ));
534 }
535
536 fn increment_pinned_client_cache_hit(&self, component: &str) {
537 self.pinned.lock().expect("pinned lock").push((
538 "increment_pinned_client_cache_hit",
539 component.to_string(),
540 1,
541 ));
542 }
543
544 fn increment_pinned_client_cache_miss(&self, component: &str) {
545 self.pinned.lock().expect("pinned lock").push((
546 "increment_pinned_client_cache_miss",
547 component.to_string(),
548 1,
549 ));
550 }
551
552 fn set_allocator_memory(&self, stat: AllocatorStat, bytes: u64) {
553 self.allocator
554 .lock()
555 .expect("allocator lock")
556 .push((stat, bytes));
557 }
558 }
559
560 struct SurfaceProbe {
563 calls: Mutex<Vec<&'static str>>,
564 }
565
566 impl SurfaceProbe {
567 fn new() -> Self {
568 Self {
569 calls: Mutex::new(Vec::new()),
570 }
571 }
572
573 fn tag(&self, name: &'static str) {
574 self.calls.lock().expect("calls lock").push(name);
575 }
576 }
577
578 impl MetricsCollector for SurfaceProbe {
579 fn record_exchange_duration(&self, _route_id: &str, _duration: Duration) {
580 self.tag("record_exchange_duration");
581 }
582 fn increment_errors(&self, _route_id: &str, _error_type: &str) {
583 self.tag("increment_errors");
584 }
585 fn increment_exchanges(&self, _route_id: &str) {
586 self.tag("increment_exchanges");
587 }
588 fn set_queue_depth(&self, _queue: &str, _depth: usize) {
589 self.tag("set_queue_depth");
590 }
591 fn record_circuit_breaker_change(&self, _route_id: &str, _from: &str, _to: &str) {
592 self.tag("record_circuit_breaker_change");
593 }
594 fn record_histogram(&self, _name: &str, _value: f64, _labels: &[(&str, &str)]) {
595 self.tag("record_histogram");
596 }
597 fn record_counter(&self, _name: &str, _value: f64, _labels: &[(&str, &str)]) {
598 self.tag("record_counter");
599 }
600 fn increment_retry_attempt(&self, _scheme: &str, _operation: &str) {
601 self.tag("increment_retry_attempt");
602 }
603 fn increment_circuit_breaker_rejection(&self, _route: &str) {
604 self.tag("increment_circuit_breaker_rejection");
605 }
606 fn set_route_state(&self, _route: &str, _state: &str) {
607 self.tag("set_route_state");
608 }
609
610 fn clear_route_state(&self, _route: &str) {
611 self.tag("clear_route_state");
612 }
613 fn record_build_info(&self, _version: &str, _git_sha: &str) {
614 self.tag("record_build_info");
615 }
616 fn record_uptime(&self, _seconds: f64) {
617 self.tag("record_uptime");
618 }
619 fn record_component_operation(&self, _component: &str, _operation: &str, _outcome: &str) {
620 self.tag("record_component_operation");
621 }
622 }
623
624 #[test]
625 fn test_noop_metrics_implements_trait() {
626 let metrics = NoOpMetrics;
627 let metrics_arc: Arc<dyn MetricsCollector> = Arc::new(metrics);
628
629 metrics_arc.record_exchange_duration("test-route", Duration::from_millis(100));
631 metrics_arc.increment_errors("test-route", "test-error");
632 metrics_arc.increment_exchanges("test-route");
633 metrics_arc.set_queue_depth("test-route", 5);
634 metrics_arc.record_circuit_breaker_change("test-route", "closed", "open");
635 }
636
637 #[test]
638 fn test_custom_metrics_collector() {
639 struct TestMetrics {
640 exchange_count: std::sync::atomic::AtomicU64,
641 }
642
643 impl MetricsCollector for TestMetrics {
644 fn record_exchange_duration(&self, route_id: &str, duration: Duration) {
645 println!("Route {} took {}ms", route_id, duration.as_millis());
647 }
648
649 fn increment_errors(&self, route_id: &str, error_type: &str) {
650 println!("Route {} had error: {}", route_id, error_type);
652 }
653
654 fn increment_exchanges(&self, route_id: &str) {
655 self.exchange_count
657 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
658 println!("Route {} processed exchange", route_id);
659 }
660
661 fn set_queue_depth(&self, queue: &str, depth: usize) {
662 println!("Queue {queue} depth: {depth}");
664 }
665
666 fn record_circuit_breaker_change(&self, route_id: &str, from: &str, to: &str) {
667 println!("Route {} circuit breaker: {} -> {}", route_id, from, to);
669 }
670 }
671
672 let test_metrics = TestMetrics {
673 exchange_count: std::sync::atomic::AtomicU64::new(0),
674 };
675 let metrics_arc: Arc<dyn MetricsCollector> = Arc::new(test_metrics);
676
677 metrics_arc.record_exchange_duration("test-route", Duration::from_millis(100));
679 metrics_arc.increment_errors("test-route", "test-error");
680 metrics_arc.increment_exchanges("test-route");
681 metrics_arc.set_queue_depth("test-route", 5);
682 metrics_arc.record_circuit_breaker_change("test-route", "closed", "open");
683
684 }
687
688 #[test]
689 fn handle_delegates_to_stored_collector() {
690 let collector = Arc::new(RecordingMetrics::new());
691 let handle = MetricsHandle::new();
692 handle.register(collector.clone());
693
694 handle.record_exchange_duration("r", Duration::from_millis(1));
695
696 let recorded = collector.durations.lock().expect("durations lock").clone();
697 assert_eq!(recorded, vec![("r".to_string(), Duration::from_millis(1))]);
698 }
699
700 #[test]
701 fn second_registration_composes_both_observe() {
702 let a = Arc::new(RecordingMetrics::new());
703 let b = Arc::new(RecordingMetrics::new());
704 let handle = MetricsHandle::new();
705 handle.register(a.clone());
706 handle.register(b.clone());
707
708 handle.increment_errors("r", "x");
709
710 let a_errors = a.errors.lock().expect("errors lock").clone();
711 let b_errors = b.errors.lock().expect("errors lock").clone();
712 assert_eq!(a_errors, vec![("r".to_string(), "x".to_string())]);
713 assert_eq!(b_errors, vec![("r".to_string(), "x".to_string())]);
714 }
715
716 #[test]
717 fn register_same_arc_is_idempotent() {
718 let a = Arc::new(RecordingMetrics::new());
719 let handle = MetricsHandle::new();
720 handle.register(a.clone());
721 handle.register(a.clone());
722
723 handle.increment_exchanges("r");
724
725 let recorded = a.exchanges.lock().expect("exchanges lock").clone();
726 assert_eq!(recorded, vec!["r".to_string()]);
727 }
728
729 #[test]
730 fn handle_defaults_to_noop() {
731 let handle = MetricsHandle::new();
732 handle.record_exchange_duration("r", Duration::from_millis(1));
733 handle.increment_errors("r", "x");
734 handle.increment_exchanges("r");
735 handle.set_queue_depth("r", 5);
736 handle.record_circuit_breaker_change("r", "closed", "open");
737 handle.record_histogram("h", 1.0, &[("k", "v")]);
738 handle.record_counter("c", 1.0, &[("k", "v")]);
739 }
740
741 #[test]
742 fn composite_delegates_retry_and_rejection() {
743 let a = Arc::new(RecordingMetrics::new());
744 let b = Arc::new(RecordingMetrics::new());
745 let composite = CompositeMetricsCollector::new(vec![
746 Arc::clone(&a) as Arc<dyn MetricsCollector>,
747 Arc::clone(&b) as Arc<dyn MetricsCollector>,
748 ]);
749
750 composite.increment_retry_attempt("kafka", "connect");
751 composite.increment_circuit_breaker_rejection("r1");
752
753 for member in [&a, &b] {
754 assert_eq!(
755 member.retries.lock().expect("retries lock").clone(),
756 vec![("kafka".to_string(), "connect".to_string())]
757 );
758 assert_eq!(
759 member.rejections.lock().expect("rejections lock").clone(),
760 vec!["r1".to_string()]
761 );
762 }
763 }
764
765 #[test]
766 fn noop_defaults_compile_and_do_nothing() {
767 let collector: Arc<dyn MetricsCollector> = Arc::new(NoOpMetrics);
768 collector.increment_retry_attempt("kafka", "connect");
770 collector.increment_circuit_breaker_rejection("r1");
771 }
772
773 #[test]
776 fn composite_delegates_full_trait_surface() {
777 let a = Arc::new(SurfaceProbe::new());
778 let b = Arc::new(SurfaceProbe::new());
779 let composite = CompositeMetricsCollector::new(vec![
780 Arc::clone(&a) as Arc<dyn MetricsCollector>,
781 Arc::clone(&b) as Arc<dyn MetricsCollector>,
782 ]);
783
784 composite.record_exchange_duration("r", Duration::from_millis(1));
785 composite.increment_errors("r", "x");
786 composite.increment_exchanges("r");
787 composite.set_queue_depth("r", 1);
788 composite.record_circuit_breaker_change("r", "closed", "open");
789 composite.record_histogram("h", 1.0, &[("k", "v")]);
790 composite.record_counter("c", 1.0, &[("k", "v")]);
791 composite.increment_retry_attempt("kafka", "connect");
792 composite.increment_circuit_breaker_rejection("r1");
793 composite.set_route_state("r", "Started");
794 composite.clear_route_state("r");
795 composite.record_build_info("1.2.3", "abc1234");
796 composite.record_uptime(0.5);
797 composite.record_component_operation("redis", "command", "success");
798
799 let expected = vec![
800 "record_exchange_duration",
801 "increment_errors",
802 "increment_exchanges",
803 "set_queue_depth",
804 "record_circuit_breaker_change",
805 "record_histogram",
806 "record_counter",
807 "increment_retry_attempt",
808 "increment_circuit_breaker_rejection",
809 "set_route_state",
810 "clear_route_state",
811 "record_build_info",
812 "record_uptime",
813 "record_component_operation",
814 ];
815 for member in [&a, &b] {
816 let calls = member.calls.lock().expect("calls lock").clone();
817 assert_eq!(calls, expected, "member missed part of the trait surface");
818 }
819 }
820
821 fn pinned_trio_expected() -> Vec<(&'static str, String, u64)> {
824 vec![
825 ("set_pinned_client_cache_size", "camel-https".to_string(), 3),
826 (
827 "increment_pinned_client_cache_hit",
828 "camel-https".to_string(),
829 1,
830 ),
831 (
832 "increment_pinned_client_cache_miss",
833 "camel-https".to_string(),
834 1,
835 ),
836 ]
837 }
838
839 #[test]
840 fn handle_forwards_pinned_cache_trio() {
841 let collector = Arc::new(RecordingMetrics::new());
842 let handle = MetricsHandle::new();
843 handle.register(collector.clone());
844
845 handle.set_pinned_client_cache_size("camel-https", 3);
846 handle.increment_pinned_client_cache_hit("camel-https");
847 handle.increment_pinned_client_cache_miss("camel-https");
848
849 let captured = collector.pinned.lock().expect("pinned lock").clone();
850 assert_eq!(captured, pinned_trio_expected());
851
852 let bystander = Arc::new(RecordingMetrics::new());
856 let unwired = MetricsHandle::new();
857 unwired.set_pinned_client_cache_size("camel-https", 3);
858 unwired.increment_pinned_client_cache_hit("camel-https");
859 unwired.increment_pinned_client_cache_miss("camel-https");
860 unwired.register(bystander.clone());
861 assert!(bystander.pinned.lock().expect("pinned lock").is_empty());
862 }
863
864 #[test]
865 fn composite_forwards_pinned_cache_trio_to_all_collectors() {
866 let a = Arc::new(RecordingMetrics::new());
867 let b = Arc::new(RecordingMetrics::new());
868 let composite = CompositeMetricsCollector::new(vec![
869 Arc::clone(&a) as Arc<dyn MetricsCollector>,
870 Arc::clone(&b) as Arc<dyn MetricsCollector>,
871 ]);
872
873 composite.set_pinned_client_cache_size("camel-https", 3);
874 composite.increment_pinned_client_cache_hit("camel-https");
875 composite.increment_pinned_client_cache_miss("camel-https");
876
877 for member in [&a, &b] {
878 let captured = member.pinned.lock().expect("pinned lock").clone();
879 assert_eq!(
880 captured,
881 pinned_trio_expected(),
882 "member missed part of the pinned-cache trio"
883 );
884 }
885 }
886
887 #[test]
890 fn allocator_stat_as_str_image_is_closed_set() {
891 let image: std::collections::BTreeSet<&'static str> = [
892 AllocatorStat::Allocated,
893 AllocatorStat::Resident,
894 AllocatorStat::Active,
895 AllocatorStat::Mapped,
896 ]
897 .iter()
898 .map(|stat| stat.as_str())
899 .collect();
900 let expected: std::collections::BTreeSet<&'static str> =
901 ["active", "allocated", "mapped", "resident"]
902 .into_iter()
903 .collect();
904 assert_eq!(image, expected);
905 }
906
907 #[test]
911 fn handle_and_composite_forward_set_allocator_memory() {
912 let expected = vec![(AllocatorStat::Resident, 4096)];
913
914 let handle_collector = Arc::new(RecordingMetrics::new());
915 let handle = MetricsHandle::new();
916 handle.register(handle_collector.clone());
917 handle.set_allocator_memory(AllocatorStat::Resident, 4096);
918 assert_eq!(
919 handle_collector
920 .allocator
921 .lock()
922 .expect("allocator lock")
923 .clone(),
924 expected,
925 "wired handle must forward exactly one allocator emission"
926 );
927
928 let composite_collector = Arc::new(RecordingMetrics::new());
929 let composite = CompositeMetricsCollector::new(vec![
930 composite_collector.clone() as Arc<dyn MetricsCollector>
931 ]);
932 composite.set_allocator_memory(AllocatorStat::Resident, 4096);
933 assert_eq!(
934 composite_collector
935 .allocator
936 .lock()
937 .expect("allocator lock")
938 .clone(),
939 expected,
940 "composite must forward exactly one allocator emission"
941 );
942
943 let bystander = Arc::new(RecordingMetrics::new());
944 let unwired = MetricsHandle::new();
945 unwired.set_allocator_memory(AllocatorStat::Resident, 4096);
946 unwired.register(bystander.clone());
947 assert!(
948 bystander
949 .allocator
950 .lock()
951 .expect("allocator lock")
952 .is_empty(),
953 "unwired-handle emissions are dropped, not replayed"
954 );
955 }
956}