Files
zeroclaw/src/observability/noop.rs
T
Giulio V 2deb91455d feat(observability): add Hands dashboard metrics and events (#3595)
Add HandStarted, HandCompleted, and HandFailed event variants to
ObserverEvent, and HandRunDuration, HandFindingsCount, HandSuccessRate
metric variants to ObserverMetric. Update all observer backends (log,
noop, verbose, prometheus, otel) to handle the new variants with
appropriate instrumentation. Prometheus backend registers hand_runs
counter, hand_duration histogram, and hand_findings counter. OTel
backend creates spans and records metrics for hand runs.
2026-03-16 18:24:47 -04:00

119 lines
3.3 KiB
Rust

use super::traits::{Observer, ObserverEvent, ObserverMetric};
use std::any::Any;
/// Zero-overhead observer — all methods compile to nothing
pub struct NoopObserver;
impl Observer for NoopObserver {
#[inline(always)]
fn record_event(&self, _event: &ObserverEvent) {}
#[inline(always)]
fn record_metric(&self, _metric: &ObserverMetric) {}
fn name(&self) -> &str {
"noop"
}
fn as_any(&self) -> &dyn Any {
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn noop_name() {
assert_eq!(NoopObserver.name(), "noop");
}
#[test]
fn noop_record_event_does_not_panic() {
let obs = NoopObserver;
obs.record_event(&ObserverEvent::HeartbeatTick);
obs.record_event(&ObserverEvent::AgentStart {
provider: "test".into(),
model: "test".into(),
});
obs.record_event(&ObserverEvent::AgentEnd {
provider: "test".into(),
model: "test".into(),
duration: Duration::from_millis(100),
tokens_used: Some(42),
cost_usd: Some(0.001),
});
obs.record_event(&ObserverEvent::AgentEnd {
provider: "test".into(),
model: "test".into(),
duration: Duration::ZERO,
tokens_used: None,
cost_usd: None,
});
obs.record_event(&ObserverEvent::ToolCall {
tool: "shell".into(),
duration: Duration::from_secs(1),
success: true,
});
obs.record_event(&ObserverEvent::ChannelMessage {
channel: "cli".into(),
direction: "inbound".into(),
});
obs.record_event(&ObserverEvent::Error {
component: "test".into(),
message: "boom".into(),
});
}
#[test]
fn noop_record_metric_does_not_panic() {
let obs = NoopObserver;
obs.record_metric(&ObserverMetric::RequestLatency(Duration::from_millis(50)));
obs.record_metric(&ObserverMetric::TokensUsed(1000));
obs.record_metric(&ObserverMetric::ActiveSessions(5));
obs.record_metric(&ObserverMetric::QueueDepth(0));
}
#[test]
fn noop_flush_does_not_panic() {
NoopObserver.flush();
}
#[test]
fn noop_hand_events_do_not_panic() {
let obs = NoopObserver;
obs.record_event(&ObserverEvent::HandStarted {
hand_name: "review".into(),
});
obs.record_event(&ObserverEvent::HandCompleted {
hand_name: "review".into(),
duration_ms: 1500,
findings_count: 3,
});
obs.record_event(&ObserverEvent::HandFailed {
hand_name: "review".into(),
error: "timeout".into(),
duration_ms: 5000,
});
}
#[test]
fn noop_hand_metrics_do_not_panic() {
let obs = NoopObserver;
obs.record_metric(&ObserverMetric::HandRunDuration {
hand_name: "review".into(),
duration: Duration::from_millis(1500),
});
obs.record_metric(&ObserverMetric::HandFindingsCount {
hand_name: "review".into(),
count: 5,
});
obs.record_metric(&ObserverMetric::HandSuccessRate {
hand_name: "review".into(),
success: true,
});
}
}