Skip to main content

netpulse/export/
csv.rs

1// src/export/csv.rs — CSV Exporter
2//
3// Appends StatsSnapshot rows to a CSV file on disk.
4// Creates the file (with header) if it doesn't exist.
5// Each row is one stats report for one target at one point in time.
6
7use 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
18/// Appends StatsSnapshots as CSV rows to a file.
19pub struct CsvExporter {
20    path: PathBuf,
21}
22
23impl CsvExporter {
24    /// Create a new CsvExporter targeting the given file.
25    /// The file and its header are created on first write if not present.
26    pub fn new(path: impl AsRef<Path>) -> Self {
27        Self {
28            path: path.as_ref().to_path_buf(),
29        }
30    }
31
32    /// Append a single StatsSnapshot row to the CSV file.
33    /// Writes the CSV header if the file is newly created.
34    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}