Skip to content

Commit

Permalink
Merge pull request #381 from slashrsm/246_html_report_graph
Browse files Browse the repository at this point in the history
Add requests per second graph to the HTML report.
  • Loading branch information
jeremyandrews authored Nov 19, 2021
2 parents 4434591 + 0a12dea commit 4988799
Show file tree
Hide file tree
Showing 3 changed files with 220 additions and 2 deletions.
2 changes: 2 additions & 0 deletions src/docs/goose-book/src/getting-started/common.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ cargo run --release -- -t 30m

By default, Goose displays [text-formatted metrics](metrics.md) when a load test finishes. It can also optionally write an HTML-formatted report if you enable the `--report-file <NAME>` run-time option, where `<NAME>` is an absolute or relative path to the report file to generate. Any file that already exists at the specified path will be overwritten.

HTML report includes some graphs that rely on [eCharts JavaScript library](https://echarts.apache.org). HTML report loads the library via CDN, which means that the graphs won't be loaded correctly if the CDN is not accessible.

### Example
_Write an HTML-formatted report to `report.html` when the load test finishes._

Expand Down
94 changes: 92 additions & 2 deletions src/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use num_format::{Locale, ToFormattedString};
use regex::RegexSet;
use serde::ser::SerializeStruct;
use serde::{Deserialize, Serialize, Serializer};
use std::cmp::Ordering;
use std::cmp::{max, Ordering};
use std::collections::{BTreeMap, HashMap, HashSet};
use std::str::FromStr;
use std::{f32, fmt};
Expand Down Expand Up @@ -396,6 +396,8 @@ 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,
// Counts requests per second. Each element of the vector represents one second.
pub requests_per_second: Vec<u32>,
}
impl GooseRequestMetricAggregate {
/// Create a new GooseRequestMetricAggregate object.
Expand All @@ -410,6 +412,7 @@ impl GooseRequestMetricAggregate {
success_count: 0,
fail_count: 0,
load_test_hash,
requests_per_second: Vec::new(),
}
}

Expand Down Expand Up @@ -451,6 +454,26 @@ 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, second: usize) {
// Each element in self.requests_per_second vector is count for a given
// second since the start of the test. Since we don't know how long the
// test will at the beginning we need to push new elements (second
// counters) as the test is running.
if self.requests_per_second.len() <= second {
for _ in 0..=(second - self.requests_per_second.len() + 1) {
self.requests_per_second.push(0);
}
}

self.requests_per_second[second] += 1;

debug!(
"incremented second {} counter: {}",
second, self.requests_per_second[second]
);
}
}
/// Implement ordering for GooseRequestMetricAggregate.
impl Ord for GooseRequestMetricAggregate {
Expand Down Expand Up @@ -2415,6 +2438,13 @@ impl GooseAttack {
if self.configuration.status_codes {
merge_request.set_status_code(request_metric.status_code);
}
if !self.configuration.report_file.is_empty() {
if let Some(starting) = self.metrics.starting {
merge_request.record_requests_per_second(
(Utc::now().timestamp() - starting.timestamp()) as usize,
);
}
}
if request_metric.success {
merge_request.success_count += 1;
} else {
Expand Down Expand Up @@ -3004,6 +3034,65 @@ impl GooseAttack {
status_code_template = "".to_string();
}

// Generate RPS graph.
let mut count = 0;
for path_metric in self.metrics.requests.values() {
count = max(count, path_metric.requests_per_second.len());
}

let mut rps = vec![0; count];
for path_metric in self.metrics.requests.values() {
for (second, count) in path_metric.requests_per_second.iter().enumerate() {
rps[second] += count;
}
}

let rps = rps
.iter()
.enumerate()
.filter(|(second, _)| {
if self.configuration.no_reset_metrics {
true
} else {
*second as i64 + starting.timestamp() >= started.timestamp()
}
})
.map(|(second, &count)| {
(
Local
.timestamp(second as i64 + starting.timestamp(), 0)
.format("%Y-%m-%d %H:%M:%S")
.to_string(),
count,
)
})
.collect::<Vec<_>>();

// If the metrics were reset when the load test was started we don't display
// the starting period on the graph.
let (starting, started) = if self.configuration.no_reset_metrics
&& Some(starting) == self.metrics.starting
&& Some(started) == self.metrics.started
{
(Some(starting), Some(started))
} else {
(None, None)
};

// If stopping was done in less than a second do not display it as it won't be visible
// on the graph.
let (stopping, stopped) = if Some(stopping) == self.metrics.stopping
&& Some(stopped) == self.metrics.stopped
&& stopped == stopping
{
(Some(stopping), Some(stopped))
} else {
(None, None)
};

let graph_rps_template =
report::graph_rps_template(rps, starting, started, stopping, stopped);

// Compile the report template.
let report = report::build_report(
&users,
Expand All @@ -3017,11 +3106,12 @@ impl GooseAttack {
tasks_template: &tasks_template,
status_codes_template: &status_code_template,
errors_template: &errors_template,
graph_rps_template: &graph_rps_template,
},
);

// Write the report to file.
if let Err(e) = report_file.write(report.as_ref()).await {
if let Err(e) = report_file.write_all(report.as_ref()).await {
return Err(GooseError::InvalidOption {
option: "--report-file".to_string(),
value: self.get_report_file_path().unwrap(),
Expand Down
126 changes: 126 additions & 0 deletions src/report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ use crate::metrics;
use std::collections::BTreeMap;
use std::mem;

use chrono::prelude::*;
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 +19,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 +407,126 @@ pub fn error_row(error: &metrics::GooseErrorMetricAggregate) -> String {
)
}

pub fn graph_rps_template(
rps: Vec<(String, u32)>,
starting: Option<DateTime<Local>>,
started: Option<DateTime<Local>>,
stopping: Option<DateTime<Local>>,
stopped: Option<DateTime<Local>>,
) -> String {
let datetime_format = "%Y-%m-%d %H:%M:%S";

let starting_area = if starting.is_some() && started.is_some() {
format!(
r#"[
{{
name: 'Starting',
xAxis: '{starting}'
}},
{{
xAxis: '{started}'
}}
],"#,
starting = starting.unwrap().format(datetime_format),
started = started.unwrap().format(datetime_format),
)
} else {
"".to_string()
};

let stopping_area = if stopping.is_some() && stopped.is_some() {
format!(
r#"[
{{
name: 'Stopping',
xAxis: '{stopping}'
}},
{{
xAxis: '{stopped}'
}}
],"#,
stopping = stopping.unwrap().format(datetime_format),
stopped = stopped.unwrap().format(datetime_format),
)
} else {
"".to_string()
};

format!(
r#"<div class="graph-rps">
<h2>Requests per second</h2>
<div id="graph-rps" style="width: 1000px; height:500px; 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('graph-rps');
var myChart = echarts.init(chartDom);
myChart.setOption({{
color: ['#2c664f'],
tooltip: {{ trigger: 'axis' }},
toolbox: {{
feature: {{
dataZoom: {{ yAxisIndex: 'none' }},
restore: {{}},
saveAsImage: {{}}
}}
}},
dataZoom: [
{{
type: 'inside',
start: 0,
end: 100,
fillerColor: 'rgba(34, 80, 61, 0.25)',
selectedDataBackground: {{
lineStyle: {{ color: '#2c664f' }},
areaStyle: {{ color: '#378063' }}
}}
}},
{{
start: 0,
end: 100,
fillerColor: 'rgba(34, 80, 61, 0.25)',
selectedDataBackground: {{
lineStyle: {{ color: '#2c664f' }},
areaStyle: {{ color: '#378063' }}
}}
}},
],
xAxis: {{ type: 'time' }},
yAxis: {{
name: 'Requests per second',
nameLocation: 'center',
nameRotate: 90,
nameGap: 45,
type: 'value'
}},
series: [
{{
type: 'line',
symbol: 'none',
sampling: 'lttb',
lineStyle: {{ color: '#2c664f' }},
areaStyle: {{ color: '#378063' }},
markArea: {{
itemStyle: {{ color: 'rgba(6, 6, 6, 0.10)' }},
data: [
{starting_area}
{stopping_area}
]
}},
data: {values},
}}
]
}});
</script>
</div>"#,
values = json!(rps),
starting_area = starting_area,
stopping_area = stopping_area
)
}

/// Build the html report.
pub fn build_report(
users: &str,
Expand Down Expand Up @@ -539,6 +662,8 @@ pub fn build_report(
{errors_template}
{graph_rps_template}
</div>
</body>
</html>"#,
Expand All @@ -554,5 +679,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 4988799

Please sign in to comment.