#[macro_use]
extern crate log;
pub mod goose;
pub mod logger;
#[cfg(feature = "gaggle")]
mod manager;
pub mod metrics;
pub mod prelude;
mod report;
mod throttle;
mod user;
mod util;
#[cfg(feature = "gaggle")]
mod worker;
use chrono::prelude::*;
use chrono::Duration;
use gumdrop::Options;
use itertools::Itertools;
use lazy_static::lazy_static;
#[cfg(feature = "gaggle")]
use nng::Socket;
use rand::seq::SliceRandom;
use serde::{Deserialize, Serialize};
use serde_json::json;
use simplelog::*;
use std::collections::hash_map::DefaultHasher;
use std::collections::{BTreeMap, HashMap};
use std::hash::{Hash, Hasher};
use std::path::PathBuf;
use std::sync::{
atomic::{AtomicBool, AtomicUsize, Ordering},
Arc,
};
use std::{fmt, io, time};
use tokio::fs::File;
use tokio::io::{AsyncWriteExt, BufWriter};
use tokio::runtime::Runtime;
use url::Url;
use crate::goose::{
GaggleUser, GooseDebug, GooseRawRequest, GooseRequest, GooseTask, GooseTaskSet, GooseUser,
GooseUserCommand,
};
use crate::metrics::{GooseErrorMetric, GooseMetric, GooseMetrics};
#[cfg(feature = "gaggle")]
use crate::worker::{register_shutdown_pipe_handler, GaggleMetrics};
const DEFAULT_PORT: &str = "5115";
lazy_static! {
static ref WORKER_ID: AtomicUsize = AtomicUsize::new(0);
}
type WeightedGooseTasks = Vec<Vec<(usize, String)>>;
type DebugLoggerHandle = Option<tokio::task::JoinHandle<()>>;
type DebugLoggerChannel = Option<flume::Sender<Option<GooseDebug>>>;
pub fn get_worker_id() -> usize {
WORKER_ID.load(Ordering::Relaxed)
}
#[cfg(not(feature = "gaggle"))]
#[derive(Debug, Clone)]
pub struct Socket {}
#[derive(Debug)]
pub enum GooseError {
Io(io::Error),
Reqwest(reqwest::Error),
FeatureNotEnabled { feature: String, detail: String },
InvalidHost {
host: String,
detail: String,
parse_error: url::ParseError,
},
InvalidOption {
option: String,
value: String,
detail: String,
},
InvalidWaitTime {
min_wait: usize,
max_wait: usize,
detail: String,
},
InvalidWeight { weight: usize, detail: String },
NoTaskSets { detail: String },
}
impl GooseError {
fn describe(&self) -> &str {
match *self {
GooseError::Io(_) => "io::Error",
GooseError::Reqwest(_) => "reqwest::Error",
GooseError::FeatureNotEnabled { .. } => "required compile-time feature not enabled",
GooseError::InvalidHost { .. } => "failed to parse hostname",
GooseError::InvalidOption { .. } => "invalid option or value specified",
GooseError::InvalidWaitTime { .. } => "invalid wait_time specified",
GooseError::InvalidWeight { .. } => "invalid weight specified",
GooseError::NoTaskSets { .. } => "no task sets defined",
}
}
}
impl fmt::Display for GooseError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
GooseError::Io(ref source) => write!(f, "GooseError: {} ({})", self.describe(), source),
GooseError::Reqwest(ref source) => {
write!(f, "GooseError: {} ({})", self.describe(), source)
}
GooseError::InvalidHost {
ref parse_error, ..
} => write!(f, "GooseError: {} ({})", self.describe(), parse_error),
_ => write!(f, "GooseError: {}", self.describe()),
}
}
}
impl std::error::Error for GooseError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match *self {
GooseError::Io(ref source) => Some(source),
GooseError::Reqwest(ref source) => Some(source),
GooseError::InvalidHost {
ref parse_error, ..
} => Some(parse_error),
_ => None,
}
}
}
impl From<reqwest::Error> for GooseError {
fn from(err: reqwest::Error) -> GooseError {
GooseError::Reqwest(err)
}
}
impl From<io::Error> for GooseError {
fn from(err: io::Error) -> GooseError {
GooseError::Io(err)
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum AttackMode {
Undefined,
StandAlone,
Manager,
Worker,
}
#[derive(Clone, Debug, PartialEq)]
pub enum AttackPhase {
Initializing,
Starting,
Running,
Stopping,
}
#[derive(Clone, Debug, PartialEq)]
pub enum GooseTaskSetScheduler {
RoundRobin,
Serial,
Random,
}
#[derive(Clone, Debug, Default)]
pub struct GooseDefaults {
host: Option<String>,
users: Option<usize>,
hatch_rate: Option<String>,
run_time: Option<usize>,
log_level: Option<u8>,
log_file: Option<String>,
verbose: Option<u8>,
running_metrics: Option<usize>,
no_reset_metrics: Option<bool>,
no_metrics: Option<bool>,
no_task_metrics: Option<bool>,
no_error_summary: Option<bool>,
report_file: Option<String>,
requests_file: Option<String>,
metrics_format: Option<String>,
debug_file: Option<String>,
debug_format: Option<String>,
no_debug_body: Option<bool>,
status_codes: Option<bool>,
throttle_requests: Option<usize>,
sticky_follow: Option<bool>,
manager: Option<bool>,
expect_workers: Option<u16>,
no_hash_check: Option<bool>,
manager_bind_host: Option<String>,
manager_bind_port: Option<u16>,
worker: Option<bool>,
manager_host: Option<String>,
manager_port: Option<u16>,
}
#[derive(Debug)]
pub enum GooseDefault {
Host,
Users,
HatchRate,
RunTime,
LogLevel,
LogFile,
Verbose,
RunningMetrics,
NoResetMetrics,
NoMetrics,
NoTaskMetrics,
NoErrorSummary,
ReportFile,
RequestsFile,
RequestsFormat,
DebugFile,
DebugFormat,
NoDebugBody,
StatusCodes,
ThrottleRequests,
StickyFollow,
Manager,
ExpectWorkers,
NoHashCheck,
ManagerBindHost,
ManagerBindPort,
Worker,
ManagerHost,
ManagerPort,
}
pub struct GooseAttackRunState {
spawn_user_timer: std::time::Instant,
spawn_user_in_ms: usize,
spawn_user_counter: usize,
drift_timer: tokio::time::Instant,
all_threads_metrics_tx: flume::Sender<GooseMetric>,
metrics_rx: flume::Receiver<GooseMetric>,
debug_logger: DebugLoggerHandle,
all_threads_debug_logger_tx: DebugLoggerChannel,
throttle_threads_tx: Option<flume::Sender<bool>>,
parent_to_throttle_tx: Option<flume::Sender<bool>>,
requests_file: Option<BufWriter<File>>,
report_file: Option<File>,
metrics_header_displayed: bool,
users: Vec<tokio::task::JoinHandle<()>>,
user_channels: Vec<flume::Sender<GooseUserCommand>>,
running_metrics_timer: std::time::Instant,
display_running_metrics: bool,
all_users_spawned: bool,
canceled: Arc<AtomicBool>,
socket: Option<Socket>,
}
#[derive(Clone)]
pub struct GooseAttack {
test_start_task: Option<GooseTask>,
test_stop_task: Option<GooseTask>,
task_sets: Vec<GooseTaskSet>,
weighted_users: Vec<GooseUser>,
weighted_gaggle_users: Vec<GaggleUser>,
defaults: GooseDefaults,
configuration: GooseConfiguration,
run_time: usize,
attack_mode: AttackMode,
attack_phase: AttackPhase,
scheduler: GooseTaskSetScheduler,
started: Option<time::Instant>,
metrics: GooseMetrics,
}
impl GooseAttack {
pub fn initialize() -> Result<GooseAttack, GooseError> {
Ok(GooseAttack {
test_start_task: None,
test_stop_task: None,
task_sets: Vec::new(),
weighted_users: Vec::new(),
weighted_gaggle_users: Vec::new(),
defaults: GooseDefaults::default(),
configuration: GooseConfiguration::parse_args_default_or_exit(),
run_time: 0,
attack_mode: AttackMode::Undefined,
attack_phase: AttackPhase::Initializing,
scheduler: GooseTaskSetScheduler::RoundRobin,
started: None,
metrics: GooseMetrics::default(),
})
}
pub fn initialize_with_config(
configuration: GooseConfiguration,
) -> Result<GooseAttack, GooseError> {
Ok(GooseAttack {
test_start_task: None,
test_stop_task: None,
task_sets: Vec::new(),
weighted_users: Vec::new(),
weighted_gaggle_users: Vec::new(),
defaults: GooseDefaults::default(),
configuration,
run_time: 0,
attack_mode: AttackMode::Undefined,
attack_phase: AttackPhase::Initializing,
scheduler: GooseTaskSetScheduler::RoundRobin,
started: None,
metrics: GooseMetrics::default(),
})
}
pub fn initialize_logger(&self) {
let debug_level;
match self.configuration.verbose {
0 => debug_level = LevelFilter::Warn,
1 => debug_level = LevelFilter::Info,
2 => debug_level = LevelFilter::Debug,
_ => debug_level = LevelFilter::Trace,
}
let log_level_value = if self.configuration.log_level > 0 {
self.configuration.log_level
} else if let Some(default_log_level) = self.defaults.log_level {
default_log_level
} else {
0
};
let log_level = match log_level_value {
0 => LevelFilter::Warn,
1 => LevelFilter::Info,
2 => LevelFilter::Debug,
_ => LevelFilter::Trace,
};
let log_file: Option<PathBuf>;
if !self.configuration.log_file.is_empty() {
log_file = Some(PathBuf::from(&self.configuration.log_file));
}
else if let Some(default_log_file) = &self.defaults.log_file {
log_file = Some(PathBuf::from(default_log_file));
}
else {
log_file = None;
}
if let Some(log_to_file) = log_file {
match CombinedLogger::init(vec![
SimpleLogger::new(debug_level, Config::default()),
WriteLogger::new(
log_level,
Config::default(),
std::fs::File::create(&log_to_file).unwrap(),
),
]) {
Ok(_) => (),
Err(e) => {
info!("failed to initialize CombinedLogger: {}", e);
}
}
info!("Writing to log file: {}", log_to_file.display());
} else {
match CombinedLogger::init(vec![SimpleLogger::new(debug_level, Config::default())]) {
Ok(_) => (),
Err(e) => {
info!("failed to initialize CombinedLogger: {}", e);
}
}
}
info!("Output verbosity level: {}", debug_level);
info!("Logfile verbosity level: {}", log_level);
}
pub fn set_scheduler(mut self, scheduler: GooseTaskSetScheduler) -> Self {
self.scheduler = scheduler;
self
}
pub fn register_taskset(mut self, mut taskset: GooseTaskSet) -> Self {
taskset.task_sets_index = self.task_sets.len();
self.task_sets.push(taskset);
self
}
pub fn test_start(mut self, task: GooseTask) -> Self {
self.test_start_task = Some(task);
self
}
pub fn test_stop(mut self, task: GooseTask) -> Self {
self.test_stop_task = Some(task);
self
}
fn allocate_tasks(&mut self) -> Vec<usize> {
trace!("allocate_tasks");
let mut u: usize = 0;
let mut v: usize;
for task_set in &self.task_sets {
if u == 0 {
u = task_set.weight;
} else {
v = task_set.weight;
trace!("calculating greatest common denominator of {} and {}", u, v);
u = util::gcd(u, v);
trace!("inner gcd: {}", u);
}
}
debug!("gcd: {}", u);
let mut available_task_sets = Vec::with_capacity(self.task_sets.len());
let mut total_task_sets = 0;
for (index, task_set) in self.task_sets.iter().enumerate() {
let weight = task_set.weight / u;
trace!(
"{}: {} has weight of {} (reduced with gcd to {})",
index,
task_set.name,
task_set.weight,
weight
);
let weighted_sets = vec![index; weight];
total_task_sets += weight;
available_task_sets.push(weighted_sets);
}
info!(
"allocating GooseTasks to GooseUsers with {:?} scheduler",
self.scheduler
);
let mut weighted_task_sets = Vec::new();
match self.scheduler {
GooseTaskSetScheduler::RoundRobin => {
let task_sets_len = available_task_sets.len();
loop {
for (task_set_index, task_sets) in available_task_sets
.iter_mut()
.enumerate()
.take(task_sets_len)
{
if let Some(task_set) = task_sets.pop() {
debug!("allocating 1 user from TaskSet {}", task_set_index);
weighted_task_sets.push(task_set);
}
}
if weighted_task_sets.len() >= total_task_sets {
break;
}
}
}
GooseTaskSetScheduler::Serial => {
for (task_set_index, task_sets) in available_task_sets.iter().enumerate() {
debug!(
"allocating all {} users from TaskSet {}",
task_sets.len(),
task_set_index
);
weighted_task_sets.append(&mut task_sets.clone());
}
}
GooseTaskSetScheduler::Random => {
loop {
let task_set = available_task_sets.choose_mut(&mut rand::thread_rng());
match task_set {
Some(set) => {
if let Some(s) = set.pop() {
weighted_task_sets.push(s);
}
}
None => warn!("randomly allocating a GooseTaskSet failed, trying again"),
}
if weighted_task_sets.len() >= total_task_sets {
break;
}
}
}
}
weighted_task_sets
}
fn weight_task_set_users(&mut self) -> Result<Vec<GooseUser>, GooseError> {
trace!("weight_task_set_users");
let weighted_task_sets = self.allocate_tasks();
info!("initializing user states...");
let mut weighted_users = Vec::new();
let mut user_count = 0;
loop {
for task_sets_index in &weighted_task_sets {
debug!(
"creating user state: {} ({})",
weighted_users.len(),
task_sets_index
);
let base_url = goose::get_base_url(
self.get_configuration_host(),
self.task_sets[*task_sets_index].host.clone(),
self.defaults.host.clone(),
)?;
weighted_users.push(GooseUser::new(
self.task_sets[*task_sets_index].task_sets_index,
base_url,
self.task_sets[*task_sets_index].min_wait,
self.task_sets[*task_sets_index].max_wait,
&self.configuration,
self.metrics.hash,
)?);
user_count += 1;
if user_count >= self.configuration.users.unwrap() {
debug!("created {} weighted_users", user_count);
return Ok(weighted_users);
}
}
}
}
fn prepare_worker_task_set_users(&mut self) -> Result<Vec<GaggleUser>, GooseError> {
trace!("prepare_worker_task_set_users");
let weighted_task_sets = self.allocate_tasks();
info!("preparing users for Workers...");
let mut weighted_users = Vec::new();
let mut user_count = 0;
loop {
for task_sets_index in &weighted_task_sets {
let base_url = goose::get_base_url(
self.get_configuration_host(),
self.task_sets[*task_sets_index].host.clone(),
self.defaults.host.clone(),
)?;
weighted_users.push(GaggleUser::new(
self.task_sets[*task_sets_index].task_sets_index,
base_url,
self.task_sets[*task_sets_index].min_wait,
self.task_sets[*task_sets_index].max_wait,
&self.configuration,
self.metrics.hash,
));
user_count += 1;
if user_count >= self.configuration.users.unwrap() {
debug!("prepared {} weighted_gaggle_users", user_count);
return Ok(weighted_users);
}
}
}
}
fn set_attack_mode(&mut self) -> Result<(), GooseError> {
let manager_is_default = if let Some(value) = self.defaults.manager {
value
} else {
false
};
let worker_is_default = if let Some(value) = self.defaults.worker {
value
} else {
false
};
if manager_is_default && worker_is_default {
return Err(GooseError::InvalidOption {
option: "GooseDefault::Worker".to_string(),
value: "true".to_string(),
detail: "The GooseDefault::Worker default can not be set together with the GooseDefault::Manager default"
.to_string(),
});
}
if self.configuration.manager || (!self.configuration.worker && manager_is_default) {
self.attack_mode = AttackMode::Manager;
if self.configuration.worker {
return Err(GooseError::InvalidOption {
option: "--worker".to_string(),
value: "true".to_string(),
detail: "The --worker flag can not be set together with the --manager flag"
.to_string(),
});
}
if self.get_debug_file_path().is_some() {
return Err(GooseError::InvalidOption {
option: "--debug-file".to_string(),
value: self.configuration.debug_file.clone(),
detail:
"The --debug-file option can not be set together with the --manager flag."
.to_string(),
});
}
}
if self.configuration.worker || (!self.configuration.manager && worker_is_default) {
self.attack_mode = AttackMode::Worker;
if self.configuration.manager {
return Err(GooseError::InvalidOption {
option: "--manager".to_string(),
value: "true".to_string(),
detail: "The --manager flag can not be set together with the --worker flag."
.to_string(),
});
}
if !self.configuration.host.is_empty() {
return Err(GooseError::InvalidOption {
option: "--host".to_string(),
value: self.configuration.host.clone(),
detail: "The --host option can not be set together with the --worker flag."
.to_string(),
});
}
}
if self.attack_mode == AttackMode::Undefined {
self.attack_mode = AttackMode::StandAlone;
if self.configuration.no_hash_check {
return Err(GooseError::InvalidOption {
option: "--no-hash-check".to_string(),
value: self.configuration.no_hash_check.to_string(),
detail: "The --no-hash-check flag can not be set without also setting the --manager flag.".to_string(),
});
}
}
Ok(())
}
fn set_attack_phase(
&mut self,
goose_attack_run_state: &mut GooseAttackRunState,
phase: AttackPhase,
) {
if self.attack_phase == phase {
return;
}
goose_attack_run_state.drift_timer = tokio::time::Instant::now();
info!("entering GooseAttack phase: {:?}", &phase);
self.attack_phase = phase;
}
fn set_expect_workers(&mut self) -> Result<(), GooseError> {
let mut key = "configuration.expect_workers";
if self.configuration.expect_workers.is_some() {
key = "--expect-workers";
} else if let Some(default_expect_workers) = self.defaults.expect_workers {
if self.attack_mode == AttackMode::Manager {
key = "set_default(GooseDefault::ExpectWorkers)";
self.configuration.expect_workers = Some(default_expect_workers);
}
}
if let Some(expect_workers) = self.configuration.expect_workers {
if self.attack_mode != AttackMode::Manager {
return Err(GooseError::InvalidOption {
option: key.to_string(),
value: expect_workers.to_string(),
detail: format!(
"{} can not be set without also setting the --manager flag.",
key
),
});
} else {
if expect_workers < 1 {
return Err(GooseError::InvalidOption {
option: key.to_string(),
value: expect_workers.to_string(),
detail: format!("{} must be set to at least 1.", key),
});
}
if expect_workers as usize > self.configuration.users.unwrap() {
return Err(GooseError::InvalidOption {
option: key.to_string(),
value: expect_workers.to_string(),
detail: format!(
"{} can not be set to a value larger than --users option.",
key
),
});
}
}
}
Ok(())
}
fn set_gaggle_host_and_port(&mut self) -> Result<(), GooseError> {
if self.attack_mode == AttackMode::Manager {
if self.configuration.manager_bind_host.is_empty() {
self.configuration.manager_bind_host =
if let Some(host) = self.defaults.manager_bind_host.clone() {
host
} else {
"0.0.0.0".to_string()
}
}
if self.configuration.manager_bind_port == 0 {
self.configuration.manager_bind_port =
if let Some(port) = self.defaults.manager_bind_port {
port
} else {
DEFAULT_PORT.to_string().parse().unwrap()
};
}
} else {
if !self.configuration.manager_bind_host.is_empty() {
return Err(GooseError::InvalidOption {
option: "--manager-bind-host".to_string(),
value: self.configuration.manager_bind_host.clone(),
detail: "The --manager-bind-host option can not be set together with the --worker flag.".to_string(),
});
}
if self.configuration.manager_bind_port != 0 {
return Err(GooseError::InvalidOption {
option: "--manager-bind-port".to_string(),
value: self.configuration.manager_bind_port.to_string(),
detail: "The --manager-bind-port option can not be set together with the --worker flag.".to_string(),
});
}
}
if self.attack_mode == AttackMode::Worker {
if self.configuration.manager_host.is_empty() {
self.configuration.manager_host =
if let Some(host) = self.defaults.manager_host.clone() {
host
} else {
"127.0.0.1".to_string()
}
}
if self.configuration.manager_port == 0 {
self.configuration.manager_port = if let Some(port) = self.defaults.manager_port {
port
} else {
DEFAULT_PORT.to_string().parse().unwrap()
};
}
} else {
if !self.configuration.manager_host.is_empty() {
return Err(GooseError::InvalidOption {
option: "--manager-host".to_string(),
value: self.configuration.manager_host.clone(),
detail:
"The --manager-host option must be set together with the --worker flag."
.to_string(),
});
}
if self.configuration.manager_port != 0 {
return Err(GooseError::InvalidOption {
option: "--manager-port".to_string(),
value: self.configuration.manager_port.to_string(),
detail:
"The --manager-port option must be set together with the --worker flag."
.to_string(),
});
}
}
Ok(())
}
fn set_users(&mut self) -> Result<(), GooseError> {
let mut key = "configuration.users";
let mut value = 0;
if let Some(users) = self.configuration.users {
key = "--users";
value = users;
} else if let Some(default_users) = self.defaults.users {
if self.attack_mode == AttackMode::Worker {
self.configuration.users = None;
} else {
key = "set_default(GooseDefault::Users)";
value = default_users;
self.configuration.users = Some(default_users);
}
} else if self.attack_mode != AttackMode::Worker {
key = "num_cpus::get()";
value = num_cpus::get();
info!("concurrent users defaulted to {} (number of CPUs)", value);
self.configuration.users = Some(value);
}
if let Some(users) = self.configuration.users {
if self.attack_mode == AttackMode::Worker {
return Err(GooseError::InvalidOption {
option: key.to_string(),
value: value.to_string(),
detail: format!("{} can not be set together with the --worker flag.", key),
});
}
if users == 0 {
return Err(GooseError::InvalidOption {
option: key.to_string(),
value: "0".to_string(),
detail: "The --users option must be set to at least 1.".to_string(),
});
}
info!("users = {}", users);
}
Ok(())
}
fn set_run_time(&mut self) -> Result<(), GooseError> {
let mut key = "configuration.run_time";
let mut value = 0;
self.run_time = if !self.configuration.run_time.is_empty() {
key = "--run-time";
value = util::parse_timespan(&self.configuration.run_time);
value
} else if let Some(default_run_time) = self.defaults.run_time {
if self.attack_mode == AttackMode::Worker {
0
} else {
key = "set_default(GooseDefault::RunTime)";
value = default_run_time;
default_run_time
}
}
else {
0
};
if self.run_time > 0 {
if self.attack_mode == AttackMode::Worker {
return Err(GooseError::InvalidOption {
option: key.to_string(),
value: value.to_string(),
detail: format!("{} can not be set together with the --worker flag.", key),
});
}
info!("run_time = {}", self.run_time);
}
Ok(())
}
fn set_hatch_rate(&mut self) -> Result<(), GooseError> {
let mut key = "configuration.hatch_rate";
let mut value = "".to_string();
if let Some(hatch_rate) = &self.configuration.hatch_rate {
key = "--hatch_rate";
value = hatch_rate.to_string();
} else if let Some(default_hatch_rate) = &self.defaults.hatch_rate {
if self.attack_mode == AttackMode::Worker {
self.configuration.hatch_rate = None;
} else {
key = "set_default(GooseDefault::HatchRate)";
value = default_hatch_rate.to_string();
self.configuration.hatch_rate = Some(default_hatch_rate.to_string());
}
} else if self.attack_mode != AttackMode::Worker {
key = "Goose default";
value = "1".to_string();
self.configuration.hatch_rate = Some(value.to_string());
}
if let Some(hatch_rate) = &self.configuration.hatch_rate {
if self.attack_mode == AttackMode::Worker {
return Err(GooseError::InvalidOption {
option: key.to_string(),
value,
detail: format!("{} can not be set together with the --worker flag.", key),
});
}
if hatch_rate.is_empty() {
return Err(GooseError::InvalidOption {
option: key.to_string(),
value,
detail: format!("{} must be set to at least 1.", key),
});
}
info!("hatch_rate = {}", hatch_rate);
}
Ok(())
}
fn set_throttle_requests(&mut self) -> Result<(), GooseError> {
let mut key = "configuration.throttle_requests";
let mut value = 0;
if self.configuration.throttle_requests > 0 {
key = "--throttle-requests";
value = self.configuration.throttle_requests;
}
if self.configuration.throttle_requests == 0 {
if let Some(default_throttle_requests) = self.defaults.throttle_requests {
if self.attack_mode != AttackMode::Manager {
key = "set_default(GooseDefault::ThrottleRequests)";
value = default_throttle_requests;
self.configuration.throttle_requests = default_throttle_requests;
}
}
}
if self.configuration.throttle_requests > 0 {
if self.attack_mode == AttackMode::Manager {
return Err(GooseError::InvalidOption {
option: key.to_string(),
value: value.to_string(),
detail: format!("{} can not be set together with the --manager flag.", key),
});
}
if self.configuration.throttle_requests == 0 {
return Err(GooseError::InvalidOption {
option: key.to_string(),
value: value.to_string(),
detail: format!("{} must be set to at least 1 request per second.", key),
});
} else if self.configuration.throttle_requests > 1_000_000 {
return Err(GooseError::InvalidOption {
option: key.to_string(),
value: value.to_string(),
detail: format!(
"{} can not be set to more than 1,000,000 requests per second.",
key
),
});
}
info!(
"throttle_requests = {}",
self.configuration.throttle_requests
);
}
Ok(())
}
fn set_no_reset_metrics(&mut self) -> Result<(), GooseError> {
let mut key = "configuration.no_reset_metrics";
let mut value = false;
if self.configuration.no_reset_metrics {
key = "--no-reset-metrics";
value = true;
} else if self.attack_mode != AttackMode::Worker {
if let Some(default_no_reset_metrics) = self.defaults.no_reset_metrics {
key = "set_default(GooseDefault::NoResetMetrics)";
value = default_no_reset_metrics;
self.configuration.no_reset_metrics = default_no_reset_metrics;
}
}
if self.configuration.no_reset_metrics && self.attack_mode == AttackMode::Worker {
return Err(GooseError::InvalidOption {
option: key.to_string(),
value: value.to_string(),
detail: format!("{} can not be set together with the --worker flag.", key),
});
}
Ok(())
}
fn set_status_codes(&mut self) -> Result<(), GooseError> {
let mut key = "configuration.status_codes";
let mut value = false;
if self.configuration.status_codes {
key = "--status-codes";
value = true;
} else if self.attack_mode != AttackMode::Worker {
if let Some(default_status_codes) = self.defaults.status_codes {
key = "set_default(GooseDefault::StatusCodes)";
value = default_status_codes;
self.configuration.status_codes = default_status_codes;
}
}
if self.configuration.status_codes && self.attack_mode == AttackMode::Worker {
return Err(GooseError::InvalidOption {
option: key.to_string(),
value: value.to_string(),
detail: format!("{} can not be set together with the --worker flag.", key),
});
}
Ok(())
}
fn set_running_metrics(&mut self) -> Result<(), GooseError> {
let mut key = "configuration.running_metrics";
let mut value = 0;
if let Some(running_metrics) = self.configuration.running_metrics {
key = "--running-metrics";
value = running_metrics;
} else if self.attack_mode != AttackMode::Worker {
if let Some(default_running_metrics) = self.defaults.running_metrics {
key = "set_default(GooseDefault::RunningMetrics)";
value = default_running_metrics;
self.configuration.running_metrics = Some(default_running_metrics);
}
}
if let Some(running_metrics) = self.configuration.running_metrics {
if self.attack_mode == AttackMode::Worker {
return Err(GooseError::InvalidOption {
option: key.to_string(),
value: value.to_string(),
detail: format!("{} can not be set together with the --worker flag.", key),
});
}
if running_metrics > 0 {
info!("running_metrics = {}", running_metrics);
}
}
Ok(())
}
fn set_no_task_metrics(&mut self) -> Result<(), GooseError> {
let mut key = "configuration.no_task_metrics";
let mut value = false;
if self.configuration.no_task_metrics {
key = "--no-task-metrics";
value = true;
} else if self.attack_mode != AttackMode::Worker {
if let Some(default_no_task_metrics) = self.defaults.no_task_metrics {
key = "set_default(GooseDefault::NoTaskMetrics)";
value = default_no_task_metrics;
self.configuration.no_task_metrics = default_no_task_metrics;
}
}
if self.configuration.no_task_metrics && self.attack_mode == AttackMode::Worker {
return Err(GooseError::InvalidOption {
option: key.to_string(),
value: value.to_string(),
detail: format!("{} can not be set together with the --worker flag.", key),
});
}
Ok(())
}
fn set_no_error_summary(&mut self) -> Result<(), GooseError> {
let mut key = "configuration.no_error_summary";
let mut value = false;
if self.configuration.no_error_summary {
key = "--no-error-summary";
value = true;
} else if self.attack_mode != AttackMode::Worker {
if let Some(default_no_error_summary) = self.defaults.no_error_summary {
key = "set_default(GooseDefault::NoErrorSummary)";
value = default_no_error_summary;
self.configuration.no_error_summary = default_no_error_summary;
}
}
if self.configuration.no_error_summary && self.attack_mode == AttackMode::Worker {
return Err(GooseError::InvalidOption {
option: key.to_string(),
value: value.to_string(),
detail: format!("{} can not be set together with the --worker flag.", key),
});
}
Ok(())
}
fn set_no_metrics(&mut self) -> Result<(), GooseError> {
let mut key = "configuration.no_metrics";
let mut value = false;
if self.configuration.no_metrics {
key = "--no-metrics";
value = true;
} else if self.attack_mode != AttackMode::Worker {
if let Some(default_no_metrics) = self.defaults.no_metrics {
key = "set_default(GooseDefault::NoMetrics)";
value = default_no_metrics;
self.configuration.no_metrics = default_no_metrics;
}
}
if self.configuration.no_metrics && self.attack_mode == AttackMode::Worker {
return Err(GooseError::InvalidOption {
option: key.to_string(),
value: value.to_string(),
detail: format!("{} can not be set together with the --worker flag.", key),
});
}
if self.configuration.no_metrics {
if self.configuration.status_codes {
return Err(GooseError::InvalidOption {
option: key.to_string(),
value: value.to_string(),
detail: format!(
"{} can not be set together with the --status-codes flag.",
key
),
});
}
if self.configuration.running_metrics.is_some() {
return Err(GooseError::InvalidOption {
option: key.to_string(),
value: value.to_string(),
detail: format!(
"{} can not be set together with the --running_metrics option.",
key
),
});
}
if !self.configuration.requests_file.is_empty() {
return Err(GooseError::InvalidOption {
option: key.to_string(),
value: value.to_string(),
detail: format!(
"{} can not be set together with the --requests-file option.",
key
),
});
}
}
Ok(())
}
fn set_sticky_follow(&mut self) -> Result<(), GooseError> {
let mut key = "configuration.sticky_follow";
let mut value = false;
if self.configuration.sticky_follow {
key = "--sticky-follow";
value = true;
} else if self.attack_mode != AttackMode::Worker {
if let Some(default_sticky_follow) = self.defaults.sticky_follow {
key = "set_default(GooseDefault::StickyFollow)";
value = default_sticky_follow;
self.configuration.sticky_follow = default_sticky_follow;
}
}
if self.configuration.sticky_follow && self.attack_mode == AttackMode::Worker {
return Err(GooseError::InvalidOption {
option: key.to_string(),
value: value.to_string(),
detail: format!("{} can not be set together with the --worker flag.", key),
});
}
Ok(())
}
#[cfg(feature = "gaggle")]
fn set_no_hash_check(&mut self) -> Result<(), GooseError> {
let mut key = "configuration.no_hash_check";
let mut value = false;
if self.configuration.no_hash_check {
key = "--no-hash-check";
value = true;
} else if self.attack_mode != AttackMode::Worker {
if let Some(default_no_hash_check) = self.defaults.no_hash_check {
key = "set_default(GooseDefault::NoHashCheck)";
value = default_no_hash_check;
self.configuration.no_hash_check = default_no_hash_check;
}
}
if self.configuration.no_hash_check && self.attack_mode == AttackMode::Worker {
return Err(GooseError::InvalidOption {
option: key.to_string(),
value: value.to_string(),
detail: format!("{} can not be set together with the --worker flag.", key),
});
}
Ok(())
}
fn get_report_file_path(&mut self) -> Option<String> {
if self.configuration.no_metrics || self.attack_mode == AttackMode::Manager {
return None;
}
if !self.configuration.report_file.is_empty() {
return Some(self.configuration.report_file.to_string());
}
if let Some(default_report_file) = &self.defaults.report_file {
return Some(default_report_file.to_string());
}
None
}
fn get_requests_file_path(&mut self) -> Option<&str> {
if self.configuration.no_metrics || self.attack_mode == AttackMode::Manager {
return None;
}
if !self.configuration.requests_file.is_empty() {
return Some(&self.configuration.requests_file);
}
if let Some(default_requests_file) = &self.defaults.requests_file {
return Some(default_requests_file);
}
None
}
fn set_requests_format(&mut self) -> Result<(), GooseError> {
if self.configuration.metrics_format.is_empty() {
if let Some(default_metrics_format) = &self.defaults.metrics_format {
self.configuration.metrics_format = default_metrics_format.to_string();
} else {
self.configuration.metrics_format = "json".to_string();
}
} else {
if self.configuration.no_metrics {
return Err(GooseError::InvalidOption {
option: "--no-metrics".to_string(),
value: "true".to_string(),
detail: "The --no-metrics flag can not be set together with the --requests-format option.".to_string(),
});
}
else if self.get_requests_file_path().is_none() {
return Err(GooseError::InvalidOption {
option: "--requests-format".to_string(),
value: self.configuration.metrics_format.clone(),
detail: "The --requests-file option must be set together with the --requests-format option.".to_string(),
});
}
}
let options = vec!["json", "csv", "raw"];
if !options.contains(&self.configuration.metrics_format.as_str()) {
return Err(GooseError::InvalidOption {
option: "--requests-format".to_string(),
value: self.configuration.metrics_format.clone(),
detail: format!(
"The --requests-format option must be set to one of: {}.",
options.join(", ")
),
});
}
Ok(())
}
fn get_debug_file_path(&self) -> Option<&str> {
if self.attack_mode == AttackMode::Manager {
return None;
}
if !self.configuration.debug_file.is_empty() {
return Some(&self.configuration.debug_file);
}
if let Some(default_debug_file) = &self.defaults.debug_file {
return Some(default_debug_file);
}
None
}
fn set_debug_format(&mut self) -> Result<(), GooseError> {
if self.configuration.debug_format.is_empty() {
if let Some(default_debug_format) = &self.defaults.debug_format {
self.configuration.debug_format = default_debug_format.to_string();
} else {
self.configuration.debug_format = "json".to_string();
}
} else {
if self.configuration.debug_file.is_empty() {
return Err(GooseError::InvalidOption {
option: "--debug-format".to_string(),
value: self.configuration.metrics_format.clone(),
detail: "The --debug-file option must be set together with the --debug-format option.".to_string(),
});
}
}
let options = vec!["json", "raw"];
if !options.contains(&self.configuration.debug_format.as_str()) {
return Err(GooseError::InvalidOption {
option: "--debug-format".to_string(),
value: self.configuration.debug_format.clone(),
detail: format!(
"The --debug-format option must be set to one of: {}.",
options.join(", ")
),
});
}
Ok(())
}
fn set_no_debug_body(&mut self) -> Result<(), GooseError> {
let mut key = "configuration.no_debug_body";
let mut value = false;
if self.configuration.no_debug_body {
key = "--no-debug-body";
value = true;
} else if self.attack_mode != AttackMode::Manager {
if let Some(default_no_debug_body) = self.defaults.no_debug_body {
key = "set_default(GooseDefault::NoDebugBody)";
value = default_no_debug_body;
self.configuration.no_debug_body = default_no_debug_body;
}
}
if self.configuration.no_debug_body && self.attack_mode == AttackMode::Manager {
return Err(GooseError::InvalidOption {
option: key.to_string(),
value: value.to_string(),
detail: format!("{} can not be set together with the --manager flag.", key),
});
}
Ok(())
}
pub fn execute(mut self) -> Result<GooseMetrics, GooseError> {
if self.configuration.version {
println!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
std::process::exit(0);
}
if self.task_sets.is_empty() {
return Err(GooseError::NoTaskSets {
detail: "No task sets are defined.".to_string(),
});
}
if self.configuration.list {
println!("Available tasks:");
for task_set in self.task_sets {
println!(" - {} (weight: {})", task_set.name, task_set.weight);
for task in task_set.tasks {
println!(" o {} (weight: {})", task.name, task.weight);
}
}
std::process::exit(0);
}
self.initialize_logger();
self.set_attack_mode()?;
self.set_users()?;
self.set_expect_workers()?;
self.set_gaggle_host_and_port()?;
self.set_run_time()?;
self.set_hatch_rate()?;
self.set_requests_format()?;
self.set_debug_format()?;
self.set_no_debug_body()?;
self.set_throttle_requests()?;
self.set_status_codes()?;
self.set_running_metrics()?;
self.set_no_reset_metrics()?;
self.set_no_task_metrics()?;
self.set_no_error_summary()?;
self.set_no_metrics()?;
self.set_sticky_follow()?;
#[cfg(feature = "gaggle")]
self.set_no_hash_check()?;
if self.configuration.host.is_empty() {
for task_set in &self.task_sets {
match &task_set.host {
Some(h) => {
if is_valid_host(h).is_ok() {
info!("host for {} configured: {}", task_set.name, h);
}
}
None => match &self.defaults.host {
Some(h) => {
if is_valid_host(h).is_ok() {
info!("host for {} configured: {}", task_set.name, h);
}
}
None => {
if self.attack_mode != AttackMode::Worker {
return Err(GooseError::InvalidOption {
option: "--host".to_string(),
value: "".to_string(),
detail: format!("A host must be defined via the --host option, the GooseAttack.set_default() function, or the GooseTaskSet.set_host() function (no host defined for {}).", task_set.name)
});
}
}
},
}
}
} else if is_valid_host(&self.configuration.host).is_ok() {
info!("global host configured: {}", self.configuration.host);
}
for task_set in &mut self.task_sets {
let (weighted_on_start_tasks, weighted_tasks, weighted_on_stop_tasks) =
weight_tasks(&task_set);
task_set.weighted_on_start_tasks = weighted_on_start_tasks;
task_set.weighted_tasks = weighted_tasks;
task_set.weighted_on_stop_tasks = weighted_on_stop_tasks;
debug!(
"weighted {} on_start: {:?} tasks: {:?} on_stop: {:?}",
task_set.name,
task_set.weighted_on_start_tasks,
task_set.weighted_tasks,
task_set.weighted_on_stop_tasks
);
}
if self.attack_mode != AttackMode::Worker {
if !self.configuration.no_metrics {
self.metrics.display_metrics = true;
}
if self.attack_mode == AttackMode::StandAlone {
self.weighted_users = self.weight_task_set_users()?;
} else if self.attack_mode == AttackMode::Manager {
self.weighted_gaggle_users = self.prepare_worker_task_set_users()?;
}
}
let mut s = DefaultHasher::new();
self.task_sets.hash(&mut s);
self.metrics.hash = s.finish();
debug!("hash: {}", self.metrics.hash);
if self.attack_mode == AttackMode::Manager {
#[cfg(feature = "gaggle")]
{
let rt = Runtime::new().unwrap();
self = rt.block_on(manager::manager_main(self));
}
#[cfg(not(feature = "gaggle"))]
{
return Err(GooseError::FeatureNotEnabled {
feature: "gaggle".to_string(), detail: "Load test must be recompiled with `--features gaggle` to start in manager mode.".to_string()
});
}
}
else if self.attack_mode == AttackMode::Worker {
#[cfg(feature = "gaggle")]
{
let rt = Runtime::new().unwrap();
self = rt.block_on(worker::worker_main(&self));
}
#[cfg(not(feature = "gaggle"))]
{
return Err(GooseError::FeatureNotEnabled {
feature: "gaggle".to_string(),
detail: "Load test must be recompiled with `--features gaggle` to start in worker mode.".to_string(),
});
}
}
else {
let rt = Runtime::new().unwrap();
self = rt.block_on(self.start_attack(None))?;
}
Ok(self.metrics)
}
fn get_configuration_host(&self) -> Option<String> {
if self.configuration.host.is_empty() {
None
} else {
Some(self.configuration.host.to_string())
}
}
fn prepare_csv(
raw_request: &GooseRawRequest,
goose_attack_run_state: &mut GooseAttackRunState,
) -> String {
let body = format!(
"{},{},\"{}\",\"{}\",\"{}\",{},{},{},{},{},{}",
raw_request.elapsed,
raw_request.method,
raw_request.name,
raw_request.url,
raw_request.final_url,
raw_request.redirected,
raw_request.response_time,
raw_request.status_code,
raw_request.success,
raw_request.update,
raw_request.user
);
if !goose_attack_run_state.metrics_header_displayed {
goose_attack_run_state.metrics_header_displayed = true;
format!(
"{},{},{},{},{},{},{},{},{},{},{}\n",
"elapsed",
"method",
"name",
"url",
"final_url",
"redirected",
"response_time",
"status_code",
"success",
"update",
"user"
) + &body
} else {
body
}
}
fn setup_debug_logger(&mut self) -> (DebugLoggerHandle, DebugLoggerChannel) {
self.configuration.debug_file = if let Some(debug_file) = self.get_debug_file_path() {
debug_file.to_string()
} else {
"".to_string()
};
if self.configuration.debug_file.is_empty() {
return (None, None);
}
let (all_threads_debug_logger, logger_receiver): (
flume::Sender<Option<GooseDebug>>,
flume::Receiver<Option<GooseDebug>>,
) = flume::unbounded();
let logger_thread = tokio::spawn(logger::logger_main(
self.configuration.clone(),
logger_receiver,
));
(Some(logger_thread), Some(all_threads_debug_logger))
}
async fn setup_throttle(
&self,
) -> (
Option<flume::Sender<bool>>,
Option<flume::Sender<bool>>,
) {
if self.configuration.throttle_requests == 0 {
return (None, None);
}
let (all_threads_throttle, throttle_receiver): (
flume::Sender<bool>,
flume::Receiver<bool>,
) = flume::bounded(self.configuration.throttle_requests);
let (parent_to_throttle_tx, throttle_rx) = flume::bounded(1);
let _ = Some(tokio::spawn(throttle::throttle_main(
self.configuration.throttle_requests,
throttle_receiver,
throttle_rx,
)));
let sender = all_threads_throttle.clone();
for _ in 1..self.configuration.throttle_requests {
let _ = sender.send_async(true).await;
}
(Some(all_threads_throttle), Some(parent_to_throttle_tx))
}
async fn prepare_report_file(&mut self) -> Result<Option<File>, GooseError> {
if let Some(report_file_path) = self.get_report_file_path() {
Ok(Some(File::create(&report_file_path).await?))
} else {
Ok(None)
}
}
async fn prepare_requests_file(&mut self) -> Result<Option<BufWriter<File>>, GooseError> {
if let Some(requests_file_path) = self.get_requests_file_path() {
Ok(Some(BufWriter::new(
File::create(&requests_file_path).await?,
)))
} else {
Ok(None)
}
}
async fn run_test_start(&self) -> Result<(), GooseError> {
if self.attack_mode != AttackMode::Worker {
match &self.test_start_task {
Some(t) => {
info!("running test_start_task");
let base_url = goose::get_base_url(
self.get_configuration_host(),
None,
self.defaults.host.clone(),
)?;
let user = GooseUser::single(base_url, &self.configuration)?;
let function = &t.function;
let _ = function(&user).await;
}
None => (),
}
}
Ok(())
}
async fn run_test_stop(&self) -> Result<(), GooseError> {
if self.attack_mode != AttackMode::Worker {
match &self.test_stop_task {
Some(t) => {
info!("running test_stop_task");
let base_url = goose::get_base_url(
self.get_configuration_host(),
None,
self.defaults.host.clone(),
)?;
let user = GooseUser::single(base_url, &self.configuration)?;
let function = &t.function;
let _ = function(&user).await;
}
None => (),
}
}
Ok(())
}
async fn initialize_attack(
&mut self,
socket: Option<Socket>,
) -> Result<GooseAttackRunState, GooseError> {
trace!("initialize_attack");
self.run_test_start().await?;
self.metrics.display_status_codes = self.configuration.status_codes;
let (all_threads_metrics_tx, metrics_rx): (
flume::Sender<GooseMetric>,
flume::Receiver<GooseMetric>,
) = flume::unbounded();
let (debug_logger, all_threads_debug_logger_tx) = self.setup_debug_logger();
let (throttle_threads_tx, parent_to_throttle_tx) = self.setup_throttle().await;
let std_now = std::time::Instant::now();
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),
})
}
};
let requests_file = match self.prepare_requests_file().await {
Ok(f) => f,
Err(e) => {
return Err(GooseError::InvalidOption {
option: "--requests-file".to_string(),
value: self.get_requests_file_path().unwrap().to_string(),
detail: format!("Failed to create request file: {}", e),
})
}
};
let goose_attack_run_state = GooseAttackRunState {
spawn_user_timer: std_now,
spawn_user_in_ms: 0,
spawn_user_counter: 0,
drift_timer: tokio::time::Instant::now(),
all_threads_metrics_tx,
metrics_rx,
debug_logger,
all_threads_debug_logger_tx,
throttle_threads_tx,
parent_to_throttle_tx,
report_file,
requests_file,
metrics_header_displayed: false,
users: Vec::new(),
user_channels: Vec::new(),
running_metrics_timer: std_now,
display_running_metrics: false,
all_users_spawned: false,
canceled: Arc::new(AtomicBool::new(false)),
socket,
};
trace!("socket: {:?}", &goose_attack_run_state.socket);
util::setup_ctrlc_handler(&goose_attack_run_state.canceled);
self.metrics
.initialize_task_metrics(&self.task_sets, &self.configuration);
self.started = Some(time::Instant::now());
self.metrics.started = Some(Local::now());
Ok(goose_attack_run_state)
}
async fn spawn_attack(
&mut self,
goose_attack_run_state: &mut GooseAttackRunState,
) -> Result<(), GooseError> {
if util::timer_expired(self.started.unwrap(), self.run_time)
|| goose_attack_run_state.canceled.load(Ordering::SeqCst)
{
self.set_attack_phase(goose_attack_run_state, AttackPhase::Stopping);
return Ok(());
}
let hatch_rate = util::get_hatch_rate(self.configuration.hatch_rate.clone());
if goose_attack_run_state.spawn_user_in_ms == 0
|| util::ms_timer_expired(
goose_attack_run_state.spawn_user_timer,
goose_attack_run_state.spawn_user_in_ms,
)
{
goose_attack_run_state.spawn_user_timer = std::time::Instant::now();
goose_attack_run_state.spawn_user_in_ms = (1_000.0 / hatch_rate) as usize;
if self.attack_mode == AttackMode::Worker {
goose_attack_run_state.spawn_user_in_ms *=
self.configuration.expect_workers.unwrap() as usize;
}
let mut thread_user =
self.weighted_users[goose_attack_run_state.spawn_user_counter].clone();
goose_attack_run_state.spawn_user_counter += 1;
thread_user.weighted_tasks = self.task_sets[thread_user.task_sets_index]
.weighted_tasks
.clone();
thread_user.weighted_on_start_tasks = self.task_sets[thread_user.task_sets_index]
.weighted_on_start_tasks
.clone();
thread_user.weighted_on_stop_tasks = self.task_sets[thread_user.task_sets_index]
.weighted_on_stop_tasks
.clone();
thread_user.weighted_users_index = self.metrics.users;
let (parent_sender, thread_receiver): (
flume::Sender<GooseUserCommand>,
flume::Receiver<GooseUserCommand>,
) = flume::unbounded();
goose_attack_run_state.user_channels.push(parent_sender);
if self.get_debug_file_path().is_some() {
thread_user.debug_logger = Some(
goose_attack_run_state
.all_threads_debug_logger_tx
.clone()
.unwrap(),
);
} else {
thread_user.debug_logger = None;
}
thread_user.throttle = if self.configuration.throttle_requests > 0 {
Some(goose_attack_run_state.throttle_threads_tx.clone().unwrap())
} else {
None
};
thread_user.channel_to_parent =
Some(goose_attack_run_state.all_threads_metrics_tx.clone());
let thread_task_set = self.task_sets[thread_user.task_sets_index].clone();
let thread_number = self.metrics.users + 1;
let is_worker = self.attack_mode == AttackMode::Worker;
if is_worker {
thread_user.config = self.configuration.clone();
}
let user = tokio::spawn(user::user_main(
thread_number,
thread_task_set,
thread_user,
thread_receiver,
is_worker,
));
goose_attack_run_state.users.push(user);
self.metrics.users += 1;
if let Some(running_metrics) = self.configuration.running_metrics {
if self.attack_mode != AttackMode::Worker
&& util::timer_expired(
goose_attack_run_state.running_metrics_timer,
running_metrics,
)
{
goose_attack_run_state.running_metrics_timer = time::Instant::now();
self.metrics.print_running();
}
}
} else {
let running_metrics = self.configuration.running_metrics.unwrap_or(0);
let sleep_duration = if running_metrics > 0
&& running_metrics * 1_000 < goose_attack_run_state.spawn_user_in_ms
{
let sleep_delay = self.configuration.running_metrics.unwrap() * 1_000;
goose_attack_run_state.spawn_user_in_ms -= sleep_delay;
tokio::time::Duration::from_millis(sleep_delay as u64)
} else {
tokio::time::Duration::from_millis(goose_attack_run_state.spawn_user_in_ms as u64)
};
debug!("sleeping {:?}...", sleep_duration);
goose_attack_run_state.drift_timer =
util::sleep_minus_drift(sleep_duration, goose_attack_run_state.drift_timer).await;
}
if self.metrics.users >= self.weighted_users.len() {
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
if self.attack_mode == AttackMode::Worker {
info!(
"[{}] launched {} users...",
get_worker_id(),
self.metrics.users
);
} else {
info!("launched {} users...", self.metrics.users);
}
self.reset_metrics(goose_attack_run_state).await?;
self.set_attack_phase(goose_attack_run_state, AttackPhase::Running);
}
Ok(())
}
async fn monitor_attack(
&mut self,
goose_attack_run_state: &mut GooseAttackRunState,
) -> Result<(), GooseError> {
if util::timer_expired(self.started.unwrap(), self.run_time)
|| goose_attack_run_state.canceled.load(Ordering::SeqCst)
{
if self.attack_mode == AttackMode::Worker {
info!(
"[{}] stopping after {} seconds...",
get_worker_id(),
self.started.unwrap().elapsed().as_secs()
);
#[cfg(feature = "gaggle")]
{
let manager = goose_attack_run_state.socket.clone().unwrap();
register_shutdown_pipe_handler(&manager);
}
} else {
info!(
"stopping after {} seconds...",
self.started.unwrap().elapsed().as_secs()
);
}
for (index, send_to_user) in goose_attack_run_state.user_channels.iter().enumerate() {
match send_to_user.send(GooseUserCommand::Exit) {
Ok(_) => {
debug!("telling user {} to exit", index);
}
Err(e) => {
info!("failed to tell user {} to exit: {}", index, e);
}
}
}
if self.attack_mode == AttackMode::Worker {
info!("[{}] waiting for users to exit", get_worker_id());
} else {
info!("waiting for users to exit");
}
if let Some(tx) = goose_attack_run_state.parent_to_throttle_tx.clone() {
let _ = tx.send(false);
}
let users = std::mem::take(&mut goose_attack_run_state.users);
futures::future::join_all(users).await;
debug!("all users exited");
if self.get_debug_file_path().is_some() {
if let Err(e) = goose_attack_run_state
.all_threads_debug_logger_tx
.clone()
.unwrap()
.send(None)
{
warn!("unexpected error telling logger thread to exit: {}", e);
};
if goose_attack_run_state.debug_logger.is_some() {
let debug_logger = std::mem::take(&mut goose_attack_run_state.debug_logger);
let _ = tokio::join!(debug_logger.unwrap());
}
}
if !self.configuration.no_metrics {
let _received_message = self.receive_metrics(goose_attack_run_state).await?;
}
#[cfg(feature = "gaggle")]
{
if self.attack_mode == AttackMode::Worker {
worker::push_metrics_to_manager(
&goose_attack_run_state.socket.clone().unwrap(),
vec![
GaggleMetrics::Requests(self.metrics.requests.clone()),
GaggleMetrics::Errors(self.metrics.errors.clone()),
GaggleMetrics::Tasks(self.metrics.tasks.clone()),
],
true,
);
}
}
self.set_attack_phase(goose_attack_run_state, AttackPhase::Stopping);
} else {
goose_attack_run_state.drift_timer = util::sleep_minus_drift(
time::Duration::from_secs(1),
goose_attack_run_state.drift_timer,
)
.await;
}
Ok(())
}
async fn sync_metrics(
&mut self,
goose_attack_run_state: &mut GooseAttackRunState,
) -> Result<(), GooseError> {
if !self.configuration.no_metrics {
if let Some(running_metrics) = self.configuration.running_metrics {
if self.attack_mode != AttackMode::Worker
&& util::timer_expired(
goose_attack_run_state.running_metrics_timer,
running_metrics,
)
{
goose_attack_run_state.running_metrics_timer = time::Instant::now();
goose_attack_run_state.display_running_metrics = true;
}
}
let received_message = self.receive_metrics(goose_attack_run_state).await?;
if self.attack_mode == AttackMode::Worker && received_message {
#[cfg(feature = "gaggle")]
{
if !worker::push_metrics_to_manager(
&goose_attack_run_state.socket.clone().unwrap(),
vec![
GaggleMetrics::Requests(self.metrics.requests.clone()),
GaggleMetrics::Tasks(self.metrics.tasks.clone()),
],
true,
) {
goose_attack_run_state
.canceled
.store(true, Ordering::SeqCst);
}
self.metrics.requests = HashMap::new();
self.metrics
.initialize_task_metrics(&self.task_sets, &self.configuration);
}
}
}
if goose_attack_run_state.display_running_metrics {
goose_attack_run_state.display_running_metrics = false;
self.metrics.duration = self.started.unwrap().elapsed().as_secs() as usize;
self.metrics.print_running();
}
Ok(())
}
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).await?;
goose_attack_run_state.all_users_spawned = true;
let users = self.configuration.users.clone().unwrap();
if !self.configuration.no_reset_metrics {
self.metrics.duration = self.started.unwrap().elapsed().as_secs() as usize;
self.metrics.print_running();
goose_attack_run_state.running_metrics_timer = time::Instant::now();
if self.metrics.display_metrics {
if self.metrics.users < users {
println!(
"{} of {} users hatched, timer expired, resetting metrics (disable with --no-reset-metrics).\n", self.metrics.users, users
);
} else {
println!(
"All {} users hatched, resetting metrics (disable with --no-reset-metrics).\n", users
);
}
}
self.metrics.requests = HashMap::new();
self.metrics
.initialize_task_metrics(&self.task_sets, &self.configuration);
self.started = Some(time::Instant::now());
} else if self.metrics.users < users {
println!(
"{} of {} users hatched, timer expired.\n",
self.metrics.users, users
);
} else {
println!("All {} users hatched.\n", self.metrics.users);
}
}
Ok(())
}
async fn stop_attack(
&mut self,
goose_attack_run_state: &mut GooseAttackRunState,
) -> Result<(), GooseError> {
self.metrics.duration = self.started.unwrap().elapsed().as_secs() as usize;
self.run_test_stop().await?;
if let Some(file) = goose_attack_run_state.requests_file.as_mut() {
info!(
"flushing requests_file: {}",
self.get_requests_file_path().unwrap()
);
let _ = file.flush().await;
};
self.metrics.final_metrics = true;
Ok(())
}
async fn write_html_report(
&mut self,
goose_attack_run_state: &mut GooseAttackRunState,
) -> Result<(), GooseError> {
if let Some(report_file) = goose_attack_run_state.report_file.as_mut() {
let started = self.metrics.started.clone().unwrap();
let start_time = started.format("%Y-%m-%d %H:%M:%S").to_string();
let end_time = (started + Duration::seconds(self.metrics.duration as i64))
.format("%Y-%m-%d %H:%M:%S")
.to_string();
let host = match self.get_configuration_host() {
Some(h) => h.to_string(),
None => "".to_string(),
};
let mut request_metrics = Vec::new();
let mut response_metrics = Vec::new();
let mut aggregate_total_count = 0;
let mut aggregate_fail_count = 0;
let mut aggregate_response_time_counter: usize = 0;
let mut aggregate_response_time_minimum: usize = 0;
let mut aggregate_response_time_maximum: usize = 0;
let mut aggregate_response_times: BTreeMap<usize, usize> = BTreeMap::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 total_request_count = request.success_count + request.fail_count;
let (requests_per_second, failures_per_second) = metrics::per_second_calculations(
self.metrics.duration,
total_request_count,
request.fail_count,
);
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.total_response_time as f32 / request.response_time_counter as f32
),
response_time_minimum: request.min_response_time,
response_time_maximum: request.max_response_time,
requests_per_second: format!("{:.2}", requests_per_second),
failures_per_second: format!("{:.2}", failures_per_second),
});
response_metrics.push(report::get_response_metric(
&method,
&name,
&request.response_times,
request.response_time_counter,
request.min_response_time,
request.max_response_time,
));
aggregate_total_count += total_request_count;
aggregate_fail_count += request.fail_count;
aggregate_response_time_counter += request.total_response_time;
aggregate_response_time_minimum = metrics::update_min_time(
aggregate_response_time_minimum,
request.min_response_time,
);
aggregate_response_time_maximum = metrics::update_max_time(
aggregate_response_time_maximum,
request.max_response_time,
);
aggregate_response_times =
metrics::merge_times(aggregate_response_times, request.response_times.clone());
}
let (aggregate_requests_per_second, aggregate_failures_per_second) =
metrics::per_second_calculations(
self.metrics.duration,
aggregate_total_count,
aggregate_fail_count,
);
request_metrics.push(report::RequestMetric {
method: "".to_string(),
name: "Aggregated".to_string(),
number_of_requests: aggregate_total_count,
number_of_failures: aggregate_fail_count,
response_time_average: format!(
"{:.2}",
aggregate_response_time_counter as f32 / aggregate_total_count as f32
),
response_time_minimum: aggregate_response_time_minimum,
response_time_maximum: aggregate_response_time_maximum,
requests_per_second: format!("{:.2}", aggregate_requests_per_second),
failures_per_second: format!("{:.2}", aggregate_failures_per_second),
});
response_metrics.push(report::get_response_metric(
"",
"Aggregated",
&aggregate_response_times,
aggregate_total_count,
aggregate_response_time_minimum,
aggregate_response_time_maximum,
));
let mut requests_rows = Vec::new();
for metric in request_metrics {
requests_rows.push(report::request_metrics_row(metric));
}
let mut responses_rows = Vec::new();
for metric in response_metrics {
responses_rows.push(report::response_metrics_row(metric));
}
let tasks_template: String;
if !self.configuration.no_task_metrics {
let mut task_metrics = Vec::new();
let mut aggregate_total_count = 0;
let mut aggregate_fail_count = 0;
let mut aggregate_task_time_counter: usize = 0;
let mut aggregate_task_time_minimum: usize = 0;
let mut aggregate_task_time_maximum: usize = 0;
let mut aggregate_task_times: BTreeMap<usize, usize> = BTreeMap::new();
for (task_set_counter, task_set) in self.metrics.tasks.iter().enumerate() {
for (task_counter, task) in task_set.iter().enumerate() {
if task_counter == 0 {
task_metrics.push(report::TaskMetric {
is_task_set: true,
task: "".to_string(),
name: task.taskset_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 = task.success_count + task.fail_count;
let (requests_per_second, failures_per_second) =
metrics::per_second_calculations(
self.metrics.duration,
total_run_count,
task.fail_count,
);
let average = match task.counter {
0 => 0.00,
_ => task.total_time as f32 / task.counter as f32,
};
task_metrics.push(report::TaskMetric {
is_task_set: false,
task: format!("{}.{}", task_set_counter, task_counter),
name: task.task_name.to_string(),
number_of_requests: total_run_count,
number_of_failures: task.fail_count,
response_time_average: format!("{:.2}", average),
response_time_minimum: task.min_time,
response_time_maximum: task.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 += task.fail_count;
aggregate_task_times =
metrics::merge_times(aggregate_task_times, task.times.clone());
aggregate_task_time_counter += &task.counter;
aggregate_task_time_minimum =
metrics::update_min_time(aggregate_task_time_minimum, task.min_time);
aggregate_task_time_maximum =
metrics::update_max_time(aggregate_task_time_maximum, task.max_time);
}
}
let (aggregate_requests_per_second, aggregate_failures_per_second) =
metrics::per_second_calculations(
self.metrics.duration,
aggregate_total_count,
aggregate_fail_count,
);
task_metrics.push(report::TaskMetric {
is_task_set: false,
task: "".to_string(),
name: "Aggregated".to_string(),
number_of_requests: aggregate_total_count,
number_of_failures: aggregate_fail_count,
response_time_average: format!(
"{:.2}",
aggregate_response_time_counter as f32 / aggregate_total_count as f32
),
response_time_minimum: aggregate_task_time_minimum,
response_time_maximum: aggregate_task_time_maximum,
requests_per_second: format!("{:.2}", aggregate_requests_per_second),
failures_per_second: format!("{:.2}", aggregate_failures_per_second),
});
let mut tasks_rows = Vec::new();
for metric in task_metrics {
tasks_rows.push(report::task_metrics_row(metric));
}
tasks_template = report::task_metrics_template(&tasks_rows.join("\n"));
} else {
tasks_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));
}
errors_template = report::errors_template(&error_rows.join("\n"));
} else {
errors_template = "".to_string();
}
let status_code_template: String;
if self.configuration.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 = metrics::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 =
metrics::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));
}
status_code_template =
report::status_code_metrics_template(&status_code_rows.join("\n"));
} else {
status_code_template = "".to_string();
}
let report = report::build_report(
&start_time,
&end_time,
&host,
report::GooseReportTemplates {
requests_template: &requests_rows.join("\n"),
responses_template: &responses_rows.join("\n"),
tasks_template: &tasks_template,
status_codes_template: &status_code_template,
errors_template: &errors_template,
},
);
if let Err(e) = report_file.write(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!(
"wrote html report file to: {}",
self.get_report_file_path().unwrap()
);
}
Ok(())
}
async fn start_attack(mut self, socket: Option<Socket>) -> Result<GooseAttack, GooseError> {
trace!("start_attack: socket({:?})", socket);
let mut goose_attack_run_state = self
.initialize_attack(socket)
.await
.expect("failed to initialize GooseAttackRunState");
self.set_attack_phase(&mut goose_attack_run_state, AttackPhase::Starting);
let mut sync_metrics_timer = std::time::Instant::now();
let mut sync_every = self.configuration.running_metrics.unwrap_or(10);
if sync_every > 10 {
sync_every = 10;
}
loop {
match self.attack_phase {
AttackPhase::Starting => self
.spawn_attack(&mut goose_attack_run_state)
.await
.expect("failed to start GooseAttack"),
AttackPhase::Running => self.monitor_attack(&mut goose_attack_run_state).await?,
AttackPhase::Stopping => {
self.stop_attack(&mut goose_attack_run_state).await?;
self.sync_metrics(&mut goose_attack_run_state).await?;
self.write_html_report(&mut goose_attack_run_state).await?;
break;
}
_ => panic!("GooseAttack entered an impossible phase"),
}
if util::timer_expired(sync_metrics_timer, sync_every) {
self.sync_metrics(&mut goose_attack_run_state).await?;
sync_metrics_timer = std::time::Instant::now();
}
}
Ok(self)
}
async fn receive_metrics(
&mut self,
goose_attack_run_state: &mut GooseAttackRunState,
) -> Result<bool, GooseError> {
let mut received_message = false;
let mut message = goose_attack_run_state.metrics_rx.try_recv();
while message.is_ok() {
received_message = true;
match message.unwrap() {
GooseMetric::Request(raw_request) => {
let formatted_log = match self.configuration.metrics_format.as_str() {
"json" => json!(raw_request).to_string(),
"csv" => GooseAttack::prepare_csv(&raw_request, goose_attack_run_state),
"raw" => format!("{:?}", raw_request),
_ => unreachable!(),
};
if let Some(file) = goose_attack_run_state.requests_file.as_mut() {
match file.write(format!("{}\n", formatted_log).as_ref()).await {
Ok(_) => (),
Err(e) => {
warn!(
"failed to write metrics to {}: {}",
self.get_requests_file_path().unwrap(),
e
);
}
}
}
if !raw_request.error.is_empty() {
self.record_error(&raw_request);
}
let key = format!("{} {}", raw_request.method, raw_request.name);
let mut merge_request = match self.metrics.requests.get(&key) {
Some(m) => m.clone(),
None => GooseRequest::new(&raw_request.name, raw_request.method, 0),
};
if raw_request.update {
if raw_request.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.set_response_time(raw_request.response_time);
if self.configuration.status_codes {
merge_request.set_status_code(raw_request.status_code);
}
if raw_request.success {
merge_request.success_count += 1;
} else {
merge_request.fail_count += 1;
}
}
self.metrics.requests.insert(key.to_string(), merge_request);
}
GooseMetric::Error(raw_error) => {
let error_key = format!(
"{}.{}.{}",
raw_error.error, raw_error.method, raw_error.name
);
let mut merge_error = match self.metrics.errors.get(&error_key) {
Some(error) => error.clone(),
None => GooseErrorMetric::new(
raw_error.method.clone(),
raw_error.name.to_string(),
raw_error.error.to_string(),
),
};
merge_error.occurrences += raw_error.occurrences;
self.metrics
.errors
.insert(error_key.to_string(), merge_error);
}
GooseMetric::Task(raw_task) => {
self.metrics.tasks[raw_task.taskset_index][raw_task.task_index]
.set_time(raw_task.run_time, raw_task.success);
}
}
message = goose_attack_run_state.metrics_rx.try_recv();
}
Ok(received_message)
}
pub fn record_error(&mut self, raw_request: &GooseRawRequest) {
if self.configuration.no_error_summary {
return;
}
let error_string = format!(
"{}.{}.{}",
raw_request.error, raw_request.method, raw_request.name
);
let mut error_metrics = match self.metrics.errors.get(&error_string) {
Some(m) => m.clone(),
None => GooseErrorMetric::new(
raw_request.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 trait GooseDefaultType<T> {
fn set_default(self, key: GooseDefault, value: T) -> Result<Box<Self>, GooseError>;
}
impl GooseDefaultType<&str> for GooseAttack {
fn set_default(mut self, key: GooseDefault, value: &str) -> Result<Box<Self>, GooseError> {
match key {
GooseDefault::HatchRate => self.defaults.hatch_rate = Some(value.to_string()),
GooseDefault::Host => self.defaults.host = Some(value.to_string()),
GooseDefault::LogFile => self.defaults.log_file = Some(value.to_string()),
GooseDefault::ReportFile => self.defaults.report_file = Some(value.to_string()),
GooseDefault::RequestsFile => self.defaults.requests_file = Some(value.to_string()),
GooseDefault::RequestsFormat => self.defaults.metrics_format = Some(value.to_string()),
GooseDefault::DebugFile => self.defaults.debug_file = Some(value.to_string()),
GooseDefault::DebugFormat => self.defaults.debug_format = Some(value.to_string()),
GooseDefault::ManagerBindHost => {
self.defaults.manager_bind_host = Some(value.to_string())
}
GooseDefault::ManagerHost => self.defaults.manager_host = Some(value.to_string()),
GooseDefault::Users
| GooseDefault::RunTime
| GooseDefault::LogLevel
| GooseDefault::Verbose
| GooseDefault::ThrottleRequests
| GooseDefault::ExpectWorkers
| GooseDefault::ManagerBindPort
| GooseDefault::ManagerPort => {
return Err(GooseError::InvalidOption {
option: format!("GooseDefault::{:?}", key),
value: value.to_string(),
detail: format!(
"set_default(GooseDefault::{:?}, {}) expected usize value, received &str",
key, value
),
});
}
GooseDefault::RunningMetrics
| GooseDefault::NoResetMetrics
| GooseDefault::NoMetrics
| GooseDefault::NoTaskMetrics
| GooseDefault::NoErrorSummary
| GooseDefault::NoDebugBody
| GooseDefault::StatusCodes
| GooseDefault::StickyFollow
| GooseDefault::Manager
| GooseDefault::NoHashCheck
| GooseDefault::Worker => {
return Err(GooseError::InvalidOption {
option: format!("GooseDefault::{:?}", key),
value: value.to_string(),
detail: format!(
"set_default(GooseDefault::{:?}, {}) expected bool value, received &str",
key, value
),
});
}
}
Ok(Box::new(self))
}
}
impl GooseDefaultType<usize> for GooseAttack {
fn set_default(mut self, key: GooseDefault, value: usize) -> Result<Box<Self>, GooseError> {
match key {
GooseDefault::Users => self.defaults.users = Some(value),
GooseDefault::RunTime => self.defaults.run_time = Some(value),
GooseDefault::RunningMetrics => self.defaults.running_metrics = Some(value),
GooseDefault::LogLevel => self.defaults.log_level = Some(value as u8),
GooseDefault::Verbose => self.defaults.verbose = Some(value as u8),
GooseDefault::ThrottleRequests => self.defaults.throttle_requests = Some(value),
GooseDefault::ExpectWorkers => self.defaults.expect_workers = Some(value as u16),
GooseDefault::ManagerBindPort => self.defaults.manager_bind_port = Some(value as u16),
GooseDefault::ManagerPort => self.defaults.manager_port = Some(value as u16),
GooseDefault::Host
| GooseDefault::HatchRate
| GooseDefault::LogFile
| GooseDefault::ReportFile
| GooseDefault::RequestsFile
| GooseDefault::RequestsFormat
| GooseDefault::DebugFile
| GooseDefault::DebugFormat
| GooseDefault::ManagerBindHost
| GooseDefault::ManagerHost => {
return Err(GooseError::InvalidOption {
option: format!("GooseDefault::{:?}", key),
value: format!("{}", value),
detail: format!(
"set_default(GooseDefault::{:?}, {}) expected &str value, received usize",
key, value
),
})
}
GooseDefault::NoResetMetrics
| GooseDefault::NoMetrics
| GooseDefault::NoTaskMetrics
| GooseDefault::NoErrorSummary
| GooseDefault::NoDebugBody
| GooseDefault::StatusCodes
| GooseDefault::StickyFollow
| GooseDefault::Manager
| GooseDefault::NoHashCheck
| GooseDefault::Worker => {
return Err(GooseError::InvalidOption {
option: format!("GooseDefault::{:?}", key),
value: format!("{}", value),
detail: format!(
"set_default(GooseDefault::{:?}, {}) expected bool value, received usize",
key, value
),
})
}
}
Ok(Box::new(self))
}
}
impl GooseDefaultType<bool> for GooseAttack {
fn set_default(mut self, key: GooseDefault, value: bool) -> Result<Box<Self>, GooseError> {
match key {
GooseDefault::NoResetMetrics => self.defaults.no_reset_metrics = Some(value),
GooseDefault::NoMetrics => self.defaults.no_metrics = Some(value),
GooseDefault::NoTaskMetrics => self.defaults.no_task_metrics = Some(value),
GooseDefault::NoErrorSummary => self.defaults.no_error_summary = Some(value),
GooseDefault::NoDebugBody => self.defaults.no_debug_body = Some(value),
GooseDefault::StatusCodes => self.defaults.status_codes = Some(value),
GooseDefault::StickyFollow => self.defaults.sticky_follow = Some(value),
GooseDefault::Manager => self.defaults.manager = Some(value),
GooseDefault::NoHashCheck => self.defaults.no_hash_check = Some(value),
GooseDefault::Worker => self.defaults.worker = Some(value),
GooseDefault::Host
| GooseDefault::LogFile
| GooseDefault::ReportFile
| GooseDefault::RequestsFile
| GooseDefault::RequestsFormat
| GooseDefault::RunningMetrics
| GooseDefault::DebugFile
| GooseDefault::DebugFormat
| GooseDefault::ManagerBindHost
| GooseDefault::ManagerHost => {
return Err(GooseError::InvalidOption {
option: format!("GooseDefault::{:?}", key),
value: format!("{}", value),
detail: format!(
"set_default(GooseDefault::{:?}, {}) expected &str value, received bool",
key, value
),
})
}
GooseDefault::Users
| GooseDefault::HatchRate
| GooseDefault::RunTime
| GooseDefault::LogLevel
| GooseDefault::Verbose
| GooseDefault::ThrottleRequests
| GooseDefault::ExpectWorkers
| GooseDefault::ManagerBindPort
| GooseDefault::ManagerPort => {
return Err(GooseError::InvalidOption {
option: format!("GooseDefault::{:?}", key),
value: format!("{}", value),
detail: format!(
"set_default(GooseDefault::{:?}, {}) expected usize value, received bool",
key, value
),
})
}
}
Ok(Box::new(self))
}
}
#[derive(Options, Debug, Clone, Serialize, Deserialize)]
pub struct GooseConfiguration {
#[options(short = "h")]
pub help: bool,
#[options(short = "V")]
pub version: bool,
#[options(short = "l", help = "Lists all tasks and exits\n")]
pub list: bool,
#[options(short = "H")]
pub host: String,
#[options(short = "u")]
pub users: Option<usize>,
#[options(short = "r", meta = "RATE")]
pub hatch_rate: Option<String>,
#[options(short = "t", meta = "TIME")]
pub run_time: String,
#[options(short = "g", count)]
pub log_level: u8,
#[options(meta = "NAME")]
pub log_file: String,
#[options(
count,
short = "v",
help = "Sets debug level (-v, -vv, etc)\n\nMetrics:"
)]
pub verbose: u8,
#[options(no_short, meta = "TIME")]
pub running_metrics: Option<usize>,
#[options(no_short)]
pub no_reset_metrics: bool,
#[options(no_short)]
pub no_metrics: bool,
#[options(no_short)]
pub no_task_metrics: bool,
#[options(no_short)]
pub no_error_summary: bool,
#[options(meta = "NAME")]
pub report_file: String,
#[options(short = "m", meta = "NAME")]
pub requests_file: String,
#[options(no_short, meta = "FORMAT")]
pub metrics_format: String,
#[options(short = "d", meta = "NAME")]
pub debug_file: String,
#[options(no_short, meta = "FORMAT")]
pub debug_format: String,
#[options(no_short)]
pub no_debug_body: bool,
#[options(no_short, help = "Tracks additional status code metrics\n\nAdvanced:")]
pub status_codes: bool,
#[options(no_short, meta = "VALUE")]
pub throttle_requests: usize,
#[options(
no_short,
help = "Follows base_url redirect with subsequent requests\n\nGaggle:"
)]
pub sticky_follow: bool,
#[options(no_short)]
pub manager: bool,
#[options(no_short, meta = "VALUE")]
pub expect_workers: Option<u16>,
#[options(no_short)]
pub no_hash_check: bool,
#[options(no_short, meta = "HOST")]
pub manager_bind_host: String,
#[options(no_short, meta = "PORT")]
pub manager_bind_port: u16,
#[options(no_short)]
pub worker: bool,
#[options(no_short, meta = "HOST")]
pub manager_host: String,
#[options(no_short, meta = "PORT")]
pub manager_port: u16,
}
fn weight_tasks(
task_set: &GooseTaskSet,
) -> (WeightedGooseTasks, WeightedGooseTasks, WeightedGooseTasks) {
trace!("weight_tasks for {}", task_set.name);
let mut sequenced_tasks: BTreeMap<usize, Vec<GooseTask>> = BTreeMap::new();
let mut sequenced_on_start_tasks: BTreeMap<usize, Vec<GooseTask>> = BTreeMap::new();
let mut sequenced_on_stop_tasks: BTreeMap<usize, Vec<GooseTask>> = BTreeMap::new();
let mut unsequenced_tasks: Vec<GooseTask> = Vec::new();
let mut unsequenced_on_start_tasks: Vec<GooseTask> = Vec::new();
let mut unsequenced_on_stop_tasks: Vec<GooseTask> = Vec::new();
let mut u: usize = 0;
let mut v: usize;
for task in &task_set.tasks {
if task.sequence > 0 {
if task.on_start {
if let Some(sequence) = sequenced_on_start_tasks.get_mut(&task.sequence) {
sequence.push(task.clone());
} else {
sequenced_on_start_tasks.insert(task.sequence, vec![task.clone()]);
}
}
if task.on_stop {
if let Some(sequence) = sequenced_on_stop_tasks.get_mut(&task.sequence) {
sequence.push(task.clone());
} else {
sequenced_on_stop_tasks.insert(task.sequence, vec![task.clone()]);
}
}
if !task.on_start && !task.on_stop {
if let Some(sequence) = sequenced_tasks.get_mut(&task.sequence) {
sequence.push(task.clone());
} else {
sequenced_tasks.insert(task.sequence, vec![task.clone()]);
}
}
} else {
if task.on_start {
unsequenced_on_start_tasks.push(task.clone());
}
if task.on_stop {
unsequenced_on_stop_tasks.push(task.clone());
}
if !task.on_start && !task.on_stop {
unsequenced_tasks.push(task.clone());
}
}
if u == 0 {
u = task.weight;
} else {
v = task.weight;
trace!("calculating greatest common denominator of {} and {}", u, v);
u = util::gcd(u, v);
trace!("inner gcd: {}", u);
}
}
debug!("gcd: {}", u);
let mut weighted_tasks: WeightedGooseTasks = Vec::new();
for (_sequence, tasks) in sequenced_tasks.iter() {
let mut sequence_weighted_tasks = Vec::new();
for task in tasks {
let weight = task.weight / u;
trace!(
"{}: {} has weight of {} (reduced with gcd to {})",
task.tasks_index,
task.name,
task.weight,
weight
);
let mut tasks = vec![(task.tasks_index, task.name.to_string()); weight];
sequence_weighted_tasks.append(&mut tasks);
}
weighted_tasks.push(sequence_weighted_tasks);
}
trace!("created weighted_tasks: {:?}", weighted_tasks);
let mut weighted_unsequenced_tasks = Vec::new();
for task in unsequenced_tasks {
let weight = task.weight / u;
trace!(
"{}: {} has weight of {} (reduced with gcd to {})",
task.tasks_index,
task.name,
task.weight,
weight
);
let mut tasks = vec![(task.tasks_index, task.name.to_string()); weight];
weighted_unsequenced_tasks.append(&mut tasks);
}
if !weighted_unsequenced_tasks.is_empty() {
weighted_tasks.push(weighted_unsequenced_tasks);
}
let mut weighted_on_start_tasks: WeightedGooseTasks = Vec::new();
for (_sequence, tasks) in sequenced_on_start_tasks.iter() {
let mut sequence_on_start_weighted_tasks = Vec::new();
for task in tasks {
let weight = task.weight / u;
trace!(
"{}: {} has weight of {} (reduced with gcd to {})",
task.tasks_index,
task.name,
task.weight,
weight
);
let mut tasks = vec![(task.tasks_index, task.name.to_string()); weight];
sequence_on_start_weighted_tasks.append(&mut tasks);
}
weighted_on_start_tasks.push(sequence_on_start_weighted_tasks);
}
trace!("created weighted_on_start_tasks: {:?}", weighted_tasks);
let mut weighted_on_start_unsequenced_tasks = Vec::new();
for task in unsequenced_on_start_tasks {
let weight = task.weight / u;
trace!(
"{}: {} has weight of {} (reduced with gcd to {})",
task.tasks_index,
task.name,
task.weight,
weight
);
let mut tasks = vec![(task.tasks_index, task.name.to_string()); weight];
weighted_on_start_unsequenced_tasks.append(&mut tasks);
}
weighted_on_start_tasks.push(weighted_on_start_unsequenced_tasks);
let mut weighted_on_stop_tasks: WeightedGooseTasks = Vec::new();
for (_sequence, tasks) in sequenced_on_stop_tasks.iter() {
let mut sequence_on_stop_weighted_tasks = Vec::new();
for task in tasks {
let weight = task.weight / u;
trace!(
"{}: {} has weight of {} (reduced with gcd to {})",
task.tasks_index,
task.name,
task.weight,
weight
);
let mut tasks = vec![(task.tasks_index, task.name.to_string()); weight];
sequence_on_stop_weighted_tasks.append(&mut tasks);
}
weighted_on_stop_tasks.push(sequence_on_stop_weighted_tasks);
}
trace!("created weighted_on_stop_tasks: {:?}", weighted_tasks);
let mut weighted_on_stop_unsequenced_tasks = Vec::new();
for task in unsequenced_on_stop_tasks {
let weight = task.weight / u;
trace!(
"{}: {} has weight of {} (reduced with gcd to {})",
task.tasks_index,
task.name,
task.weight,
weight
);
let mut tasks = vec![(task.tasks_index, task.name.to_string()); weight];
weighted_on_stop_unsequenced_tasks.append(&mut tasks);
}
weighted_on_stop_tasks.push(weighted_on_stop_unsequenced_tasks);
(
weighted_on_start_tasks,
weighted_tasks,
weighted_on_stop_tasks,
)
}
fn is_valid_host(host: &str) -> Result<bool, GooseError> {
Url::parse(host).map_err(|parse_error| GooseError::InvalidHost {
host: host.to_string(),
detail: "Invalid host.".to_string(),
parse_error,
})?;
Ok(true)
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn valid_host() {
assert_eq!(is_valid_host("http://example.com").is_ok(), true);
assert_eq!(is_valid_host("example.com").is_ok(), false);
assert_eq!(is_valid_host("http://example.com/").is_ok(), true);
assert_eq!(is_valid_host("example.com/").is_ok(), false);
assert_eq!(
is_valid_host("https://www.example.com/and/with/path").is_ok(),
true
);
assert_eq!(
is_valid_host("www.example.com/and/with/path").is_ok(),
false
);
assert_eq!(is_valid_host("foo://example.com").is_ok(), true);
assert_eq!(is_valid_host("file:///path/to/file").is_ok(), true);
assert_eq!(is_valid_host("/path/to/file").is_ok(), false);
assert_eq!(is_valid_host("http://").is_ok(), false);
assert_eq!(is_valid_host("http://foo").is_ok(), true);
assert_eq!(is_valid_host("http:///example.com").is_ok(), true);
assert_eq!(is_valid_host("http:// example.com").is_ok(), false);
}
#[test]
fn set_defaults() {
let host = "http://example.com/".to_string();
let users: usize = 10;
let run_time: usize = 10;
let hatch_rate = "2".to_string();
let log_level: usize = 1;
let log_file = "custom-goose.log".to_string();
let verbose: usize = 0;
let report_file = "custom-goose-report.html".to_string();
let requests_file = "custom-goose-metrics.log".to_string();
let metrics_format = "raw".to_string();
let debug_file = "custom-goose-debug.log".to_string();
let debug_format = "raw".to_string();
let throttle_requests: usize = 25;
let expect_workers: usize = 5;
let manager_bind_host = "127.0.0.1".to_string();
let manager_bind_port: usize = 1221;
let manager_host = "127.0.0.1".to_string();
let manager_port: usize = 1221;
let goose_attack = GooseAttack::initialize()
.unwrap()
.set_default(GooseDefault::Host, host.as_str())
.unwrap()
.set_default(GooseDefault::Users, users)
.unwrap()
.set_default(GooseDefault::RunTime, run_time)
.unwrap()
.set_default(GooseDefault::HatchRate, hatch_rate.as_str())
.unwrap()
.set_default(GooseDefault::LogLevel, log_level)
.unwrap()
.set_default(GooseDefault::LogFile, log_file.as_str())
.unwrap()
.set_default(GooseDefault::Verbose, verbose)
.unwrap()
.set_default(GooseDefault::RunningMetrics, 15)
.unwrap()
.set_default(GooseDefault::NoResetMetrics, true)
.unwrap()
.set_default(GooseDefault::NoMetrics, true)
.unwrap()
.set_default(GooseDefault::NoTaskMetrics, true)
.unwrap()
.set_default(GooseDefault::NoErrorSummary, true)
.unwrap()
.set_default(GooseDefault::ReportFile, report_file.as_str())
.unwrap()
.set_default(GooseDefault::RequestsFile, requests_file.as_str())
.unwrap()
.set_default(GooseDefault::RequestsFormat, metrics_format.as_str())
.unwrap()
.set_default(GooseDefault::DebugFile, debug_file.as_str())
.unwrap()
.set_default(GooseDefault::DebugFormat, debug_format.as_str())
.unwrap()
.set_default(GooseDefault::NoDebugBody, true)
.unwrap()
.set_default(GooseDefault::StatusCodes, true)
.unwrap()
.set_default(GooseDefault::ThrottleRequests, throttle_requests)
.unwrap()
.set_default(GooseDefault::StickyFollow, true)
.unwrap()
.set_default(GooseDefault::Manager, true)
.unwrap()
.set_default(GooseDefault::ExpectWorkers, expect_workers)
.unwrap()
.set_default(GooseDefault::NoHashCheck, true)
.unwrap()
.set_default(GooseDefault::ManagerBindHost, manager_bind_host.as_str())
.unwrap()
.set_default(GooseDefault::ManagerBindPort, manager_bind_port)
.unwrap()
.set_default(GooseDefault::Worker, true)
.unwrap()
.set_default(GooseDefault::ManagerHost, manager_host.as_str())
.unwrap()
.set_default(GooseDefault::ManagerPort, manager_port)
.unwrap();
assert!(goose_attack.defaults.host == Some(host));
assert!(goose_attack.defaults.users == Some(users));
assert!(goose_attack.defaults.run_time == Some(run_time));
assert!(goose_attack.defaults.hatch_rate == Some(hatch_rate));
assert!(goose_attack.defaults.log_level == Some(log_level as u8));
assert!(goose_attack.defaults.log_file == Some(log_file));
assert!(goose_attack.defaults.no_debug_body == Some(true));
assert!(goose_attack.defaults.verbose == Some(verbose as u8));
assert!(goose_attack.defaults.running_metrics == Some(15));
assert!(goose_attack.defaults.no_reset_metrics == Some(true));
assert!(goose_attack.defaults.no_metrics == Some(true));
assert!(goose_attack.defaults.no_task_metrics == Some(true));
assert!(goose_attack.defaults.no_error_summary == Some(true));
assert!(goose_attack.defaults.report_file == Some(report_file));
assert!(goose_attack.defaults.requests_file == Some(requests_file));
assert!(goose_attack.defaults.metrics_format == Some(metrics_format));
assert!(goose_attack.defaults.debug_file == Some(debug_file));
assert!(goose_attack.defaults.debug_format == Some(debug_format));
assert!(goose_attack.defaults.status_codes == Some(true));
assert!(goose_attack.defaults.throttle_requests == Some(throttle_requests));
assert!(goose_attack.defaults.sticky_follow == Some(true));
assert!(goose_attack.defaults.manager == Some(true));
assert!(goose_attack.defaults.expect_workers == Some(expect_workers as u16));
assert!(goose_attack.defaults.no_hash_check == Some(true));
assert!(goose_attack.defaults.manager_bind_host == Some(manager_bind_host));
assert!(goose_attack.defaults.manager_bind_port == Some(manager_bind_port as u16));
assert!(goose_attack.defaults.worker == Some(true));
assert!(goose_attack.defaults.manager_host == Some(manager_host));
assert!(goose_attack.defaults.manager_port == Some(manager_port as u16));
}
}