Skip to main content

rusmes_loadtest/
reporter.rs

1//! Report generation for load test results
2
3use crate::metrics::LoadTestMetrics;
4use anyhow::Result;
5use std::fs::File;
6use std::io::Write;
7use std::path::Path;
8
9/// Report generator
10pub struct Reporter;
11
12impl Reporter {
13    /// Generate JSON report
14    pub fn generate_json(metrics: &LoadTestMetrics, path: &Path) -> Result<()> {
15        let report = JsonReport::from_metrics(metrics);
16        let json = serde_json::to_string_pretty(&report)?;
17
18        let mut file = File::create(path)?;
19        file.write_all(json.as_bytes())?;
20
21        Ok(())
22    }
23
24    /// Generate CSV report
25    pub fn generate_csv(metrics: &LoadTestMetrics, path: &Path) -> Result<()> {
26        let mut file = File::create(path)?;
27
28        writeln!(file, "metric,value")?;
29        writeln!(file, "total_requests,{}", metrics.total_requests)?;
30        writeln!(file, "successful_requests,{}", metrics.successful_requests)?;
31        writeln!(file, "failed_requests,{}", metrics.failed_requests)?;
32        writeln!(file, "success_rate,{:.4}", metrics.success_rate())?;
33        writeln!(
34            file,
35            "requests_per_second,{:.2}",
36            metrics.requests_per_second()
37        )?;
38        writeln!(file, "bytes_sent,{}", metrics.bytes_sent)?;
39        writeln!(file, "bytes_received,{}", metrics.bytes_received)?;
40
41        if let Some(duration) = metrics.duration() {
42            writeln!(file, "duration_secs,{:.2}", duration.as_secs_f64())?;
43        }
44
45        let stats = metrics.latency_stats();
46        writeln!(
47            file,
48            "latency_min_ms,{:.2}",
49            stats.min.as_secs_f64() * 1000.0
50        )?;
51        writeln!(
52            file,
53            "latency_max_ms,{:.2}",
54            stats.max.as_secs_f64() * 1000.0
55        )?;
56        writeln!(
57            file,
58            "latency_mean_ms,{:.2}",
59            stats.mean.as_secs_f64() * 1000.0
60        )?;
61        writeln!(
62            file,
63            "latency_p50_ms,{:.2}",
64            stats.p50.as_secs_f64() * 1000.0
65        )?;
66        writeln!(
67            file,
68            "latency_p95_ms,{:.2}",
69            stats.p95.as_secs_f64() * 1000.0
70        )?;
71        writeln!(
72            file,
73            "latency_p99_ms,{:.2}",
74            stats.p99.as_secs_f64() * 1000.0
75        )?;
76        writeln!(
77            file,
78            "latency_p999_ms,{:.2}",
79            stats.p999.as_secs_f64() * 1000.0
80        )?;
81
82        Ok(())
83    }
84
85    /// Generate HTML report
86    pub fn generate_html(metrics: &LoadTestMetrics, path: &Path) -> Result<()> {
87        let stats = metrics.latency_stats();
88        let duration = metrics
89            .duration()
90            .map(|d| format!("{:.2}s", d.as_secs_f64()))
91            .unwrap_or_else(|| "N/A".to_string());
92
93        let html = format!(
94            r#"<!DOCTYPE html>
95<html>
96<head>
97    <meta charset="UTF-8">
98    <title>Load Test Report</title>
99    <style>
100        body {{
101            font-family: Arial, sans-serif;
102            margin: 20px;
103            background-color: #f5f5f5;
104        }}
105        .container {{
106            max-width: 1200px;
107            margin: 0 auto;
108            background-color: white;
109            padding: 20px;
110            border-radius: 5px;
111            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
112        }}
113        h1 {{
114            color: #333;
115            border-bottom: 2px solid #4CAF50;
116            padding-bottom: 10px;
117        }}
118        h2 {{
119            color: #555;
120            margin-top: 30px;
121        }}
122        table {{
123            width: 100%;
124            border-collapse: collapse;
125            margin: 20px 0;
126        }}
127        th, td {{
128            padding: 12px;
129            text-align: left;
130            border-bottom: 1px solid #ddd;
131        }}
132        th {{
133            background-color: #4CAF50;
134            color: white;
135        }}
136        tr:hover {{
137            background-color: #f5f5f5;
138        }}
139        .success {{
140            color: #4CAF50;
141        }}
142        .error {{
143            color: #f44336;
144        }}
145        .metric-value {{
146            font-weight: bold;
147            font-size: 1.2em;
148        }}
149    </style>
150</head>
151<body>
152    <div class="container">
153        <h1>Load Test Report</h1>
154
155        <h2>Summary</h2>
156        <table>
157            <tr>
158                <th>Metric</th>
159                <th>Value</th>
160            </tr>
161            <tr>
162                <td>Duration</td>
163                <td class="metric-value">{}</td>
164            </tr>
165            <tr>
166                <td>Total Requests</td>
167                <td class="metric-value">{}</td>
168            </tr>
169            <tr>
170                <td>Successful Requests</td>
171                <td class="metric-value success">{}</td>
172            </tr>
173            <tr>
174                <td>Failed Requests</td>
175                <td class="metric-value error">{}</td>
176            </tr>
177            <tr>
178                <td>Success Rate</td>
179                <td class="metric-value">{:.2}%</td>
180            </tr>
181            <tr>
182                <td>Throughput</td>
183                <td class="metric-value">{:.2} req/s</td>
184            </tr>
185        </table>
186
187        <h2>Data Transfer</h2>
188        <table>
189            <tr>
190                <th>Metric</th>
191                <th>Value</th>
192            </tr>
193            <tr>
194                <td>Bytes Sent</td>
195                <td class="metric-value">{} bytes ({:.2} MB)</td>
196            </tr>
197            <tr>
198                <td>Bytes Received</td>
199                <td class="metric-value">{} bytes ({:.2} MB)</td>
200            </tr>
201        </table>
202
203        <h2>Latency Statistics</h2>
204        <table>
205            <tr>
206                <th>Percentile</th>
207                <th>Latency</th>
208            </tr>
209            <tr>
210                <td>Minimum</td>
211                <td class="metric-value">{:.2}ms</td>
212            </tr>
213            <tr>
214                <td>Mean</td>
215                <td class="metric-value">{:.2}ms</td>
216            </tr>
217            <tr>
218                <td>Maximum</td>
219                <td class="metric-value">{:.2}ms</td>
220            </tr>
221            <tr>
222                <td>p50</td>
223                <td class="metric-value">{:.2}ms</td>
224            </tr>
225            <tr>
226                <td>p95</td>
227                <td class="metric-value">{:.2}ms</td>
228            </tr>
229            <tr>
230                <td>p99</td>
231                <td class="metric-value">{:.2}ms</td>
232            </tr>
233            <tr>
234                <td>p99.9</td>
235                <td class="metric-value">{:.2}ms</td>
236            </tr>
237        </table>
238
239        <h2>Errors</h2>
240        <p>Total Errors: {}</p>
241        {}
242    </div>
243</body>
244</html>"#,
245            duration,
246            metrics.total_requests,
247            metrics.successful_requests,
248            metrics.failed_requests,
249            metrics.success_rate() * 100.0,
250            metrics.requests_per_second(),
251            metrics.bytes_sent,
252            metrics.bytes_sent as f64 / 1_000_000.0,
253            metrics.bytes_received,
254            metrics.bytes_received as f64 / 1_000_000.0,
255            stats.min.as_secs_f64() * 1000.0,
256            stats.mean.as_secs_f64() * 1000.0,
257            stats.max.as_secs_f64() * 1000.0,
258            stats.p50.as_secs_f64() * 1000.0,
259            stats.p95.as_secs_f64() * 1000.0,
260            stats.p99.as_secs_f64() * 1000.0,
261            stats.p999.as_secs_f64() * 1000.0,
262            metrics.errors.len(),
263            if metrics.errors.is_empty() {
264                "<p>No errors occurred during the test.</p>".to_string()
265            } else {
266                let mut error_html = String::from("<ul>");
267                for error in metrics.errors.iter().take(20) {
268                    error_html.push_str(&format!("<li>{}</li>", error));
269                }
270                error_html.push_str("</ul>");
271                error_html
272            }
273        );
274
275        let mut file = File::create(path)?;
276        file.write_all(html.as_bytes())?;
277
278        Ok(())
279    }
280
281    /// Generate Prometheus metrics format
282    pub fn generate_prometheus_metrics(metrics: &LoadTestMetrics) -> String {
283        let stats = metrics.latency_stats();
284
285        format!(
286            "# HELP loadtest_total_requests Total number of requests\n\
287             # TYPE loadtest_total_requests counter\n\
288             loadtest_total_requests {}\n\
289             # HELP loadtest_successful_requests Number of successful requests\n\
290             # TYPE loadtest_successful_requests counter\n\
291             loadtest_successful_requests {}\n\
292             # HELP loadtest_failed_requests Number of failed requests\n\
293             # TYPE loadtest_failed_requests counter\n\
294             loadtest_failed_requests {}\n\
295             # HELP loadtest_success_rate Success rate (0.0-1.0)\n\
296             # TYPE loadtest_success_rate gauge\n\
297             loadtest_success_rate {:.4}\n\
298             # HELP loadtest_requests_per_second Request throughput\n\
299             # TYPE loadtest_requests_per_second gauge\n\
300             loadtest_requests_per_second {:.2}\n\
301             # HELP loadtest_bytes_sent Total bytes sent\n\
302             # TYPE loadtest_bytes_sent counter\n\
303             loadtest_bytes_sent {}\n\
304             # HELP loadtest_bytes_received Total bytes received\n\
305             # TYPE loadtest_bytes_received counter\n\
306             loadtest_bytes_received {}\n\
307             # HELP loadtest_latency_seconds Latency in seconds\n\
308             # TYPE loadtest_latency_seconds summary\n\
309             loadtest_latency_seconds{{quantile=\"0.5\"}} {:.6}\n\
310             loadtest_latency_seconds{{quantile=\"0.95\"}} {:.6}\n\
311             loadtest_latency_seconds{{quantile=\"0.99\"}} {:.6}\n\
312             loadtest_latency_seconds{{quantile=\"0.999\"}} {:.6}\n\
313             loadtest_latency_seconds_sum {:.6}\n\
314             loadtest_latency_seconds_count {}\n",
315            metrics.total_requests,
316            metrics.successful_requests,
317            metrics.failed_requests,
318            metrics.success_rate(),
319            metrics.requests_per_second(),
320            metrics.bytes_sent,
321            metrics.bytes_received,
322            stats.p50.as_secs_f64(),
323            stats.p95.as_secs_f64(),
324            stats.p99.as_secs_f64(),
325            stats.p999.as_secs_f64(),
326            stats.mean.as_secs_f64() * metrics.successful_requests as f64,
327            metrics.successful_requests,
328        )
329    }
330}
331
332/// JSON report structure
333#[derive(Debug, serde::Serialize, serde::Deserialize)]
334struct JsonReport {
335    duration_secs: f64,
336    total_requests: u64,
337    successful_requests: u64,
338    failed_requests: u64,
339    success_rate: f64,
340    requests_per_second: f64,
341    bytes_sent: u64,
342    bytes_received: u64,
343    latency: JsonLatencyStats,
344    errors: Vec<String>,
345}
346
347#[derive(Debug, serde::Serialize, serde::Deserialize)]
348struct JsonLatencyStats {
349    min_ms: f64,
350    max_ms: f64,
351    mean_ms: f64,
352    p50_ms: f64,
353    p95_ms: f64,
354    p99_ms: f64,
355    p999_ms: f64,
356}
357
358impl JsonReport {
359    fn from_metrics(metrics: &LoadTestMetrics) -> Self {
360        let stats = metrics.latency_stats();
361
362        Self {
363            duration_secs: metrics.duration().map(|d| d.as_secs_f64()).unwrap_or(0.0),
364            total_requests: metrics.total_requests,
365            successful_requests: metrics.successful_requests,
366            failed_requests: metrics.failed_requests,
367            success_rate: metrics.success_rate(),
368            requests_per_second: metrics.requests_per_second(),
369            bytes_sent: metrics.bytes_sent,
370            bytes_received: metrics.bytes_received,
371            latency: JsonLatencyStats {
372                min_ms: stats.min.as_secs_f64() * 1000.0,
373                max_ms: stats.max.as_secs_f64() * 1000.0,
374                mean_ms: stats.mean.as_secs_f64() * 1000.0,
375                p50_ms: stats.p50.as_secs_f64() * 1000.0,
376                p95_ms: stats.p95.as_secs_f64() * 1000.0,
377                p99_ms: stats.p99.as_secs_f64() * 1000.0,
378                p999_ms: stats.p999.as_secs_f64() * 1000.0,
379            },
380            errors: metrics.errors.clone(),
381        }
382    }
383}
384
385#[cfg(test)]
386mod tests {
387    use super::*;
388    use std::time::Duration;
389    use tempfile::TempDir;
390
391    #[test]
392    fn test_json_report_generation() {
393        let temp_dir = TempDir::new().unwrap();
394        let report_path = temp_dir.path().join("report.json");
395
396        let mut metrics = LoadTestMetrics::new();
397        metrics.record_success(Duration::from_millis(10), 100, 50);
398        metrics.record_success(Duration::from_millis(20), 100, 50);
399
400        let result = Reporter::generate_json(&metrics, &report_path);
401        assert!(result.is_ok());
402        assert!(report_path.exists());
403
404        let content = std::fs::read_to_string(&report_path).unwrap();
405        assert!(content.contains("total_requests"));
406        assert!(content.contains("latency"));
407    }
408
409    #[test]
410    fn test_csv_report_generation() {
411        let temp_dir = TempDir::new().unwrap();
412        let report_path = temp_dir.path().join("report.csv");
413
414        let mut metrics = LoadTestMetrics::new();
415        metrics.record_success(Duration::from_millis(10), 100, 50);
416
417        let result = Reporter::generate_csv(&metrics, &report_path);
418        assert!(result.is_ok());
419        assert!(report_path.exists());
420
421        let content = std::fs::read_to_string(&report_path).unwrap();
422        assert!(content.contains("metric,value"));
423        assert!(content.contains("total_requests"));
424    }
425
426    #[test]
427    fn test_html_report_generation() {
428        let temp_dir = TempDir::new().unwrap();
429        let report_path = temp_dir.path().join("report.html");
430
431        let mut metrics = LoadTestMetrics::new();
432        metrics.record_success(Duration::from_millis(10), 100, 50);
433
434        let result = Reporter::generate_html(&metrics, &report_path);
435        assert!(result.is_ok());
436        assert!(report_path.exists());
437
438        let content = std::fs::read_to_string(&report_path).unwrap();
439        assert!(content.contains("<html>"));
440        assert!(content.contains("Load Test Report"));
441    }
442
443    #[test]
444    fn test_prometheus_metrics_generation() {
445        let mut metrics = LoadTestMetrics::new();
446        metrics.record_success(Duration::from_millis(10), 100, 50);
447        metrics.record_success(Duration::from_millis(20), 100, 50);
448
449        let prometheus = Reporter::generate_prometheus_metrics(&metrics);
450        assert!(prometheus.contains("loadtest_total_requests"));
451        assert!(prometheus.contains("loadtest_latency_seconds"));
452        assert!(prometheus.contains("quantile"));
453    }
454}