zeroclaw/src/observability/mod.rs
argenis de la rosa eba544dbd4 feat(observability): implement Prometheus metrics backend with /metrics endpoint
- Adds PrometheusObserver backend with counters, histograms, and gauges
- Tracks agent starts/duration, tool calls, channel messages, heartbeat ticks, errors, request latency, tokens, sessions, queue depth
- Adds GET /metrics endpoint to gateway for Prometheus scraping
- Adds provider/model labels to AgentStart and AgentEnd events for better observability
- Adds as_any() method to Observer trait for backend-specific downcast

Metrics exposed:
- zeroclaw_agent_starts_total (Counter) with provider/model labels
- zeroclaw_agent_duration_seconds (Histogram) with provider/model labels
- zeroclaw_tool_calls_total (Counter) with tool/success labels
- zeroclaw_tool_duration_seconds (Histogram) with tool label
- zeroclaw_channel_messages_total (Counter) with channel/direction labels
- zeroclaw_heartbeat_ticks_total (Counter)
- zeroclaw_errors_total (Counter) with component label
- zeroclaw_request_latency_seconds (Histogram)
- zeroclaw_tokens_used_last (Gauge)
- zeroclaw_active_sessions (Gauge)
- zeroclaw_queue_depth (Gauge)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 12:06:05 +08:00

96 lines
2.5 KiB
Rust

pub mod log;
pub mod multi;
pub mod noop;
pub mod prometheus;
pub mod traits;
pub use self::log::LogObserver;
pub use noop::NoopObserver;
pub use prometheus::PrometheusObserver;
pub use traits::{Observer, ObserverEvent};
use crate::config::ObservabilityConfig;
/// Factory: create the right observer from config
pub fn create_observer(config: &ObservabilityConfig) -> Box<dyn Observer> {
match config.backend.as_str() {
"log" => Box::new(LogObserver::new()),
"prometheus" => Box::new(PrometheusObserver::new()),
"none" | "noop" => Box::new(NoopObserver),
_ => {
tracing::warn!(
"Unknown observability backend '{}', falling back to noop",
config.backend
);
Box::new(NoopObserver)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn factory_none_returns_noop() {
let cfg = ObservabilityConfig {
backend: "none".into(),
..Default::default()
};
assert_eq!(create_observer(&cfg).name(), "noop");
}
#[test]
fn factory_noop_returns_noop() {
let cfg = ObservabilityConfig {
backend: "noop".into(),
..Default::default()
};
assert_eq!(create_observer(&cfg).name(), "noop");
}
#[test]
fn factory_log_returns_log() {
let cfg = ObservabilityConfig {
backend: "log".into(),
..Default::default()
};
assert_eq!(create_observer(&cfg).name(), "log");
}
#[test]
fn factory_prometheus_returns_prometheus() {
let cfg = ObservabilityConfig {
backend: "prometheus".into(),
..Default::default()
};
assert_eq!(create_observer(&cfg).name(), "prometheus");
}
#[test]
fn factory_unknown_falls_back_to_noop() {
let cfg = ObservabilityConfig {
backend: "xyzzy_unknown".into(),
..Default::default()
};
assert_eq!(create_observer(&cfg).name(), "noop");
}
#[test]
fn factory_empty_string_falls_back_to_noop() {
let cfg = ObservabilityConfig {
backend: String::new(),
..Default::default()
};
assert_eq!(create_observer(&cfg).name(), "noop");
}
#[test]
fn factory_garbage_falls_back_to_noop() {
let cfg = ObservabilityConfig {
backend: "xyzzy_garbage_123".into(),
..Default::default()
};
assert_eq!(create_observer(&cfg).name(), "noop");
}
}