swanling/
report.rs

1//! Optionally writes an html-formatted summary report after running a load test.
2
3use crate::metrics;
4
5use std::collections::BTreeMap;
6use std::mem;
7
8use serde::Serialize;
9
10/// The following templates are necessary to build an html-formatted summary report.
11#[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/// Defines the metrics reported about requests.
23#[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/// Defines the metrics reported about Coordinated Omission requests.
37#[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/// Defines the metrics reported about responses.
47#[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/// Defines the metrics reported about tasks.
62#[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
76/// Defines the metrics reported about status codes.
77pub struct StatusCodeMetric {
78    pub method: String,
79    pub name: String,
80    pub status_codes: String,
81}
82
83/// Helper to generate a single response metric.
84pub 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    // Calculate percentiles in a loop.
93    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    // Now take the Strings out of the Vector and build a ResponseMetric object.
105    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
119/// Build an individual row of raw request metrics in the html report.
120pub 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
145/// Build an individual row of response metrics in the html report.
146pub 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
173/// If Coordinated Omission Mitigation is triggered, add a relevant request table to the
174/// html report.
175pub 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
198/// Build an individual row of Coordinated Omission Mitigation request metrics in
199/// the html report.
200pub 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
217/// If Coordinated Omission Mitigation is triggered, add a relevant response table to the
218/// html report.
219pub 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
247/// Build an individual row of Coordinated Omission Mitigation request metrics in
248/// the html report.
249pub 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
276/// If status code metrics are enabled, add a status code metrics table to the
277/// html report.
278pub 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
299/// Build an individual row of status code metrics in the html report.
300pub 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
313/// If task metrics are enabled, add a task metrics table to the html report.
314pub 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
340/// Build an individual row of task metrics in the html report.
341pub 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
374/// If there are errors, add an errors table to the html report.
375pub 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
395/// Build an individual error row in the html report.
396pub 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
407/// Build the html report.
408pub 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}