use regex::RegexSet;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::str::FromStr;
use tokio::fs::File;
use tokio::io::{AsyncWriteExt, BufWriter};
use crate::config::{GooseConfigure, GooseValue};
use crate::goose::GooseDebug;
use crate::metrics::{GooseErrorMetric, GooseRequestMetric, ScenarioMetric, TransactionMetric};
use crate::{GooseConfiguration, GooseDefaults, GooseError};
pub(crate) type GooseLoggerJoinHandle =
Option<tokio::task::JoinHandle<std::result::Result<(), GooseError>>>;
pub(crate) type GooseLoggerTx = Option<flume::Sender<Option<GooseLog>>>;
#[macro_export]
#[doc(hidden)]
macro_rules! format_csv_row {
($( $field:expr ),+ $(,)?) => {{
[$( $field.to_string() ),*]
.iter()
.map(|s| {
if s.contains('"') || s.contains(',') || s.contains('\n') {
format!("\"{}\"", s.replace('"', "\"\""))
} else {
s.clone()
}
})
.collect::<Vec<String>>()
.join(",")
}};
}
pub use format_csv_row;
#[derive(Debug, Deserialize, Serialize)]
pub enum GooseLog {
Debug(GooseDebug),
Error(GooseErrorMetric),
Request(GooseRequestMetric),
Transaction(TransactionMetric),
Scenario(ScenarioMetric),
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub enum GooseLogFormat {
Csv,
Json,
Raw,
Pretty,
}
impl FromStr for GooseLogFormat {
type Err = GooseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let log_format = RegexSet::new([
r"(?i)^csv$",
r"(?i)^(json|jsn)$",
r"(?i)^raw$",
r"(?i)^pretty$",
])
.expect("failed to compile log_format RegexSet");
let matches = log_format.matches(s);
if matches.matched(0) {
Ok(GooseLogFormat::Csv)
} else if matches.matched(1) {
Ok(GooseLogFormat::Json)
} else if matches.matched(2) {
Ok(GooseLogFormat::Raw)
} else if matches.matched(3) {
Ok(GooseLogFormat::Pretty)
} else {
Err(GooseError::InvalidOption {
option: format!("GooseLogFormat::{s:?}"),
value: s.to_string(),
detail: "Invalid log_format, expected: csv, json, or raw".to_string(),
})
}
}
}
fn debug_csv_header() -> String {
format_csv_row!("tag", "request", "header", "body")
}
fn error_csv_header() -> String {
format_csv_row!(
"elapsed",
"raw",
"name",
"final_url",
"redirected",
"response_time",
"status_code",
"user",
"error",
)
}
fn requests_csv_header() -> String {
format_csv_row!(
"elapsed",
"scenario_index",
"scenario_name",
"transaction_index",
"transaction_name",
"raw",
"name",
"final_url",
"redirected",
"response_time",
"status_code",
"success",
"update",
"user",
"error",
"coordinated_omission_elapsed",
"user_cadence",
)
}
fn transactions_csv_header() -> String {
format_csv_row!(
"elapsed",
"scenario_index",
"transaction_index",
"name",
"run_time",
"success",
"user",
)
}
fn scenarios_csv_header() -> String {
format_csv_row!("elapsed", "name", "index", "run_time", "user",)
}
pub(crate) trait GooseLogger<T> {
fn format_message(&self, message: T) -> String;
}
impl GooseLogger<GooseDebug> for GooseConfiguration {
fn format_message(&self, message: GooseDebug) -> String {
if let Some(debug_format) = self.debug_format.as_ref() {
match debug_format {
GooseLogFormat::Json => json!(message).to_string(),
GooseLogFormat::Raw => format!("{message:?}"),
GooseLogFormat::Pretty => format!("{message:#?}"),
GooseLogFormat::Csv => {
format_csv_row!(
message.tag,
format!("{:?}", message.request),
format!("{:?}", message.header),
format!("{:?}", message.body)
)
}
}
} else {
unreachable!()
}
}
}
impl GooseLogger<GooseErrorMetric> for GooseConfiguration {
fn format_message(&self, message: GooseErrorMetric) -> String {
if let Some(error_format) = self.error_format.as_ref() {
match error_format {
GooseLogFormat::Json => json!(message).to_string(),
GooseLogFormat::Raw => format!("{message:?}"),
GooseLogFormat::Pretty => format!("{message:#?}"),
GooseLogFormat::Csv => {
format_csv_row!(
message.elapsed,
format!("{:?}", message.raw),
message.name,
message.final_url,
message.redirected,
message.response_time,
message.status_code,
message.user,
message.error,
)
}
}
} else {
unreachable!()
}
}
}
impl GooseLogger<GooseRequestMetric> for GooseConfiguration {
fn format_message(&self, message: GooseRequestMetric) -> String {
if let Some(request_format) = self.request_format.as_ref() {
match request_format {
GooseLogFormat::Json => json!(message).to_string(),
GooseLogFormat::Raw => format!("{message:?}"),
GooseLogFormat::Pretty => format!("{message:#?}"),
GooseLogFormat::Csv => {
format_csv_row!(
message.elapsed,
message.scenario_index,
message.scenario_name,
message.transaction_index,
message.transaction_name,
format!("{:?}", message.raw),
message.name,
message.final_url,
message.redirected,
message.response_time,
message.status_code,
message.success,
message.update,
message.user,
message.error,
message.coordinated_omission_elapsed,
message.user_cadence,
)
}
}
} else {
unreachable!()
}
}
}
impl GooseLogger<TransactionMetric> for GooseConfiguration {
fn format_message(&self, message: TransactionMetric) -> String {
if let Some(transaction_format) = self.transaction_format.as_ref() {
match transaction_format {
GooseLogFormat::Json => json!(message).to_string(),
GooseLogFormat::Raw => format!("{message:?}"),
GooseLogFormat::Pretty => format!("{message:#?}"),
GooseLogFormat::Csv => {
format_csv_row!(
message.elapsed,
message.scenario_index,
message.transaction_index,
message.name,
message.run_time,
message.success,
message.user,
)
}
}
} else {
unreachable!()
}
}
}
impl GooseLogger<ScenarioMetric> for GooseConfiguration {
fn format_message(&self, message: ScenarioMetric) -> String {
if let Some(scenario_format) = self.scenario_format.as_ref() {
match scenario_format {
GooseLogFormat::Json => json!(message).to_string(),
GooseLogFormat::Raw => format!("{message:?}"),
GooseLogFormat::Pretty => format!("{message:#?}"),
GooseLogFormat::Csv => {
format_csv_row!(
message.elapsed,
message.name,
message.index,
message.run_time,
message.user,
)
}
}
} else {
unreachable!()
}
}
}
impl GooseConfiguration {
pub(crate) fn configure_loggers(&mut self, defaults: &GooseDefaults) {
self.debug_log = self
.get_value(vec![
GooseValue {
value: Some(self.debug_log.to_string()),
filter: self.debug_log.is_empty(),
message: "",
},
GooseValue {
value: defaults.debug_log.clone(),
filter: defaults.debug_log.is_none(),
message: "",
},
])
.unwrap_or_default();
self.debug_format = self.get_value(vec![
GooseValue {
value: self.debug_format.clone(),
filter: self.debug_format.is_none(),
message: "",
},
GooseValue {
value: defaults.debug_format.clone(),
filter: defaults.debug_format.is_none(),
message: "",
},
GooseValue {
value: Some(GooseLogFormat::Json),
filter: false,
message: "",
},
]);
self.error_log = self
.get_value(vec![
GooseValue {
value: Some(self.error_log.to_string()),
filter: self.error_log.is_empty(),
message: "",
},
GooseValue {
value: defaults.error_log.clone(),
filter: defaults.error_log.is_none(),
message: "",
},
])
.unwrap_or_default();
self.error_format = self.get_value(vec![
GooseValue {
value: self.error_format.clone(),
filter: self.error_format.is_none(),
message: "",
},
GooseValue {
value: defaults.error_format.clone(),
filter: defaults.error_format.is_none(),
message: "",
},
GooseValue {
value: Some(GooseLogFormat::Json),
filter: false,
message: "",
},
]);
self.request_log = self
.get_value(vec![
GooseValue {
value: Some(self.request_log.to_string()),
filter: self.request_log.is_empty(),
message: "",
},
GooseValue {
value: defaults.request_log.clone(),
filter: defaults.request_log.is_none(),
message: "",
},
])
.unwrap_or_default();
self.request_format = self.get_value(vec![
GooseValue {
value: self.request_format.clone(),
filter: self.request_format.is_none(),
message: "",
},
GooseValue {
value: defaults.request_format.clone(),
filter: defaults.request_format.is_none(),
message: "",
},
GooseValue {
value: Some(GooseLogFormat::Json),
filter: false,
message: "",
},
]);
self.request_body = self
.get_value(vec![
GooseValue {
value: Some(self.request_body),
filter: !self.request_body,
message: "request_body",
},
GooseValue {
value: defaults.request_body,
filter: defaults.request_body.is_none(),
message: "request_body",
},
])
.unwrap_or(false);
self.transaction_log = self
.get_value(vec![
GooseValue {
value: Some(self.transaction_log.to_string()),
filter: self.transaction_log.is_empty(),
message: "",
},
GooseValue {
value: defaults.transaction_log.clone(),
filter: defaults.transaction_log.is_none(),
message: "",
},
])
.unwrap_or_default();
self.transaction_format = self.get_value(vec![
GooseValue {
value: self.transaction_format.clone(),
filter: self.transaction_format.is_none(),
message: "",
},
GooseValue {
value: defaults.transaction_format.clone(),
filter: defaults.transaction_format.is_none(),
message: "",
},
GooseValue {
value: Some(GooseLogFormat::Json),
filter: false,
message: "",
},
]);
self.scenario_log = self
.get_value(vec![
GooseValue {
value: Some(self.scenario_log.to_string()),
filter: self.scenario_log.is_empty(),
message: "",
},
GooseValue {
value: defaults.scenario_log.clone(),
filter: defaults.scenario_log.is_none(),
message: "",
},
])
.unwrap_or_default();
self.scenario_format = self.get_value(vec![
GooseValue {
value: self.scenario_format.clone(),
filter: self.scenario_format.is_none(),
message: "",
},
GooseValue {
value: defaults.scenario_format.clone(),
filter: defaults.scenario_format.is_none(),
message: "",
},
GooseValue {
value: Some(GooseLogFormat::Json),
filter: false,
message: "",
},
]);
}
pub(crate) async fn setup_loggers(
&mut self,
defaults: &GooseDefaults,
) -> Result<(GooseLoggerJoinHandle, GooseLoggerTx), GooseError> {
self.configure_loggers(defaults);
if self.debug_log.is_empty()
&& self.request_log.is_empty()
&& self.transaction_log.is_empty()
&& self.scenario_log.is_empty()
&& self.error_log.is_empty()
{
return Ok((None, None));
}
let (all_threads_logger_tx, logger_rx): (
flume::Sender<Option<GooseLog>>,
flume::Receiver<Option<GooseLog>>,
) = flume::unbounded();
let configuration = self.clone();
let logger_handle = tokio::spawn(async move { configuration.logger_main(logger_rx).await });
Ok((Some(logger_handle), Some(all_threads_logger_tx)))
}
async fn open_log_file(
&self,
log_file_path: &str,
log_file_type: &str,
buffer_capacity: usize,
) -> std::option::Option<tokio::io::BufWriter<tokio::fs::File>> {
if log_file_path.is_empty() {
None
} else {
match File::create(log_file_path).await {
Ok(f) => {
info!("[logger]: writing {log_file_type} to: {log_file_path}");
Some(BufWriter::with_capacity(buffer_capacity, f))
}
Err(e) => {
error!("[logger]: failed to create {log_file_type} ({log_file_path}): {e}");
None
}
}
}
}
async fn write_to_log_file(
&self,
log_file: &mut tokio::io::BufWriter<tokio::fs::File>,
formatted_message: String,
) {
match log_file
.write(format!("{formatted_message}\n").as_ref())
.await
{
Ok(_) => (),
Err(e) => {
warn!("failed to write to {}: {e}", &self.debug_log);
}
}
}
pub(crate) async fn logger_main(
self: GooseConfiguration,
receiver: flume::Receiver<Option<GooseLog>>,
) -> Result<(), GooseError> {
let mut debug_log = self
.open_log_file(
&self.debug_log,
"debug file",
if self.no_debug_body {
64 * 1024
} else {
8 * 1024 * 1024
},
)
.await;
if self.debug_format == Some(GooseLogFormat::Csv) {
if let Some(log_file) = debug_log.as_mut() {
self.write_to_log_file(log_file, debug_csv_header()).await;
}
}
let mut error_log = self
.open_log_file(&self.error_log, "error log", 64 * 1024)
.await;
if self.error_format == Some(GooseLogFormat::Csv) {
if let Some(log_file) = error_log.as_mut() {
self.write_to_log_file(log_file, error_csv_header()).await;
}
}
let mut request_log = self
.open_log_file(
&self.request_log,
"request log",
if self.request_body {
8 * 1024 * 1024
} else {
64 * 1024
},
)
.await;
if self.request_format == Some(GooseLogFormat::Csv) {
if let Some(log_file) = request_log.as_mut() {
self.write_to_log_file(log_file, requests_csv_header())
.await;
}
}
let mut transaction_log = self
.open_log_file(&self.transaction_log, "transaction log", 64 * 1024)
.await;
if self.transaction_format == Some(GooseLogFormat::Csv) {
if let Some(log_file) = transaction_log.as_mut() {
self.write_to_log_file(log_file, transactions_csv_header())
.await;
}
}
let mut scenario_log = self
.open_log_file(&self.scenario_log, "scenario log", 64 * 1024)
.await;
if self.scenario_format == Some(GooseLogFormat::Csv) {
if let Some(log_file) = scenario_log.as_mut() {
self.write_to_log_file(log_file, scenarios_csv_header())
.await;
}
}
while let Ok(received_message) = receiver.recv_async().await {
if let Some(message) = received_message {
let formatted_message;
if let Some(log_file) = match message {
GooseLog::Debug(debug_message) => {
formatted_message = self.format_message(debug_message).to_string();
debug_log.as_mut()
}
GooseLog::Error(error_message) => {
formatted_message = self.format_message(error_message).to_string();
error_log.as_mut()
}
GooseLog::Request(request_message) => {
formatted_message = self.format_message(request_message).to_string();
request_log.as_mut()
}
GooseLog::Transaction(transaction_message) => {
formatted_message = self.format_message(transaction_message).to_string();
transaction_log.as_mut()
}
GooseLog::Scenario(scenario_message) => {
formatted_message = self.format_message(scenario_message).to_string();
scenario_log.as_mut()
}
} {
self.write_to_log_file(log_file, formatted_message).await;
}
} else {
break;
}
}
if let Some(debug_log_file) = debug_log.as_mut() {
info!("[logger]: flushing debug_log: {}", &self.debug_log);
let _ = debug_log_file.flush().await;
};
if let Some(requests_log_file) = request_log.as_mut() {
info!("[logger]: flushing request_log: {}", &self.request_log);
let _ = requests_log_file.flush().await;
}
if let Some(transactions_log_file) = transaction_log.as_mut() {
info!(
"[logger]: flushing transaction_log: {}",
&self.transaction_log
);
let _ = transactions_log_file.flush().await;
}
if let Some(scenarios_log_file) = scenario_log.as_mut() {
info!("[logger]: flushing scenario: {}", &self.scenario_log);
let _ = scenarios_log_file.flush().await;
}
if let Some(error_log_file) = error_log.as_mut() {
info!("[logger]: flushing error_log: {}", &self.error_log);
let _ = error_log_file.flush().await;
};
Ok(())
}
}