Skip to main content

mockforge_analytics/
export.rs

1//! Data export functionality
2
3use crate::database::AnalyticsDatabase;
4use crate::error::Result;
5use crate::models::AnalyticsFilter;
6use std::io::Write;
7
8impl AnalyticsDatabase {
9    /// Export metrics to CSV format
10    ///
11    /// # Errors
12    ///
13    /// Returns an error if the database query or CSV writing fails.
14    pub async fn export_to_csv<W: Write>(
15        &self,
16        writer: &mut W,
17        filter: &AnalyticsFilter,
18    ) -> Result<usize> {
19        // Write CSV header
20        writeln!(
21            writer,
22            "timestamp,protocol,method,endpoint,status_code,request_count,error_count,avg_latency_ms,p95_latency_ms,bytes_sent,bytes_received"
23        )?;
24
25        let aggregates = self.get_minute_aggregates(filter).await?;
26
27        for agg in &aggregates {
28            #[allow(clippy::cast_precision_loss)]
29            let avg_latency = if agg.request_count > 0 {
30                agg.latency_sum / agg.request_count as f64
31            } else {
32                0.0
33            };
34
35            writeln!(
36                writer,
37                "{},{},{},{},{},{},{},{:.2},{:.2},{},{}",
38                agg.timestamp,
39                agg.protocol,
40                agg.method.as_deref().unwrap_or(""),
41                agg.endpoint.as_deref().unwrap_or(""),
42                agg.status_code.unwrap_or(0),
43                agg.request_count,
44                agg.error_count,
45                avg_latency,
46                agg.latency_p95.unwrap_or(0.0),
47                agg.bytes_sent,
48                agg.bytes_received
49            )?;
50        }
51
52        Ok(aggregates.len())
53    }
54
55    /// Export metrics to JSON format
56    ///
57    /// # Errors
58    ///
59    /// Returns an error if the database query or JSON serialization fails.
60    pub async fn export_to_json(&self, filter: &AnalyticsFilter) -> Result<String> {
61        let aggregates = self.get_minute_aggregates(filter).await?;
62        let json = serde_json::to_string_pretty(&aggregates)?;
63        Ok(json)
64    }
65
66    /// Export endpoint stats to CSV
67    ///
68    /// # Errors
69    ///
70    /// Returns an error if the database query or CSV writing fails.
71    pub async fn export_endpoints_to_csv<W: Write>(
72        &self,
73        writer: &mut W,
74        workspace_id: Option<&str>,
75        limit: i64,
76    ) -> Result<usize> {
77        writeln!(
78            writer,
79            "endpoint,protocol,method,total_requests,total_errors,error_rate,avg_latency_ms,p95_latency_ms,bytes_sent,bytes_received"
80        )?;
81
82        let endpoints = self.get_top_endpoints(limit, workspace_id).await?;
83
84        for ep in &endpoints {
85            #[allow(clippy::cast_precision_loss)]
86            let error_rate = if ep.total_requests > 0 {
87                (ep.total_errors as f64 / ep.total_requests as f64) * 100.0
88            } else {
89                0.0
90            };
91
92            writeln!(
93                writer,
94                "{},{},{},{},{},{:.2},{:.2},{:.2},{},{}",
95                ep.endpoint,
96                ep.protocol,
97                ep.method.as_deref().unwrap_or(""),
98                ep.total_requests,
99                ep.total_errors,
100                error_rate,
101                ep.avg_latency_ms.unwrap_or(0.0),
102                ep.p95_latency_ms.unwrap_or(0.0),
103                ep.total_bytes_sent,
104                ep.total_bytes_received
105            )?;
106        }
107
108        Ok(endpoints.len())
109    }
110
111    /// Export error events to CSV
112    ///
113    /// # Errors
114    ///
115    /// Returns an error if the database query or CSV writing fails.
116    pub async fn export_errors_to_csv<W: Write>(
117        &self,
118        writer: &mut W,
119        filter: &AnalyticsFilter,
120        limit: i64,
121    ) -> Result<usize> {
122        writeln!(
123            writer,
124            "timestamp,protocol,method,endpoint,status_code,error_type,error_category,error_message,client_ip,trace_id"
125        )?;
126
127        let errors = self.get_recent_errors(limit, filter).await?;
128
129        for err in &errors {
130            writeln!(
131                writer,
132                "{},{},{},{},{},{},{},{},{},{}",
133                err.timestamp,
134                err.protocol,
135                err.method.as_deref().unwrap_or(""),
136                err.endpoint.as_deref().unwrap_or(""),
137                err.status_code.unwrap_or(0),
138                err.error_type.as_deref().unwrap_or(""),
139                err.error_category.as_deref().unwrap_or(""),
140                err.error_message.as_deref().unwrap_or(""),
141                err.client_ip.as_deref().unwrap_or(""),
142                err.trace_id.as_deref().unwrap_or("")
143            )?;
144        }
145
146        Ok(errors.len())
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153    use std::path::Path;
154
155    #[tokio::test]
156    async fn test_export_to_csv() {
157        let db = AnalyticsDatabase::new(Path::new(":memory:")).await.unwrap();
158        db.run_migrations().await.unwrap();
159
160        let mut buffer = Vec::new();
161        let filter = AnalyticsFilter::default();
162
163        let count = db.export_to_csv(&mut buffer, &filter).await.unwrap();
164        assert_eq!(count, 0); // No data yet
165
166        let csv = String::from_utf8(buffer).unwrap();
167        assert!(csv.contains("timestamp,protocol"));
168    }
169}