use std::fmt::Write as _;
use std::sync::atomic::{AtomicU64, Ordering};
const MICRODOLLAR: f64 = 1_000_000.0;
#[derive(Debug)]
#[non_exhaustive]
pub struct AgnosMetrics {
crews_total: AtomicU64,
crews_active: AtomicU64,
tasks_completed: AtomicU64,
tasks_failed: AtomicU64,
inference_tokens_total: AtomicU64,
inference_cost_micro_usd: AtomicU64,
}
impl Default for AgnosMetrics {
fn default() -> Self {
Self::new()
}
}
impl AgnosMetrics {
#[must_use]
pub fn new() -> Self {
Self {
crews_total: AtomicU64::new(0),
crews_active: AtomicU64::new(0),
tasks_completed: AtomicU64::new(0),
tasks_failed: AtomicU64::new(0),
inference_tokens_total: AtomicU64::new(0),
inference_cost_micro_usd: AtomicU64::new(0),
}
}
pub fn record_crew_started(&self) {
self.crews_total.fetch_add(1, Ordering::Relaxed);
self.crews_active.fetch_add(1, Ordering::Relaxed);
tracing::trace!("metrics: crew started");
}
pub fn record_crew_completed(&self) {
self.crews_active
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |v| {
Some(v.saturating_sub(1))
})
.ok();
tracing::trace!("metrics: crew completed");
}
pub fn record_task_completed(&self) {
self.tasks_completed.fetch_add(1, Ordering::Relaxed);
}
pub fn record_task_failed(&self) {
self.tasks_failed.fetch_add(1, Ordering::Relaxed);
}
pub fn record_inference(&self, tokens: u64, cost_usd: f64) {
self.inference_tokens_total
.fetch_add(tokens, Ordering::Relaxed);
let micro = (cost_usd * MICRODOLLAR) as u64;
self.inference_cost_micro_usd
.fetch_add(micro, Ordering::Relaxed);
tracing::trace!(tokens, cost_usd, "metrics: inference recorded");
}
#[must_use]
#[inline]
pub fn crews_total(&self) -> u64 {
self.crews_total.load(Ordering::Relaxed)
}
#[must_use]
#[inline]
pub fn crews_active(&self) -> u64 {
self.crews_active.load(Ordering::Relaxed)
}
#[must_use]
#[inline]
pub fn tasks_completed(&self) -> u64 {
self.tasks_completed.load(Ordering::Relaxed)
}
#[must_use]
#[inline]
pub fn tasks_failed(&self) -> u64 {
self.tasks_failed.load(Ordering::Relaxed)
}
#[must_use]
#[inline]
pub fn inference_tokens_total(&self) -> u64 {
self.inference_tokens_total.load(Ordering::Relaxed)
}
#[must_use]
#[inline]
pub fn inference_cost_usd(&self) -> f64 {
self.inference_cost_micro_usd.load(Ordering::Relaxed) as f64 / MICRODOLLAR
}
#[must_use]
pub fn gather(&self) -> String {
let mut out = String::with_capacity(1024);
let _ = writeln!(
out,
"# HELP agnosai_crews_total Total number of crews created."
);
let _ = writeln!(out, "# TYPE agnosai_crews_total counter");
let _ = writeln!(out, "agnosai_crews_total {}", self.crews_total());
let _ = writeln!(
out,
"# HELP agnosai_crews_active Number of currently active crews."
);
let _ = writeln!(out, "# TYPE agnosai_crews_active gauge");
let _ = writeln!(out, "agnosai_crews_active {}", self.crews_active());
let _ = writeln!(
out,
"# HELP agnosai_tasks_completed_total Total tasks completed successfully."
);
let _ = writeln!(out, "# TYPE agnosai_tasks_completed_total counter");
let _ = writeln!(
out,
"agnosai_tasks_completed_total {}",
self.tasks_completed()
);
let _ = writeln!(
out,
"# HELP agnosai_tasks_failed_total Total tasks that failed."
);
let _ = writeln!(out, "# TYPE agnosai_tasks_failed_total counter");
let _ = writeln!(out, "agnosai_tasks_failed_total {}", self.tasks_failed());
let _ = writeln!(
out,
"# HELP agnosai_inference_tokens_total Total inference tokens processed."
);
let _ = writeln!(out, "# TYPE agnosai_inference_tokens_total counter");
let _ = writeln!(
out,
"agnosai_inference_tokens_total {}",
self.inference_tokens_total()
);
let _ = writeln!(
out,
"# HELP agnosai_inference_cost_usd_total Total inference cost in USD."
);
let _ = writeln!(out, "# TYPE agnosai_inference_cost_usd_total counter");
let _ = writeln!(
out,
"agnosai_inference_cost_usd_total {:.6}",
self.inference_cost_usd()
);
out
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_metrics_all_zero() {
let m = AgnosMetrics::new();
assert_eq!(m.crews_total(), 0);
assert_eq!(m.crews_active(), 0);
assert_eq!(m.tasks_completed(), 0);
assert_eq!(m.tasks_failed(), 0);
assert_eq!(m.inference_tokens_total(), 0);
assert!((m.inference_cost_usd() - 0.0).abs() < f64::EPSILON);
}
#[test]
fn record_crew_lifecycle() {
let m = AgnosMetrics::new();
m.record_crew_started();
assert_eq!(m.crews_total(), 1);
assert_eq!(m.crews_active(), 1);
m.record_crew_started();
assert_eq!(m.crews_total(), 2);
assert_eq!(m.crews_active(), 2);
m.record_crew_completed();
assert_eq!(m.crews_total(), 2);
assert_eq!(m.crews_active(), 1);
m.record_crew_completed();
assert_eq!(m.crews_active(), 0);
}
#[test]
fn crew_completed_does_not_underflow() {
let m = AgnosMetrics::new();
m.record_crew_completed();
assert_eq!(m.crews_active(), 0);
}
#[test]
fn record_tasks() {
let m = AgnosMetrics::new();
m.record_task_completed();
m.record_task_completed();
m.record_task_failed();
assert_eq!(m.tasks_completed(), 2);
assert_eq!(m.tasks_failed(), 1);
}
#[test]
fn record_inference() {
let m = AgnosMetrics::new();
m.record_inference(1000, 0.05);
m.record_inference(2000, 0.10);
assert_eq!(m.inference_tokens_total(), 3000);
assert!((m.inference_cost_usd() - 0.15).abs() < 1e-6);
}
#[test]
fn gather_format() {
let m = AgnosMetrics::new();
m.record_crew_started();
m.record_task_completed();
m.record_inference(500, 0.025);
let output = m.gather();
assert!(output.contains("# TYPE agnosai_crews_total counter"));
assert!(output.contains("agnosai_crews_total 1"));
assert!(output.contains("# TYPE agnosai_crews_active gauge"));
assert!(output.contains("agnosai_crews_active 1"));
assert!(output.contains("agnosai_tasks_completed_total 1"));
assert!(output.contains("agnosai_tasks_failed_total 0"));
assert!(output.contains("agnosai_inference_tokens_total 500"));
assert!(output.contains("agnosai_inference_cost_usd_total 0.025000"));
}
#[test]
fn gather_empty_metrics() {
let m = AgnosMetrics::new();
let output = m.gather();
assert!(output.contains("agnosai_crews_total 0"));
assert!(output.contains("agnosai_crews_active 0"));
assert!(output.contains("agnosai_tasks_completed_total 0"));
assert!(output.contains("agnosai_tasks_failed_total 0"));
assert!(output.contains("agnosai_inference_tokens_total 0"));
assert!(output.contains("agnosai_inference_cost_usd_total 0.000000"));
}
#[test]
fn default_is_same_as_new() {
let d = AgnosMetrics::default();
assert_eq!(d.crews_total(), 0);
assert_eq!(d.crews_active(), 0);
}
}