mod common;
pub mod coordinated_omission;
pub(crate) use common::ReportData;
pub use coordinated_omission::{CadenceCalculator, CoMetricsSummary, CoordinatedOmissionMetrics};
use crate::config::GooseDefaults;
use crate::goose::{get_base_url, GooseMethod, Scenario};
use crate::logger::GooseLog;
use crate::metrics::common::ReportOptions;
use crate::report;
use crate::test_plan::{TestPlanHistory, TestPlanStepAction};
use crate::util;
use crate::{GooseAttack, GooseAttackRunState, GooseConfiguration, GooseError};
use chrono::prelude::*;
use itertools::Itertools;
use num_format::{Locale, ToFormattedString};
use regex::RegexSet;
use reqwest::StatusCode;
use serde::ser::SerializeStruct;
use serde::{Deserialize, Serialize, Serializer};
use std::cmp::Ordering;
use std::collections::{BTreeMap, HashMap, HashSet};
use std::ffi::OsStr;
use std::fmt::Write;
use std::io::BufWriter;
use std::path::PathBuf;
use std::str::FromStr;
use std::{f32, fmt};
use tokio::fs::File;
use tokio::io::AsyncWriteExt;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum GooseMetric {
Request(Box<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 TransactionDetail<'a> {
pub scenario_index: usize,
pub scenario_name: &'a str,
pub transaction_index: &'a str,
pub transaction_name: &'a str,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GooseRequestMetric {
pub elapsed: u64,
pub scenario_index: usize,
pub scenario_name: String,
pub transaction_index: String,
pub transaction_name: String,
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,
transaction_detail: TransactionDetail,
name: &str,
elapsed: u128,
user: usize,
) -> Self {
GooseRequestMetric {
elapsed: elapsed as u64,
scenario_index: transaction_detail.scenario_index,
scenario_name: transaction_detail.scenario_name.to_string(),
transaction_index: transaction_detail.transaction_index.to_string(),
transaction_name: transaction_detail.transaction_name.to_string(),
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!("[metrics]: 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!("[metrics]: got {status_code:?} counter: {c}");
*c + 1
}
None => {
debug!("[metrics]: no match for counter: {status_code}");
1
}
};
self.status_code_counts.insert(status_code, counter);
debug!("[metrics]: incremented {status_code} counter: {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 {rounded_time:?} counter: {c}");
*c + 1
}
None => {
debug!("no match for counter: {rounded_time}");
1
}
};
debug!("incremented {rounded_time} counter: {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 {rounded_time} counter: {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 {rounded_time} counter: {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 coordinated_omission_metrics: Option<CoordinatedOmissionMetrics>,
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(())
}
pub(crate) 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"),
)?;
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", 11)?;
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.serialize_field(
"coordinated_omission_metrics",
&self.coordinated_omission_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)?;
if let Some(co_metrics) = &self.coordinated_omission_metrics {
write!(fmt, "{co_metrics}")?;
}
self.fmt_overview(fmt)?;
Ok(())
}
}
#[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} users hatched, resetting metrics (disable with --no-reset-metrics).\n"
);
}
}
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
{
if let Some(co_metrics) = &mut self.metrics.coordinated_omission_metrics {
let synthetic_count = (request_metric.coordinated_omission_elapsed
as i64
- request_metric.user_cadence as i64
- request_metric.response_time as i64)
/ request_metric.user_cadence as i64;
if synthetic_count > 0 {
co_metrics.record_co_event(
std::time::Duration::from_millis(request_metric.user_cadence),
std::time::Duration::from_millis(
request_metric.coordinated_omission_elapsed,
),
synthetic_count as u32,
request_metric.user,
request_metric.scenario_name.clone(),
);
}
}
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 {
if let Some(co_metrics) = &mut self.metrics.coordinated_omission_metrics {
co_metrics.record_actual_request();
}
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
};
}
async fn process_reports(&self, write: bool) -> Result<(), GooseError> {
let create = |path: PathBuf| async move {
File::create(&path)
.await
.map_err(|err| GooseError::InvalidOption {
option: "--report-file".to_string(),
value: path.to_string_lossy().to_string(),
detail: format!("Failed to create report file: {err}"),
})
};
for report in &self.configuration.report_file {
let path = PathBuf::from(report);
match path.extension().map(OsStr::to_string_lossy).as_deref() {
Some("html" | "htm") => {
let file = create(path).await?;
if write {
self.write_html_report(file, report).await?;
}
}
Some("json") => {
let file = create(path).await?;
if write {
self.write_json_report(file).await?;
}
}
Some("md") => {
let file = create(path).await?;
if write {
self.write_markdown_report(file).await?;
}
}
None => {
return Err(GooseError::InvalidOption {
option: "--report-file".to_string(),
value: report.clone(),
detail: "Missing file extension for report".to_string(),
})
}
Some(ext) => {
return Err(GooseError::InvalidOption {
option: "--report-file".to_string(),
value: report.clone(),
detail: format!("Unknown report file type: {ext}"),
})
}
}
}
Ok(())
}
pub(crate) async fn create_reports(&self) -> Result<(), GooseError> {
self.process_reports(false).await
}
pub(crate) async fn write_reports(&self) -> Result<(), GooseError> {
self.process_reports(true).await
}
pub(crate) async fn write_json_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,
);
serde_json::to_writer_pretty(BufWriter::new(report_file.into_std().await), &data)?;
Ok(())
}
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)
}
pub(crate) async fn write_html_report(
&self,
mut report_file: File,
path: &str,
) -> Result<(), GooseError> {
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 ReportData {
raw_metrics: _,
raw_request_metrics,
raw_response_metrics,
co_request_metrics,
co_response_metrics,
scenario_metrics,
transaction_metrics,
errors,
status_code_metrics,
coordinated_omission_metrics: _,
} = 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,
);
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 = co_request_metrics
.map(|co_request_metrics| {
let co_request_rows = co_request_metrics
.into_iter()
.map(report::coordinated_omission_request_metrics_row)
.join("\n");
report::coordinated_omission_request_metrics_template(&co_request_rows)
})
.unwrap_or_default();
let co_responses_template = co_response_metrics
.map(|co_response_metrics| {
let co_response_rows = co_response_metrics
.into_iter()
.map(report::coordinated_omission_response_metrics_row)
.join("\n");
report::coordinated_omission_response_metrics_template(&co_response_rows)
})
.unwrap_or_default();
let co_metrics_template =
if let Some(co_metrics) = &self.metrics.coordinated_omission_metrics {
let co_summary = co_metrics.get_summary();
report::coordinated_omission_metrics_template(&co_summary)
} else {
String::new()
};
let scenarios_template = scenario_metrics
.map(|scenario_metric| {
let scenarios_rows = scenario_metric
.into_iter()
.map(report::scenario_metrics_row)
.join("\n");
report::scenario_metrics_template(
&scenarios_rows,
self.graph_data
.get_scenarios_per_second_graph(!self.configuration.no_granular_report)
.get_markup(&self.metrics.history, test_start_time),
)
})
.unwrap_or_default();
let transactions_template = transaction_metrics
.map(|transaction_metrics| {
let transactions_rows = transaction_metrics
.into_iter()
.map(report::transaction_metrics_row)
.join("\n");
report::transaction_metrics_template(
&transactions_rows,
self.graph_data
.get_transactions_per_second_graph(!self.configuration.no_granular_report)
.get_markup(&self.metrics.history, test_start_time),
)
})
.unwrap_or_default();
let errors_template = errors
.map(|errors| {
let error_rows = errors.into_iter().map(report::error_row).join("\n");
report::errors_template(
&error_rows,
self.graph_data
.get_errors_per_second_graph(!self.configuration.no_granular_report)
.get_markup(&self.metrics.history, test_start_time),
)
})
.unwrap_or_default();
let status_code_template = status_code_metrics
.map(|status_code_metrics| {
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"))
})
.unwrap_or_default();
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),
co_metrics_template: &co_metrics_template,
},
);
if let Err(e) = report_file.write_all(report.as_ref()).await {
return Err(GooseError::InvalidOption {
option: "--report-file".to_string(),
value: path.to_string(),
detail: format!("Failed to create report file: {e}"),
});
};
report_file.flush().await?;
info!("html report file written to: {path}");
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,
) -> usize {
let percentile_request = (total_requests as f32 * percent).round() as usize;
debug!("percentile: {percent}, request {percentile_request} of total {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 min;
} else if *value > max {
return max;
} else {
return *value;
}
}
}
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_eq!(
calculate_response_time_percentile(&response_times, 3, 1, 3, 0.5),
2
);
response_times.insert(3, 2);
assert_eq!(
calculate_response_time_percentile(&response_times, 4, 1, 3, 0.5),
2
);
assert_eq!(
calculate_response_time_percentile(&response_times, 4, 1, 3, 0.25),
1
);
assert_eq!(
calculate_response_time_percentile(&response_times, 4, 1, 3, 0.75),
3
);
assert_eq!(
calculate_response_time_percentile(&response_times, 4, 1, 3, 1.0),
3
);
assert_eq!(
calculate_response_time_percentile(&response_times, 4, 2, 3, 0.25),
2
);
assert_eq!(
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_eq!(
calculate_response_time_percentile(&response_times, 115, 1, 200, 0.9),
50
);
assert_eq!(
calculate_response_time_percentile(&response_times, 115, 1, 200, 0.99),
100
);
assert_eq!(
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,
TransactionDetail {
scenario_index: 0,
scenario_name: "LoadTestUser",
transaction_index: 5.to_string().as_str(),
transaction_name: "front page",
},
"/",
0,
0,
);
assert_eq!(request_metric.raw.method, GooseMethod::Get);
assert_eq!(request_metric.scenario_index, 0);
assert_eq!(request_metric.scenario_name, "LoadTestUser");
assert_eq!(request_metric.transaction_index, "5");
assert_eq!(request_metric.transaction_name, "front page");
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 = reqwest::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);
}
}