1use crate::metrics::LoadTestMetrics;
4use anyhow::Result;
5use std::fs::File;
6use std::io::Write;
7use std::path::Path;
8
9pub struct Reporter;
11
12impl Reporter {
13 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 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 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 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#[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}