Skip to content

Add Feature: Download Report File #1516

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Aug 18, 2020
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion examples/use_as_lib.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import gevent
from locust import HttpUser, task, between
from locust.env import Environment
from locust.stats import stats_printer
from locust.stats import stats_printer, stats_history
from locust.log import setup_logging

setup_logging("INFO", None)
@@ -29,6 +29,9 @@ def task_404(self):
# start a greenlet that periodically outputs the current stats
gevent.spawn(stats_printer(env.stats))

# start a greenlet that save current stats to history
gevent.spawn(stats_history(env.runner))

# start the test
env.runner.start(1, spawn_rate=10)

5 changes: 4 additions & 1 deletion locust/main.py
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@
from .env import Environment
from .log import setup_logging, greenlet_exception_logger
from . import stats
from .stats import print_error_report, print_percentile_stats, print_stats, stats_printer
from .stats import print_error_report, print_percentile_stats, print_stats, stats_printer, stats_history
from .stats import StatsCSV, StatsCSVFileWriter
from .user import User
from .user.inspectuser import get_task_ratio_dict, print_task_ratio
@@ -336,6 +336,8 @@ def timelimit_stop():
if options.csv_prefix:
gevent.spawn(stats_csv_writer.stats_writer).link_exception(greenlet_exception_handler)

gevent.spawn(stats_history, runner)

def shutdown():
"""
Shut down locust by firing quitting event, printing/writing stats and exiting
@@ -364,6 +366,7 @@ def shutdown():
print_percentile_stats(runner.stats)

print_error_report(runner.stats)

sys.exit(code)

# install SIGTERM handler
24 changes: 23 additions & 1 deletion locust/stats.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import datetime
import hashlib
import time
from collections import namedtuple, OrderedDict
@@ -18,10 +19,14 @@
"""Default interval for how frequently results are written to console."""
CONSOLE_STATS_INTERVAL_SEC = 2

"""Default interval for how frequently results are written to history."""
HISTORY_STATS_INTERVAL_SEC = 5

"""Default interval for how frequently CSV files are written if this option is configured."""
CSV_STATS_INTERVAL_SEC = 1
CSV_STATS_FLUSH_INTERVAL_SEC = 10


"""
Default window size/resolution - in seconds - when calculating the current
response time percentile
@@ -112,6 +117,7 @@ def __init__(self, use_response_times_cache=True):
self.entries = {}
self.errors = {}
self.total = StatsEntry(self, "Aggregated", None, use_response_times_cache=self.use_response_times_cache)
self.history = []

@property
def num_requests(self):
@@ -167,6 +173,7 @@ def reset_all(self):
self.errors = {}
for r in self.entries.values():
r.reset()
self.history = []

def clear_all(self):
"""
@@ -175,6 +182,7 @@ def clear_all(self):
self.total = StatsEntry(self, "Aggregated", None, use_response_times_cache=self.use_response_times_cache)
self.entries = {}
self.errors = {}
self.history = []

def serialize_stats(self):
return [self.entries[key].get_stripped_report() for key in self.entries.keys() if not (self.entries[key].num_requests == 0 and self.entries[key].num_failures == 0)]
@@ -736,17 +744,31 @@ def print_error_report(stats):
console_logger.info("-" * (80 + STATS_NAME_WIDTH))
console_logger.info("")


def stats_printer(stats):
def stats_printer_func():
while True:
print_stats(stats)
gevent.sleep(CONSOLE_STATS_INTERVAL_SEC)
return stats_printer_func


def sort_stats(stats):
return [stats[key] for key in sorted(stats.keys())]

def stats_history(runner):
"""Save current stats info to history for charts of report."""
while True:
stats = runner.stats
r = {
'time': datetime.datetime.now().strftime("%H:%M:%S"),
'current_rps': stats.total.current_rps or 0,
'current_fail_per_sec': stats.total.current_fail_per_sec or 0,
'response_time_percentile_95': stats.total.get_current_response_time_percentile(0.95) or 0,
'response_time_percentile_50': stats.total.get_current_response_time_percentile(0.5) or 0,
'user_count': runner.user_count or 0,
}
stats.history.append(r)
gevent.sleep(HISTORY_STATS_INTERVAL_SEC)

class StatsCSV():
"""Write statistics to csv_writer stream."""
3 changes: 2 additions & 1 deletion locust/templates/index.html
Original file line number Diff line number Diff line change
@@ -193,7 +193,8 @@ <h2>Edit running load test</h2>
<a href="./stats/requests_full_history/csv">Download full request statistics history CSV</a><br>
{% endif %}
<a href="./stats/failures/csv">Download failures CSV</a><br>
<a href="./exceptions/csv">Download exceptions CSV</a>
<a href="./exceptions/csv">Download exceptions CSV</a><br>
<a href="./stats/report" target="_blank">Download Report</a><br>
</div>
</div>
<div style="display:none;">
257 changes: 257 additions & 0 deletions locust/templates/report.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
<!DOCTYPE html>
<html>
<head>
<title>Test Report</title>
<style>
.container {
width: 1000px;
margin: 0 auto;
padding: 10px;
background: #173529;
font-family: Arial, Helvetica, sans-serif;
font-size: 14px;
color: #fff;
}

.info span{
color: #b3c3bc;
}

table {
border-collapse: collapse;
text-align: center;
width: 100%;
}

td, th {
border: 1px solid #cad9ea;
color: #666;
height: 30px;
}

thead th {
background-color: #cce8eb;
width: 100px;
}

tr:nth-child(odd) {
background: #fff;
}

tr:nth-child(even) {
background: #f5fafa;
}

.charts-container .chart {
width: 100%;
height: 350px;
margin-bottom: 30px;
}

.download {
float: right;
}

.download a {
color: #00ca5a;
}
</style>
</head>
<body>
<div class="container">
<h1>Locust Test Report</h1>

<div class="info">
<p class="download"><a href="?download=1">Download the Report</a></p>
<p>During: <span>{{ start_time }} - {{ end_time }}</span></p>
<p>Target Host: <span>{{ host }}</span></p>
</div>

<div class="requests">
<h2>Request Statistics</h2>
<table>
<thead>
<tr>
<th>Method</th>
<th>Name</th>
<th># Requests</th>
<th># Fails</th>
<th>Average (ms)</th>
<th>Min (ms)</th>
<th>Max (ms)</th>
<th>Average size (bytes)</th>
<th>RPS</th>
<th>Failures/s</th>
</tr>
</thead>
<tbody>
{% for s in requests_statistics %}
<tr>
<td>{{ s.method or "" }}</td>
<td>{{ s.name }}</td>
<td>{{ int(s.num_requests) }}</td>
<td>{{ int(s.num_failures) }}</td>
<td>{{ int(s.avg_response_time) }}</td>
<td>{{ int(s.min_response_time or 0) }}</td>
<td>{{ int(s.max_response_time) }}</td>
<td>{{ int(s.avg_content_length) }}</td>
<td>{{ round(s.total_rps, 1) }}</td>
<td>{{ round(s.total_fail_per_sec, 1) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>

<div class="responses">
<h2>Response Time Statistics</h2>
<table>
<thead>
<tr>
<th>Method</th>
<th>Name</th>
<th>50%ile (ms)</th>
<th>60%ile (ms)</th>
<th>70%ile (ms)</th>
<th>80%ile (ms)</th>
<th>90%ile (ms)</th>
<th>95%ile (ms)</th>
<th>99%ile (ms)</th>
<th>100%ile (ms)</th>
</tr>
</thead>
<tbody>
{% for s in requests_statistics %}
<tr>
<td>{{ s.method or "" }}</td>
<td>{{ s.name }}</td>
<td>{{ int(s.get_response_time_percentile(0.5)) }}</td>
<td>{{ int(s.get_response_time_percentile(0.6)) }}</td>
<td>{{ int(s.get_response_time_percentile(0.7)) }}</td>
<td>{{ int(s.get_response_time_percentile(0.8)) }}</td>
<td>{{ int(s.get_response_time_percentile(0.9)) }}</td>
<td>{{ int(s.get_response_time_percentile(0.95)) }}</td>
<td>{{ int(s.get_response_time_percentile(0.99)) }}</td>
<td>{{ int(s.get_response_time_percentile(1)) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>

{% if failures_statistics %}
<div class="failures">
<h2>Failures Statistics</h2>
<table>
<thead>
<tr>
<th>Method</th>
<th>Name</th>
<th>Error</th>
<th>Occurrences</th>
</tr>
</thead>
<tbody>
{% for s in failures_statistics %}
<tr>
<td>{{ s.method or "" }}</td>
<td>{{ s.name }}</td>
<td>{{ s.error }}</td>
<td>{{ s.occurrences }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}

{% if exceptions_statistics %}
<div class="exceptions">
<h2>Exceptions Statistics</h2>
<table>
<thead>
<tr>
<th>Count</th>
<th>Message</th>
<th>Traceback</th>
<th>Nodes</th>
</tr>
</thead>
<tbody>
{% for s in exceptions_statistics %}
<tr>
<td>{{ s.count }}</td>
<td>{{ s.msg }}</td>
<td>{{ s.traceback }}</td>
<td>{{ s.nodes }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}

{% if history %}
<div class="charts-container">
<h2>Charts</h2>
</div>
{% endif %}
</div>

{# <script type="text/javascript" src="/static/jquery-1.11.3.min.js"></script> #}
{# <script type="text/javascript" src="/static/echarts.common.min.js"></script> #}
{# <script type="text/javascript" src="/static/vintage.js"></script> #}
{# <script type="text/javascript" src="/static/chart.js"></script> #}
<script>
{{ static_js|safe }}
</script>

<script>

var rpsChart = new LocustLineChart($(".charts-container"), "Total Requests per Second", ["RPS", "Failures/s"], "reqs/s", ['#00ca5a', '#ff6d6d']);
var responseTimeChart = new LocustLineChart($(".charts-container"), "Response Times (ms)", ["Median Response Time", "95% percentile"], "ms");
var usersChart = new LocustLineChart($(".charts-container"), "Number of Users", ["Users"], "users");

rpsChart.chart.setOption({
xAxis: {
data: [ {% for r in history %}"{{ r.time }}", {% endfor %} ],
},
series: [
{
data: [ {% for r in history %}{{ r.current_rps }}, {% endfor %} ]
},
{
data: [ {% for r in history %}{{ r.current_fail_per_sec }}, {% endfor %} ]
},
]
});

responseTimeChart.chart.setOption({
xAxis: {
data: [ {% for r in history %}"{{ r.time }}", {% endfor %} ],
},
series: [
{
data: [ {% for r in history %}{{ r.response_time_percentile_50 }}, {% endfor %} ]
},
{
data: [ {% for r in history %}{{ r.response_time_percentile_95 }}, {% endfor %} ]
},
]
});

usersChart.chart.setOption({
xAxis: {
data: [ {% for r in history %}"{{ r.time }}", {% endfor %} ],
},
series: [
{
data: [ {% for r in history %}{{ r.user_count }}, {% endfor %} ]
},
]
});


</script>
</body>
</html>
Loading