Skip to content
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

[Feature/2419] Modernize the report.html when using the --modern-ui Flag #2420

Merged
merged 5 commits into from
Oct 11, 2023
Merged
Show file tree
Hide file tree
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
115 changes: 82 additions & 33 deletions locust/html.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from jinja2 import Environment, FileSystemLoader
import os
import glob
import pathlib
import datetime
from itertools import chain
Expand All @@ -9,6 +10,10 @@
from html import escape
from json import dumps
from .runners import MasterRunner, STATE_STOPPED, STATE_STOPPING
from flask import render_template as flask_render_template


PERCENTILES_FOR_HTML_REPORT = [0.50, 0.6, 0.7, 0.8, 0.9, 0.95, 0.99, 1.0]


def render_template(file, **kwargs):
Expand All @@ -18,7 +23,13 @@ def render_template(file, **kwargs):
return template.render(**kwargs)


def get_html_report(environment, show_download_link=True):
def get_html_report(
environment,
static_path=os.path.join(os.path.dirname(__file__), "static"),
show_download_link=True,
use_modern_ui=False,
theme="",
):
stats = environment.runner.stats

start_ts = stats.start_time
Expand Down Expand Up @@ -47,22 +58,27 @@ def get_html_report(environment, show_download_link=True):
history = stats.history

static_js = []
js_files = ["jquery-1.11.3.min.js", "echarts.common.min.js", "vintage.js", "chart.js", "tasks.js"]
if use_modern_ui:
js_files = [os.path.basename(filepath) for filepath in glob.glob(os.path.join(static_path, "*.js"))]
else:
js_files = ["jquery-1.11.3.min.js", "echarts.common.min.js", "vintage.js", "chart.js", "tasks.js"]

for js_file in js_files:
path = os.path.join(os.path.dirname(__file__), "static", js_file)
static_js.append("// " + js_file)
path = os.path.join(static_path, js_file)
static_js.append("// " + js_file + "\n")
with open(path, encoding="utf8") as f:
static_js.append(f.read())
static_js.extend(["", ""])

static_css = []
css_files = ["tables.css"]
for css_file in css_files:
path = os.path.join(os.path.dirname(__file__), "static", "css", css_file)
static_css.append("/* " + css_file + " */")
with open(path, encoding="utf8") as f:
static_css.append(f.read())
static_css.extend(["", ""])
if not use_modern_ui:
static_css = []
css_files = ["tables.css"]
for css_file in css_files:
path = os.path.join(static_path, "css", css_file)
static_css.append("/* " + css_file + " */")
with open(path, encoding="utf8") as f:
static_css.append(f.read())
static_css.extend(["", ""])

is_distributed = isinstance(environment.runner, MasterRunner)
user_spawned = (
Expand All @@ -77,26 +93,59 @@ def get_html_report(environment, show_download_link=True):
"total": get_ratio(environment.user_classes, user_spawned, True),
}

res = render_template(
"report.html",
int=int,
round=round,
escape=escape,
str=str,
requests_statistics=requests_statistics,
failures_statistics=failures_statistics,
exceptions_statistics=exceptions_statistics,
start_time=start_time,
end_time=end_time,
host=host,
history=history,
static_js="\n".join(static_js),
static_css="\n".join(static_css),
show_download_link=show_download_link,
locustfile=environment.locustfile,
tasks=dumps(task_data),
percentile1=stats_module.PERCENTILES_TO_CHART[0],
percentile2=stats_module.PERCENTILES_TO_CHART[1],
)
if use_modern_ui:
res = flask_render_template(
"report.html",
template_args={
"is_report": True,
"requests_statistics": [stat.to_dict(escape_string_values=True) for stat in requests_statistics],
"failures_statistics": [stat.to_dict() for stat in failures_statistics],
"exceptions_statistics": [stat for stat in exceptions_statistics],
"response_time_statistics": [
{
"name": escape(stat.name),
"method": escape(stat.method or ""),
**{
str(percentile): stat.get_response_time_percentile(percentile)
for percentile in PERCENTILES_FOR_HTML_REPORT
},
}
for stat in requests_statistics
],
"start_time": start_time,
"end_time": end_time,
"host": escape(str(host)),
"history": history,
"show_download_link": show_download_link,
"locustfile": escape(str(environment.locustfile)),
"tasks": task_data,
"percentile1": stats_module.PERCENTILES_TO_CHART[0],
"percentile2": stats_module.PERCENTILES_TO_CHART[1],
},
theme=theme,
static_js="\n".join(static_js),
)
else:
res = render_template(
"report.html",
int=int,
round=round,
escape=escape,
str=str,
requests_statistics=requests_statistics,
failures_statistics=failures_statistics,
exceptions_statistics=exceptions_statistics,
start_time=start_time,
end_time=end_time,
host=host,
history=history,
static_js="\n".join(static_js),
static_css="\n".join(static_css),
show_download_link=show_download_link,
locustfile=environment.locustfile,
tasks=dumps(task_data),
percentile1=stats_module.PERCENTILES_TO_CHART[0],
percentile2=stats_module.PERCENTILES_TO_CHART[1],
)

return res
28 changes: 28 additions & 0 deletions locust/stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import csv
import signal
import gevent
from html import escape
from .util.rounding import proper_round

from typing import (
TYPE_CHECKING,
Expand Down Expand Up @@ -685,6 +687,24 @@ def _cache_response_times(self, t: int) -> None:
for _ in range(len(self.response_times_cache) - cache_size):
self.response_times_cache.popitem(last=False)

def to_dict(self, escape_string_values=False):
return {
"method": escape(self.method or "") if escape_string_values else self.method,
"name": escape(self.name) if escape_string_values else self.name,
"safe_name": escape(self.name, quote=False),
"num_requests": self.num_requests,
"num_failures": self.num_failures,
"avg_response_time": self.avg_response_time,
"min_response_time": 0 if self.min_response_time is None else proper_round(self.min_response_time),
"max_response_time": proper_round(self.max_response_time),
"current_rps": self.current_rps,
"current_fail_per_sec": self.current_fail_per_sec,
"median_response_time": self.median_response_time,
"ninetieth_response_time": self.get_response_time_percentile(0.9),
"ninety_ninth_response_time": self.get_response_time_percentile(0.99),
"avg_content_length": self.avg_content_length,
}


class StatsError:
def __init__(self, method: str, name: str, error: Exception | str | None, occurrences: int = 0):
Expand Down Expand Up @@ -745,6 +765,14 @@ def _getattr(obj: "StatsError", key: str, default: Optional[Any]) -> Optional[An
def unserialize(cls, data: StatsErrorDict) -> "StatsError":
return cls(data["method"], data["name"], data["error"], data["occurrences"])

def to_dict(self, escape_string_values=False):
return {
"method": escape(self.method),
"name": escape(self.name),
"error": escape(self.parse_error(self.error)),
"occurrences": self.occurrences,
}


def avg(values: List[float | int]) -> float:
return sum(values, 0.0) / max(len(values), 1)
Expand Down
27 changes: 26 additions & 1 deletion locust/test/test_web.py
Original file line number Diff line number Diff line change
Expand Up @@ -993,6 +993,18 @@ def tick(self):
self.assertRegex(response.text, re_spawn_rate)
self.assertNotRegex(response.text, re_disabled_spawn_rate)

def test_html_stats_report(self):
self.environment.locustfile = "locust.py"
self.environment.host = "http://localhost"

response = requests.get("http://127.0.0.1:%i/stats/report" % self.web_port)
self.assertEqual(200, response.status_code)

d = pq(response.content.decode("utf-8"))

self.assertIn("Script: <span>locust.py</span>", str(d))
self.assertIn("Target Host: <span>http://localhost</span>", str(d))


class TestWebUIAuth(LocustTestCase):
def setUp(self):
Expand Down Expand Up @@ -1133,7 +1145,7 @@ def test_request_stats_full_history_csv(self):
self.assertEqual("Aggregated", rows[3][3])


class TestModernWebUi(LocustTestCase, _HeaderCheckMixin):
class TestModernWebUI(LocustTestCase, _HeaderCheckMixin):
def setUp(self):
super().setUp()

Expand Down Expand Up @@ -1186,3 +1198,16 @@ def test_web_ui_no_runner(self):
self.assertEqual("Error: Locust Environment does not have any runner", response.text)
finally:
web_ui.stop()

def test_html_stats_report(self):
self.environment.locustfile = "locust.py"
self.environment.host = "http://localhost"

response = requests.get("http://127.0.0.1:%i/stats/report" % self.web_port)
self.assertEqual(200, response.status_code)

d = pq(response.content.decode("utf-8"))

self.assertTrue(d("#root"))
self.assertIn('"locustfile": "locust.py"', str(d))
self.assertIn('"host": "http://localhost"', str(d))
41 changes: 18 additions & 23 deletions locust/web.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
from .stats import StatsCSV
from .user.inspectuser import get_ratio
from .util.cache import memoize
from .util.rounding import proper_round
from .util.timespan import parse_timespan
from .html import get_html_report
from flask_cors import CORS
Expand Down Expand Up @@ -111,9 +110,9 @@ def __init__(
self.app = app
app.jinja_env.add_extension("jinja2.ext.do")
app.debug = True
root_path = os.path.dirname(os.path.abspath(__file__))
app.root_path = root_path
self.webui_build_path = f"{root_path}/webui/dist"
self.root_path = os.path.dirname(os.path.abspath(__file__))
app.root_path = self.root_path
self.webui_build_path = f"{self.root_path}/webui/dist"
self.app.config["BASIC_AUTH_ENABLED"] = False
self.auth: Optional[BasicAuth] = None
self.greenlet: Optional[gevent.Greenlet] = None
Expand Down Expand Up @@ -278,7 +277,20 @@ def reset_stats() -> str:
@app.route("/stats/report")
@self.auth_required_if_enabled
def stats_report() -> Response:
res = get_html_report(self.environment, show_download_link=not request.args.get("download"))
if self.modern_ui:
self.set_static_modern_ui()
static_path = f"{self.webui_build_path}/assets"
else:
static_path = f"{self.root_path}/static"

theme = request.args.get("theme", "")
res = get_html_report(
self.environment,
static_path=static_path,
show_download_link=not request.args.get("download"),
use_modern_ui=self.modern_ui,
theme=theme,
)
if request.args.get("download"):
res = app.make_response(res)
res.headers["Content-Disposition"] = f"attachment;filename=report_{time()}.html"
Expand Down Expand Up @@ -367,24 +379,7 @@ def request_stats() -> Response:
return jsonify(report)

for s in chain(sort_stats(environment.runner.stats.entries), [environment.runner.stats.total]):
stats.append(
{
"method": s.method,
"name": s.name,
"safe_name": escape(s.name, quote=False),
"num_requests": s.num_requests,
"num_failures": s.num_failures,
"avg_response_time": s.avg_response_time,
"min_response_time": 0 if s.min_response_time is None else proper_round(s.min_response_time),
"max_response_time": proper_round(s.max_response_time),
"current_rps": s.current_rps,
"current_fail_per_sec": s.current_fail_per_sec,
"median_response_time": s.median_response_time,
"ninetieth_response_time": s.get_response_time_percentile(0.9),
"ninety_ninth_response_time": s.get_response_time_percentile(0.99),
"avg_content_length": s.avg_content_length,
}
)
stats.append(s.to_dict())

for e in environment.runner.errors.values():
err_dict = e.serialize()
Expand Down
4 changes: 4 additions & 0 deletions locust/webui/.eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
"pattern": "App",
"group": "internal"
},
{
"pattern": "Report",
"group": "internal"
},
{
"pattern": "{api,components,constants,hooks,redux,styles,types,utils}/**",
"group": "internal"
Expand Down
7 changes: 0 additions & 7 deletions locust/webui/dist/assets/index-0c9ff576.js

This file was deleted.

Loading
Loading