use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use chrono::{DateTime, Duration, Utc};
use serde_json::Value;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UsagePeriod {
Hour1,
Hours24,
Days7,
Days30,
}
impl UsagePeriod {
fn delta(self) -> Duration {
match self {
UsagePeriod::Hour1 => Duration::hours(1),
UsagePeriod::Hours24 => Duration::hours(24),
UsagePeriod::Days7 => Duration::days(7),
UsagePeriod::Days30 => Duration::days(30),
}
}
pub fn parse(s: &str) -> Self {
match s {
"1h" => UsagePeriod::Hour1,
"7d" => UsagePeriod::Days7,
"30d" => UsagePeriod::Days30,
_ => UsagePeriod::Hours24,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct UsageSummary {
pub module_id: String,
pub calls: u64,
pub errors: u64,
pub latency_ms: f64,
}
fn default_audit_path() -> Option<PathBuf> {
dirs::home_dir().map(|h| h.join(".apcore-cli").join("audit.jsonl"))
}
pub fn compute_summary(
audit_path: Option<&Path>,
period: UsagePeriod,
now: DateTime<Utc>,
) -> HashMap<String, UsageSummary> {
let path: PathBuf = match audit_path {
Some(p) => p.to_path_buf(),
None => match default_audit_path() {
Some(p) => p,
None => return HashMap::new(),
},
};
if !path.exists() {
return HashMap::new();
}
let raw = match fs::read_to_string(&path) {
Ok(s) => s,
Err(_) => return HashMap::new(),
};
let cutoff = now - period.delta();
let mut counts: HashMap<String, u64> = HashMap::new();
let mut errors: HashMap<String, u64> = HashMap::new();
let mut latency_sum: HashMap<String, u64> = HashMap::new();
for line in raw.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
let entry: Value = match serde_json::from_str(line) {
Ok(v) => v,
Err(_) => continue,
};
let ts_str = match entry.get("timestamp").and_then(|v| v.as_str()) {
Some(s) => s,
None => continue,
};
let ts = match DateTime::parse_from_rfc3339(ts_str) {
Ok(t) => t.with_timezone(&Utc),
Err(_) => continue,
};
if ts < cutoff {
continue;
}
let module_id = match entry.get("module_id").and_then(|v| v.as_str()) {
Some(s) if !s.is_empty() => s.to_string(),
_ => continue,
};
*counts.entry(module_id.clone()).or_insert(0) += 1;
if entry.get("status").and_then(|v| v.as_str()) == Some("error") {
*errors.entry(module_id.clone()).or_insert(0) += 1;
}
let duration = entry
.get("duration_ms")
.and_then(|v| v.as_u64())
.unwrap_or(0);
*latency_sum.entry(module_id).or_insert(0) += duration;
}
let mut out = HashMap::new();
for (id, calls) in counts {
let err_count = errors.get(&id).copied().unwrap_or(0);
let lat_total = latency_sum.get(&id).copied().unwrap_or(0);
let avg = if calls > 0 {
lat_total as f64 / calls as f64
} else {
0.0
};
out.insert(
id.clone(),
UsageSummary {
module_id: id,
calls,
errors: err_count,
latency_ms: avg,
},
);
}
out
}
pub fn sort_modules_by_usage(modules: &mut [Value], field: &str, reverse: bool) -> bool {
let summary = compute_summary(None, UsagePeriod::Hours24, Utc::now());
if summary.is_empty() {
modules.sort_by(|a, b| {
let aid = a.get("module_id").and_then(|v| v.as_str()).unwrap_or("");
let bid = b.get("module_id").and_then(|v| v.as_str()).unwrap_or("");
aid.cmp(bid)
});
if reverse {
modules.reverse();
}
return false;
}
let key = |m: &Value| -> f64 {
let id = m.get("module_id").and_then(|v| v.as_str()).unwrap_or("");
match summary.get(id) {
Some(s) => match field {
"calls" => s.calls as f64,
"errors" => s.errors as f64,
"latency" => s.latency_ms,
_ => 0.0,
},
None => 0.0,
}
};
modules.sort_by(|a, b| {
let ka = key(a);
let kb = key(b);
let primary = ka.partial_cmp(&kb).unwrap_or(std::cmp::Ordering::Equal);
let primary = if reverse { primary.reverse() } else { primary };
if primary != std::cmp::Ordering::Equal {
return primary;
}
let aid = a.get("module_id").and_then(|v| v.as_str()).unwrap_or("");
let bid = b.get("module_id").and_then(|v| v.as_str()).unwrap_or("");
aid.cmp(bid)
});
true
}