use crate::{
metrics::{format_number, GooseErrorMetricAggregate, ReportData},
report::{
common::OrEmpty, RequestMetric, ResponseMetric, ScenarioMetric, StatusCodeMetric,
TransactionMetric,
},
test_plan::TestPlanStepAction,
GooseError,
};
use chrono::{Local, TimeZone};
use std::io::Write;
struct Markdown<'m, 'w, W: Write> {
w: &'w mut W,
data: ReportData<'m>,
}
pub(crate) fn write_markdown_report<W: Write>(
w: &mut W,
data: ReportData,
) -> Result<(), GooseError> {
Markdown { w, data }.write()
}
impl<W: Write> Markdown<'_, '_, W> {
pub fn write(mut self) -> Result<(), GooseError> {
self.write_header()?;
self.write_plan_overview()?;
self.write_request_metrics()?;
self.write_response_metrics()?;
self.write_status_code_metrics()?;
self.write_transaction_metrics()?;
self.write_scenario_metrics()?;
self.write_error_metrics()?;
self.write_coordinated_omission_metrics()?;
Ok(())
}
fn write_header(&mut self) -> Result<(), GooseError> {
writeln!(
self.w,
r#"
# Goose Attack Report
"#
)?;
Ok(())
}
fn write_plan_overview(&mut self) -> Result<(), GooseError> {
write!(
self.w,
r#"
## Plan Overview
| Action | Started | Stopped | Elapsed | Users |
| ------ | ------- | ------- | ------- | ----: |
"#
)?;
for step in self.data.raw_metrics.history.windows(2) {
let (seconds, minutes, hours) = self
.data
.raw_metrics
.get_seconds_minutes_hours(&step[0].timestamp, &step[1].timestamp);
let started = Local
.timestamp_opt(step[0].timestamp.timestamp(), 0)
.unwrap()
.format("%y-%m-%d %H:%M:%S");
let stopped = Local
.timestamp_opt(step[1].timestamp.timestamp(), 0)
.unwrap()
.format("%y-%m-%d %H:%M:%S");
let users = match &step[0].action {
TestPlanStepAction::Maintaining => {
format!("{}", step[0].users)
}
TestPlanStepAction::Increasing => {
format!("{} → {}", step[0].users, step[1].users)
}
TestPlanStepAction::Decreasing | TestPlanStepAction::Canceling => {
format!("{} ← {}", step[1].users, step[0].users,)
}
TestPlanStepAction::Finished => {
unreachable!("there shouldn't be a step after finished");
}
};
writeln!(
self.w,
r#"| {action:?} | {started} | {stopped} | {hours:02}:{minutes:02}:{seconds:02} | {users} |"#,
action = step[0].action,
)?;
}
Ok(())
}
fn write_request_metrics(&mut self) -> Result<(), GooseError> {
write!(
self.w,
r#"
## Request Metrics
| Method | Name | # Requests | # Fails | Average (ms) | Min (ms) | Max (ms) | RPS | Failures/s |
| ------ | ---- | ---------: | ------: | -----------: | -------: | -------: | --: | ---------: |
"#
)?;
for RequestMetric {
method,
name,
number_of_requests,
number_of_failures,
response_time_average,
response_time_minimum,
response_time_maximum,
requests_per_second,
failures_per_second,
} in &self.data.raw_request_metrics
{
writeln!(
self.w,
r#"| {method} | {name} | {number_of_requests} | {number_of_failures } | {response_time_average:.2 } | {response_time_minimum} | {response_time_maximum} | {requests_per_second:.2} | {failures_per_second:.2} |"#,
)?;
}
Ok(())
}
fn write_response_metrics(&mut self) -> Result<(), GooseError> {
write!(
self.w,
r#"
## Response Time Metrics
| Method | Name | 50%ile (ms) | 60%ile (ms) | 70%ile (ms) | 80%ile (ms) | 90%ile (ms) | 95%ile (ms) | 99%ile (ms) | 100%ile (ms) |
| ------ | ---- | ----------: | ----------: | ----------: | ----------: | ----------: | ----------: | ----------: | -----------: |
"#
)?;
for ResponseMetric {
method,
name,
percentile_50,
percentile_60,
percentile_70,
percentile_80,
percentile_90,
percentile_95,
percentile_99,
percentile_100,
} in &self.data.raw_response_metrics
{
writeln!(
self.w,
r#"| {method} | {name} | {percentile_50} | {percentile_60 } | {percentile_70 } | {percentile_80} | {percentile_90} | {percentile_95} | {percentile_99} | {percentile_100} |"#,
percentile_50 = format_number(*percentile_50),
percentile_60 = format_number(*percentile_60),
percentile_70 = format_number(*percentile_70),
percentile_80 = format_number(*percentile_80),
percentile_90 = format_number(*percentile_90),
percentile_95 = format_number(*percentile_95),
percentile_99 = format_number(*percentile_99),
percentile_100 = format_number(*percentile_100),
)?;
}
Ok(())
}
fn write_status_code_metrics(&mut self) -> Result<(), GooseError> {
let Some(status_code_metrics) = &self.data.status_code_metrics else {
return Ok(());
};
write!(
self.w,
r#"
## Status Code Metrics
| Method | Name | Status Codes |
| ------ | ---- | ------------ |
"#
)?;
for StatusCodeMetric {
method,
name,
status_codes,
} in status_code_metrics
{
writeln!(self.w, r#"| {method} | {name} | {status_codes} |"#)?;
}
Ok(())
}
fn write_transaction_metrics(&mut self) -> Result<(), GooseError> {
let Some(transaction_metrics) = &self.data.transaction_metrics else {
return Ok(());
};
write!(
self.w,
r#"
## Transaction Metrics
| Transaction | # Times Run | # Fails | Average (ms) | Min (ms) | Max (ms) | RPS | Failures/s |
| ----------- | ----------: | ------: | -----------: | -------: | -------: | --: | ---------: |
"#
)?;
for 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,
} in transaction_metrics
{
match is_scenario {
true => writeln!(self.w, r#"| **{name}** |"#)?,
false => writeln!(
self.w,
r#"| {transaction} {name} | {number_of_requests} | {number_of_failures} | {response_time_average:.2} | {response_time_minimum} | {response_time_maximum} | {requests_per_second:.2} | {failures_per_second:.2} |"#,
response_time_average = OrEmpty(*response_time_average),
requests_per_second = OrEmpty(*requests_per_second),
failures_per_second = OrEmpty(*failures_per_second),
)?,
}
}
Ok(())
}
fn write_scenario_metrics(&mut self) -> Result<(), GooseError> {
let Some(scenario_metrics) = &self.data.scenario_metrics else {
return Ok(());
};
write!(
self.w,
r#"
## Scenario Metrics
| Transaction | # Users | # Times Run | Average (ms) | Min (ms) | Max (ms) | Scenarios/s | Iterations |
| ----------- | ------: | ----------: | -----------: | -------: | -------: | ----------: | ---------: |
"#
)?;
for ScenarioMetric {
name,
users,
count,
response_time_average,
response_time_minimum,
response_time_maximum,
count_per_second,
iterations,
} in scenario_metrics
{
writeln!(
self.w,
r#"| {name} | {users} | {count} | {response_time_average:.2} | {response_time_minimum} | {response_time_maximum} | {count_per_second:.2} | {iterations:.2} |"#
)?;
}
Ok(())
}
fn write_error_metrics(&mut self) -> Result<(), GooseError> {
let Some(errors) = &self.data.errors else {
return Ok(());
};
write!(
self.w,
r#"
## Error Metrics
| Method | Name | # | Error |
| ------ | ---- | --: | ----- |
"#
)?;
for GooseErrorMetricAggregate {
method,
name,
error,
occurrences,
} in errors
{
writeln!(self.w, r#"| {method} | {name} | {occurrences} | {error} |"#)?;
}
Ok(())
}
fn write_coordinated_omission_metrics(&mut self) -> Result<(), GooseError> {
let Some(co_metrics) = &self.data.coordinated_omission_metrics else {
return Ok(());
};
write!(
self.w,
r#"
## Coordinated Omission Metrics
"#
)?;
writeln!(self.w, "**Overall Metrics:**")?;
writeln!(self.w, "- Total CO Events: {}", co_metrics.total_co_events)?;
writeln!(
self.w,
"- Synthetic Requests Generated: {}",
format_number(co_metrics.synthetic_requests as usize)
)?;
writeln!(
self.w,
"- Actual Requests: {}",
format_number(co_metrics.actual_requests as usize)
)?;
writeln!(
self.w,
"- Synthetic Percentage: {:.1}%",
co_metrics.synthetic_percentage
)?;
writeln!(self.w)?;
writeln!(self.w, "**Severity Breakdown:**")?;
writeln!(self.w, "- Minor (2-5x): {}", co_metrics.minor_count)?;
writeln!(self.w, "- Moderate (5-10x): {}", co_metrics.moderate_count)?;
writeln!(self.w, "- Severe (10-20x): {}", co_metrics.severe_count)?;
writeln!(self.w, "- Critical (>20x): {}", co_metrics.critical_count)?;
writeln!(self.w)?;
if !co_metrics.per_user_events.is_empty() {
writeln!(self.w, "**Per-User CO Events:**")?;
writeln!(self.w)?;
writeln!(self.w, "| User | CO Events | Severity Breakdown |")?;
writeln!(self.w, "| ---: | --------: | ------------------ |")?;
for (user_id, count, severity) in &co_metrics.per_user_events {
writeln!(self.w, "| {user_id} | {count} | {severity} |")?;
}
writeln!(self.w)?;
}
if !co_metrics.per_scenario_events.is_empty() {
writeln!(self.w, "**Per-Scenario CO Events:**")?;
writeln!(self.w)?;
writeln!(self.w, "| Scenario | CO Events | Synthetic Requests |")?;
writeln!(self.w, "| -------- | --------: | -----------------: |")?;
for (scenario, count, synthetic) in &co_metrics.per_scenario_events {
writeln!(
self.w,
"| {} | {} | {} |",
scenario,
count,
format_number(*synthetic)
)?;
}
}
Ok(())
}
}