Skip to main content

rustyclaw_core/observability/
mod.rs

1//! Observability subsystem for agent runtime telemetry.
2//!
3//! This module provides traits and implementations for recording events and
4//! metrics from the agent runtime. The modular design supports multiple backends
5//! (console logging, Prometheus, OpenTelemetry) via the [`Observer`] trait.
6//!
7//! Adapted from ZeroClaw (MIT OR Apache-2.0 licensed).
8
9pub mod log;
10pub mod traits;
11
12pub use log::LogObserver;
13pub use traits::{Observer, ObserverEvent, ObserverMetric};
14
15use std::sync::Arc;
16
17/// Composite observer that dispatches to multiple backends.
18///
19/// Useful for sending telemetry to both local logs and external systems
20/// (e.g., Prometheus + structured logging).
21pub struct CompositeObserver {
22    observers: Vec<Arc<dyn Observer>>,
23}
24
25impl CompositeObserver {
26    /// Create a composite observer from a list of observer implementations.
27    pub fn new(observers: Vec<Arc<dyn Observer>>) -> Self {
28        Self { observers }
29    }
30
31    /// Add an observer to the composite.
32    pub fn add(&mut self, observer: Arc<dyn Observer>) {
33        self.observers.push(observer);
34    }
35}
36
37impl Observer for CompositeObserver {
38    fn record_event(&self, event: &ObserverEvent) {
39        for observer in &self.observers {
40            observer.record_event(event);
41        }
42    }
43
44    fn record_metric(&self, metric: &ObserverMetric) {
45        for observer in &self.observers {
46            observer.record_metric(metric);
47        }
48    }
49
50    fn flush(&self) {
51        for observer in &self.observers {
52            observer.flush();
53        }
54    }
55
56    fn name(&self) -> &str {
57        "composite"
58    }
59
60    fn as_any(&self) -> &dyn std::any::Any {
61        self
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68    use std::sync::Mutex;
69    use std::time::Duration;
70
71    #[derive(Default)]
72    struct CountingObserver {
73        events: Mutex<u64>,
74        metrics: Mutex<u64>,
75        flushes: Mutex<u64>,
76    }
77
78    impl Observer for CountingObserver {
79        fn record_event(&self, _event: &ObserverEvent) {
80            *self.events.lock().unwrap() += 1;
81        }
82
83        fn record_metric(&self, _metric: &ObserverMetric) {
84            *self.metrics.lock().unwrap() += 1;
85        }
86
87        fn flush(&self) {
88            *self.flushes.lock().unwrap() += 1;
89        }
90
91        fn name(&self) -> &str {
92            "counting"
93        }
94
95        fn as_any(&self) -> &dyn std::any::Any {
96            self
97        }
98    }
99
100    #[test]
101    fn composite_dispatches_to_all_backends() {
102        let obs1 = Arc::new(CountingObserver::default());
103        let obs2 = Arc::new(CountingObserver::default());
104
105        let composite = CompositeObserver::new(vec![obs1.clone(), obs2.clone()]);
106
107        composite.record_event(&ObserverEvent::HeartbeatTick);
108        composite.record_metric(&ObserverMetric::TokensUsed(100));
109        composite.flush();
110
111        assert_eq!(*obs1.events.lock().unwrap(), 1);
112        assert_eq!(*obs2.events.lock().unwrap(), 1);
113        assert_eq!(*obs1.metrics.lock().unwrap(), 1);
114        assert_eq!(*obs2.metrics.lock().unwrap(), 1);
115        assert_eq!(*obs1.flushes.lock().unwrap(), 1);
116        assert_eq!(*obs2.flushes.lock().unwrap(), 1);
117    }
118
119    #[test]
120    fn composite_name() {
121        let composite = CompositeObserver::new(vec![]);
122        assert_eq!(composite.name(), "composite");
123    }
124}