use std::fmt;
use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct RunStats {
pub total_runs: u64,
pub completed_runs: u64,
pub failed_runs: u64,
pub cancelled_runs: u64,
pub active_runs: u64,
pub total_cost_usd: Decimal,
pub total_duration_ms: u64,
}
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum HistoryPeriod {
#[serde(rename = "24h")]
TwentyFourHours,
#[default]
#[serde(rename = "7d")]
SevenDays,
#[serde(rename = "30d")]
ThirtyDays,
#[serde(rename = "90d")]
NinetyDays,
}
impl fmt::Display for HistoryPeriod {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::TwentyFourHours => write!(f, "24h"),
Self::SevenDays => write!(f, "7d"),
Self::ThirtyDays => write!(f, "30d"),
Self::NinetyDays => write!(f, "90d"),
}
}
}
impl HistoryPeriod {
pub fn default_granularity(&self) -> HistoryGranularity {
match self {
Self::TwentyFourHours => HistoryGranularity::OneHour,
Self::SevenDays | Self::ThirtyDays => HistoryGranularity::OneDay,
Self::NinetyDays => HistoryGranularity::OneWeek,
}
}
pub fn hours(&self) -> i64 {
match self {
Self::TwentyFourHours => 24,
Self::SevenDays => 7 * 24,
Self::ThirtyDays => 30 * 24,
Self::NinetyDays => 90 * 24,
}
}
}
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum HistoryGranularity {
#[serde(rename = "1h")]
OneHour,
#[serde(rename = "1d")]
OneDay,
#[serde(rename = "1w")]
OneWeek,
}
impl fmt::Display for HistoryGranularity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::OneHour => write!(f, "1h"),
Self::OneDay => write!(f, "1d"),
Self::OneWeek => write!(f, "1w"),
}
}
}
impl HistoryGranularity {
pub fn pg_interval(&self) -> &'static str {
match self {
Self::OneHour => "hour",
Self::OneDay => "day",
Self::OneWeek => "week",
}
}
pub fn seconds(&self) -> i64 {
match self {
Self::OneHour => 3600,
Self::OneDay => 86400,
Self::OneWeek => 604800,
}
}
}
#[derive(Debug, Clone)]
pub struct StatsHistoryFilter {
pub workflow_name: Option<String>,
pub period: HistoryPeriod,
pub granularity: HistoryGranularity,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StatsHistoryBucket {
pub time: DateTime<Utc>,
pub completed: u64,
pub failed: u64,
pub cancelled: u64,
pub avg_duration_ms: u64,
pub p95_duration_ms: u64,
pub total_cost_usd: Decimal,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_is_zeros() {
let stats = RunStats::default();
assert_eq!(stats.total_runs, 0);
assert_eq!(stats.completed_runs, 0);
assert_eq!(stats.failed_runs, 0);
assert_eq!(stats.cancelled_runs, 0);
assert_eq!(stats.active_runs, 0);
assert_eq!(stats.total_cost_usd, Decimal::ZERO);
assert_eq!(stats.total_duration_ms, 0);
}
#[test]
fn serde_roundtrip() {
let stats = RunStats {
total_runs: 100,
completed_runs: 80,
failed_runs: 15,
cancelled_runs: 5,
active_runs: 0,
total_cost_usd: Decimal::new(4250, 2),
total_duration_ms: 3600000,
};
let json = serde_json::to_string(&stats).expect("serialize");
let back: RunStats = serde_json::from_str(&json).expect("deserialize");
assert_eq!(stats.total_runs, back.total_runs);
assert_eq!(stats.completed_runs, back.completed_runs);
assert_eq!(stats.failed_runs, back.failed_runs);
assert_eq!(stats.cancelled_runs, back.cancelled_runs);
assert_eq!(stats.active_runs, back.active_runs);
assert_eq!(stats.total_cost_usd, back.total_cost_usd);
assert_eq!(stats.total_duration_ms, back.total_duration_ms);
}
}