Skip to content

Commit

Permalink
#246: Add requests per second graph to the HTML report.
Browse files Browse the repository at this point in the history
  • Loading branch information
slashrsm committed Nov 8, 2021
1 parent b9e3a80 commit 537e093
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 3 deletions.
56 changes: 53 additions & 3 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ const DEFAULT_PORT: &str = "5115";
/// --debug-format FORMAT Sets debug log format (csv, json, raw, pretty)
/// --no-debug-body Do not include the response body in the debug log
/// --status-codes Tracks additional status code metrics
/// --requests-per-second Tracks additional requests per second metrics
/// --rps-bucket Size (in seconds) of the time-based requests per second bucket (default: 10)
///
/// Advanced:
/// --no-telnet Doesn't enable telnet Controller
Expand Down Expand Up @@ -187,10 +189,18 @@ pub struct GooseConfiguration {
#[options(no_short)]
pub no_debug_body: bool,
/// Tracks additional status code metrics
// Add a blank line and then an Advanced: header after this option
#[options(no_short, help = "Tracks additional status code metrics\n\nAdvanced:")]
#[options(no_short, help = "Tracks additional status code metrics")]
pub status_codes: bool,

/// Tracks additional request per second metrics
#[options(no_short, help = "Tracks additional request per second metrics")]
pub requests_per_second: bool,
// Size (in seconds) of the time-based requests per second bucket (default: 10)
// Add a blank line and then an Advanced: header after this option
#[options(
no_short,
help = "Size of the time bucket in time-based requests per second calculations\n\nAdvanced:"
)]
pub rps_bucket: usize,
/// Doesn't enable telnet Controller
#[options(no_short)]
pub no_telnet: bool,
Expand Down Expand Up @@ -319,6 +329,10 @@ pub(crate) struct GooseDefaults {
pub co_mitigation: Option<GooseCoordinatedOmissionMitigation>,
/// An optional default to track additional status code metrics.
pub status_codes: Option<bool>,
/// An optional default to track requests per second metrics.
pub requests_per_second: Option<bool>,
// An optional default size of the requests per second bucket size.
pub rps_bucket: Option<usize>,
/// An optional default maximum requests per second.
pub throttle_requests: Option<usize>,
/// An optional default to follows base_url redirect with subsequent request.
Expand Down Expand Up @@ -1495,6 +1509,42 @@ impl GooseConfiguration {
])
.unwrap_or(false);

// Configure `requests_per_second`.
self.requests_per_second = self
.get_value(vec![
// Use --requests-per-second if set.
GooseValue {
value: Some(self.requests_per_second),
filter: !self.requests_per_second,
message: "requests_per_second",
},
// Otherwise use GooseDefault if set.
GooseValue {
value: defaults.requests_per_second,
filter: defaults.requests_per_second.is_none() || self.worker,
message: "requests_per_second",
},
])
.unwrap_or(false);

// Configure `rps_bucket`.
self.rps_bucket = self
.get_value(vec![
// Use --requests-per-second-bucket if set.
GooseValue {
value: Some(self.rps_bucket),
filter: self.rps_bucket == 0,
message: "rps_bucket",
},
// Otherwise use GooseDefault if set.
GooseValue {
value: defaults.rps_bucket,
filter: defaults.rps_bucket.is_none() || self.worker,
message: "rps_bucket",
},
])
.unwrap_or(10);

// Configure `no_telnet`.
self.no_telnet = self
.get_value(vec![
Expand Down
64 changes: 64 additions & 0 deletions src/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
//! [`GooseErrorMetrics`] are displayed in tables.

use chrono::prelude::*;
use chrono::Utc;
use http::StatusCode;
use itertools::Itertools;
use num_format::{Locale, ToFormattedString};
Expand Down Expand Up @@ -396,6 +397,10 @@ pub struct GooseRequestMetricAggregate {
/// The hash is primarily used when running a distributed Gaggle, allowing the Manager to confirm
/// that all Workers are running the same load test plan.
pub load_test_hash: u64,
// Timestamp when the metric collection was started.
start_time: i64,
// Counts requests per time bucket for time-based requests per second metric calculation.
pub requests_per_second: HashMap<usize, u64>,
}
impl GooseRequestMetricAggregate {
/// Create a new GooseRequestMetricAggregate object.
Expand All @@ -410,6 +415,8 @@ impl GooseRequestMetricAggregate {
success_count: 0,
fail_count: 0,
load_test_hash,
start_time: Utc::now().timestamp(),
requests_per_second: HashMap::new(),
}
}

Expand Down Expand Up @@ -451,6 +458,32 @@ impl GooseRequestMetricAggregate {
self.status_code_counts.insert(status_code, counter);
debug!("incremented {} counter: {}", status_code, counter);
}

/// Record request in requests per second metric.
pub(crate) fn record_requests_per_second(&mut self, bucket_size: usize) {
let time_bucket = (Utc::now().timestamp() - self.start_time) as usize / bucket_size;

let counter = match self.requests_per_second.get(&time_bucket) {
Some(previous_count) => {
debug!(
"got time bucket {:?} counter: {}",
time_bucket, previous_count
);
*previous_count + 1
}

None => {
debug!("no match for time bucket counter: {}", time_bucket);
1
}
};

self.requests_per_second.insert(time_bucket, counter);
debug!(
"incremented time bucket {} counter: {}",
time_bucket, counter
);
}
}
/// Implement ordering for GooseRequestMetricAggregate.
impl Ord for GooseRequestMetricAggregate {
Expand Down Expand Up @@ -2415,6 +2448,9 @@ impl GooseAttack {
if self.configuration.status_codes {
merge_request.set_status_code(request_metric.status_code);
}
if self.configuration.requests_per_second {
merge_request.record_requests_per_second(self.configuration.rps_bucket);
}
if request_metric.success {
merge_request.success_count += 1;
} else {
Expand Down Expand Up @@ -2951,6 +2987,33 @@ impl GooseAttack {
errors_template = "".to_string();
}

let graph_rps_template: String;
if self.configuration.requests_per_second {
let mut aggregated_count: HashMap<usize, usize> = HashMap::new();
for (_path, path_metric) in self.metrics.requests.iter() {
for (bucket, count) in path_metric.requests_per_second.iter() {
let count = match aggregated_count.get(bucket) {
Some(prev) => prev + *count as usize,

None => *count as usize,
};
aggregated_count.insert(*bucket, count);
}
}

let mut rps: Vec<f32> = vec![0.0; aggregated_count.len()];
for (bucket, count) in aggregated_count.iter() {
let (requests_per_second, _failures_per_second) =
per_second_calculations(self.configuration.rps_bucket, *count, 0);

rps[*bucket as usize] = requests_per_second;
}

graph_rps_template = report::graph_rps_template(rps, self.configuration.rps_bucket);
} else {
graph_rps_template = "".to_string();
}

// Only build the status_code template if --status-codes is enabled.
let status_code_template: String;
if self.configuration.status_codes {
Expand Down Expand Up @@ -3017,6 +3080,7 @@ impl GooseAttack {
tasks_template: &tasks_template,
status_codes_template: &status_code_template,
errors_template: &errors_template,
graph_rps_template: &graph_rps_template,
},
);

Expand Down
47 changes: 47 additions & 0 deletions src/report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use std::collections::BTreeMap;
use std::mem;

use serde::Serialize;
use serde_json::json;

/// The following templates are necessary to build an html-formatted summary report.
#[derive(Debug)]
Expand All @@ -17,6 +18,7 @@ pub struct GooseReportTemplates<'a> {
pub tasks_template: &'a str,
pub status_codes_template: &'a str,
pub errors_template: &'a str,
pub graph_rps_template: &'a str,
}

/// Defines the metrics reported about requests.
Expand Down Expand Up @@ -404,6 +406,48 @@ pub fn error_row(error: &metrics::GooseErrorMetricAggregate) -> String {
)
}

pub fn graph_rps_template(rps: Vec<f32>, bucket_size: usize) -> String {
let values = json!(rps);
let buckets = json!((0..rps.len())
.map(|bucket| format!(
"{}s - {}s",
(bucket * bucket_size),
((bucket + 1) * bucket_size)
))
.collect::<Vec<_>>());

format!(
r#"<div class="graph-rps">
<h2>Requests per second</h2>
<div id="main" style="width: 1000px; height:660px; background: white;"></div>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.2.2/dist/echarts.min.js"></script>
<script type="text/javascript">
var chartDom = document.getElementById('main');
var myChart = echarts.init(chartDom);
myChart.setOption({{
xAxis: {{
type: 'category',
data: {buckets}
}},
yAxis: {{
type: 'value'
}},
series: [
{{
data: {values},
type: 'line'
}}
]
}});
</script>
</div>"#,
values = values,
buckets = buckets,
)
}

/// Build the html report.
pub fn build_report(
users: &str,
Expand Down Expand Up @@ -539,6 +583,8 @@ pub fn build_report(
{errors_template}
{graph_rps_template}
</div>
</body>
</html>"#,
Expand All @@ -554,5 +600,6 @@ pub fn build_report(
tasks_template = templates.tasks_template,
status_codes_template = templates.status_codes_template,
errors_template = templates.errors_template,
graph_rps_template = templates.graph_rps_template,
)
}

0 comments on commit 537e093

Please sign in to comment.