Skip to content

Commit

Permalink
feat: implement markdown report
Browse files Browse the repository at this point in the history
  • Loading branch information
ctron committed Aug 1, 2024
1 parent 2d37472 commit ef178bd
Show file tree
Hide file tree
Showing 4 changed files with 335 additions and 3 deletions.
23 changes: 21 additions & 2 deletions src/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@

mod common;

pub(crate) use common::ReportData;

use crate::config::GooseDefaults;
use crate::goose::{get_base_url, GooseMethod, Scenario};
use crate::logger::GooseLog;
use crate::metrics::common::{ReportData, ReportOptions};
use crate::metrics::common::ReportOptions;
use crate::report;
use crate::test_plan::{TestPlanHistory, TestPlanStepAction};
use crate::util;
Expand Down Expand Up @@ -2426,7 +2428,7 @@ impl GooseMetrics {
}

// Determine the seconds, minutes and hours between two chrono:DateTimes.
fn get_seconds_minutes_hours(
pub(crate) fn get_seconds_minutes_hours(
&self,
start: &chrono::DateTime<chrono::Utc>,
end: &chrono::DateTime<chrono::Utc>,
Expand Down Expand Up @@ -3022,6 +3024,9 @@ impl GooseAttack {
Some("json") => {
self.write_json_report(create(path).await?).await?;
}
Some("md") => {
self.write_markdown_report(create(path).await?).await?;
}
None => {
return Err(GooseError::InvalidOption {
option: "--report-file".to_string(),
Expand Down Expand Up @@ -3058,6 +3063,20 @@ impl GooseAttack {
Ok(())
}

/// Write a Markdown report.
pub(crate) async fn write_markdown_report(&self, report_file: File) -> Result<(), GooseError> {
let data = common::prepare_data(
ReportOptions {
no_transaction_metrics: self.configuration.no_transaction_metrics,
no_scenario_metrics: self.configuration.no_scenario_metrics,
no_status_codes: self.configuration.no_status_codes,
},
&self.metrics,
);

report::write_markdown_report(&mut BufWriter::new(report_file.into_std().await), data)
}

// Write an HTML-formatted report.
pub(crate) async fn write_html_report(
&self,
Expand Down
2 changes: 1 addition & 1 deletion src/metrics/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use itertools::Itertools;
use std::collections::{BTreeMap, HashMap};

#[derive(Debug, serde::Serialize)]
pub struct ReportData<'m> {
pub(crate) struct ReportData<'m> {
pub raw_metrics: &'m GooseMetrics,

pub raw_request_metrics: Vec<RequestMetric>,
Expand Down
3 changes: 3 additions & 0 deletions src/report.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
//! Optionally writes an html-formatted summary report after running a load test.
mod common;
mod markdown;

pub(crate) use markdown::write_markdown_report;

use crate::{
metrics::{self, format_number},
Expand Down
310 changes: 310 additions & 0 deletions src/report/markdown.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,310 @@
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<'m, 'w, W: Write> Markdown<'m, 'w, 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()?;

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)
// @TODO: error handling
.unwrap()
.format("%y-%m-%d %H:%M:%S");
let stopped = Local
.timestamp_opt(step[1].timestamp.timestamp(), 0)
// @TODO: error handling
.unwrap()
.format("%y-%m-%d %H:%M:%S");

let users = match &step[0].action {
// For maintaining just show the current number of users.
TestPlanStepAction::Maintaining => {
format!("{}", step[0].users)
}
// For increasing show the current number of users to the new number of users.
TestPlanStepAction::Increasing => {
format!("{} &rarr; {}", step[0].users, step[1].users)
}
// For decreasing show the new number of users from the current number of users.
TestPlanStepAction::Decreasing | TestPlanStepAction::Canceling => {
format!("{} &larr; {}", 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) | 50%ile (ms) | 60%ile (ms) | 70%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(())
}
}

0 comments on commit ef178bd

Please sign in to comment.