use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum Metric {
Iterations,
TokensUsed,
CostUsd,
WallClockSeconds,
FailedDispatches,
Retries,
StaleIterations,
ValidationPassRate,
}
impl Metric {
pub fn as_str(self) -> &'static str {
match self {
Metric::Iterations => "iterations",
Metric::TokensUsed => "tokens_used",
Metric::CostUsd => "cost_usd",
Metric::WallClockSeconds => "wall_clock_seconds",
Metric::FailedDispatches => "failed_dispatches",
Metric::Retries => "retries",
Metric::StaleIterations => "stale_iterations",
Metric::ValidationPassRate => "validation_pass_rate",
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Alert {
pub id: String,
pub metric: Metric,
#[serde(default)]
pub above: Option<f64>,
#[serde(default)]
pub below: Option<f64>,
#[serde(default)]
pub message: Option<String>,
}
impl Alert {
pub fn fires_at(&self, value: f64) -> bool {
self.above.is_some_and(|t| value > t) || self.below.is_some_and(|t| value < t)
}
pub fn describe(&self, value: f64) -> String {
if let Some(m) = &self.message {
return format!("{} ({} = {})", m, self.metric.as_str(), trim(value));
}
let bound = match (self.above, self.below) {
(Some(t), _) if value > t => format!("above {}", trim(t)),
(_, Some(t)) if value < t => format!("below {}", trim(t)),
_ => "outside its bounds".to_string(),
};
format!("{} is {} ({bound})", self.metric.as_str(), trim(value))
}
}
fn trim(v: f64) -> String {
if v.fract() == 0.0 && v.abs() < 1e15 {
format!("{}", v as i64)
} else {
format!("{v:.4}")
.trim_end_matches('0')
.trim_end_matches('.')
.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn alert(above: Option<f64>, below: Option<f64>) -> Alert {
Alert {
id: "a".into(),
metric: Metric::CostUsd,
above,
below,
message: None,
}
}
#[test]
fn an_alert_fires_strictly_past_its_threshold() {
let a = alert(Some(2.0), None);
assert!(!a.fires_at(2.0), "at the threshold is not past it");
assert!(a.fires_at(2.01));
let b = alert(None, Some(0.5));
assert!(b.fires_at(0.4));
assert!(!b.fires_at(0.5));
}
#[test]
fn the_default_message_names_the_metric_and_the_bound() {
assert_eq!(
alert(Some(2.0), None).describe(3.5),
"cost_usd is 3.5 (above 2)"
);
}
}