use crate::config::GooseDefaults;
use crate::goose::{get_base_url, GooseMethod, Scenario};
use crate::logger::GooseLog;
use crate::report;
use crate::test_plan::{TestPlanHistory, TestPlanStepAction};
use crate::util;
use crate::{GooseAttack, GooseAttackRunState, GooseConfiguration, GooseError};
use chrono::prelude::*;
use http::StatusCode;
use itertools::Itertools;
use num_format::{Locale, ToFormattedString};
use regex::RegexSet;
use serde::ser::SerializeStruct;
use serde::{Deserialize, Serialize, Serializer};
use std::cmp::Ordering;
use std::collections::{BTreeMap, HashMap, HashSet};
use std::fmt::Write;
use std::str::FromStr;
use std::{f32, fmt};
use tokio::io::AsyncWriteExt;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum GooseMetric {
Request(GooseRequestMetric),
Transaction(TransactionMetric),
Scenario(ScenarioMetric),
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub enum GooseCoordinatedOmissionMitigation {
Average,
Maximum,
Minimum,
Disabled,
}
impl FromStr for GooseCoordinatedOmissionMitigation {
type Err = GooseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let co_mitigation = RegexSet::new([
r"(?i)^(average|ave|aver|avg|mean)$",
r"(?i)^(maximum|ma|max|maxi)$",
r"(?i)^(minimum|mi|min|mini)$",
r"(?i)^(disabled|di|dis|disable|none|no)$",
])
.expect("failed to compile co_mitigation RegexSet");
let matches = co_mitigation.matches(s);
if matches.matched(0) {
Ok(GooseCoordinatedOmissionMitigation::Average)
} else if matches.matched(1) {
Ok(GooseCoordinatedOmissionMitigation::Maximum)
} else if matches.matched(2) {
Ok(GooseCoordinatedOmissionMitigation::Minimum)
} else if matches.matched(3) {
Ok(GooseCoordinatedOmissionMitigation::Disabled)
} else {
Err(GooseError::InvalidOption {
option: format!("GooseCoordinatedOmissionMitigation::{:?}", s),
value: s.to_string(),
detail:
"Invalid co_mitigation, expected: average, disabled, maximum, median, or minimum"
.to_string(),
})
}
}
}
pub type GooseRequestMetrics = HashMap<String, GooseRequestMetricAggregate>;
pub type TransactionMetrics = Vec<Vec<TransactionMetricAggregate>>;
pub type ScenarioMetrics = Vec<ScenarioMetricAggregate>;
pub type GooseErrorMetrics = BTreeMap<String, GooseErrorMetricAggregate>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GooseRawRequest {
pub method: GooseMethod,
pub url: String,
pub headers: Vec<String>,
pub body: String,
}
impl GooseRawRequest {
pub(crate) fn new(
method: GooseMethod,
url: &str,
headers: Vec<String>,
body: &str,
) -> GooseRawRequest {
GooseRawRequest {
method,
url: url.to_string(),
headers,
body: body.to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GooseRequestMetric {
pub elapsed: u64,
pub raw: GooseRawRequest,
pub name: String,
pub final_url: String,
pub redirected: bool,
pub response_time: u64,
pub status_code: u16,
pub success: bool,
pub update: bool,
pub user: usize,
pub error: String,
pub coordinated_omission_elapsed: u64,
pub user_cadence: u64,
}
impl GooseRequestMetric {
pub(crate) fn new(raw: GooseRawRequest, name: &str, elapsed: u128, user: usize) -> Self {
GooseRequestMetric {
elapsed: elapsed as u64,
raw,
name: name.to_string(),
final_url: "".to_string(),
redirected: false,
response_time: 0,
status_code: 0,
success: true,
update: false,
user,
error: "".to_string(),
coordinated_omission_elapsed: 0,
user_cadence: 0,
}
}
pub(crate) fn set_final_url(&mut self, final_url: &str) {
self.final_url = final_url.to_string();
if self.final_url != self.raw.url {
self.redirected = true;
}
}
pub(crate) fn set_response_time(&mut self, response_time: u128) {
self.response_time = response_time as u64;
}
pub(crate) fn set_status_code(&mut self, status_code: Option<StatusCode>) {
self.status_code = match status_code {
Some(status_code) => status_code.as_u16(),
None => 0,
};
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GooseRequestMetricAggregate {
pub path: String,
pub method: GooseMethod,
pub raw_data: GooseRequestMetricTimingData,
pub coordinated_omission_data: Option<GooseRequestMetricTimingData>,
pub status_code_counts: HashMap<u16, usize>,
pub success_count: usize,
pub fail_count: usize,
pub load_test_hash: u64,
}
impl GooseRequestMetricAggregate {
pub(crate) fn new(path: &str, method: GooseMethod, load_test_hash: u64) -> Self {
trace!("new request");
GooseRequestMetricAggregate {
path: path.to_string(),
method,
raw_data: GooseRequestMetricTimingData::new(None),
coordinated_omission_data: None,
status_code_counts: HashMap::new(),
success_count: 0,
fail_count: 0,
load_test_hash,
}
}
pub(crate) fn record_time(&mut self, time_elapsed: u64, coordinated_omission_mitigation: bool) {
if !coordinated_omission_mitigation {
self.raw_data.record_time(time_elapsed);
}
if let Some(coordinated_omission_data) = self.coordinated_omission_data.as_mut() {
coordinated_omission_data.record_time(time_elapsed);
}
else if coordinated_omission_mitigation {
let mut coordinated_omission_data = self.raw_data.clone();
coordinated_omission_data.record_time(time_elapsed);
self.coordinated_omission_data = Some(coordinated_omission_data);
}
}
pub(crate) fn set_status_code(&mut self, status_code: u16) {
let counter = match self.status_code_counts.get(&status_code) {
Some(c) => {
debug!("got {:?} counter: {}", status_code, c);
*c + 1
}
None => {
debug!("no match for counter: {}", status_code);
1
}
};
self.status_code_counts.insert(status_code, counter);
debug!("incremented {} counter: {}", status_code, counter);
}
}
impl Eq for GooseRequestMetricAggregate {}
impl Ord for GooseRequestMetricAggregate {
fn cmp(&self, other: &Self) -> Ordering {
(&self.method, &self.path).cmp(&(&other.method, &other.path))
}
}
impl PartialOrd for GooseRequestMetricAggregate {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct GooseRequestMetricTimingData {
pub times: BTreeMap<usize, usize>,
pub minimum_time: usize,
pub maximum_time: usize,
pub total_time: usize,
pub counter: usize,
}
impl GooseRequestMetricTimingData {
pub(crate) fn new(metric_data: Option<&GooseRequestMetricTimingData>) -> Self {
trace!("new GooseRequestMetricTimingData");
if let Some(data) = metric_data {
data.clone()
} else {
GooseRequestMetricTimingData {
times: BTreeMap::new(),
minimum_time: 0,
maximum_time: 0,
total_time: 0,
counter: 0,
}
}
}
pub(crate) fn record_time(&mut self, time_elapsed: u64) {
let time = time_elapsed as usize;
if time > 0 && (self.minimum_time == 0 || time < self.minimum_time) {
self.minimum_time = time;
}
if time > self.maximum_time {
self.maximum_time = time;
}
self.total_time += time;
self.counter += 1;
let rounded_time = if time_elapsed < 100 {
time
}
else if time_elapsed < 500 {
((time_elapsed as f64 / 10.0).round() * 10.0) as usize
}
else if time_elapsed < 1000 {
((time_elapsed as f64 / 100.0).round() * 100.0) as usize
}
else {
((time_elapsed as f64 / 1000.0).round() * 1000.0) as usize
};
let counter = match self.times.get(&rounded_time) {
Some(c) => {
debug!("got {:?} counter: {}", rounded_time, c);
*c + 1
}
None => {
debug!("no match for counter: {}", rounded_time);
1
}
};
debug!("incremented {} counter: {}", rounded_time, counter);
self.times.insert(rounded_time, counter);
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScenarioMetric {
pub elapsed: u64,
pub name: String,
pub index: usize,
pub run_time: u64,
pub user: usize,
}
impl ScenarioMetric {
pub(crate) fn new(
elapsed: u128,
scenario_name: &str,
index: usize,
run_time: u128,
user: usize,
) -> Self {
ScenarioMetric {
elapsed: elapsed as u64,
name: scenario_name.to_string(),
index,
run_time: run_time as u64,
user,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransactionMetric {
pub elapsed: u64,
pub scenario_index: usize,
pub transaction_index: usize,
pub name: String,
pub run_time: u64,
pub success: bool,
pub user: usize,
}
impl TransactionMetric {
pub(crate) fn new(
elapsed: u128,
scenario_index: usize,
transaction_index: usize,
name: String,
user: usize,
) -> Self {
TransactionMetric {
elapsed: elapsed as u64,
scenario_index,
transaction_index,
name,
run_time: 0,
success: true,
user,
}
}
pub(crate) fn set_time(&mut self, time: u128, success: bool) {
self.run_time = time as u64;
self.success = success;
}
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct TransactionMetricAggregate {
pub scenario_index: usize,
pub scenario_name: String,
pub transaction_index: usize,
pub transaction_name: String,
pub times: BTreeMap<usize, usize>,
pub min_time: usize,
pub max_time: usize,
pub total_time: usize,
pub counter: usize,
pub success_count: usize,
pub fail_count: usize,
}
impl TransactionMetricAggregate {
pub(crate) fn new(
scenario_index: usize,
scenario_name: &str,
transaction_index: usize,
transaction_name: &str,
) -> Self {
TransactionMetricAggregate {
scenario_index,
scenario_name: scenario_name.to_string(),
transaction_index,
transaction_name: transaction_name.to_string(),
times: BTreeMap::new(),
min_time: 0,
max_time: 0,
total_time: 0,
counter: 0,
success_count: 0,
fail_count: 0,
}
}
pub(crate) fn set_time(&mut self, time: u64, success: bool) {
let time_usize = time as usize;
if self.min_time == 0 || time_usize < self.min_time {
self.min_time = time_usize;
}
if time_usize > self.max_time {
self.max_time = time_usize;
}
self.total_time += time_usize;
self.counter += 1;
if success {
self.success_count += 1;
} else {
self.fail_count += 1;
}
let rounded_time = match time {
0..=100 => time_usize,
101..=500 => ((time as f64 / 10.0).round() * 10.0) as usize,
501..=1000 => ((time as f64 / 100.0).round() * 10.0) as usize,
_ => ((time as f64 / 1000.0).round() * 10.0) as usize,
};
let counter = match self.times.get(&rounded_time) {
Some(c) => *c + 1,
None => 1,
};
self.times.insert(rounded_time, counter);
debug!("incremented {} counter: {}", rounded_time, counter);
}
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct ScenarioMetricAggregate {
pub index: usize,
pub name: String,
pub users: HashSet<usize>,
pub times: BTreeMap<usize, usize>,
pub min_time: usize,
pub max_time: usize,
pub total_time: usize,
pub counter: usize,
}
impl ScenarioMetricAggregate {
pub(crate) fn new(index: usize, name: &str) -> Self {
ScenarioMetricAggregate {
index,
name: name.to_string(),
users: HashSet::new(),
times: BTreeMap::new(),
min_time: 0,
max_time: 0,
total_time: 0,
counter: 0,
}
}
pub(crate) fn update(&mut self, time: u64, user: usize) {
self.users.insert(user);
let time_usize = time as usize;
if self.min_time == 0 || time_usize < self.min_time {
self.min_time = time_usize;
}
if time_usize > self.max_time {
self.max_time = time_usize;
}
self.total_time += time_usize;
self.counter += 1;
let rounded_time = match time {
0..=100 => time_usize,
101..=500 => ((time as f64 / 10.0).round() * 10.0) as usize,
501..=1000 => ((time as f64 / 100.0).round() * 10.0) as usize,
_ => ((time as f64 / 1000.0).round() * 10.0) as usize,
};
let counter = match self.times.get(&rounded_time) {
Some(c) => *c + 1,
None => 1,
};
self.times.insert(rounded_time, counter);
debug!("incremented {} counter: {}", rounded_time, counter);
}
}
#[derive(Clone, Debug, Default)]
pub struct GooseMetrics {
pub hash: u64,
pub history: Vec<TestPlanHistory>,
pub duration: usize,
pub maximum_users: usize,
pub total_users: usize,
pub requests: GooseRequestMetrics,
pub transactions: TransactionMetrics,
pub scenarios: ScenarioMetrics,
pub errors: GooseErrorMetrics,
pub hosts: HashSet<String>,
pub(crate) final_metrics: bool,
pub(crate) display_status_codes: bool,
pub(crate) display_metrics: bool,
}
impl GooseMetrics {
pub(crate) fn initialize_transaction_metrics(
&mut self,
scenarios: &[Scenario],
config: &GooseConfiguration,
defaults: &GooseDefaults,
) -> Result<(), GooseError> {
self.transactions = Vec::new();
if !config.no_metrics {
for scenario in scenarios {
if !config.no_transaction_metrics {
let mut transaction_vector = Vec::new();
for transaction in &scenario.transactions {
transaction_vector.push(TransactionMetricAggregate::new(
scenario.scenarios_index,
&scenario.name,
transaction.transactions_index,
&transaction.name,
));
}
self.transactions.push(transaction_vector);
}
self.hosts.insert(
get_base_url(
if !config.host.is_empty() {
Some(config.host.to_string())
} else {
None
},
scenario.host.clone(),
defaults.host.clone(),
)?
.to_string(),
);
}
}
Ok(())
}
pub(crate) fn initialize_scenario_metrics(
&mut self,
scenarios: &[Scenario],
config: &GooseConfiguration,
) {
if !config.no_metrics && !config.no_scenario_metrics {
self.scenarios = Vec::new();
for scenario in scenarios {
self.scenarios.push(ScenarioMetricAggregate::new(
scenario.scenarios_index,
&scenario.name,
));
}
}
}
pub(crate) fn print_running(&self) {
if self.display_metrics {
info!(
"printing running metrics after {} seconds...",
self.duration
);
println!("{}", self);
}
}
pub(crate) fn fmt_requests(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.requests.is_empty() {
return Ok(());
}
writeln!(
fmt,
"\n === PER REQUEST METRICS ===\n ------------------------------------------------------------------------------"
)?;
writeln!(
fmt,
" {:<24} | {:>13} | {:>14} | {:>8} | {:>7}",
"Name", "# reqs", "# fails", "req/s", "fail/s"
)?;
writeln!(
fmt,
" ------------------------------------------------------------------------------"
)?;
let mut aggregate_fail_count = 0;
let mut aggregate_total_count = 0;
for (request_key, request) in self.requests.iter().sorted() {
let total_count = request.success_count + request.fail_count;
let fail_percent = if request.fail_count > 0 {
request.fail_count as f32 / total_count as f32 * 100.0
} else {
0.0
};
let (reqs, fails) =
per_second_calculations(self.duration, total_count, request.fail_count);
let reqs_precision = determine_precision(reqs);
let fails_precision = determine_precision(fails);
if fail_percent as usize == 100 || fail_percent as usize == 0 {
let fail_and_percent = format!(
"{} ({}%)",
request.fail_count.to_formatted_string(&Locale::en),
fail_percent as usize
);
writeln!(
fmt,
" {:<24} | {:>13} | {:>14} | {:>8.reqs_p$} | {:>7.fails_p$}",
util::truncate_string(request_key, 24),
total_count.to_formatted_string(&Locale::en),
fail_and_percent,
reqs,
fails,
reqs_p = reqs_precision,
fails_p = fails_precision,
)?;
} else {
let fail_and_percent = format!(
"{} ({:.1}%)",
request.fail_count.to_formatted_string(&Locale::en),
fail_percent
);
writeln!(
fmt,
" {:<24} | {:>13} | {:>14} | {:>8.reqs_p$} | {:>7.fails_p$}",
util::truncate_string(request_key, 24),
total_count.to_formatted_string(&Locale::en),
fail_and_percent,
reqs,
fails,
reqs_p = reqs_precision,
fails_p = fails_precision,
)?;
}
aggregate_total_count += total_count;
aggregate_fail_count += request.fail_count;
}
if self.requests.len() > 1 {
let aggregate_fail_percent = if aggregate_fail_count > 0 {
aggregate_fail_count as f32 / aggregate_total_count as f32 * 100.0
} else {
0.0
};
writeln!(
fmt,
" -------------------------+---------------+----------------+----------+--------"
)?;
let (reqs, fails) =
per_second_calculations(self.duration, aggregate_total_count, aggregate_fail_count);
let reqs_precision = determine_precision(reqs);
let fails_precision = determine_precision(fails);
if aggregate_fail_percent as usize == 100 || aggregate_fail_percent as usize == 0 {
let fail_and_percent = format!(
"{} ({}%)",
aggregate_fail_count.to_formatted_string(&Locale::en),
aggregate_fail_percent as usize
);
writeln!(
fmt,
" {:<24} | {:>13} | {:>14} | {:>8.reqs_p$} | {:>7.fails_p$}",
"Aggregated",
aggregate_total_count.to_formatted_string(&Locale::en),
fail_and_percent,
reqs,
fails,
reqs_p = reqs_precision,
fails_p = fails_precision,
)?;
} else {
let fail_and_percent = format!(
"{} ({:.1}%)",
aggregate_fail_count.to_formatted_string(&Locale::en),
aggregate_fail_percent
);
writeln!(
fmt,
" {:<24} | {:>13} | {:>14} | {:>8.reqs_p$} | {:>7.fails_p$}",
"Aggregated",
aggregate_total_count.to_formatted_string(&Locale::en),
fail_and_percent,
reqs,
fails,
reqs_p = reqs_precision,
fails_p = fails_precision,
)?;
}
}
Ok(())
}
pub(crate) fn fmt_transactions(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.transactions.is_empty() || !self.display_metrics {
return Ok(());
}
writeln!(
fmt,
"\n === PER TRANSACTION METRICS ===\n ------------------------------------------------------------------------------"
)?;
writeln!(
fmt,
" {:<24} | {:>13} | {:>14} | {:>8} | {:>7}",
"Name", "# times run", "# fails", "trans/s", "fail/s"
)?;
writeln!(
fmt,
" ------------------------------------------------------------------------------"
)?;
let mut aggregate_fail_count = 0;
let mut aggregate_total_count = 0;
let mut transaction_count = 0;
for scenario in &self.transactions {
let mut displayed_scenario = false;
for transaction in scenario {
transaction_count += 1;
let total_count = transaction.success_count + transaction.fail_count;
let fail_percent = if transaction.fail_count > 0 {
transaction.fail_count as f32 / total_count as f32 * 100.0
} else {
0.0
};
let (runs, fails) =
per_second_calculations(self.duration, total_count, transaction.fail_count);
let runs_precision = determine_precision(runs);
let fails_precision = determine_precision(fails);
if !displayed_scenario {
writeln!(
fmt,
" {:24 }",
util::truncate_string(
&format!(
"{}: {}",
transaction.scenario_index + 1,
&transaction.scenario_name
),
60
),
)?;
displayed_scenario = true;
}
if fail_percent as usize == 100 || fail_percent as usize == 0 {
let fail_and_percent = format!(
"{} ({}%)",
transaction.fail_count.to_formatted_string(&Locale::en),
fail_percent as usize
);
writeln!(
fmt,
" {:<24} | {:>13} | {:>14} | {:>8.runs_p$} | {:>7.fails_p$}",
util::truncate_string(
&format!(
" {}: {}",
transaction.transaction_index + 1,
transaction.transaction_name
),
24
),
total_count.to_formatted_string(&Locale::en),
fail_and_percent,
runs,
fails,
runs_p = runs_precision,
fails_p = fails_precision,
)?;
} else {
let fail_and_percent = format!(
"{} ({:.1}%)",
transaction.fail_count.to_formatted_string(&Locale::en),
fail_percent
);
writeln!(
fmt,
" {:<24} | {:>13} | {:>14} | {:>8.runs_p$} | {:>7.fails_p$}",
util::truncate_string(
&format!(
" {}: {}",
transaction.transaction_index + 1,
transaction.transaction_name
),
24
),
total_count.to_formatted_string(&Locale::en),
fail_and_percent,
runs,
fails,
runs_p = runs_precision,
fails_p = fails_precision,
)?;
}
aggregate_total_count += total_count;
aggregate_fail_count += transaction.fail_count;
}
}
if transaction_count > 1 {
let aggregate_fail_percent = if aggregate_fail_count > 0 {
aggregate_fail_count as f32 / aggregate_total_count as f32 * 100.0
} else {
0.0
};
writeln!(
fmt,
" -------------------------+---------------+----------------+----------+--------"
)?;
let (runs, fails) =
per_second_calculations(self.duration, aggregate_total_count, aggregate_fail_count);
let runs_precision = determine_precision(runs);
let fails_precision = determine_precision(fails);
if aggregate_fail_percent as usize == 100 || aggregate_fail_percent as usize == 0 {
let fail_and_percent = format!(
"{} ({}%)",
aggregate_fail_count.to_formatted_string(&Locale::en),
aggregate_fail_percent as usize
);
writeln!(
fmt,
" {:<24} | {:>13} | {:>14} | {:>8.runs_p$} | {:>7.fails_p$}",
"Aggregated",
aggregate_total_count.to_formatted_string(&Locale::en),
fail_and_percent,
runs,
fails,
runs_p = runs_precision,
fails_p = fails_precision,
)?;
} else {
let fail_and_percent = format!(
"{} ({:.1}%)",
aggregate_fail_count.to_formatted_string(&Locale::en),
aggregate_fail_percent
);
writeln!(
fmt,
" {:<24} | {:>13} | {:>14} | {:>8.runs_p$} | {:>7.fails_p$}",
"Aggregated",
aggregate_total_count.to_formatted_string(&Locale::en),
fail_and_percent,
runs,
fails,
runs_p = runs_precision,
fails_p = fails_precision,
)?;
}
}
Ok(())
}
pub(crate) fn fmt_transaction_times(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.transactions.is_empty() || !self.display_metrics {
return Ok(());
}
let mut aggregate_transaction_times: BTreeMap<usize, usize> = BTreeMap::new();
let mut aggregate_total_transaction_time: usize = 0;
let mut aggregate_transaction_time_counter: usize = 0;
let mut aggregate_min_transaction_time: usize = 0;
let mut aggregate_max_transaction_time: usize = 0;
writeln!(
fmt,
" ------------------------------------------------------------------------------"
)?;
writeln!(
fmt,
" {:<24} | {:>11} | {:>10} | {:>11} | {:>10}",
"Name", "Avg (ms)", "Min", "Max", "Median"
)?;
writeln!(
fmt,
" ------------------------------------------------------------------------------"
)?;
let mut transaction_count = 0;
for scenario in &self.transactions {
let mut displayed_scenario = false;
for transaction in scenario {
transaction_count += 1;
if !displayed_scenario {
writeln!(
fmt,
" {:24}",
util::truncate_string(
&format!(
"{}: {}",
transaction.scenario_index + 1,
&transaction.scenario_name
),
60
),
)?;
displayed_scenario = true;
}
aggregate_transaction_times =
merge_times(aggregate_transaction_times, transaction.times.clone());
aggregate_total_transaction_time += &transaction.total_time;
aggregate_transaction_time_counter += &transaction.counter;
aggregate_min_transaction_time =
update_min_time(aggregate_min_transaction_time, transaction.min_time);
aggregate_max_transaction_time =
update_max_time(aggregate_max_transaction_time, transaction.max_time);
let average = match transaction.counter {
0 => 0.00,
_ => transaction.total_time as f32 / transaction.counter as f32,
};
let average_precision = determine_precision(average);
writeln!(
fmt,
" {:<24} | {:>11.avg_precision$} | {:>10} | {:>11} | {:>10}",
util::truncate_string(
&format!(
" {}: {}",
transaction.transaction_index + 1,
transaction.transaction_name
),
24
),
average,
format_number(transaction.min_time),
format_number(transaction.max_time),
format_number(util::median(
&transaction.times,
transaction.counter,
transaction.min_time,
transaction.max_time
)),
avg_precision = average_precision,
)?;
}
}
if transaction_count > 1 {
let average = match aggregate_transaction_time_counter {
0 => 0.00,
_ => {
aggregate_total_transaction_time as f32
/ aggregate_transaction_time_counter as f32
}
};
let average_precision = determine_precision(average);
writeln!(
fmt,
" -------------------------+-------------+------------+-------------+-----------"
)?;
writeln!(
fmt,
" {:<24} | {:>11.avg_precision$} | {:>10} | {:>11} | {:>10}",
"Aggregated",
average,
format_number(aggregate_min_transaction_time),
format_number(aggregate_max_transaction_time),
format_number(util::median(
&aggregate_transaction_times,
aggregate_transaction_time_counter,
aggregate_min_transaction_time,
aggregate_max_transaction_time
)),
avg_precision = average_precision,
)?;
}
Ok(())
}
pub(crate) fn fmt_scenarios(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.scenarios.is_empty() || !self.display_metrics {
return Ok(());
}
writeln!(
fmt,
"\n === PER SCENARIO METRICS ===\n ------------------------------------------------------------------------------"
)?;
writeln!(
fmt,
" {:<24} | {:>8} | {:>12} | {:>11} | {:10}",
"Name", "# users", "# times run", "scenarios/s", "iterations"
)?;
writeln!(
fmt,
" ------------------------------------------------------------------------------"
)?;
let mut aggregate_users = 0;
let mut aggregate_counter = 0;
for scenario in &self.scenarios {
aggregate_users += scenario.users.len();
aggregate_counter += scenario.counter;
let (runs, _fails) = per_second_calculations(self.duration, scenario.counter, 0);
let runs_precision = determine_precision(runs);
let iterations = scenario.counter as f32 / scenario.users.len() as f32;
let iterations_precision = determine_precision(iterations);
writeln!(
fmt,
" {:24 } | {:>8} | {:>12} | {:>11.runs_p$} | {:>10.iterations_p$}",
util::truncate_string(&format!("{}: {}", scenario.index + 1, &scenario.name,), 24),
scenario.users.len(),
scenario.counter,
runs,
iterations,
runs_p = runs_precision,
iterations_p = iterations_precision,
)?;
}
if self.scenarios.len() > 1 {
let (aggregate_runs, _fails) =
per_second_calculations(self.duration, aggregate_counter, 0);
let aggregate_runs_precision = determine_precision(aggregate_runs);
let aggregate_iterations = aggregate_counter as f32 / aggregate_users as f32;
let aggregate_iterations_precision = determine_precision(aggregate_iterations);
writeln!(
fmt,
" -------------------------+----------+--------------+-------------+------------"
)?;
writeln!(
fmt,
" {:24 } | {:>8} | {:>12} | {:>11.runs_p$} | {:>10.iterations_p$}",
"Aggregated",
aggregate_users,
aggregate_counter,
aggregate_runs,
aggregate_iterations,
runs_p = aggregate_runs_precision,
iterations_p = aggregate_iterations_precision,
)?;
}
Ok(())
}
pub(crate) fn fmt_scenario_times(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.scenarios.is_empty() || !self.display_metrics {
return Ok(());
}
let mut aggregate_scenario_times: BTreeMap<usize, usize> = BTreeMap::new();
let mut aggregate_total_scenario_time: usize = 0;
let mut aggregate_scenario_time_counter: usize = 0;
let mut aggregate_min_scenario_time: usize = 0;
let mut aggregate_max_scenario_time: usize = 0;
writeln!(
fmt,
" ------------------------------------------------------------------------------"
)?;
writeln!(
fmt,
" {:<24} | {:>11} | {:>10} | {:>11} | {:>10}",
"Name", "Avg (ms)", "Min", "Max", "Median"
)?;
writeln!(
fmt,
" ------------------------------------------------------------------------------"
)?;
for scenario in &self.scenarios {
aggregate_scenario_times =
merge_times(aggregate_scenario_times, scenario.times.clone());
aggregate_total_scenario_time += &scenario.total_time;
aggregate_scenario_time_counter += &scenario.counter;
aggregate_min_scenario_time =
update_min_time(aggregate_min_scenario_time, scenario.min_time);
aggregate_max_scenario_time =
update_max_time(aggregate_max_scenario_time, scenario.max_time);
let average = match scenario.counter {
0 => 0.00,
_ => scenario.total_time as f32 / scenario.counter as f32,
};
let average_precision = determine_precision(average);
writeln!(
fmt,
" {:<24} | {:>11.avg_precision$} | {:>10} | {:>11} | {:>10}",
util::truncate_string(&format!(" {}: {}", scenario.index + 1, scenario.name), 24),
average,
format_number(scenario.min_time),
format_number(scenario.max_time),
format_number(util::median(
&scenario.times,
scenario.counter,
scenario.min_time,
scenario.max_time
)),
avg_precision = average_precision,
)?;
}
if self.scenarios.len() > 1 {
let average = match aggregate_scenario_time_counter {
0 => 0.00,
_ => aggregate_total_scenario_time as f32 / aggregate_scenario_time_counter as f32,
};
let average_precision = determine_precision(average);
writeln!(
fmt,
" -------------------------+-------------+------------+-------------+-----------"
)?;
writeln!(
fmt,
" {:<24} | {:>11.avg_precision$} | {:>10} | {:>11} | {:>10}",
"Aggregated",
average,
format_number(aggregate_min_scenario_time),
format_number(aggregate_max_scenario_time),
format_number(util::median(
&aggregate_scenario_times,
aggregate_scenario_time_counter,
aggregate_min_scenario_time,
aggregate_max_scenario_time
)),
avg_precision = average_precision,
)?;
}
Ok(())
}
pub(crate) fn fmt_response_times(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.requests.is_empty() {
return Ok(());
}
let mut aggregate_raw_times: BTreeMap<usize, usize> = BTreeMap::new();
let mut aggregate_raw_total_time: usize = 0;
let mut aggregate_raw_counter: usize = 0;
let mut aggregate_raw_min_time: usize = 0;
let mut aggregate_raw_max_time: usize = 0;
writeln!(
fmt,
" ------------------------------------------------------------------------------"
)?;
writeln!(
fmt,
" {:<24} | {:>11} | {:>10} | {:>11} | {:>10}",
"Name", "Avg (ms)", "Min", "Max", "Median"
)?;
writeln!(
fmt,
" ------------------------------------------------------------------------------"
)?;
let mut co_data = false;
for (request_key, request) in self.requests.iter().sorted() {
if !co_data && request.coordinated_omission_data.is_some() {
co_data = true;
}
let raw_average = match request.raw_data.counter {
0 => 0.0,
_ => request.raw_data.total_time as f32 / request.raw_data.counter as f32,
};
let raw_average_precision = determine_precision(raw_average);
aggregate_raw_times = merge_times(aggregate_raw_times, request.raw_data.times.clone());
aggregate_raw_total_time += &request.raw_data.total_time;
aggregate_raw_counter += &request.raw_data.counter;
aggregate_raw_min_time =
update_min_time(aggregate_raw_min_time, request.raw_data.minimum_time);
aggregate_raw_max_time =
update_max_time(aggregate_raw_max_time, request.raw_data.maximum_time);
writeln!(
fmt,
" {:<24} | {:>11.raw_avg_precision$} | {:>10} | {:>11} | {:>10}",
util::truncate_string(request_key, 24),
raw_average,
format_number(request.raw_data.minimum_time),
format_number(request.raw_data.maximum_time),
format_number(util::median(
&request.raw_data.times,
request.raw_data.counter,
request.raw_data.minimum_time,
request.raw_data.maximum_time,
)),
raw_avg_precision = raw_average_precision,
)?;
}
let raw_average = match aggregate_raw_counter {
0 => 0.0,
_ => aggregate_raw_total_time as f32 / aggregate_raw_counter as f32,
};
let raw_average_precision = determine_precision(raw_average);
if self.requests.len() > 1 {
writeln!(
fmt,
" -------------------------+-------------+------------+-------------+-----------"
)?;
writeln!(
fmt,
" {:<24} | {:>11.avg_precision$} | {:>10} | {:>11} | {:>10}",
"Aggregated",
raw_average,
format_number(aggregate_raw_min_time),
format_number(aggregate_raw_max_time),
format_number(util::median(
&aggregate_raw_times,
aggregate_raw_counter,
aggregate_raw_min_time,
aggregate_raw_max_time
)),
avg_precision = raw_average_precision,
)?;
}
if !co_data {
return Ok(());
}
writeln!(
fmt,
" ------------------------------------------------------------------------------"
)?;
writeln!(fmt, " Adjusted for Coordinated Omission:")?;
let mut aggregate_co_times: BTreeMap<usize, usize> = BTreeMap::new();
let mut aggregate_co_total_time: usize = 0;
let mut aggregate_co_counter: usize = 0;
let mut aggregate_co_min_time: usize = 0;
let mut aggregate_co_max_time: usize = 0;
writeln!(
fmt,
" ------------------------------------------------------------------------------"
)?;
writeln!(
fmt,
" {:<24} | {:>11} | {:>10} | {:>11} | {:>10}",
"Name", "Avg (ms)", "Std Dev", "Max", "Median"
)?;
writeln!(
fmt,
" ------------------------------------------------------------------------------"
)?;
for (request_key, request) in self.requests.iter().sorted() {
let co_average;
let standard_deviation;
let co_minimum;
let co_maximum;
if let Some(co_data) = request.coordinated_omission_data.as_ref() {
let raw_average = match request.raw_data.counter {
0 => 0.0,
_ => request.raw_data.total_time as f32 / request.raw_data.counter as f32,
};
co_average = match co_data.counter {
0 => 0.0,
_ => co_data.total_time as f32 / co_data.counter as f32,
};
standard_deviation = util::standard_deviation(raw_average, co_average);
aggregate_co_times = merge_times(aggregate_co_times, co_data.times.clone());
aggregate_co_counter += co_data.counter;
aggregate_co_min_time =
update_min_time(aggregate_co_min_time, co_data.minimum_time);
aggregate_co_max_time =
update_max_time(aggregate_raw_max_time, co_data.maximum_time);
aggregate_co_total_time += co_data.total_time;
co_minimum = co_data.minimum_time;
co_maximum = co_data.maximum_time;
} else {
co_average = 0.0;
standard_deviation = 0.0;
co_minimum = 0;
co_maximum = 0;
}
let co_average_precision = determine_precision(co_average);
let standard_deviation_precision = determine_precision(standard_deviation);
if let Some(co_data) = request.coordinated_omission_data.as_ref() {
writeln!(
fmt,
" {:<24} | {:>11.co_avg_precision$} | {:>10.sd_precision$} | {:>11} | {:>10}",
util::truncate_string(request_key, 24),
co_average,
standard_deviation,
format_number(co_maximum),
format_number(util::median(
&co_data.times,
co_data.counter,
co_minimum,
co_maximum,
)),
co_avg_precision = co_average_precision,
sd_precision = standard_deviation_precision,
)?;
} else {
writeln!(
fmt,
" {:<24} | {:>11} | {:>10} | {:>11} | {:>10}",
util::truncate_string(request_key, 24),
"-",
"-",
"-",
"-",
)?;
}
}
if self.requests.len() > 1 {
let co_average = match aggregate_co_counter {
0 => 0.0,
_ => aggregate_co_total_time as f32 / aggregate_co_counter as f32,
};
let co_average_precision = determine_precision(co_average);
let standard_deviation = util::standard_deviation(raw_average, co_average);
let standard_deviation_precision = determine_precision(standard_deviation);
writeln!(
fmt,
" -------------------------+-------------+------------+-------------+-----------"
)?;
writeln!(
fmt,
" {:<24} | {:>11.avg_precision$} | {:>10.sd_precision$} | {:>11} | {:>10}",
"Aggregated",
co_average,
standard_deviation,
format_number(aggregate_co_max_time),
format_number(util::median(
&aggregate_co_times,
aggregate_co_counter,
aggregate_co_min_time,
aggregate_co_max_time
)),
avg_precision = co_average_precision,
sd_precision = standard_deviation_precision,
)?;
}
Ok(())
}
pub(crate) fn fmt_percentiles(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
if !self.final_metrics {
return Ok(());
}
let mut raw_aggregate_response_times: BTreeMap<usize, usize> = BTreeMap::new();
let mut raw_aggregate_total_response_time: usize = 0;
let mut raw_aggregate_response_time_counter: usize = 0;
let mut raw_aggregate_min_response_time: usize = 0;
let mut raw_aggregate_max_response_time: usize = 0;
writeln!(
fmt,
" ------------------------------------------------------------------------------"
)?;
writeln!(
fmt,
" Slowest page load within specified percentile of requests (in ms):"
)?;
writeln!(
fmt,
" ------------------------------------------------------------------------------"
)?;
writeln!(
fmt,
" {:<24} | {:>6} | {:>6} | {:>6} | {:>6} | {:>6} | {:>6}",
"Name", "50%", "75%", "98%", "99%", "99.9%", "99.99%"
)?;
writeln!(
fmt,
" ------------------------------------------------------------------------------"
)?;
let mut co_data = false;
for (request_key, request) in self.requests.iter().sorted() {
if !co_data && request.coordinated_omission_data.is_some() {
co_data = true;
}
raw_aggregate_response_times =
merge_times(raw_aggregate_response_times, request.raw_data.times.clone());
raw_aggregate_total_response_time += &request.raw_data.total_time;
raw_aggregate_response_time_counter += &request.raw_data.counter;
raw_aggregate_min_response_time = update_min_time(
raw_aggregate_min_response_time,
request.raw_data.minimum_time,
);
raw_aggregate_max_response_time = update_max_time(
raw_aggregate_max_response_time,
request.raw_data.maximum_time,
);
writeln!(
fmt,
" {:<24} | {:>6} | {:>6} | {:>6} | {:>6} | {:>6} | {:>6}",
util::truncate_string(request_key, 24),
calculate_response_time_percentile(
&request.raw_data.times,
request.raw_data.counter,
request.raw_data.minimum_time,
request.raw_data.maximum_time,
0.5
),
calculate_response_time_percentile(
&request.raw_data.times,
request.raw_data.counter,
request.raw_data.minimum_time,
request.raw_data.maximum_time,
0.75
),
calculate_response_time_percentile(
&request.raw_data.times,
request.raw_data.counter,
request.raw_data.minimum_time,
request.raw_data.maximum_time,
0.98
),
calculate_response_time_percentile(
&request.raw_data.times,
request.raw_data.counter,
request.raw_data.minimum_time,
request.raw_data.maximum_time,
0.99
),
calculate_response_time_percentile(
&request.raw_data.times,
request.raw_data.counter,
request.raw_data.minimum_time,
request.raw_data.maximum_time,
0.999
),
calculate_response_time_percentile(
&request.raw_data.times,
request.raw_data.counter,
request.raw_data.minimum_time,
request.raw_data.maximum_time,
0.9999
),
)?;
}
if self.requests.len() > 1 {
writeln!(
fmt,
" -------------------------+--------+--------+--------+--------+--------+-------"
)?;
writeln!(
fmt,
" {:<24} | {:>6} | {:>6} | {:>6} | {:>6} | {:>6} | {:>6}",
"Aggregated",
calculate_response_time_percentile(
&raw_aggregate_response_times,
raw_aggregate_response_time_counter,
raw_aggregate_min_response_time,
raw_aggregate_max_response_time,
0.5
),
calculate_response_time_percentile(
&raw_aggregate_response_times,
raw_aggregate_response_time_counter,
raw_aggregate_min_response_time,
raw_aggregate_max_response_time,
0.75
),
calculate_response_time_percentile(
&raw_aggregate_response_times,
raw_aggregate_response_time_counter,
raw_aggregate_min_response_time,
raw_aggregate_max_response_time,
0.98
),
calculate_response_time_percentile(
&raw_aggregate_response_times,
raw_aggregate_response_time_counter,
raw_aggregate_min_response_time,
raw_aggregate_max_response_time,
0.99
),
calculate_response_time_percentile(
&raw_aggregate_response_times,
raw_aggregate_response_time_counter,
raw_aggregate_min_response_time,
raw_aggregate_max_response_time,
0.999
),
calculate_response_time_percentile(
&raw_aggregate_response_times,
raw_aggregate_response_time_counter,
raw_aggregate_min_response_time,
raw_aggregate_max_response_time,
0.9999
),
)?;
}
if !co_data {
return Ok(());
}
let mut co_aggregate_response_times: BTreeMap<usize, usize> = BTreeMap::new();
let mut co_aggregate_total_response_time: usize = 0;
let mut co_aggregate_response_time_counter: usize = 0;
let mut co_aggregate_min_response_time: usize = 0;
let mut co_aggregate_max_response_time: usize = 0;
writeln!(
fmt,
" ------------------------------------------------------------------------------"
)?;
writeln!(fmt, " Adjusted for Coordinated Omission:")?;
writeln!(
fmt,
" ------------------------------------------------------------------------------"
)?;
writeln!(
fmt,
" {:<24} | {:>6} | {:>6} | {:>6} | {:>6} | {:>6} | {:>6}",
"Name", "50%", "75%", "98%", "99%", "99.9%", "99.99%"
)?;
writeln!(
fmt,
" ------------------------------------------------------------------------------"
)?;
for (request_key, request) in self.requests.iter().sorted() {
if let Some(coordinated_omission_data) = request.coordinated_omission_data.as_ref() {
co_aggregate_response_times = merge_times(
co_aggregate_response_times,
coordinated_omission_data.times.clone(),
);
co_aggregate_total_response_time += &coordinated_omission_data.total_time;
co_aggregate_response_time_counter += &coordinated_omission_data.counter;
co_aggregate_min_response_time = update_min_time(
co_aggregate_min_response_time,
coordinated_omission_data.minimum_time,
);
co_aggregate_max_response_time = update_max_time(
co_aggregate_max_response_time,
coordinated_omission_data.maximum_time,
);
writeln!(
fmt,
" {:<24} | {:>6} | {:>6} | {:>6} | {:>6} | {:>6} | {:>6}",
util::truncate_string(request_key, 24),
calculate_response_time_percentile(
&coordinated_omission_data.times,
coordinated_omission_data.counter,
coordinated_omission_data.minimum_time,
coordinated_omission_data.maximum_time,
0.5
),
calculate_response_time_percentile(
&coordinated_omission_data.times,
coordinated_omission_data.counter,
coordinated_omission_data.minimum_time,
coordinated_omission_data.maximum_time,
0.75
),
calculate_response_time_percentile(
&coordinated_omission_data.times,
coordinated_omission_data.counter,
coordinated_omission_data.minimum_time,
coordinated_omission_data.maximum_time,
0.98
),
calculate_response_time_percentile(
&coordinated_omission_data.times,
coordinated_omission_data.counter,
coordinated_omission_data.minimum_time,
coordinated_omission_data.maximum_time,
0.99
),
calculate_response_time_percentile(
&coordinated_omission_data.times,
coordinated_omission_data.counter,
coordinated_omission_data.minimum_time,
coordinated_omission_data.maximum_time,
0.999
),
calculate_response_time_percentile(
&coordinated_omission_data.times,
coordinated_omission_data.counter,
coordinated_omission_data.minimum_time,
coordinated_omission_data.maximum_time,
0.9999
),
)?;
} else {
writeln!(
fmt,
" {:<24} | {:>6} | {:>6} | {:>6} | {:>6} | {:>6} | {:>6}",
util::truncate_string(request_key, 24),
"-",
"-",
"-",
"-",
"-",
"-"
)?;
}
}
if self.requests.len() > 1 {
writeln!(
fmt,
" -------------------------+--------+--------+--------+--------+--------+-------"
)?;
writeln!(
fmt,
" {:<24} | {:>6} | {:>6} | {:>6} | {:>6} | {:>6} | {:>6}",
"Aggregated",
calculate_response_time_percentile(
&co_aggregate_response_times,
co_aggregate_response_time_counter,
co_aggregate_min_response_time,
co_aggregate_max_response_time,
0.5
),
calculate_response_time_percentile(
&co_aggregate_response_times,
co_aggregate_response_time_counter,
co_aggregate_min_response_time,
co_aggregate_max_response_time,
0.75
),
calculate_response_time_percentile(
&co_aggregate_response_times,
co_aggregate_response_time_counter,
co_aggregate_min_response_time,
co_aggregate_max_response_time,
0.98
),
calculate_response_time_percentile(
&co_aggregate_response_times,
co_aggregate_response_time_counter,
co_aggregate_min_response_time,
co_aggregate_max_response_time,
0.99
),
calculate_response_time_percentile(
&co_aggregate_response_times,
co_aggregate_response_time_counter,
co_aggregate_min_response_time,
co_aggregate_max_response_time,
0.999
),
calculate_response_time_percentile(
&co_aggregate_response_times,
co_aggregate_response_time_counter,
co_aggregate_min_response_time,
co_aggregate_max_response_time,
0.9999
),
)?;
}
Ok(())
}
pub(crate) fn fmt_status_codes(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
if !self.display_status_codes {
return Ok(());
}
writeln!(
fmt,
" ------------------------------------------------------------------------------"
)?;
writeln!(fmt, " {:<24} | {:>51} ", "Name", "Status codes")?;
writeln!(
fmt,
" ------------------------------------------------------------------------------"
)?;
let mut aggregated_status_code_counts: HashMap<u16, usize> = HashMap::new();
for (request_key, request) in self.requests.iter().sorted() {
let codes = prepare_status_codes(
&request.status_code_counts,
&mut Some(&mut aggregated_status_code_counts),
);
writeln!(
fmt,
" {:<24} | {:>51}",
util::truncate_string(request_key, 24),
codes,
)?;
}
writeln!(
fmt,
" -------------------------+----------------------------------------------------"
)?;
let codes = prepare_status_codes(&aggregated_status_code_counts, &mut None);
writeln!(fmt, " {:<24} | {:>51} ", "Aggregated", codes)?;
Ok(())
}
pub(crate) fn fmt_errors(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
if !self.final_metrics || self.errors.is_empty() {
return Ok(());
}
let mut errors: Vec<(usize, String)> = Vec::new();
for error in self.errors.values() {
errors.push((
error.occurrences,
format!("{} {}: {}", error.method, error.name, error.error),
));
}
writeln!(
fmt,
"\n === ERRORS ===\n ------------------------------------------------------------------------------"
)?;
writeln!(fmt, " {:<11} | Error", "Count")?;
writeln!(
fmt,
" ------------------------------------------------------------------------------"
)?;
for (occurrences, error) in errors.iter().sorted().rev() {
writeln!(fmt, " {:<12} {}", format_number(*occurrences), error)?;
}
writeln!(
fmt,
" ------------------------------------------------------------------------------"
)?;
Ok(())
}
fn get_seconds_minutes_hours(
&self,
start: &chrono::DateTime<chrono::Utc>,
end: &chrono::DateTime<chrono::Utc>,
) -> (i64, i64, i64) {
let duration = end.timestamp() - start.timestamp();
let seconds = duration % 60;
let minutes = (duration / 60) % 60;
let hours = duration / 60 / 60;
(seconds, minutes, hours)
}
pub(crate) fn fmt_overview(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
if !self.final_metrics || self.history.is_empty() {
return Ok(());
}
writeln!(
fmt,
"\n === OVERVIEW ===\n ------------------------------------------------------------------------------"
)?;
writeln!(
fmt,
" {:<12} {:<21} {:<19} {:<10} Users",
"Action", "Started", "Stopped", "Elapsed",
)?;
writeln!(
fmt,
" ------------------------------------------------------------------------------"
)?;
for step in self.history.windows(2) {
let (seconds, minutes, hours) =
self.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")
.to_string();
let stopped = Local
.timestamp_opt(step[1].timestamp.timestamp(), 0)
.unwrap()
.format("%Y-%m-%d %H:%M:%S")
.to_string();
match &step[0].action {
TestPlanStepAction::Maintaining => {
writeln!(
fmt,
" {:<12} {} - {} ({:02}:{:02}:{:02}, {})",
format!("{:?}:", step[0].action),
started,
stopped,
hours,
minutes,
seconds,
step[0].users,
)?;
}
TestPlanStepAction::Increasing => {
writeln!(
fmt,
" {:<12} {} - {} ({:02}:{:02}:{:02}, {} -> {})",
format!("{:?}:", step[0].action),
started,
stopped,
hours,
minutes,
seconds,
step[0].users,
step[1].users,
)?;
}
TestPlanStepAction::Decreasing | TestPlanStepAction::Canceling => {
writeln!(
fmt,
" {:<12} {} - {} ({:02}:{:02}:{:02}, {} <- {})",
format!("{:?}:", step[0].action),
started,
stopped,
hours,
minutes,
seconds,
step[1].users,
step[0].users,
)?;
}
TestPlanStepAction::Finished => {
unreachable!("there shouldn't be a step after finished");
}
}
}
match self.hosts.len() {
0 => {
writeln!(fmt, "\n Target host: undefined")?;
}
1 => {
for host in &self.hosts {
writeln!(fmt, "\n Target host: {}", host)?;
}
}
_ => {
writeln!(fmt, "\n Target hosts: ")?;
for host in &self.hosts {
writeln!(fmt, " - {}", host,)?;
}
}
}
writeln!(
fmt,
" {} v{}",
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_VERSION"),
)?;
if self.hosts.len() == 1 {}
writeln!(
fmt,
" ------------------------------------------------------------------------------"
)?;
Ok(())
}
}
impl Serialize for GooseMetrics {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut s = serializer.serialize_struct("GooseMetrics", 10)?;
s.serialize_field("hash", &self.hash)?;
s.serialize_field("duration", &self.duration)?;
s.serialize_field("maximum_users", &self.maximum_users)?;
s.serialize_field("total_users", &self.total_users)?;
s.serialize_field("requests", &self.requests)?;
s.serialize_field("transactions", &self.transactions)?;
s.serialize_field("errors", &self.errors)?;
s.serialize_field("final_metrics", &self.final_metrics)?;
s.serialize_field("display_status_codes", &self.display_status_codes)?;
s.serialize_field("display_metrics", &self.display_metrics)?;
s.end()
}
}
impl fmt::Display for GooseMetrics {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
self.fmt_scenarios(fmt)?;
self.fmt_scenario_times(fmt)?;
self.fmt_transactions(fmt)?;
self.fmt_transaction_times(fmt)?;
self.fmt_requests(fmt)?;
self.fmt_response_times(fmt)?;
self.fmt_percentiles(fmt)?;
self.fmt_status_codes(fmt)?;
self.fmt_errors(fmt)?;
self.fmt_overview(fmt)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GooseErrorMetric {
pub elapsed: u64,
pub raw: GooseRawRequest,
pub name: String,
pub final_url: String,
pub redirected: bool,
pub response_time: u64,
pub status_code: u16,
pub user: usize,
pub error: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
pub struct GooseErrorMetricAggregate {
pub method: GooseMethod,
pub name: String,
pub error: String,
pub occurrences: usize,
}
impl GooseErrorMetricAggregate {
pub(crate) fn new(method: GooseMethod, name: String, error: String) -> Self {
GooseErrorMetricAggregate {
method,
name,
error,
occurrences: 0,
}
}
}
impl GooseAttack {
pub(crate) async fn sync_metrics(
&mut self,
goose_attack_run_state: &mut GooseAttackRunState,
flush: bool,
) -> Result<(), GooseError> {
if !self.configuration.no_metrics {
if let Some(running_metrics) = self.configuration.running_metrics {
if util::timer_expired(
goose_attack_run_state.running_metrics_timer,
running_metrics,
) {
goose_attack_run_state.running_metrics_timer = std::time::Instant::now();
goose_attack_run_state.display_running_metrics = true;
}
};
self.receive_metrics(goose_attack_run_state, flush).await?;
}
if goose_attack_run_state.display_running_metrics {
goose_attack_run_state.display_running_metrics = false;
self.update_duration();
self.metrics.print_running();
}
Ok(())
}
pub(crate) async fn reset_metrics(
&mut self,
goose_attack_run_state: &mut GooseAttackRunState,
) -> Result<(), GooseError> {
if !goose_attack_run_state.all_users_spawned {
self.sync_metrics(goose_attack_run_state, true).await?;
goose_attack_run_state.all_users_spawned = true;
if self.configuration.test_plan.is_none() && self.configuration.iterations == 0 {
let users = self.configuration.users.unwrap();
if !self.configuration.no_reset_metrics {
self.update_duration();
self.metrics.print_running();
goose_attack_run_state.running_metrics_timer = std::time::Instant::now();
if self.metrics.display_metrics {
if goose_attack_run_state.active_users < users {
println!(
"{} of {} users hatched, timer expired, resetting metrics (disable with --no-reset-metrics).\n", goose_attack_run_state.active_users, users
);
} else {
println!(
"All {} users hatched, resetting metrics (disable with --no-reset-metrics).\n", users
);
}
}
self.metrics.requests = HashMap::new();
self.metrics
.initialize_scenario_metrics(&self.scenarios, &self.configuration);
self.metrics.initialize_transaction_metrics(
&self.scenarios,
&self.configuration,
&self.defaults,
)?;
self.started = Some(std::time::Instant::now());
} else if goose_attack_run_state.active_users < users {
println!(
"{} of {} users hatched, timer expired.\n",
goose_attack_run_state.active_users, users
);
} else {
println!(
"All {} users hatched.\n",
goose_attack_run_state.active_users
);
}
} else {
println!("{} users hatched.", goose_attack_run_state.active_users);
}
}
Ok(())
}
async fn record_request_metric(&mut self, request_metric: &GooseRequestMetric) {
let key = format!("{} {}", request_metric.raw.method, request_metric.name);
let mut merge_request = match self.metrics.requests.get(&key) {
Some(m) => m.clone(),
None => GooseRequestMetricAggregate::new(
&request_metric.name,
request_metric.raw.method.clone(),
0,
),
};
if request_metric.update {
if request_metric.success {
merge_request.success_count += 1;
merge_request.fail_count -= 1;
} else {
merge_request.success_count -= 1;
merge_request.fail_count += 1;
}
}
else {
merge_request.record_time(
request_metric.response_time,
request_metric.coordinated_omission_elapsed > 0,
);
if !self.configuration.no_status_codes {
merge_request.set_status_code(request_metric.status_code);
}
if request_metric.success {
merge_request.success_count += 1;
} else {
merge_request.fail_count += 1;
}
}
self.metrics.requests.insert(key, merge_request);
}
pub(crate) async fn receive_metrics(
&mut self,
goose_attack_run_state: &mut GooseAttackRunState,
flush: bool,
) -> Result<bool, GooseError> {
let mut received_message = false;
let mut message = goose_attack_run_state.metrics_rx.try_recv();
let receive_timeout = 400;
let receive_started = std::time::Instant::now();
while message.is_ok() {
received_message = true;
match message.unwrap() {
GooseMetric::Request(request_metric) => {
if !request_metric.error.is_empty() {
self.record_error(&request_metric, goose_attack_run_state);
}
if request_metric.coordinated_omission_elapsed > 0
&& request_metric.user_cadence > 0
{
let mut co_metric = request_metric.clone();
let mut response_time = request_metric.coordinated_omission_elapsed as i64
- request_metric.user_cadence as i64
- request_metric.response_time as i64;
loop {
if response_time > request_metric.response_time as i64 {
co_metric.response_time = response_time as u64;
self.record_request_metric(&co_metric).await;
response_time -= request_metric.user_cadence as i64;
} else {
break;
}
}
} else {
self.record_request_metric(&request_metric).await;
if !self.configuration.report_file.is_empty() {
let seconds_since_start = (request_metric.elapsed / 1000) as usize;
let key =
format!("{} {}", request_metric.raw.method, request_metric.name);
self.graph_data
.record_requests_per_second(&key, seconds_since_start);
self.graph_data.record_average_response_time_per_second(
key.clone(),
seconds_since_start,
request_metric.response_time,
);
if !request_metric.success {
self.graph_data
.record_errors_per_second(&key, seconds_since_start);
}
}
}
}
GooseMetric::Transaction(raw_transaction) => {
self.metrics.transactions[raw_transaction.scenario_index]
[raw_transaction.transaction_index]
.set_time(raw_transaction.run_time, raw_transaction.success);
if !self.configuration.report_file.is_empty() {
self.graph_data.record_transactions_per_second(
(raw_transaction.elapsed / 1000) as usize,
);
}
}
GooseMetric::Scenario(raw_scenario) => {
self.metrics.scenarios[raw_scenario.index]
.update(raw_scenario.run_time, raw_scenario.user);
if !self.configuration.report_file.is_empty() {
self.graph_data
.record_scenarios_per_second((raw_scenario.elapsed / 1000) as usize);
}
}
}
if !flush && util::ms_timer_expired(receive_started, receive_timeout) {
break;
}
message = goose_attack_run_state.metrics_rx.try_recv();
}
Ok(received_message)
}
pub(crate) fn record_error(
&mut self,
raw_request: &GooseRequestMetric,
goose_attack_run_state: &mut GooseAttackRunState,
) {
if !self.configuration.error_log.is_empty() {
if let Some(logger) = goose_attack_run_state.all_threads_logger_tx.as_ref() {
if let Err(e) = logger.send(Some(GooseLog::Error(GooseErrorMetric {
elapsed: raw_request.elapsed,
raw: raw_request.raw.clone(),
name: raw_request.name.clone(),
final_url: raw_request.final_url.clone(),
redirected: raw_request.redirected,
response_time: raw_request.response_time,
status_code: raw_request.status_code,
user: raw_request.user,
error: raw_request.error.clone(),
}))) {
if let flume::SendError(Some(ref message)) = e {
info!("Failed to write to error log (receiver dropped?): flume::SendError({:?})", message);
} else {
info!("Failed to write to error log: (receiver dropped?) {:?}", e);
}
}
}
}
if self.configuration.no_error_summary {
return;
}
let error_string = format!(
"{}.{}.{}",
raw_request.error, raw_request.raw.method, raw_request.name
);
let mut error_metrics = match self.metrics.errors.get(&error_string) {
Some(m) => m.clone(),
None => GooseErrorMetricAggregate::new(
raw_request.raw.method.clone(),
raw_request.name.to_string(),
raw_request.error.to_string(),
),
};
error_metrics.occurrences += 1;
self.metrics.errors.insert(error_string, error_metrics);
}
pub(crate) fn update_duration(&mut self) {
self.metrics.duration = if self.started.is_some() {
self.started.unwrap().elapsed().as_secs_f32().round() as usize
} else {
0
};
}
pub(crate) async fn write_html_report(&mut self) -> Result<(), GooseError> {
let report_file = match self.prepare_report_file().await {
Ok(f) => f,
Err(e) => {
return Err(GooseError::InvalidOption {
option: "--report-file".to_string(),
value: self.get_report_file_path().unwrap(),
detail: format!("Failed to create report file: {}", e),
})
}
};
if let Some(mut report_file) = report_file {
let test_start_time = self.metrics.history.first().unwrap().timestamp;
let users = self.metrics.maximum_users.to_string();
let mut steps_overview = String::new();
for step in self.metrics.history.windows(2) {
let (seconds, minutes, hours) = self
.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");
match &step[0].action {
TestPlanStepAction::Maintaining => {
let _ = write!(steps_overview,
"<tr><td>{:?}</td><td>{}</td><td>{}</td><td>{:02}:{:02}:{:02}</td><td>{}</td></tr>",
step[0].action,
started,
stopped,
hours,
minutes,
seconds,
step[0].users,
);
}
TestPlanStepAction::Increasing => {
let _ = write!(steps_overview,
"<tr><td>{:?}</td><td>{}</td><td>{}</td><td>{:02}:{:02}:{:02}</td><td>{} → {}</td></tr>",
step[0].action,
started,
stopped,
hours,
minutes,
seconds,
step[0].users,
step[1].users,
);
}
TestPlanStepAction::Decreasing | TestPlanStepAction::Canceling => {
let _ = write!(steps_overview,
"<tr><td>{:?}</td><td>{}</td><td>{}</td><td>{:02}:{:02}:{:02}</td><td>{} ← {}</td></tr>",
step[0].action,
started,
stopped,
hours,
minutes,
seconds,
step[1].users,
step[0].users,
);
}
TestPlanStepAction::Finished => {
unreachable!("there shouldn't be a step after finished");
}
}
}
let hosts = &self.metrics.hosts.clone().into_iter().join(", ");
let mut raw_request_metrics = Vec::new();
let mut co_request_metrics = Vec::new();
let mut raw_response_metrics = Vec::new();
let mut co_response_metrics = Vec::new();
let mut raw_aggregate_total_count = 0;
let mut co_aggregate_total_count = 0;
let mut raw_aggregate_fail_count = 0;
let mut raw_aggregate_response_time_counter: usize = 0;
let mut raw_aggregate_response_time_minimum: usize = 0;
let mut raw_aggregate_response_time_maximum: usize = 0;
let mut raw_aggregate_response_times: BTreeMap<usize, usize> = BTreeMap::new();
let mut co_aggregate_response_time_counter: usize = 0;
let mut co_aggregate_response_time_maximum: usize = 0;
let mut co_aggregate_response_times: BTreeMap<usize, usize> = BTreeMap::new();
let mut co_data = false;
for (request_key, request) in self.metrics.requests.iter().sorted() {
if !co_data && request.coordinated_omission_data.is_some() {
co_data = true;
}
let method = format!("{}", request.method);
let name = request_key
.strip_prefix(&format!("{} ", request.method))
.unwrap()
.to_string();
let total_request_count = request.success_count + request.fail_count;
let (requests_per_second, failures_per_second) = per_second_calculations(
self.metrics.duration,
total_request_count,
request.fail_count,
);
raw_request_metrics.push(report::RequestMetric {
method: method.to_string(),
name: name.to_string(),
number_of_requests: total_request_count,
number_of_failures: request.fail_count,
response_time_average: format!(
"{:.2}",
request.raw_data.total_time as f32 / request.raw_data.counter as f32
),
response_time_minimum: request.raw_data.minimum_time,
response_time_maximum: request.raw_data.maximum_time,
requests_per_second: format!("{:.2}", requests_per_second),
failures_per_second: format!("{:.2}", failures_per_second),
});
raw_response_metrics.push(report::get_response_metric(
&method,
&name,
&request.raw_data.times,
request.raw_data.counter,
request.raw_data.minimum_time,
request.raw_data.maximum_time,
));
raw_aggregate_total_count += total_request_count;
raw_aggregate_fail_count += request.fail_count;
raw_aggregate_response_time_counter += request.raw_data.total_time;
raw_aggregate_response_time_minimum = update_min_time(
raw_aggregate_response_time_minimum,
request.raw_data.minimum_time,
);
raw_aggregate_response_time_maximum = update_max_time(
raw_aggregate_response_time_maximum,
request.raw_data.maximum_time,
);
raw_aggregate_response_times =
merge_times(raw_aggregate_response_times, request.raw_data.times.clone());
}
let (raw_aggregate_requests_per_second, raw_aggregate_failures_per_second) =
per_second_calculations(
self.metrics.duration,
raw_aggregate_total_count,
raw_aggregate_fail_count,
);
raw_request_metrics.push(report::RequestMetric {
method: "".to_string(),
name: "Aggregated".to_string(),
number_of_requests: raw_aggregate_total_count,
number_of_failures: raw_aggregate_fail_count,
response_time_average: format!(
"{:.2}",
raw_aggregate_response_time_counter as f32 / raw_aggregate_total_count as f32
),
response_time_minimum: raw_aggregate_response_time_minimum,
response_time_maximum: raw_aggregate_response_time_maximum,
requests_per_second: format!("{:.2}", raw_aggregate_requests_per_second),
failures_per_second: format!("{:.2}", raw_aggregate_failures_per_second),
});
raw_response_metrics.push(report::get_response_metric(
"",
"Aggregated",
&raw_aggregate_response_times,
raw_aggregate_total_count,
raw_aggregate_response_time_minimum,
raw_aggregate_response_time_maximum,
));
let mut raw_requests_rows = Vec::new();
for metric in raw_request_metrics {
raw_requests_rows.push(report::raw_request_metrics_row(metric));
}
let mut raw_responses_rows = Vec::new();
for metric in raw_response_metrics {
raw_responses_rows.push(report::response_metrics_row(metric));
}
let co_requests_template: String;
let co_responses_template: String;
if co_data {
for (request_key, request) in self.metrics.requests.iter().sorted() {
if let Some(coordinated_omission_data) =
request.coordinated_omission_data.as_ref()
{
let method = format!("{}", request.method);
let name = request_key
.strip_prefix(&format!("{} ", request.method))
.unwrap()
.to_string();
let raw_average =
request.raw_data.total_time as f32 / request.raw_data.counter as f32;
let co_average = coordinated_omission_data.total_time as f32
/ coordinated_omission_data.counter as f32;
co_request_metrics.push(report::CORequestMetric {
method: method.to_string(),
name: name.to_string(),
response_time_average: format!("{:.2}", co_average),
response_time_standard_deviation: format!(
"{:.2}",
util::standard_deviation(raw_average, co_average)
),
response_time_maximum: coordinated_omission_data.maximum_time,
});
co_response_metrics.push(report::get_response_metric(
&method,
&name,
&coordinated_omission_data.times,
coordinated_omission_data.counter,
coordinated_omission_data.minimum_time,
coordinated_omission_data.maximum_time,
));
co_aggregate_response_time_counter += coordinated_omission_data.total_time;
co_aggregate_response_time_maximum = update_max_time(
co_aggregate_response_time_maximum,
coordinated_omission_data.maximum_time,
);
co_aggregate_response_times = merge_times(
co_aggregate_response_times,
coordinated_omission_data.times.clone(),
);
}
let total_request_count = request.success_count + request.fail_count;
co_aggregate_total_count += total_request_count;
}
let co_average =
co_aggregate_response_time_counter as f32 / co_aggregate_total_count as f32;
let raw_average =
raw_aggregate_response_time_counter as f32 / raw_aggregate_total_count as f32;
co_request_metrics.push(report::CORequestMetric {
method: "".to_string(),
name: "Aggregated".to_string(),
response_time_average: format!(
"{:.2}",
co_aggregate_response_time_counter as f32 / co_aggregate_total_count as f32
),
response_time_standard_deviation: format!(
"{:.2}",
util::standard_deviation(raw_average, co_average),
),
response_time_maximum: co_aggregate_response_time_maximum,
});
co_response_metrics.push(report::get_response_metric(
"",
"Aggregated",
&co_aggregate_response_times,
co_aggregate_total_count,
raw_aggregate_response_time_minimum,
co_aggregate_response_time_maximum,
));
let mut co_request_rows = Vec::new();
for metric in co_request_metrics {
co_request_rows.push(report::coordinated_omission_request_metrics_row(metric));
}
co_requests_template = report::coordinated_omission_request_metrics_template(
&co_request_rows.join("\n"),
);
let mut co_response_rows = Vec::new();
for metric in co_response_metrics {
co_response_rows
.push(report::coordinated_omission_response_metrics_row(metric));
}
co_responses_template = report::coordinated_omission_response_metrics_template(
&co_response_rows.join("\n"),
);
} else {
co_requests_template = "".to_string();
co_responses_template = "".to_string();
}
let transactions_template: String;
if !self.configuration.no_transaction_metrics {
let mut transaction_metrics = Vec::new();
let mut aggregate_total_count = 0;
let mut aggregate_fail_count = 0;
let mut aggregate_transaction_time_counter: usize = 0;
let mut aggregate_transaction_time_minimum: usize = 0;
let mut aggregate_transaction_time_maximum: usize = 0;
let mut aggregate_transaction_times: BTreeMap<usize, usize> = BTreeMap::new();
for (scenario_counter, scenario) in self.metrics.transactions.iter().enumerate() {
for (transaction_counter, transaction) in scenario.iter().enumerate() {
if transaction_counter == 0 {
transaction_metrics.push(report::TransactionMetric {
is_scenario: true,
transaction: "".to_string(),
name: transaction.scenario_name.to_string(),
number_of_requests: 0,
number_of_failures: 0,
response_time_average: "".to_string(),
response_time_minimum: 0,
response_time_maximum: 0,
requests_per_second: "".to_string(),
failures_per_second: "".to_string(),
});
}
let total_run_count = transaction.success_count + transaction.fail_count;
let (requests_per_second, failures_per_second) = per_second_calculations(
self.metrics.duration,
total_run_count,
transaction.fail_count,
);
let average = match transaction.counter {
0 => 0.00,
_ => transaction.total_time as f32 / transaction.counter as f32,
};
transaction_metrics.push(report::TransactionMetric {
is_scenario: false,
transaction: format!("{}.{}", scenario_counter, transaction_counter),
name: transaction.transaction_name.to_string(),
number_of_requests: total_run_count,
number_of_failures: transaction.fail_count,
response_time_average: format!("{:.2}", average),
response_time_minimum: transaction.min_time,
response_time_maximum: transaction.max_time,
requests_per_second: format!("{:.2}", requests_per_second),
failures_per_second: format!("{:.2}", failures_per_second),
});
aggregate_total_count += total_run_count;
aggregate_fail_count += transaction.fail_count;
aggregate_transaction_times =
merge_times(aggregate_transaction_times, transaction.times.clone());
aggregate_transaction_time_counter += &transaction.counter;
aggregate_transaction_time_minimum = update_min_time(
aggregate_transaction_time_minimum,
transaction.min_time,
);
aggregate_transaction_time_maximum = update_max_time(
aggregate_transaction_time_maximum,
transaction.max_time,
);
}
}
let (aggregate_requests_per_second, aggregate_failures_per_second) =
per_second_calculations(
self.metrics.duration,
aggregate_total_count,
aggregate_fail_count,
);
transaction_metrics.push(report::TransactionMetric {
is_scenario: false,
transaction: "".to_string(),
name: "Aggregated".to_string(),
number_of_requests: aggregate_total_count,
number_of_failures: aggregate_fail_count,
response_time_average: format!(
"{:.2}",
raw_aggregate_response_time_counter as f32 / aggregate_total_count as f32
),
response_time_minimum: aggregate_transaction_time_minimum,
response_time_maximum: aggregate_transaction_time_maximum,
requests_per_second: format!("{:.2}", aggregate_requests_per_second),
failures_per_second: format!("{:.2}", aggregate_failures_per_second),
});
let mut transactions_rows = Vec::new();
for metric in transaction_metrics {
transactions_rows.push(report::transaction_metrics_row(metric));
}
transactions_template = report::transaction_metrics_template(
&transactions_rows.join("\n"),
self.graph_data
.get_transactions_per_second_graph(!self.configuration.no_granular_report)
.get_markup(&self.metrics.history, test_start_time),
);
} else {
transactions_template = "".to_string();
}
let scenarios_template: String;
if !self.configuration.no_scenario_metrics {
let mut scenario_metrics = Vec::new();
let mut aggregate_users = 0;
let mut aggregate_count = 0;
let mut aggregate_scenario_time_counter: usize = 0;
let mut aggregate_scenario_time_minimum: usize = 0;
let mut aggregate_scenario_time_maximum: usize = 0;
let mut aggregate_scenario_times: BTreeMap<usize, usize> = BTreeMap::new();
let mut aggregate_iterations = 0.0;
let mut aggregate_response_time_counter = 0.0;
for scenario in &self.metrics.scenarios {
let (count_per_second, _failures_per_second) =
per_second_calculations(self.metrics.duration, scenario.counter, 0);
let average = match scenario.counter {
0 => 0.00,
_ => scenario.total_time as f32 / scenario.counter as f32,
};
let iterations = scenario.counter as f32 / scenario.users.len() as f32;
scenario_metrics.push(report::ScenarioMetric {
name: scenario.name.to_string(),
users: scenario.users.len(),
count: scenario.counter,
response_time_average: format!("{:.2}", average),
response_time_minimum: scenario.min_time,
response_time_maximum: scenario.max_time,
count_per_second: format!("{:.2}", count_per_second),
iterations: format!("{:.2}", iterations),
});
aggregate_users += scenario.users.len();
aggregate_count += scenario.counter;
aggregate_scenario_times =
merge_times(aggregate_scenario_times, scenario.times.clone());
aggregate_scenario_time_counter += &scenario.counter;
aggregate_scenario_time_minimum =
update_min_time(aggregate_scenario_time_minimum, scenario.min_time);
aggregate_scenario_time_maximum =
update_max_time(aggregate_scenario_time_maximum, scenario.max_time);
aggregate_iterations += iterations;
aggregate_response_time_counter += scenario.total_time as f32;
}
let (aggregate_count_per_second, _aggregate_failures_per_second) =
per_second_calculations(self.metrics.duration, aggregate_count, 0);
scenario_metrics.push(report::ScenarioMetric {
name: "Aggregated".to_string(),
users: aggregate_users,
count: aggregate_count,
response_time_average: format!(
"{:.2}",
aggregate_response_time_counter as f32 / aggregate_count as f32
),
response_time_minimum: aggregate_scenario_time_minimum,
response_time_maximum: aggregate_scenario_time_maximum,
count_per_second: format!("{:.2}", aggregate_count_per_second),
iterations: format!("{:.2}", aggregate_iterations),
});
let mut scenarios_rows = Vec::new();
for metric in scenario_metrics {
scenarios_rows.push(report::scenario_metrics_row(metric));
}
scenarios_template = report::scenario_metrics_template(
&scenarios_rows.join("\n"),
self.graph_data
.get_scenarios_per_second_graph(!self.configuration.no_granular_report)
.get_markup(&self.metrics.history, test_start_time),
);
} else {
scenarios_template = "".to_string();
}
let errors_template: String = if !self.metrics.errors.is_empty() {
let mut error_rows = Vec::new();
for error in self.metrics.errors.values() {
error_rows.push(report::error_row(error));
}
report::errors_template(
&error_rows.join("\n"),
self.graph_data
.get_errors_per_second_graph(!self.configuration.no_granular_report)
.get_markup(&self.metrics.history, test_start_time),
)
} else {
"".to_string()
};
let status_code_template: String = if !self.configuration.no_status_codes {
let mut status_code_metrics = Vec::new();
let mut aggregated_status_code_counts: HashMap<u16, usize> = HashMap::new();
for (request_key, request) in self.metrics.requests.iter().sorted() {
let method = format!("{}", request.method);
let name = request_key
.strip_prefix(&format!("{} ", request.method))
.unwrap()
.to_string();
let codes = prepare_status_codes(
&request.status_code_counts,
&mut Some(&mut aggregated_status_code_counts),
);
status_code_metrics.push(report::StatusCodeMetric {
method,
name,
status_codes: codes,
});
}
let aggregated_codes =
prepare_status_codes(&aggregated_status_code_counts, &mut None);
status_code_metrics.push(report::StatusCodeMetric {
method: "".to_string(),
name: "Aggregated".to_string(),
status_codes: aggregated_codes,
});
let mut status_code_rows = Vec::new();
for metric in status_code_metrics {
status_code_rows.push(report::status_code_metrics_row(metric));
}
report::status_code_metrics_template(&status_code_rows.join("\n"))
} else {
"".to_string()
};
let report = report::build_report(
&users,
&steps_overview,
hosts,
report::GooseReportTemplates {
raw_requests_template: &raw_requests_rows.join("\n"),
raw_responses_template: &raw_responses_rows.join("\n"),
co_requests_template: &co_requests_template,
co_responses_template: &co_responses_template,
transactions_template: &transactions_template,
scenarios_template: &scenarios_template,
status_codes_template: &status_code_template,
errors_template: &errors_template,
graph_rps_template: &self
.graph_data
.get_requests_per_second_graph(!self.configuration.no_granular_report)
.get_markup(&self.metrics.history, test_start_time),
graph_average_response_time_template: &self
.graph_data
.get_average_response_time_graph(!self.configuration.no_granular_report)
.get_markup(&self.metrics.history, test_start_time),
graph_users_per_second: &self
.graph_data
.get_active_users_graph(!self.configuration.no_granular_report)
.get_markup(&self.metrics.history, test_start_time),
},
);
if let Err(e) = report_file.write_all(report.as_ref()).await {
return Err(GooseError::InvalidOption {
option: "--report-file".to_string(),
value: self.get_report_file_path().unwrap(),
detail: format!("Failed to create report file: {}", e),
});
};
report_file.flush().await?;
info!(
"html report file written to: {}",
self.get_report_file_path().unwrap()
);
}
Ok(())
}
}
pub(crate) fn per_second_calculations(duration: usize, total: usize, fail: usize) -> (f32, f32) {
let requests_per_second;
let fails_per_second;
if duration == 0 {
requests_per_second = 0.0;
fails_per_second = 0.0;
} else {
requests_per_second = total as f32 / duration as f32;
fails_per_second = fail as f32 / duration as f32;
}
(requests_per_second, fails_per_second)
}
fn determine_precision(value: f32) -> usize {
if value < 1000.0 {
2
} else {
0
}
}
pub(crate) fn format_number(number: usize) -> String {
(number).to_formatted_string(&Locale::en)
}
pub(crate) fn merge_times(
mut global_response_times: BTreeMap<usize, usize>,
local_response_times: BTreeMap<usize, usize>,
) -> BTreeMap<usize, usize> {
for (response_time, count) in &local_response_times {
let counter = match global_response_times.get(response_time) {
Some(c) => *c + count,
None => *count,
};
global_response_times.insert(*response_time, counter);
}
global_response_times
}
pub(crate) fn update_min_time(mut global_min: usize, min: usize) -> usize {
if global_min == 0 || (min > 0 && min < global_min) {
global_min = min;
}
global_min
}
pub(crate) fn update_max_time(mut global_max: usize, max: usize) -> usize {
if global_max < max {
global_max = max;
}
global_max
}
pub(crate) fn calculate_response_time_percentile(
response_times: &BTreeMap<usize, usize>,
total_requests: usize,
min: usize,
max: usize,
percent: f32,
) -> String {
let percentile_request = (total_requests as f32 * percent).round() as usize;
debug!(
"percentile: {}, request {} of total {}",
percent, percentile_request, total_requests
);
let mut total_count: usize = 0;
for (value, counter) in response_times {
total_count += counter;
if total_count >= percentile_request {
if *value < min {
return format_number(min);
} else if *value > max {
return format_number(max);
} else {
return format_number(*value);
}
}
}
format_number(0)
}
pub(crate) fn prepare_status_codes(
status_code_counts: &HashMap<u16, usize>,
aggregate_counts: &mut Option<&mut HashMap<u16, usize>>,
) -> String {
let mut codes: String = "".to_string();
for (status_code, count) in status_code_counts {
if codes.is_empty() {
codes = format!(
"{} [{}]",
count.to_formatted_string(&Locale::en),
status_code
);
} else {
codes = format!(
"{}, {} [{}]",
codes.clone(),
count.to_formatted_string(&Locale::en),
status_code
);
}
if let Some(aggregate_status_code_counts) = aggregate_counts.as_mut() {
let new_count = if let Some(existing_status_code_count) =
aggregate_status_code_counts.get(status_code)
{
*existing_status_code_count + *count
} else {
*count
};
aggregate_status_code_counts.insert(*status_code, new_count);
}
}
codes
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn max_response_time() {
let mut max_response_time = 99;
max_response_time = update_max_time(max_response_time, 101);
assert_eq!(max_response_time, 101);
max_response_time = update_max_time(max_response_time, 1);
assert_eq!(max_response_time, 101);
}
#[test]
fn min_response_time() {
let mut min_response_time = 11;
min_response_time = update_min_time(min_response_time, 9);
assert_eq!(min_response_time, 9);
min_response_time = update_min_time(min_response_time, 22);
assert_eq!(min_response_time, 9);
min_response_time = update_min_time(min_response_time, 0);
assert_eq!(min_response_time, 9);
}
#[test]
fn response_time_merge() {
let mut global_response_times: BTreeMap<usize, usize> = BTreeMap::new();
let local_response_times: BTreeMap<usize, usize> = BTreeMap::new();
global_response_times = merge_times(global_response_times, local_response_times.clone());
assert_eq!(&global_response_times, &local_response_times);
}
#[test]
fn max_response_time_percentile() {
let mut response_times: BTreeMap<usize, usize> = BTreeMap::new();
response_times.insert(1, 1);
response_times.insert(2, 1);
response_times.insert(3, 1);
assert!(calculate_response_time_percentile(&response_times, 3, 1, 3, 0.5) == "2");
response_times.insert(3, 2);
assert!(calculate_response_time_percentile(&response_times, 4, 1, 3, 0.5) == "2");
assert!(calculate_response_time_percentile(&response_times, 4, 1, 3, 0.25) == "1");
assert!(calculate_response_time_percentile(&response_times, 4, 1, 3, 0.75) == "3");
assert!(calculate_response_time_percentile(&response_times, 4, 1, 3, 1.0) == "3");
assert!(calculate_response_time_percentile(&response_times, 4, 2, 3, 0.25) == "2");
assert!(calculate_response_time_percentile(&response_times, 4, 1, 2, 0.75) == "2");
response_times.insert(10, 25);
response_times.insert(20, 25);
response_times.insert(30, 25);
response_times.insert(50, 25);
response_times.insert(100, 10);
response_times.insert(200, 1);
assert!(calculate_response_time_percentile(&response_times, 115, 1, 200, 0.9) == "50");
assert!(calculate_response_time_percentile(&response_times, 115, 1, 200, 0.99) == "100");
assert!(calculate_response_time_percentile(&response_times, 115, 1, 200, 0.999) == "200");
}
#[test]
fn calculate_per_second() {
let mut duration = 0;
let mut total = 10;
let fail = 10;
let (requests_per_second, fails_per_second) =
per_second_calculations(duration, total, fail);
assert!(requests_per_second == 0.0);
assert!(fails_per_second == 0.0);
total = 100;
let (requests_per_second, fails_per_second) =
per_second_calculations(duration, total, fail);
assert!(requests_per_second == 0.0);
assert!(fails_per_second == 0.0);
duration = 10;
let (requests_per_second, fails_per_second) =
per_second_calculations(duration, total, fail);
assert!((requests_per_second - 10.0).abs() < f32::EPSILON);
assert!((fails_per_second - 1.0).abs() < f32::EPSILON);
}
#[test]
fn goose_raw_request() {
const PATH: &str = "http://127.0.0.1/";
let raw_request = GooseRawRequest::new(GooseMethod::Get, PATH, vec![], "");
let mut request_metric = GooseRequestMetric::new(raw_request, "/", 0, 0);
assert_eq!(request_metric.raw.method, GooseMethod::Get);
assert_eq!(request_metric.raw.url, PATH.to_string());
assert_eq!(request_metric.name, "/".to_string());
assert_eq!(request_metric.response_time, 0);
assert_eq!(request_metric.status_code, 0);
assert!(request_metric.success);
assert!(!request_metric.update);
let response_time = 123;
request_metric.set_response_time(response_time);
assert_eq!(request_metric.raw.method, GooseMethod::Get);
assert_eq!(request_metric.name, "/".to_string());
assert_eq!(request_metric.raw.url, PATH.to_string());
assert_eq!(request_metric.response_time, response_time as u64);
assert_eq!(request_metric.status_code, 0);
assert!(request_metric.success);
assert!(!request_metric.update);
let status_code = http::StatusCode::OK;
request_metric.set_status_code(Some(status_code));
assert_eq!(request_metric.raw.method, GooseMethod::Get);
assert_eq!(request_metric.name, "/".to_string());
assert_eq!(request_metric.raw.url, PATH.to_string());
assert_eq!(request_metric.response_time, response_time as u64);
assert_eq!(request_metric.status_code, 200);
assert!(request_metric.success);
assert!(!request_metric.update);
}
#[test]
fn goose_request() {
let mut request = GooseRequestMetricAggregate::new("/", GooseMethod::Get, 0);
assert_eq!(request.path, "/".to_string());
assert_eq!(request.method, GooseMethod::Get);
assert_eq!(request.raw_data.times.len(), 0);
assert_eq!(request.raw_data.minimum_time, 0);
assert_eq!(request.raw_data.maximum_time, 0);
assert_eq!(request.raw_data.total_time, 0);
assert_eq!(request.raw_data.counter, 0);
assert_eq!(request.status_code_counts.len(), 0);
assert_eq!(request.success_count, 0);
assert_eq!(request.fail_count, 0);
request.record_time(1, false);
assert_eq!(request.raw_data.times.len(), 1);
assert_eq!(request.raw_data.times[&1], 1);
assert_eq!(request.raw_data.minimum_time, 1);
assert_eq!(request.raw_data.maximum_time, 1);
assert_eq!(request.raw_data.total_time, 1);
assert_eq!(request.raw_data.counter, 1);
assert_eq!(request.path, "/".to_string());
assert_eq!(request.method, GooseMethod::Get);
assert_eq!(request.status_code_counts.len(), 0);
assert_eq!(request.success_count, 0);
assert_eq!(request.fail_count, 0);
request.record_time(10, false);
assert_eq!(request.raw_data.times.len(), 2);
assert_eq!(request.raw_data.times[&10], 1);
assert_eq!(request.raw_data.minimum_time, 1);
assert_eq!(request.raw_data.maximum_time, 10);
assert_eq!(request.raw_data.total_time, 11);
assert_eq!(request.raw_data.counter, 2);
assert_eq!(request.path, "/".to_string());
assert_eq!(request.method, GooseMethod::Get);
assert_eq!(request.status_code_counts.len(), 0);
assert_eq!(request.success_count, 0);
assert_eq!(request.fail_count, 0);
request.record_time(10, false);
assert_eq!(request.raw_data.times.len(), 2);
assert_eq!(request.raw_data.times[&10], 2);
assert_eq!(request.raw_data.minimum_time, 1);
assert_eq!(request.raw_data.maximum_time, 10);
assert_eq!(request.raw_data.total_time, 21);
assert_eq!(request.raw_data.counter, 3);
request.record_time(101, false);
assert_eq!(request.raw_data.times.len(), 3);
assert_eq!(request.raw_data.times[&100], 1);
assert_eq!(request.raw_data.minimum_time, 1);
assert_eq!(request.raw_data.maximum_time, 101);
assert_eq!(request.raw_data.total_time, 122);
assert_eq!(request.raw_data.counter, 4);
request.record_time(102, false);
assert_eq!(request.raw_data.times.len(), 3);
assert_eq!(request.raw_data.times[&100], 2);
assert_eq!(request.raw_data.minimum_time, 1);
assert_eq!(request.raw_data.maximum_time, 102);
assert_eq!(request.raw_data.total_time, 224);
assert_eq!(request.raw_data.counter, 5);
request.record_time(155, false);
assert_eq!(request.raw_data.times.len(), 4);
assert_eq!(request.raw_data.times[&160], 1);
assert_eq!(request.raw_data.minimum_time, 1);
assert_eq!(request.raw_data.maximum_time, 155);
assert_eq!(request.raw_data.total_time, 379);
assert_eq!(request.raw_data.counter, 6);
request.record_time(2345, false);
assert_eq!(request.raw_data.times.len(), 5);
assert_eq!(request.raw_data.times[&2000], 1);
assert_eq!(request.raw_data.minimum_time, 1);
assert_eq!(request.raw_data.maximum_time, 2345);
assert_eq!(request.raw_data.total_time, 2724);
assert_eq!(request.raw_data.counter, 7);
request.record_time(987654321, false);
assert_eq!(request.raw_data.times.len(), 6);
assert_eq!(request.raw_data.times[&987654000], 1);
assert_eq!(request.raw_data.minimum_time, 1);
assert_eq!(request.raw_data.maximum_time, 987654321);
assert_eq!(request.raw_data.total_time, 987657045);
assert_eq!(request.raw_data.counter, 8);
request.set_status_code(200);
assert_eq!(request.status_code_counts.len(), 1);
assert_eq!(request.status_code_counts[&200], 1);
assert_eq!(request.success_count, 0);
assert_eq!(request.fail_count, 0);
assert_eq!(request.raw_data.times.len(), 6);
assert_eq!(request.raw_data.minimum_time, 1);
assert_eq!(request.raw_data.maximum_time, 987654321);
assert_eq!(request.raw_data.total_time, 987657045);
assert_eq!(request.raw_data.counter, 8);
request.set_status_code(200);
assert_eq!(request.status_code_counts.len(), 1);
assert_eq!(request.status_code_counts[&200], 2);
request.set_status_code(0);
assert_eq!(request.status_code_counts.len(), 2);
assert_eq!(request.status_code_counts[&0], 1);
request.set_status_code(500);
assert_eq!(request.status_code_counts.len(), 3);
assert_eq!(request.status_code_counts[&500], 1);
request.set_status_code(308);
assert_eq!(request.status_code_counts.len(), 4);
assert_eq!(request.status_code_counts[&308], 1);
request.set_status_code(200);
assert_eq!(request.status_code_counts.len(), 4);
assert_eq!(request.status_code_counts[&200], 3);
assert_eq!(request.success_count, 0);
assert_eq!(request.fail_count, 0);
assert_eq!(request.raw_data.times.len(), 6);
assert_eq!(request.raw_data.minimum_time, 1);
assert_eq!(request.raw_data.maximum_time, 987654321);
assert_eq!(request.raw_data.total_time, 987657045);
assert_eq!(request.raw_data.counter, 8);
}
}