1use crate::metrics;
4
5use std::collections::BTreeMap;
6use std::mem;
7
8use serde::Serialize;
9
10#[derive(Debug)]
12pub struct SwanlingReportTemplates<'a> {
13 pub raw_requests_template: &'a str,
14 pub raw_responses_template: &'a str,
15 pub co_requests_template: &'a str,
16 pub co_responses_template: &'a str,
17 pub tasks_template: &'a str,
18 pub status_codes_template: &'a str,
19 pub errors_template: &'a str,
20}
21
22#[derive(Debug, Clone, Serialize)]
24pub struct RequestMetric {
25 pub method: String,
26 pub name: String,
27 pub number_of_requests: usize,
28 pub number_of_failures: usize,
29 pub response_time_average: String,
30 pub response_time_minimum: usize,
31 pub response_time_maximum: usize,
32 pub requests_per_second: String,
33 pub failures_per_second: String,
34}
35
36#[derive(Debug, Clone, Serialize)]
38pub struct CORequestMetric {
39 pub method: String,
40 pub name: String,
41 pub response_time_average: String,
42 pub response_time_standard_deviation: String,
43 pub response_time_maximum: usize,
44}
45
46#[derive(Debug, Clone, Serialize)]
48pub struct ResponseMetric {
49 pub method: String,
50 pub name: String,
51 pub percentile_50: String,
52 pub percentile_60: String,
53 pub percentile_70: String,
54 pub percentile_80: String,
55 pub percentile_90: String,
56 pub percentile_95: String,
57 pub percentile_99: String,
58 pub percentile_100: String,
59}
60
61#[derive(Debug, Clone, Serialize)]
63pub struct TaskMetric {
64 pub is_task_set: bool,
65 pub task: String,
66 pub name: String,
67 pub number_of_requests: usize,
68 pub number_of_failures: usize,
69 pub response_time_average: String,
70 pub response_time_minimum: usize,
71 pub response_time_maximum: usize,
72 pub requests_per_second: String,
73 pub failures_per_second: String,
74}
75
76pub struct StatusCodeMetric {
78 pub method: String,
79 pub name: String,
80 pub status_codes: String,
81}
82
83pub fn get_response_metric(
85 method: &str,
86 name: &str,
87 response_times: &BTreeMap<usize, usize>,
88 total_request_count: usize,
89 response_time_minimum: usize,
90 response_time_maximum: usize,
91) -> ResponseMetric {
92 let mut percentiles = Vec::new();
94 for percent in &[0.5, 0.6, 0.7, 0.8, 0.9, 0.95, 0.99, 1.0] {
95 percentiles.push(metrics::calculate_response_time_percentile(
96 response_times,
97 total_request_count,
98 response_time_minimum,
99 response_time_maximum,
100 *percent,
101 ));
102 }
103
104 ResponseMetric {
106 method: method.to_string(),
107 name: name.to_string(),
108 percentile_50: mem::take(&mut percentiles[0]),
109 percentile_60: mem::take(&mut percentiles[1]),
110 percentile_70: mem::take(&mut percentiles[2]),
111 percentile_80: mem::take(&mut percentiles[3]),
112 percentile_90: mem::take(&mut percentiles[4]),
113 percentile_95: mem::take(&mut percentiles[5]),
114 percentile_99: mem::take(&mut percentiles[6]),
115 percentile_100: mem::take(&mut percentiles[7]),
116 }
117}
118
119pub fn raw_request_metrics_row(metric: RequestMetric) -> String {
121 format!(
122 r#"<tr>
123 <td>{method}</td>
124 <td>{name}</td>
125 <td>{number_of_requests}</td>
126 <td>{number_of_failures}</td>
127 <td>{response_time_average}</td>
128 <td>{response_time_minimum}</td>
129 <td>{response_time_maximum}</td>
130 <td>{requests_per_second}</td>
131 <td>{failures_per_second}</td>
132 </tr>"#,
133 method = metric.method,
134 name = metric.name,
135 number_of_requests = metric.number_of_requests,
136 number_of_failures = metric.number_of_failures,
137 response_time_average = metric.response_time_average,
138 response_time_minimum = metric.response_time_minimum,
139 response_time_maximum = metric.response_time_maximum,
140 requests_per_second = metric.requests_per_second,
141 failures_per_second = metric.failures_per_second,
142 )
143}
144
145pub fn response_metrics_row(metric: ResponseMetric) -> String {
147 format!(
148 r#"<tr>
149 <td>{method}</td>
150 <td>{name}</td>
151 <td>{percentile_50}</td>
152 <td>{percentile_60}</td>
153 <td>{percentile_70}</td>
154 <td>{percentile_80}</td>
155 <td>{percentile_90}</td>
156 <td>{percentile_95}</td>
157 <td>{percentile_99}</td>
158 <td>{percentile_100}</td>
159 </tr>"#,
160 method = metric.method,
161 name = metric.name,
162 percentile_50 = metric.percentile_50,
163 percentile_60 = metric.percentile_60,
164 percentile_70 = metric.percentile_70,
165 percentile_80 = metric.percentile_80,
166 percentile_90 = metric.percentile_90,
167 percentile_95 = metric.percentile_95,
168 percentile_99 = metric.percentile_99,
169 percentile_100 = metric.percentile_100,
170 )
171}
172
173pub fn coordinated_omission_request_metrics_template(co_requests_rows: &str) -> String {
176 format!(
177 r#"<div class="CO requests">
178 <h2>Request Metrics With Coordinated Omission Mitigation</h2>
179 <table>
180 <thead>
181 <tr>
182 <th>Method</th>
183 <th>Name</th>
184 <th>Average (ms)</th>
185 <th>Standard deviation (ms)</th>
186 <th>Max (ms)</th>
187 </tr>
188 </thead>
189 <tbody>
190 {co_requests_rows}
191 </tbody>
192 </table>
193 </div>"#,
194 co_requests_rows = co_requests_rows,
195 )
196}
197
198pub fn coordinated_omission_request_metrics_row(metric: CORequestMetric) -> String {
201 format!(
202 r#"<tr>
203 <td>{method}</td>
204 <td>{name}</td>
205 <td>{average})</td>
206 <td>{standard_deviation}</td>
207 <td>{maximum}</td>
208 </tr>"#,
209 method = metric.method,
210 name = metric.name,
211 average = metric.response_time_average,
212 standard_deviation = metric.response_time_standard_deviation,
213 maximum = metric.response_time_maximum,
214 )
215}
216
217pub fn coordinated_omission_response_metrics_template(co_responses_rows: &str) -> String {
220 format!(
221 r#"<div class="responses">
222 <h2>Response Time Metrics With Coordinated Omission Mitigation</h2>
223 <table>
224 <thead>
225 <tr>
226 <th>Method</th>
227 <th>Name</th>
228 <th>50%ile (ms)</th>
229 <th>60%ile (ms)</th>
230 <th>70%ile (ms)</th>
231 <th>80%ile (ms)</th>
232 <th>90%ile (ms)</th>
233 <th>95%ile (ms)</th>
234 <th>99%ile (ms)</th>
235 <th>100%ile (ms)</th>
236 </tr>
237 </thead>
238 <tbody>
239 {co_responses_rows}
240 </tbody>
241 </table>
242 </div>"#,
243 co_responses_rows = co_responses_rows,
244 )
245}
246
247pub fn coordinated_omission_response_metrics_row(metric: ResponseMetric) -> String {
250 format!(
251 r#"<tr>
252 <td>{method}</td>
253 <td>{name}</td>
254 <td>{percentile_50}</td>
255 <td>{percentile_60}</td>
256 <td>{percentile_70}</td>
257 <td>{percentile_80}</td>
258 <td>{percentile_90}</td>
259 <td>{percentile_95}</td>
260 <td>{percentile_99}</td>
261 <td>{percentile_100}</td>
262 </tr>"#,
263 method = metric.method,
264 name = metric.name,
265 percentile_50 = metric.percentile_50,
266 percentile_60 = metric.percentile_60,
267 percentile_70 = metric.percentile_70,
268 percentile_80 = metric.percentile_80,
269 percentile_90 = metric.percentile_90,
270 percentile_95 = metric.percentile_95,
271 percentile_99 = metric.percentile_99,
272 percentile_100 = metric.percentile_100,
273 )
274}
275
276pub fn status_code_metrics_template(status_code_rows: &str) -> String {
279 format!(
280 r#"<div class="status_codes">
281 <h2>Status Code Metrics</h2>
282 <table>
283 <thead>
284 <tr>
285 <th>Method</th>
286 <th colspan="2">Name</th>
287 <th colspan="3">Status Codes</th>
288 </tr>
289 </thead>
290 <tbody>
291 {status_code_rows}
292 </tbody>
293 </table>
294 </div>"#,
295 status_code_rows = status_code_rows,
296 )
297}
298
299pub fn status_code_metrics_row(metric: StatusCodeMetric) -> String {
301 format!(
302 r#"<tr>
303 <td>{method}</td>
304 <td colspan="2">{name}</td>
305 <td colspan="3">{status_codes}</td>
306 </tr>"#,
307 method = metric.method,
308 name = metric.name,
309 status_codes = metric.status_codes,
310 )
311}
312
313pub fn task_metrics_template(task_rows: &str) -> String {
315 format!(
316 r#"<div class="tasks">
317 <h2>Task Metrics</h2>
318 <table>
319 <thead>
320 <tr>
321 <th colspan="2">Task</th>
322 <th># Times Run</th>
323 <th># Fails</th>
324 <th>Average (ms)</th>
325 <th>Min (ms)</th>
326 <th>Max (ms)</th>
327 <th>RPS</th>
328 <th>Failures/s</th>
329 </tr>
330 </thead>
331 <tbody>
332 {task_rows}
333 </tbody>
334 </table>
335 </div>"#,
336 task_rows = task_rows,
337 )
338}
339
340pub fn task_metrics_row(metric: TaskMetric) -> String {
342 if metric.is_task_set {
343 format!(
344 r#"<tr>
345 <td colspan="10" align="left"><strong>{name}</strong></td>
346 </tr>"#,
347 name = metric.name,
348 )
349 } else {
350 format!(
351 r#"<tr>
352 <td colspan="2">{task} {name}</strong></td>
353 <td>{number_of_requests}</td>
354 <td>{number_of_failures}</td>
355 <td>{response_time_average}</td>
356 <td>{response_time_minimum}</td>
357 <td>{response_time_maximum}</td>
358 <td>{requests_per_second}</td>
359 <td>{failures_per_second}</td>
360 </tr>"#,
361 task = metric.task,
362 name = metric.name,
363 number_of_requests = metrics::format_number(metric.number_of_requests),
364 number_of_failures = metrics::format_number(metric.number_of_failures),
365 response_time_average = metric.response_time_average,
366 response_time_minimum = metric.response_time_minimum,
367 response_time_maximum = metric.response_time_maximum,
368 requests_per_second = metric.requests_per_second,
369 failures_per_second = metric.failures_per_second,
370 )
371 }
372}
373
374pub fn errors_template(error_rows: &str) -> String {
376 format!(
377 r#"<div class="errors">
378 <h2>Errors</h2>
379 <table>
380 <thead>
381 <tr>
382 <th>#</th>
383 <th colspan="3">Error</th>
384 </tr>
385 </thead>
386 <tbody>
387 {error_rows}
388 </tbody>
389 </table>
390 </div>"#,
391 error_rows = error_rows,
392 )
393}
394
395pub fn error_row(error: &metrics::SwanlingErrorMetricAggregate) -> String {
397 format!(
398 r#"<tr>
399 <td>{occurrences}</td>
400 <td colspan="4">{error}</strong></td>
401 </tr>"#,
402 occurrences = error.occurrences,
403 error = error.error,
404 )
405}
406
407pub fn build_report(
409 start_time: &str,
410 end_time: &str,
411 host: &str,
412 templates: SwanlingReportTemplates,
413) -> String {
414 format!(
415 r#"<!DOCTYPE html>
416<html>
417<head>
418 <title>Swanling Attack Report</title>
419 <style>
420 .container {{
421 width: 1000px;
422 margin: 0 auto;
423 padding: 10px;
424 background: #173529;
425 font-family: Arial, Helvetica, sans-serif;
426 font-size: 14px;
427 color: #fff;
428 }}
429
430 .info span{{
431 color: #b3c3bc;
432 }}
433
434 table {{
435 border-collapse: collapse;
436 text-align: center;
437 width: 100%;
438 }}
439
440 td, th {{
441 border: 1px solid #cad9ea;
442 color: #666;
443 height: 30px;
444 }}
445
446 thead th {{
447 background-color: #cce8eb;
448 width: 100px;
449 }}
450
451 tr:nth-child(odd) {{
452 background: #fff;
453 }}
454
455 tr:nth-child(even) {{
456 background: #f5fafa;
457 }}
458
459 .charts-container .chart {{
460 width: 100%;
461 height: 350px;
462 margin-bottom: 30px;
463 }}
464
465 .download {{
466 float: right;
467 }}
468
469 .download a {{
470 color: #00ca5a;
471 }}
472 </style>
473</head>
474<body>
475 <div class="container">
476 <h1>Swanling Attack Report</h1>
477
478 <div class="info">
479 <p>During: <span>{start_time} - {end_time}</span></p>
480 <p>Target Host: <span>{host}</span></p>
481 </div>
482
483 <div class="requests">
484 <h2>Request Metrics</h2>
485 <table>
486 <thead>
487 <tr>
488 <th>Method</th>
489 <th>Name</th>
490 <th># Requests</th>
491 <th># Fails</th>
492 <th>Average (ms)</th>
493 <th>Min (ms)</th>
494 <th>Max (ms)</th>
495 <th>RPS</th>
496 <th>Failures/s</th>
497 </tr>
498 </thead>
499 <tbody>
500 {raw_requests_template}
501 </tbody>
502 </table>
503 </div>
504
505 {co_requests_template}
506
507 <div class="responses">
508 <h2>Response Time Metrics</h2>
509 <table>
510 <thead>
511 <tr>
512 <th>Method</th>
513 <th>Name</th>
514 <th>50%ile (ms)</th>
515 <th>60%ile (ms)</th>
516 <th>70%ile (ms)</th>
517 <th>80%ile (ms)</th>
518 <th>90%ile (ms)</th>
519 <th>95%ile (ms)</th>
520 <th>99%ile (ms)</th>
521 <th>100%ile (ms)</th>
522 </tr>
523 </thead>
524 <tbody>
525 {raw_responses_template}
526 </tbody>
527 </table>
528 </div>
529
530 {co_responses_template}
531
532 {status_codes_template}
533
534 {tasks_template}
535
536 {errors_template}
537
538 </div>
539</body>
540</html>"#,
541 start_time = start_time,
542 end_time = end_time,
543 host = host,
544 raw_requests_template = templates.raw_requests_template,
545 raw_responses_template = templates.raw_responses_template,
546 co_requests_template = templates.co_requests_template,
547 co_responses_template = templates.co_responses_template,
548 tasks_template = templates.tasks_template,
549 status_codes_template = templates.status_codes_template,
550 errors_template = templates.errors_template,
551 )
552}