Skip to content

Commit

Permalink
Merge pull request #516 from mTsBucy1/fix-csv
Browse files Browse the repository at this point in the history
Refactor CSV Logging
  • Loading branch information
jeremyandrews committed Sep 9, 2022
2 parents 5dd61d1 + a273d6b commit 1f665a8
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 100 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## 0.16.4-dev
- [#512](https://github.com/tag1consulting/goose/pull/512) include proper HTTP method and path in logs and html report when using `GooseRequest::builder()`
- [#514](https://github.com/tag1consulting/goose/pull/514) fix panic when an empty wait time interval is set
- [#516](https://github.com/tag1consulting/goose/pull/516) fix unescaped inner quotes in csv logs

## 0.16.3 July 17, 2022
- [#498](https://github.com/tag1consulting/goose/issues/498) ignore `GooseDefault::Host` if set to an empty string
Expand Down
202 changes: 102 additions & 100 deletions src/logger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,32 @@ pub(crate) type GooseLoggerJoinHandle =
/// Optional unbounded sender from all GooseUsers to logger thread, if enabled.
pub(crate) type GooseLoggerTx = Option<flume::Sender<Option<GooseLog>>>;

/// Formats comma separated arguments into a csv row according to RFC 4180. Every argument has to be `Display`.
///
/// Specifically, this encloses all values with double quotes `"` which contain a comma, a quote or a new line.
/// Inner quotes are doubled according to RFC 4180 2.7.
/// The fields are joined by commas `,`, but *not* terminated with a line ending.
#[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') {
// Enclose in quotes and escape inner quotes
format!("\"{}\"", s.replace('"', "\"\""))
} else {
// Because into_iter is not available in edition 2018
s.clone()
}
})
.collect::<Vec<String>>()
.join(",")
}};
}
pub use format_csv_row;

/// If enabled, the logger thread can accept any of the following types of messages, and will
/// write them to the correct log file.
#[derive(Debug, Deserialize, Serialize)]
Expand Down Expand Up @@ -214,15 +240,12 @@ impl FromStr for GooseLogFormat {

// @TODO this should be automatically derived from the structure.
fn debug_csv_header() -> String {
// No quotes needed in header.
format!("{},{},{},{}", "tag", "request", "header", "body")
format_csv_row!("tag", "request", "header", "body")
}

// @TODO this should be automatically derived from the structure.
fn error_csv_header() -> String {
// No quotes needed in header.
format!(
"{},{},{},{},{},{},{},{},{}",
format_csv_row!(
"elapsed",
"raw",
"name",
Expand All @@ -237,9 +260,7 @@ fn error_csv_header() -> String {

// @TODO this should be automatically derived from the structure.
fn requests_csv_header() -> String {
// No quotes needed in header.
format!(
"{},{},{},{},{},{},{},{},{},{},{},{},{}",
format_csv_row!(
"elapsed",
"raw",
"name",
Expand All @@ -258,29 +279,27 @@ fn requests_csv_header() -> String {

// @TODO this should be automatically derived from the structure.
fn transactions_csv_header() -> String {
format!(
// No quotes needed in header.
"{},{},{},{},{},{},{}",
"elapsed", "scenario_index", "transaction_index", "name", "run_time", "success", "user",
format_csv_row!(
"elapsed",
"scenario_index",
"transaction_index",
"name",
"run_time",
"success",
"user",
)
}

// @TODO this should be automatically derived from the structure.
fn scenarios_csv_header() -> String {
format!(
// No quotes needed in header.
"{},{},{},{},{}",
"elapsed", "name", "index", "run_time", "user",
)
format_csv_row!("elapsed", "name", "index", "run_time", "user",)
}

/// Two traits that must be implemented by all loggers provided through this thread.
pub(crate) trait GooseLogger<T> {
/// Converts a rust structure to a formatted string.
/// @TODO: rework with .to_string()
fn format_message(&self, message: T) -> String;
/// Helper that makes a best-effort to convert a supported rust structure to a CSV row.
fn prepare_csv(&self, message: &T) -> String;
}
/// Traits for GooseDebug logs.
impl GooseLogger<GooseDebug> for GooseConfiguration {
Expand All @@ -294,24 +313,22 @@ impl GooseLogger<GooseDebug> for GooseConfiguration {
GooseLogFormat::Raw => format!("{:?}", message),
// Pretty format is Debug Pretty output for GooseRawRequest structure.
GooseLogFormat::Pretty => format!("{:#?}", message),
// Not yet implemented.
GooseLogFormat::Csv => self.prepare_csv(&message),
// Csv format with `,` separator and `"` quotes.
GooseLogFormat::Csv => {
// @TODO: properly handle Option<>; flatten raw request in own columns
format_csv_row!(
message.tag,
format!("{:?}", message.request),
format!("{:?}", message.header),
format!("{:?}", message.body)
)
}
}
} else {
// A log format is required.
unreachable!()
}
}

/// Converts a GooseDebug structure to a CSV row.
fn prepare_csv(&self, debug: &GooseDebug) -> String {
// Put quotes around all fields, as they are all strings.
// @TODO: properly handle Option<>; also, escape inner quotes etc.
format!(
"\"{}\",\"{:?}\",\"{:?}\",\"{:?}\"",
debug.tag, debug.request, debug.header, debug.body
)
}
}
/// Traits for GooseErrorMetric logs.
impl GooseLogger<GooseErrorMetric> for GooseConfiguration {
Expand All @@ -325,31 +342,26 @@ impl GooseLogger<GooseErrorMetric> for GooseConfiguration {
GooseLogFormat::Raw => format!("{:?}", message),
// Pretty format is Debug Pretty output for GooseErrorMetric structure.
GooseLogFormat::Pretty => format!("{:#?}", message),
// Not yet implemented.
GooseLogFormat::Csv => self.prepare_csv(&message),
// Csv format with `,` separator and `"` quotes.
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 {
// A log format is required.
unreachable!()
}
}

/// Converts a GooseErrorMetric structure to a CSV row.
fn prepare_csv(&self, request: &GooseErrorMetric) -> String {
format!(
// Put quotes around name, url, final_url and error as they are strings.
"{},\"{:?}\",\"{}\",\"{}\",{},{},{},{},\"{}\"",
request.elapsed,
request.raw,
request.name,
request.final_url,
request.redirected,
request.response_time,
request.status_code,
request.user,
request.error,
)
}
}
/// Traits for GooseRequestMetric logs.
impl GooseLogger<GooseRequestMetric> for GooseConfiguration {
Expand All @@ -363,35 +375,30 @@ impl GooseLogger<GooseRequestMetric> for GooseConfiguration {
GooseLogFormat::Raw => format!("{:?}", message),
// Pretty format is Debug Pretty output for GooseRequestMetric structure.
GooseLogFormat::Pretty => format!("{:#?}", message),
// Not yet implemented.
GooseLogFormat::Csv => self.prepare_csv(&message),
// Csv format with `,` separator and `"` quotes.
GooseLogFormat::Csv => {
format_csv_row!(
message.elapsed,
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 {
// A log format is required.
unreachable!()
}
}

/// Converts a GooseRequestMetric structure to a CSV row.
fn prepare_csv(&self, request: &GooseRequestMetric) -> String {
format!(
// Put quotes around name, url and final_url as they are strings.
"{},\"{:?}\",\"{}\",\"{}\",{},{},{},{},{},{},{},{},{}",
request.elapsed,
request.raw,
request.name,
request.final_url,
request.redirected,
request.response_time,
request.status_code,
request.success,
request.update,
request.user,
request.error,
request.coordinated_omission_elapsed,
request.user_cadence,
)
}
}
/// Traits for TransactionMetric logs.
impl GooseLogger<TransactionMetric> for GooseConfiguration {
Expand All @@ -405,29 +412,24 @@ impl GooseLogger<TransactionMetric> for GooseConfiguration {
GooseLogFormat::Raw => format!("{:?}", message),
// Pretty format is Debug Pretty output for TransactionMetric structure.
GooseLogFormat::Pretty => format!("{:#?}", message),
// Not yet implemented.
GooseLogFormat::Csv => self.prepare_csv(&message),
// Csv format with `,` separator and `"` quotes.
GooseLogFormat::Csv => {
format_csv_row!(
message.elapsed,
message.scenario_index,
message.transaction_index,
message.name,
message.run_time,
message.success,
message.user,
)
}
}
} else {
// A log format is required.
unreachable!()
}
}

/// Converts a TransactionMetric structure to a CSV row.
fn prepare_csv(&self, request: &TransactionMetric) -> String {
format!(
// Put quotes around name as it is a string.
"{},{},{},\"{}\",{},{},{}",
request.elapsed,
request.scenario_index,
request.transaction_index,
request.name,
request.run_time,
request.success,
request.user,
)
}
}

/// Traits for ScenarioMetric logs.
Expand All @@ -442,22 +444,22 @@ impl GooseLogger<ScenarioMetric> for GooseConfiguration {
GooseLogFormat::Raw => format!("{:?}", message),
// Pretty format is Debug Pretty output for ScenarioMetric structure.
GooseLogFormat::Pretty => format!("{:#?}", message),
// Not yet implemented.
GooseLogFormat::Csv => self.prepare_csv(&message),
// Csv format with `,` separator and `"` quotes.
GooseLogFormat::Csv => {
format_csv_row!(
message.elapsed,
message.name,
message.index,
message.run_time,
message.user,
)
}
}
} else {
// A log format is required.
unreachable!()
}
}

/// Converts a ScenarioMetric structure to a CSV row.
fn prepare_csv(&self, scenario: &ScenarioMetric) -> String {
format!(
"{},{},{},{},{}",
scenario.elapsed, scenario.name, scenario.index, scenario.run_time, scenario.user,
)
}
}

/// Helpers to launch and control configured loggers.
Expand Down
9 changes: 9 additions & 0 deletions tests/logs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -741,3 +741,12 @@ async fn test_all_logs_pretty() {
async fn test_all_logs_pretty_gaggle() {
run_gaggle_test(TestType::All, "pretty").await;
}

#[test]
fn test_csv_row_macro() {
let row = goose::logger::format_csv_row!(1, '"', "hello , ");
assert_eq!(r#"1,"""","hello , ""#, row);

let row = goose::logger::format_csv_row!(format!("{:?}", (1, 2)), "你好", "A\nNew Day",);
assert_eq!("\"(1, 2)\",你好,\"A\nNew Day\"", row);
}

0 comments on commit 1f665a8

Please sign in to comment.