1use crate::stats::StatsSnapshot;
8use anyhow::{Context, Result};
9use std::{
10 fs::OpenOptions,
11 io::{BufWriter, Write},
12 path::{Path, PathBuf},
13};
14
15const CSV_HEADER: &str = "timestamp,target,sample_count,loss_pct,rtt_min_us,rtt_mean_us,\
16 rtt_p50_us,rtt_p90_us,rtt_p95_us,rtt_p99_us,jitter_us,max_burst_loss,reorder_count\n";
17
18pub struct CsvExporter {
20 path: PathBuf,
21}
22
23impl CsvExporter {
24 pub fn new(path: impl AsRef<Path>) -> Self {
27 Self {
28 path: path.as_ref().to_path_buf(),
29 }
30 }
31
32 pub fn emit(&self, snapshot: &StatsSnapshot) -> Result<()> {
35 let is_new = !self.path.exists();
36
37 let file = OpenOptions::new()
38 .create(true)
39 .append(true)
40 .open(&self.path)
41 .with_context(|| format!("cannot open CSV file {:?}", self.path))?;
42
43 let mut writer = BufWriter::new(file);
44
45 if is_new {
46 writer
47 .write_all(CSV_HEADER.as_bytes())
48 .context("failed to write CSV header")?;
49 }
50
51 let now = chrono::Utc::now().to_rfc3339();
52
53 writeln!(
54 writer,
55 "{},{},{},{:.2},{},{:.2},{},{},{},{},{:.2},{},{}",
56 now,
57 snapshot.target,
58 snapshot.sample_count,
59 snapshot.loss_pct,
60 snapshot.rtt_min_us.unwrap_or(0),
61 snapshot.rtt_mean_us.unwrap_or(0.0),
62 snapshot.rtt_p50_us.unwrap_or(0),
63 snapshot.rtt_p90_us.unwrap_or(0),
64 snapshot.rtt_p95_us.unwrap_or(0),
65 snapshot.rtt_p99_us.unwrap_or(0),
66 snapshot.jitter_us.unwrap_or(0.0),
67 snapshot.max_burst_loss,
68 snapshot.reorder_count,
69 )
70 .context("failed to write CSV row")?;
71
72 writer.flush().context("failed to flush CSV writer")
73 }
74}