use prometheus::{Counter, CounterVec, Encoder, Opts, Registry, TextEncoder};
pub struct GatewayMetrics {
registry: Registry,
requests: CounterVec,
tokens: CounterVec,
config_reload_failures: Counter,
audit_drops: CounterVec,
}
impl GatewayMetrics {
pub fn new() -> anyhow::Result<Self> {
let registry = Registry::new();
let requests = CounterVec::new(
Opts::new("arbit_requests_total", "Total requests processed by arbit"),
&["agent", "outcome"],
)?;
registry.register(Box::new(requests.clone()))?;
let tokens = CounterVec::new(
Opts::new(
"arbit_tokens_total",
"Estimated token count processed by arbit (4-chars-per-token heuristic)",
),
&["agent", "direction"],
)?;
registry.register(Box::new(tokens.clone()))?;
let config_reload_failures = Counter::new(
"arbit_config_reload_failures_total",
"Number of times a config reload attempt failed (parse or I/O error)",
)?;
registry.register(Box::new(config_reload_failures.clone()))?;
let audit_drops = CounterVec::new(
Opts::new(
"arbit_audit_drops_total",
"Audit entries dropped because the backend channel was full",
),
&["backend"],
)?;
registry.register(Box::new(audit_drops.clone()))?;
Ok(Self {
registry,
requests,
tokens,
config_reload_failures,
audit_drops,
})
}
pub fn record(&self, agent: &str, outcome: &str) {
self.requests.with_label_values(&[agent, outcome]).inc();
}
pub fn record_tokens(&self, agent: &str, input_tokens: u32, output_tokens: u32) {
if input_tokens > 0 {
self.tokens
.with_label_values(&[agent, "input"])
.inc_by(f64::from(input_tokens));
}
if output_tokens > 0 {
self.tokens
.with_label_values(&[agent, "output"])
.inc_by(f64::from(output_tokens));
}
}
pub fn record_config_reload_failure(&self) {
self.config_reload_failures.inc();
}
pub fn record_audit_drop(&self, backend: &str) {
self.audit_drops.with_label_values(&[backend]).inc();
}
pub fn render(&self) -> String {
let encoder = TextEncoder::new();
let families = self.registry.gather();
let mut buf = Vec::new();
let _ = encoder.encode(&families, &mut buf);
String::from_utf8(buf).unwrap_or_default()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn record_tokens_updates_counter() {
let m = GatewayMetrics::new().unwrap();
m.record_tokens("agent-a", 10, 25);
let rendered = m.render();
assert!(rendered.contains("arbit_tokens_total"));
assert!(rendered.contains(r#"direction="input""#));
assert!(rendered.contains(r#"direction="output""#));
}
#[test]
fn zero_tokens_not_recorded() {
let m = GatewayMetrics::new().unwrap();
m.record_tokens("agent-a", 0, 0);
let rendered = m.render();
assert!(!rendered.contains(r#"agent="agent-a""#));
}
#[test]
fn multiple_agents_tracked_independently() {
let m = GatewayMetrics::new().unwrap();
m.record_tokens("cursor", 5, 10);
m.record_tokens("claude", 20, 40);
let rendered = m.render();
assert!(rendered.contains(r#"agent="cursor""#));
assert!(rendered.contains(r#"agent="claude""#));
}
}