mod common;
mod markdown;
pub mod print;
#[cfg(feature = "pdf-reports")]
pub(crate) mod chrome;
#[cfg(feature = "pdf-reports")]
pub mod pdf;
pub(crate) use markdown::write_markdown_report;
use crate::{
metrics::{
self, format_number, format_value, ApplyBaseline, DeltaTo, GooseRequestMetricTimingData,
Millis, MillisFloat, NullableFloat, Value,
},
report::common::OrEmpty,
};
use serde::ser::{SerializeStruct, Serializer};
use serde::{Deserialize, Serialize};
#[derive(Debug)]
pub(crate) struct GooseReportTemplates<'a> {
pub notes_template: &'a str,
pub raw_requests_template: &'a str,
pub raw_responses_template: &'a str,
pub co_requests_template: &'a str,
pub co_responses_template: &'a str,
pub transactions_template: &'a str,
pub scenarios_template: &'a str,
pub status_codes_template: &'a str,
pub errors_template: &'a str,
pub transaction_errors_template: &'a str,
pub thresholds_template: &'a str,
pub graph_rps_template: &'a str,
pub graph_average_response_time_template: &'a str,
pub graph_latency_heatmap_template: &'a str,
pub graph_users_per_second: &'a str,
pub co_metrics_template: &'a str,
}
#[derive(Debug, Clone, Deserialize)]
pub(crate) struct RequestMetric {
pub method: String,
pub name: String,
pub number_of_requests: Value<usize>,
pub number_of_failures: Value<usize>,
pub response_time_average: Value<MillisFloat>,
pub response_time_minimum: Value<Millis>,
pub response_time_maximum: Value<Millis>,
pub requests_per_second: Value<NullableFloat>,
pub failures_per_second: Value<NullableFloat>,
#[serde(default)]
pub is_breakdown: bool,
}
impl DeltaTo for RequestMetric {
fn delta_to(&mut self, other: &Self) {
self.number_of_requests.eval(other.number_of_requests);
self.number_of_failures.eval(other.number_of_failures);
self.response_time_average.eval(other.response_time_average);
self.response_time_minimum.eval(other.response_time_minimum);
self.response_time_maximum.eval(other.response_time_maximum);
self.requests_per_second.eval(other.requests_per_second);
self.failures_per_second.eval(other.failures_per_second);
}
}
#[derive(Debug, Clone, Deserialize)]
pub(crate) struct CORequestMetric {
pub method: String,
pub name: String,
pub response_time_average: Value<MillisFloat>,
pub response_time_standard_deviation: Value<MillisFloat>,
pub response_time_maximum: Value<Millis>,
}
impl DeltaTo for CORequestMetric {
fn delta_to(&mut self, other: &Self) {
self.response_time_average.eval(other.response_time_average);
self.response_time_standard_deviation
.eval(other.response_time_standard_deviation);
self.response_time_maximum.eval(other.response_time_maximum);
}
}
#[derive(Debug, Clone, Deserialize)]
pub(crate) struct ResponseMetric {
pub method: String,
pub name: String,
pub percentile_50: Value<Millis>,
pub percentile_60: Value<Millis>,
pub percentile_70: Value<Millis>,
pub percentile_80: Value<Millis>,
pub percentile_90: Value<Millis>,
pub percentile_95: Value<Millis>,
pub percentile_99: Value<Millis>,
pub percentile_100: Value<Millis>,
}
impl DeltaTo for ResponseMetric {
fn delta_to(&mut self, other: &Self) {
self.percentile_50.eval(other.percentile_50);
self.percentile_60.eval(other.percentile_60);
self.percentile_70.eval(other.percentile_70);
self.percentile_80.eval(other.percentile_80);
self.percentile_90.eval(other.percentile_90);
self.percentile_95.eval(other.percentile_95);
self.percentile_99.eval(other.percentile_99);
self.percentile_100.eval(other.percentile_100);
}
}
#[derive(Debug, Clone, Deserialize)]
pub(crate) struct TransactionMetric {
pub is_scenario: bool,
pub transaction: String,
pub name: String,
pub number_of_requests: Value<usize>,
pub number_of_failures: Value<usize>,
pub response_time_average: Option<Value<MillisFloat>>,
pub response_time_minimum: Value<Millis>,
pub response_time_maximum: Value<Millis>,
pub requests_per_second: Option<Value<NullableFloat>>,
pub failures_per_second: Option<Value<NullableFloat>>,
}
impl DeltaTo for TransactionMetric {
fn delta_to(&mut self, other: &Self) {
self.number_of_requests.eval(other.number_of_requests);
self.number_of_failures.eval(other.number_of_failures);
self.response_time_average.eval(other.response_time_average);
self.response_time_minimum.eval(other.response_time_minimum);
self.response_time_maximum.eval(other.response_time_maximum);
self.requests_per_second.eval(other.requests_per_second);
self.failures_per_second.eval(other.failures_per_second);
}
}
#[derive(Debug, Clone, Deserialize)]
pub(crate) struct ScenarioMetric {
pub name: String,
pub users: Value<usize>,
pub count: Value<usize>,
pub response_time_average: Value<MillisFloat>,
pub response_time_minimum: Value<Millis>,
pub response_time_maximum: Value<Millis>,
pub count_per_second: Value<NullableFloat>,
pub iterations: Value<NullableFloat>,
}
impl DeltaTo for ScenarioMetric {
fn delta_to(&mut self, other: &Self) {
self.users.eval(other.users);
self.count.eval(other.count);
self.response_time_average.eval(other.response_time_average);
self.response_time_minimum.eval(other.response_time_minimum);
self.response_time_maximum.eval(other.response_time_maximum);
self.count_per_second.eval(other.count_per_second);
self.iterations.eval(other.iterations);
}
}
fn serialize_time<S: SerializeStruct>(
row: &mut S,
key: &'static str,
key_us: &'static str,
time: &Value<Millis>,
) -> Result<(), S::Error> {
row.serialize_field(key, time)?;
row.serialize_field(key_us, &time.value().0)
}
impl Serialize for RequestMetric {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let RequestMetric {
method,
name,
number_of_requests,
number_of_failures,
response_time_average,
response_time_minimum,
response_time_maximum,
requests_per_second,
failures_per_second,
is_breakdown: _,
} = self;
let mut row = serializer.serialize_struct("RequestMetric", 11)?;
row.serialize_field("method", method)?;
row.serialize_field("name", name)?;
row.serialize_field("number_of_requests", number_of_requests)?;
row.serialize_field("number_of_failures", number_of_failures)?;
row.serialize_field("response_time_average", response_time_average)?;
serialize_time(
&mut row,
"response_time_minimum",
"response_time_minimum_us",
response_time_minimum,
)?;
serialize_time(
&mut row,
"response_time_maximum",
"response_time_maximum_us",
response_time_maximum,
)?;
row.serialize_field("requests_per_second", requests_per_second)?;
row.serialize_field("failures_per_second", failures_per_second)?;
row.end()
}
}
impl Serialize for CORequestMetric {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let CORequestMetric {
method,
name,
response_time_average,
response_time_standard_deviation,
response_time_maximum,
} = self;
let mut row = serializer.serialize_struct("CORequestMetric", 6)?;
row.serialize_field("method", method)?;
row.serialize_field("name", name)?;
row.serialize_field("response_time_average", response_time_average)?;
row.serialize_field(
"response_time_standard_deviation",
response_time_standard_deviation,
)?;
serialize_time(
&mut row,
"response_time_maximum",
"response_time_maximum_us",
response_time_maximum,
)?;
row.end()
}
}
impl Serialize for ResponseMetric {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let ResponseMetric {
method,
name,
percentile_50,
percentile_60,
percentile_70,
percentile_80,
percentile_90,
percentile_95,
percentile_99,
percentile_100,
} = self;
let mut row = serializer.serialize_struct("ResponseMetric", 18)?;
row.serialize_field("method", method)?;
row.serialize_field("name", name)?;
serialize_time(&mut row, "percentile_50", "percentile_50_us", percentile_50)?;
serialize_time(&mut row, "percentile_60", "percentile_60_us", percentile_60)?;
serialize_time(&mut row, "percentile_70", "percentile_70_us", percentile_70)?;
serialize_time(&mut row, "percentile_80", "percentile_80_us", percentile_80)?;
serialize_time(&mut row, "percentile_90", "percentile_90_us", percentile_90)?;
serialize_time(&mut row, "percentile_95", "percentile_95_us", percentile_95)?;
serialize_time(&mut row, "percentile_99", "percentile_99_us", percentile_99)?;
serialize_time(
&mut row,
"percentile_100",
"percentile_100_us",
percentile_100,
)?;
row.end()
}
}
impl Serialize for TransactionMetric {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let TransactionMetric {
is_scenario,
transaction,
name,
number_of_requests,
number_of_failures,
response_time_average,
response_time_minimum,
response_time_maximum,
requests_per_second,
failures_per_second,
} = self;
let mut row = serializer.serialize_struct("TransactionMetric", 12)?;
row.serialize_field("is_scenario", is_scenario)?;
row.serialize_field("transaction", transaction)?;
row.serialize_field("name", name)?;
row.serialize_field("number_of_requests", number_of_requests)?;
row.serialize_field("number_of_failures", number_of_failures)?;
row.serialize_field("response_time_average", response_time_average)?;
serialize_time(
&mut row,
"response_time_minimum",
"response_time_minimum_us",
response_time_minimum,
)?;
serialize_time(
&mut row,
"response_time_maximum",
"response_time_maximum_us",
response_time_maximum,
)?;
row.serialize_field("requests_per_second", requests_per_second)?;
row.serialize_field("failures_per_second", failures_per_second)?;
row.end()
}
}
impl Serialize for ScenarioMetric {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let ScenarioMetric {
name,
users,
count,
response_time_average,
response_time_minimum,
response_time_maximum,
count_per_second,
iterations,
} = self;
let mut row = serializer.serialize_struct("ScenarioMetric", 10)?;
row.serialize_field("name", name)?;
row.serialize_field("users", users)?;
row.serialize_field("count", count)?;
row.serialize_field("response_time_average", response_time_average)?;
serialize_time(
&mut row,
"response_time_minimum",
"response_time_minimum_us",
response_time_minimum,
)?;
serialize_time(
&mut row,
"response_time_maximum",
"response_time_maximum_us",
response_time_maximum,
)?;
row.serialize_field("count_per_second", count_per_second)?;
row.serialize_field("iterations", iterations)?;
row.end()
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub(crate) struct StatusCodeMetric {
pub method: String,
pub name: String,
pub status_codes: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ErrorMetric {
pub method: String,
pub name: String,
pub error: String,
pub occurrences: Value<usize>,
}
impl DeltaTo for ErrorMetric {
fn delta_to(&mut self, other: &Self) {
self.occurrences.eval(other.occurrences);
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct TransactionErrorMetric {
pub transaction: String,
pub scenario: String,
pub name: String,
pub error: String,
pub occurrences: Value<usize>,
}
impl DeltaTo for TransactionErrorMetric {
fn delta_to(&mut self, other: &Self) {
self.occurrences.eval(other.occurrences);
}
}
pub(crate) fn get_response_metric(
method: &str,
name: &str,
data: &GooseRequestMetricTimingData,
) -> ResponseMetric {
let mut percentiles = Vec::new();
for percent in &[0.5, 0.6, 0.7, 0.8, 0.9, 0.95, 0.99, 1.0] {
percentiles.push(Millis(data.percentile_us(*percent)));
}
ResponseMetric {
method: method.to_string(),
name: name.to_string(),
percentile_50: percentiles[0].into(),
percentile_60: percentiles[1].into(),
percentile_70: percentiles[2].into(),
percentile_80: percentiles[3].into(),
percentile_90: percentiles[4].into(),
percentile_95: percentiles[5].into(),
percentile_99: percentiles[6].into(),
percentile_100: percentiles[7].into(),
}
}
pub(crate) fn raw_request_metrics_row(metric: RequestMetric) -> String {
if metric.is_breakdown {
format!(
r#"<tr class="status-breakdown">
<td colspan="2"> └─ {method}</td>
<td>{number_of_requests}</td>
<td></td>
<td>{response_time_average}</td>
<td>{response_time_minimum}</td>
<td>{response_time_maximum}</td>
<td></td>
<td></td>
</tr>"#,
method = metric.method,
number_of_requests = format_value(&metric.number_of_requests),
response_time_average = format_value(&metric.response_time_average),
response_time_minimum = format_value(&metric.response_time_minimum),
response_time_maximum = format_value(&metric.response_time_maximum),
)
} else {
format!(
r#"<tr>
<td>{method}</td>
<td>{name}</td>
<td>{number_of_requests}</td>
<td>{number_of_failures}</td>
<td>{response_time_average}</td>
<td>{response_time_minimum}</td>
<td>{response_time_maximum}</td>
<td>{requests_per_second:.2}</td>
<td>{failures_per_second:.2}</td>
</tr>"#,
method = metric.method,
name = metric.name,
number_of_requests = format_value(&metric.number_of_requests),
number_of_failures = format_value(&metric.number_of_failures),
response_time_average = format_value(&metric.response_time_average),
response_time_minimum = format_value(&metric.response_time_minimum),
response_time_maximum = format_value(&metric.response_time_maximum),
requests_per_second = metric.requests_per_second,
failures_per_second = metric.failures_per_second,
)
}
}
pub(crate) fn response_metrics_row(metric: ResponseMetric) -> String {
format!(
r#"<tr>
<td>{method}</td>
<td>{name}</td>
<td>{percentile_50}</td>
<td>{percentile_60}</td>
<td>{percentile_70}</td>
<td>{percentile_80}</td>
<td>{percentile_90}</td>
<td>{percentile_95}</td>
<td>{percentile_99}</td>
<td>{percentile_100}</td>
</tr>"#,
method = metric.method,
name = metric.name,
percentile_50 = format_value(&metric.percentile_50),
percentile_60 = format_value(&metric.percentile_60),
percentile_70 = format_value(&metric.percentile_70),
percentile_80 = format_value(&metric.percentile_80),
percentile_90 = format_value(&metric.percentile_90),
percentile_95 = format_value(&metric.percentile_95),
percentile_99 = format_value(&metric.percentile_99),
percentile_100 = format_value(&metric.percentile_100),
)
}
pub(crate) fn coordinated_omission_request_metrics_template(co_requests_rows: &str) -> String {
format!(
r#"<div class="CO requests">
<h2>Request Metrics With Coordinated Omission Mitigation</h2>
<table>
<thead>
<tr>
<th>Method</th>
<th>Name</th>
<th>Average (ms)</th>
<th>Standard deviation (ms)</th>
<th>Max (ms)</th>
</tr>
</thead>
<tbody>
{co_requests_rows}
</tbody>
</table>
</div>"#,
)
}
pub(crate) fn coordinated_omission_request_metrics_row(metric: CORequestMetric) -> String {
format!(
r#"<tr>
<td>{method}</td>
<td>{name}</td>
<td>{average}</td>
<td>{standard_deviation}</td>
<td>{maximum}</td>
</tr>"#,
method = metric.method,
name = metric.name,
average = format_value(&metric.response_time_average),
standard_deviation = format_value(&metric.response_time_standard_deviation),
maximum = format_value(&metric.response_time_maximum),
)
}
pub(crate) fn coordinated_omission_response_metrics_template(co_responses_rows: &str) -> String {
format!(
r#"<div class="responses">
<h2>Response Time Metrics With Coordinated Omission Mitigation</h2>
<table>
<thead>
<tr>
<th>Method</th>
<th>Name</th>
<th>50%ile (ms)</th>
<th>60%ile (ms)</th>
<th>70%ile (ms)</th>
<th>80%ile (ms)</th>
<th>90%ile (ms)</th>
<th>95%ile (ms)</th>
<th>99%ile (ms)</th>
<th>100%ile (ms)</th>
</tr>
</thead>
<tbody>
{co_responses_rows}
</tbody>
</table>
</div>"#,
)
}
pub(crate) fn coordinated_omission_response_metrics_row(metric: ResponseMetric) -> String {
format!(
r#"<tr>
<td>{method}</td>
<td>{name}</td>
<td>{percentile_50}</td>
<td>{percentile_60}</td>
<td>{percentile_70}</td>
<td>{percentile_80}</td>
<td>{percentile_90}</td>
<td>{percentile_95}</td>
<td>{percentile_99}</td>
<td>{percentile_100}</td>
</tr>"#,
method = metric.method,
name = metric.name,
percentile_50 = format_value(&metric.percentile_50),
percentile_60 = format_value(&metric.percentile_60),
percentile_70 = format_value(&metric.percentile_70),
percentile_80 = format_value(&metric.percentile_80),
percentile_90 = format_value(&metric.percentile_90),
percentile_95 = format_value(&metric.percentile_95),
percentile_99 = format_value(&metric.percentile_99),
percentile_100 = format_value(&metric.percentile_100),
)
}
pub(crate) fn status_code_metrics_template(status_code_rows: &str) -> String {
format!(
r#"<div class="status_codes">
<h2>Status Code Metrics</h2>
<table>
<thead>
<tr>
<th>Method</th>
<th colspan="2">Name</th>
<th colspan="3">Status Codes</th>
</tr>
</thead>
<tbody>
{status_code_rows}
</tbody>
</table>
</div>"#,
)
}
pub(crate) fn status_code_metrics_row(metric: StatusCodeMetric) -> String {
format!(
r#"<tr>
<td>{method}</td>
<td colspan="2">{name}</td>
<td colspan="3">{status_codes}</td>
</tr>"#,
method = metric.method,
name = metric.name,
status_codes = metric.status_codes,
)
}
pub(crate) fn transaction_metrics_template(transaction_rows: &str, graph: String) -> String {
format!(
r#"<div class="transactions">
<h2>Transaction Metrics</h2>
{graph}
<table>
<thead>
<tr>
<th colspan="2">Transaction</th>
<th># Times Run</th>
<th># Fails</th>
<th>Average (ms)</th>
<th>Min (ms)</th>
<th>Max (ms)</th>
<th>RPS</th>
<th>Failures/s</th>
</tr>
</thead>
<tbody>
{transaction_rows}
</tbody>
</table>
</div>"#,
)
}
pub(crate) fn transaction_metrics_row(metric: TransactionMetric) -> String {
if metric.is_scenario {
format!(
r#"<tr>
<td colspan="10" align="left"><strong>{name}</strong></td>
</tr>"#,
name = metric.name,
)
} else {
format!(
r#"<tr>
<td colspan="2">{transaction} {name}</strong></td>
<td>{number_of_requests}</td>
<td>{number_of_failures}</td>
<td>{response_time_average}</td>
<td>{response_time_minimum}</td>
<td>{response_time_maximum}</td>
<td>{requests_per_second:.2}</td>
<td>{failures_per_second:.2}</td>
</tr>"#,
transaction = metric.transaction,
name = metric.name,
number_of_requests = format_value(&metric.number_of_requests),
number_of_failures = format_value(&metric.number_of_failures),
response_time_average =
OrEmpty(metric.response_time_average.as_ref().map(format_value)),
response_time_minimum = format_value(&metric.response_time_minimum),
response_time_maximum = format_value(&metric.response_time_maximum),
requests_per_second = OrEmpty(metric.requests_per_second),
failures_per_second = OrEmpty(metric.failures_per_second),
)
}
}
pub(crate) fn scenario_metrics_template(scenario_rows: &str, graph: String) -> String {
format!(
r#"<div class="scenarios">
<h2>Scenario Metrics</h2>
{graph}
<table>
<thead>
<tr>
<th colspan="2">Scenario</th>
<th># Users</th>
<th># Times Run</th>
<th>Average (ms)</th>
<th>Min (ms)</th>
<th>Max (ms)</th>
<th>Scenarios/s</th>
<th>Iterations</th>
</tr>
</thead>
<tbody>
{scenario_rows}
</tbody>
</table>
</div>"#,
)
}
pub(crate) fn scenario_metrics_row(metric: ScenarioMetric) -> String {
format!(
r#"<tr>
<td colspan="2">{name}</strong></td>
<td>{users}</td>
<td>{count}</td>
<td>{response_time_average}</td>
<td>{response_time_minimum}</td>
<td>{response_time_maximum}</td>
<td>{count_per_second:.2}</td>
<td>{iterations:.2}</td>
</tr>"#,
name = metric.name,
users = format_value(&metric.users),
count = format_value(&metric.count),
response_time_average = format_value(&metric.response_time_average),
response_time_minimum = format_value(&metric.response_time_minimum),
response_time_maximum = format_value(&metric.response_time_maximum),
count_per_second = metric.count_per_second,
iterations = metric.iterations,
)
}
pub(crate) fn errors_template(error_rows: &str, graph: String) -> String {
format!(
r#"<div class="errors">
<h2>Errors</h2>
{graph}
<table>
<thead>
<tr>
<th>#</th>
<th colspan="3">Error</th>
</tr>
</thead>
<tbody>
{error_rows}
</tbody>
</table>
</div>"#,
)
}
pub fn error_row(error: &ErrorMetric) -> String {
format!(
r#"<tr>
<td>{occurrences}</td>
<td colspan="4">{error}</strong></td>
</tr>"#,
occurrences = format_value(&error.occurrences),
error = error.error,
)
}
pub(crate) fn transaction_errors_template(rows: &str) -> String {
format!(
r#"<div class="transaction-errors">
<h2>Transaction Errors</h2>
<table>
<thead>
<tr>
<th>#</th>
<th>Transaction</th>
<th>Scenario</th>
<th>Name</th>
<th>Error</th>
</tr>
</thead>
<tbody>
{rows}
</tbody>
</table>
</div>"#,
)
}
pub fn transaction_error_row(error: &TransactionErrorMetric) -> String {
format!(
r#"<tr>
<td>{occurrences}</td>
<td>{transaction}</td>
<td>{scenario}</td>
<td>{name}</td>
<td>{error}</td>
</tr>"#,
occurrences = format_value(&error.occurrences),
transaction = escape_html(&error.transaction),
scenario = escape_html(&error.scenario),
name = escape_html(&error.name),
error = escape_html(&error.error),
)
}
const THRESHOLDS_STYLE: &str = r#"
.threshold-fail td {
font-weight: bold;
color: #a40000;
background: #fdeaea;
}
.threshold-fail td:first-child {
border-left: 6px solid #a40000;
}
.threshold-pass td:first-child {
border-left: 6px solid #2e7d32;
}"#;
pub(crate) fn thresholds_template(threshold_rows: &str) -> String {
format!(
r#"
<div class="thresholds">
<h2>Thresholds</h2>
<table>
<thead>
<tr>
<th colspan="2">Rule</th>
<th>Observed</th>
<th colspan="2">Result</th>
</tr>
</thead>
<tbody>
{threshold_rows}
</tbody>
</table>
</div>"#,
)
}
pub(crate) fn threshold_row(result: &crate::threshold::ThresholdResult) -> String {
let (class, mark) = if result.passed {
("threshold-pass", "✓")
} else {
("threshold-fail", "✗")
};
format!(
r#"<tr class="{class}">
<td colspan="2"><code>{rule}</code></td>
<td>{observed}</td>
<td colspan="2">{mark} {verdict}</td>
</tr>"#,
rule = escape_html(&result.rule),
observed = escape_html(&result.observed),
verdict = result.verdict(),
)
}
pub(crate) fn coordinated_omission_metrics_template(
co_summary: &metrics::CoMetricsSummary,
) -> String {
let severity_rows = co_summary
.severity_breakdown
.iter()
.map(|(severity, count)| {
format!(
r#"<tr>
<td>{severity:?}</td>
<td>{count}</td>
</tr>"#
)
})
.collect::<Vec<String>>()
.join("\n");
format!(
r#"<div class="co-metrics">
<h2>Coordinated Omission Event Metrics</h2>
<table>
<thead>
<tr>
<th colspan="2">Summary</th>
</tr>
</thead>
<tbody>
<tr>
<td>Total CO Events</td>
<td>{}</td>
</tr>
<tr>
<td>Events per minute</td>
<td>{:.2}</td>
</tr>
<tr>
<td>Actual requests</td>
<td>{}</td>
</tr>
<tr>
<td>Synthetic requests</td>
<td>{} ({:.1}%)</td>
</tr>
</tbody>
</table>
<table style="margin-top: 20px;">
<thead>
<tr>
<th>Severity</th>
<th>Count</th>
</tr>
</thead>
<tbody>
{}
</tbody>
</table>
</div>"#,
co_summary.total_co_events,
co_summary.events_per_minute,
format_number(co_summary.actual_requests as usize),
format_number(co_summary.synthetic_requests as usize),
co_summary.synthetic_percentage,
severity_rows
)
}
pub(crate) fn escape_html(text: &str) -> String {
let mut escaped = String::with_capacity(text.len());
for c in text.chars() {
match c {
'&' => escaped.push_str("&"),
'<' => escaped.push_str("<"),
'>' => escaped.push_str(">"),
'"' => escaped.push_str("""),
'\'' => escaped.push_str("'"),
_ => escaped.push(c),
}
}
escaped
}
pub(crate) fn build_report(
users: &str,
steps_rows: &str,
hosts: &str,
templates: GooseReportTemplates,
) -> String {
let pkg_name = env!("CARGO_PKG_NAME");
let pkg_version = env!("CARGO_PKG_VERSION");
format!(
r#"<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Goose Attack Report</title>
<style>
.container {{
width: 1000px;
margin: 0 auto;
padding: 10px;
background: #173529;
font-family: Arial, Helvetica, sans-serif;
font-size: 14px;
color: #fff;
}}
.info span{{
color: #b3c3bc;
}}
.notes {{
white-space: pre-wrap;
}}
table {{
border-collapse: collapse;
text-align: center;
width: 100%;
}}
td, th {{
border: 1px solid #cad9ea;
color: #666;
height: 30px;
}}
thead th {{
background-color: #cce8eb;
width: 100px;
}}
tr:nth-child(odd) {{
background: #fff;
}}
tr:nth-child(even) {{
background: #f5fafa;
}}
.charts-container .chart {{
width: 100%;
height: 350px;
margin-bottom: 30px;
}}
.download {{
float: right;
}}
.download a {{
color: #00ca5a;
}}
.status-breakdown td {{
font-style: italic;
color: #888;
}}
.graph {{
margin-bottom: 1em;
}}{thresholds_style}
</style>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.2.2/dist/echarts.min.js"></script>
</head>
<body>
<div class="container">
<h1>Goose Attack Report</h1>
{notes_template}
<div class="info">
<p>Users: <span>{users}</span> </p>
<p>Target Host: <span>{hosts}</span></p>
<p><span><small><em>{pkg_name} v{pkg_version}</em></small></span></p>
<h2>Plan overview</h2>
<table>
<thead>
<tr>
<th>Action</th>
<th>Started</th>
<th>Stopped</th>
<th>Elapsed</th>
<th>Users</th>
</tr>
</thead>
<tbody>
{steps_rows}
</tbody>
</table>
</div>{thresholds_template}
<div class="requests">
<h2>Request Metrics</h2>
{graph_rps_template}
<table>
<thead>
<tr>
<th>Method</th>
<th>Name</th>
<th># Requests</th>
<th># Fails</th>
<th>Average (ms)</th>
<th>Min (ms)</th>
<th>Max (ms)</th>
<th>RPS</th>
<th>Failures/s</th>
</tr>
</thead>
<tbody>
{raw_requests_template}
</tbody>
</table>
</div>
{co_requests_template}
<div class="responses">
<h2>Response Time Metrics</h2>
{graph_average_response_time_template}
{graph_latency_heatmap_template}
<table>
<thead>
<tr>
<th>Method</th>
<th>Name</th>
<th>50%ile (ms)</th>
<th>60%ile (ms)</th>
<th>70%ile (ms)</th>
<th>80%ile (ms)</th>
<th>90%ile (ms)</th>
<th>95%ile (ms)</th>
<th>99%ile (ms)</th>
<th>100%ile (ms)</th>
</tr>
</thead>
<tbody>
{raw_responses_template}
</tbody>
</table>
</div>
{co_responses_template}
{status_codes_template}
{transactions_template}
{scenarios_template}
<div class="users">
<h2>User Metrics</h2>
{graph_users_per_second}
</div>
{errors_template}
{transaction_errors_template}
{co_metrics_template}
</div>
</body>
</html>"#,
notes_template = templates.notes_template,
users = users,
steps_rows = steps_rows,
hosts = hosts,
pkg_name = pkg_name,
pkg_version = pkg_version,
raw_requests_template = templates.raw_requests_template,
raw_responses_template = templates.raw_responses_template,
co_requests_template = templates.co_requests_template,
co_responses_template = templates.co_responses_template,
transactions_template = templates.transactions_template,
scenarios_template = templates.scenarios_template,
status_codes_template = templates.status_codes_template,
errors_template = templates.errors_template,
transaction_errors_template = templates.transaction_errors_template,
thresholds_template = templates.thresholds_template,
thresholds_style = if templates.thresholds_template.is_empty() {
""
} else {
THRESHOLDS_STYLE
},
graph_rps_template = templates.graph_rps_template,
graph_average_response_time_template = templates.graph_average_response_time_template,
graph_latency_heatmap_template = templates.graph_latency_heatmap_template,
graph_users_per_second = templates.graph_users_per_second,
co_metrics_template = templates.co_metrics_template,
)
}