use crate::stats::StatsSnapshot;
use anyhow::{Context, Result};
use std::{
fs::OpenOptions,
io::{BufWriter, Write},
path::{Path, PathBuf},
};
const CSV_HEADER: &str = "timestamp,target,sample_count,loss_pct,rtt_min_us,rtt_mean_us,\
rtt_p50_us,rtt_p90_us,rtt_p95_us,rtt_p99_us,jitter_us,max_burst_loss,reorder_count\n";
pub struct CsvExporter {
path: PathBuf,
}
impl CsvExporter {
pub fn new(path: impl AsRef<Path>) -> Self {
Self {
path: path.as_ref().to_path_buf(),
}
}
pub fn emit(&self, snapshot: &StatsSnapshot) -> Result<()> {
let is_new = !self.path.exists();
let file = OpenOptions::new()
.create(true)
.append(true)
.open(&self.path)
.with_context(|| format!("cannot open CSV file {:?}", self.path))?;
let mut writer = BufWriter::new(file);
if is_new {
writer
.write_all(CSV_HEADER.as_bytes())
.context("failed to write CSV header")?;
}
let now = chrono::Utc::now().to_rfc3339();
writeln!(
writer,
"{},{},{},{:.2},{},{:.2},{},{},{},{},{:.2},{},{}",
now,
snapshot.target,
snapshot.sample_count,
snapshot.loss_pct,
snapshot.rtt_min_us.unwrap_or(0),
snapshot.rtt_mean_us.unwrap_or(0.0),
snapshot.rtt_p50_us.unwrap_or(0),
snapshot.rtt_p90_us.unwrap_or(0),
snapshot.rtt_p95_us.unwrap_or(0),
snapshot.rtt_p99_us.unwrap_or(0),
snapshot.jitter_us.unwrap_or(0.0),
snapshot.max_burst_loss,
snapshot.reorder_count,
)
.context("failed to write CSV row")?;
writer.flush().context("failed to flush CSV writer")
}
}