zeroclaw/src/observability/multi.rs
argenis de la rosa 05cb353f7f feat: initial release — ZeroClaw v0.1.0
- 22 AI providers (OpenRouter, Anthropic, OpenAI, Mistral, etc.)
- 7 channels (CLI, Telegram, Discord, Slack, iMessage, Matrix, Webhook)
- 5-step onboarding wizard with Project Context personalization
- OpenClaw-aligned system prompt (SOUL.md, IDENTITY.md, USER.md, AGENTS.md, etc.)
- SQLite memory backend with auto-save
- Skills system with on-demand loading
- Security: autonomy levels, command allowlists, cost limits
- 532 tests passing, 0 clippy warnings
2026-02-13 12:19:14 -05:00

155 lines
4.4 KiB
Rust

use super::traits::{Observer, ObserverEvent, ObserverMetric};
/// Combine multiple observers — fan-out events to all backends
pub struct MultiObserver {
observers: Vec<Box<dyn Observer>>,
}
impl MultiObserver {
pub fn new(observers: Vec<Box<dyn Observer>>) -> Self {
Self { observers }
}
}
impl Observer for MultiObserver {
fn record_event(&self, event: &ObserverEvent) {
for obs in &self.observers {
obs.record_event(event);
}
}
fn record_metric(&self, metric: &ObserverMetric) {
for obs in &self.observers {
obs.record_metric(metric);
}
}
fn flush(&self) {
for obs in &self.observers {
obs.flush();
}
}
fn name(&self) -> &str {
"multi"
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;
/// Test observer that counts calls
struct CountingObserver {
event_count: Arc<AtomicUsize>,
metric_count: Arc<AtomicUsize>,
flush_count: Arc<AtomicUsize>,
}
impl CountingObserver {
fn new(
event_count: Arc<AtomicUsize>,
metric_count: Arc<AtomicUsize>,
flush_count: Arc<AtomicUsize>,
) -> Self {
Self {
event_count,
metric_count,
flush_count,
}
}
}
impl Observer for CountingObserver {
fn record_event(&self, _event: &ObserverEvent) {
self.event_count.fetch_add(1, Ordering::SeqCst);
}
fn record_metric(&self, _metric: &ObserverMetric) {
self.metric_count.fetch_add(1, Ordering::SeqCst);
}
fn flush(&self) {
self.flush_count.fetch_add(1, Ordering::SeqCst);
}
fn name(&self) -> &str {
"counting"
}
}
#[test]
fn multi_name() {
let m = MultiObserver::new(vec![]);
assert_eq!(m.name(), "multi");
}
#[test]
fn multi_empty_no_panic() {
let m = MultiObserver::new(vec![]);
m.record_event(&ObserverEvent::HeartbeatTick);
m.record_metric(&ObserverMetric::TokensUsed(10));
m.flush();
}
#[test]
fn multi_fans_out_events() {
let ec1 = Arc::new(AtomicUsize::new(0));
let mc1 = Arc::new(AtomicUsize::new(0));
let fc1 = Arc::new(AtomicUsize::new(0));
let ec2 = Arc::new(AtomicUsize::new(0));
let mc2 = Arc::new(AtomicUsize::new(0));
let fc2 = Arc::new(AtomicUsize::new(0));
let m = MultiObserver::new(vec![
Box::new(CountingObserver::new(ec1.clone(), mc1.clone(), fc1.clone())),
Box::new(CountingObserver::new(ec2.clone(), mc2.clone(), fc2.clone())),
]);
m.record_event(&ObserverEvent::HeartbeatTick);
m.record_event(&ObserverEvent::HeartbeatTick);
m.record_event(&ObserverEvent::HeartbeatTick);
assert_eq!(ec1.load(Ordering::SeqCst), 3);
assert_eq!(ec2.load(Ordering::SeqCst), 3);
}
#[test]
fn multi_fans_out_metrics() {
let ec1 = Arc::new(AtomicUsize::new(0));
let mc1 = Arc::new(AtomicUsize::new(0));
let fc1 = Arc::new(AtomicUsize::new(0));
let ec2 = Arc::new(AtomicUsize::new(0));
let mc2 = Arc::new(AtomicUsize::new(0));
let fc2 = Arc::new(AtomicUsize::new(0));
let m = MultiObserver::new(vec![
Box::new(CountingObserver::new(ec1.clone(), mc1.clone(), fc1.clone())),
Box::new(CountingObserver::new(ec2.clone(), mc2.clone(), fc2.clone())),
]);
m.record_metric(&ObserverMetric::TokensUsed(100));
m.record_metric(&ObserverMetric::RequestLatency(Duration::from_millis(5)));
assert_eq!(mc1.load(Ordering::SeqCst), 2);
assert_eq!(mc2.load(Ordering::SeqCst), 2);
}
#[test]
fn multi_fans_out_flush() {
let ec = Arc::new(AtomicUsize::new(0));
let mc = Arc::new(AtomicUsize::new(0));
let fc1 = Arc::new(AtomicUsize::new(0));
let fc2 = Arc::new(AtomicUsize::new(0));
let m = MultiObserver::new(vec![
Box::new(CountingObserver::new(ec.clone(), mc.clone(), fc1.clone())),
Box::new(CountingObserver::new(ec.clone(), mc.clone(), fc2.clone())),
]);
m.flush();
assert_eq!(fc1.load(Ordering::SeqCst), 1);
assert_eq!(fc2.load(Ordering::SeqCst), 1);
}
}