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

Locust UI as a Module #2804

Merged
merged 31 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
e8336d3
Add locust lib build
andrewbaldwin44 Jul 10, 2024
f48a915
Configure LocustUi exports, props, and types
andrewbaldwin44 Jul 12, 2024
5412e28
Update with correct types
andrewbaldwin44 Jul 15, 2024
0be8bb5
Provide additional exports
andrewbaldwin44 Jul 15, 2024
73040d1
Add documentation
andrewbaldwin44 Jul 15, 2024
098a6ba
Remove react-redux dependency
andrewbaldwin44 Jul 15, 2024
982cc5f
Fix build
andrewbaldwin44 Jul 15, 2024
847621d
Fix tests
andrewbaldwin44 Jul 15, 2024
7f3bf8d
Update documentation
andrewbaldwin44 Jul 15, 2024
be810c2
Update docs
andrewbaldwin44 Jul 16, 2024
582d05b
Remove example
andrewbaldwin44 Jul 16, 2024
f0b8dbf
Argument parser
andrewbaldwin44 Jul 16, 2024
d2fe8ac
Update README.md
andrewbaldwin44 Jul 16, 2024
a69221c
Remove example
andrewbaldwin44 Jul 16, 2024
c120a5a
Frontend tests
andrewbaldwin44 Jul 16, 2024
b729da0
Fix build path
andrewbaldwin44 Jul 16, 2024
e6afb3d
precommit
andrewbaldwin44 Jul 17, 2024
5e6b25a
precommit
andrewbaldwin44 Jul 17, 2024
91f6aec
Add LICENSE
andrewbaldwin44 Jul 18, 2024
0eb95a9
Update vite lib config
andrewbaldwin44 Jul 18, 2024
aa74d48
Update exports
andrewbaldwin44 Jul 18, 2024
df76527
Add repository to package.json
andrewbaldwin44 Jul 18, 2024
77349a2
Remove private flag
andrewbaldwin44 Jul 18, 2024
0602266
Update README
andrewbaldwin44 Jul 18, 2024
33cfef3
Add generic to LineChart
andrewbaldwin44 Jul 25, 2024
a18cb04
Allow for using locust MUI theme in lib
andrewbaldwin44 Jul 29, 2024
66b3277
Add chart formatting
andrewbaldwin44 Jul 29, 2024
50fa56c
Add immediate
andrewbaldwin44 Jul 29, 2024
7becbd3
Add swarm_start
andrewbaldwin44 Jul 29, 2024
7feee72
Update chartFormatter type
andrewbaldwin44 Jul 31, 2024
f265537
Update README
andrewbaldwin44 Aug 1, 2024
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
7 changes: 3 additions & 4 deletions docs/extending-locust.rst
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,11 @@ Extending Web UI
As an alternative to adding simple web routes, you can use `Flask Blueprints
<https://flask.palletsprojects.com/en/1.1.x/blueprints/>`_ and `templates
<https://flask.palletsprojects.com/en/1.1.x/tutorial/templates/>`_ to not only add routes but also extend
the web UI to allow you to show custom data along side the built-in Locust stats. This is more advanced
as it involves also writing and including HTML and Javascript files to be served by routes but can
the web UI to allow you to show custom data along side the built-in Locust stats. This is more advanced but can
greatly enhance the utility and customizability of the web UI.

A working example of extending the web UI, complete with HTML and Javascript example files, can be found
in the `examples directory <https://github.com/locustio/locust/tree/master/examples/>`_ of the Locust
Working examples of extending the web UI can be found
in the `examples directory <https://github.com/locustio/locust/tree/master/examples>`_ of the Locust
source code.

* ``extend_modern_web_ui.py``: Display a table with content-length for each call.
Expand Down
7 changes: 7 additions & 0 deletions locust/argument_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,13 @@ def setup_parser_arguments(parser):
help="Enable select boxes in the web interface to choose from all available User classes and Shape classes",
env_var="LOCUST_USERCLASS_PICKER",
)
web_ui_group.add_argument(
"--build-path",
type=str,
default="",
help=configargparse.SUPPRESS,
env_var="LOCUST_BUILD_PATH",
)
web_ui_group.add_argument(
"--legacy-ui",
default=False,
Expand Down
2 changes: 2 additions & 0 deletions locust/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ def create_web_ui(
stats_csv_writer: StatsCSV | None = None,
delayed_start=False,
userclass_picker_is_active=False,
build_path: str | None = None,
) -> WebUI:
"""
Creates a :class:`WebUI <locust.web.WebUI>` instance for this Environment and start running the web server
Expand All @@ -197,6 +198,7 @@ def create_web_ui(
stats_csv_writer=stats_csv_writer,
delayed_start=delayed_start,
userclass_picker_is_active=userclass_picker_is_active,
build_path=build_path,
)
return self.web_ui

Expand Down
24 changes: 6 additions & 18 deletions locust/html.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
from itertools import chain
from json import dumps

from jinja2 import Environment, FileSystemLoader
from jinja2 import Environment as JinjaEnvironment
from jinja2 import FileSystemLoader

from . import stats as stats_module
from .runners import STATE_STOPPED, STATE_STOPPING, MasterRunner
Expand All @@ -14,13 +15,11 @@
from .util.date import format_utc_timestamp

PERCENTILES_FOR_HTML_REPORT = [0.50, 0.6, 0.7, 0.8, 0.9, 0.95, 0.99, 1.0]
ROOT_PATH = os.path.dirname(os.path.abspath(__file__))
BUILD_PATH = os.path.join(ROOT_PATH, "webui", "dist")
STATIC_PATH = os.path.join(BUILD_PATH, "assets")
DEFAULT_BUILD_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "webui", "dist")


def render_template(file, **kwargs):
env = Environment(loader=FileSystemLoader(BUILD_PATH), extensions=["jinja2.ext.do"])
def render_template_from(file, build_path=DEFAULT_BUILD_PATH, **kwargs):
env = JinjaEnvironment(loader=FileSystemLoader(build_path))
template = env.get_template(file)
return template.render(**kwargs)

Expand Down Expand Up @@ -56,16 +55,6 @@ def get_html_report(
update_stats_history(environment.runner)
history = stats.history

static_js = []
js_files = [os.path.basename(filepath) for filepath in glob.glob(os.path.join(STATIC_PATH, "*.js"))]

for js_file in js_files:
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(["", ""])

is_distributed = isinstance(environment.runner, MasterRunner)
user_spawned = (
environment.runner.reported_user_classes_count if is_distributed else environment.runner.user_classes_count
Expand All @@ -79,7 +68,7 @@ def get_html_report(
"total": get_ratio(environment.user_classes, user_spawned, True),
}

return render_template(
return render_template_from(
"report.html",
template_args={
"is_report": True,
Expand Down Expand Up @@ -107,5 +96,4 @@ def get_html_report(
"percentiles_to_chart": stats_module.PERCENTILES_TO_CHART,
},
theme=theme,
static_js="\n".join(static_js),
)
1 change: 1 addition & 0 deletions locust/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,7 @@ def ensure_user_class_name(config):
stats_csv_writer=stats_csv_writer,
delayed_start=True,
userclass_picker_is_active=options.class_picker,
build_path=options.build_path,
)
else:
web_ui = None
Expand Down
18 changes: 11 additions & 7 deletions locust/web.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
from . import __version__ as version
from . import argument_parser
from . import stats as stats_module
from .html import BUILD_PATH, ROOT_PATH, STATIC_PATH, get_html_report
from .html import DEFAULT_BUILD_PATH, get_html_report, render_template_from
from .log import get_logs, greenlet_exception_logger
from .runners import STATE_MISSING, STATE_RUNNING, MasterRunner
from .stats import StatsCSV, StatsCSVFileWriter, StatsErrorDict, sort_stats
Expand Down Expand Up @@ -96,6 +96,7 @@ def __init__(
stats_csv_writer: StatsCSV | None = None,
delayed_start=False,
userclass_picker_is_active=False,
build_path: str | None = None,
):
"""
Create WebUI instance and start running the web server in a separate greenlet (self.greenlet)
Expand Down Expand Up @@ -124,14 +125,11 @@ def __init__(
self.app = app
app.jinja_env.add_extension("jinja2.ext.do")
app.debug = True
app.root_path = ROOT_PATH
self.webui_build_path = BUILD_PATH
self.greenlet: gevent.Greenlet | None = None
self._swarm_greenlet: gevent.Greenlet | None = None
self.template_args = {}
self.auth_args = {}
self.app.template_folder = BUILD_PATH
self.app.static_folder = STATIC_PATH
self.app.template_folder = build_path or DEFAULT_BUILD_PATH
self.app.static_url_path = "/assets/"
# ensures static js files work on Windows
mimetypes.add_type("application/javascript", ".js")
Expand All @@ -158,7 +156,13 @@ def handle_exception(error):

@app.route("/assets/<path:path>")
def send_assets(path):
return send_from_directory(os.path.join(self.webui_build_path, "assets"), path)
directory = (
os.path.join(self.app.template_folder, "assets")
if os.path.exists(os.path.join(app.template_folder, "assets", path))
else os.path.join(DEFAULT_BUILD_PATH, "assets")
)

return send_from_directory(directory, path)

@app.route("/")
@self.auth_required_if_enabled
Expand Down Expand Up @@ -499,7 +503,7 @@ def login():
if not self.web_login:
return redirect(url_for("index"))

return render_template(
return render_template_from(
"auth.html",
auth_args=self.auth_args,
)
Expand Down
2 changes: 1 addition & 1 deletion locust/webui/.eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"group": "internal"
},
{
"pattern": "{api,components,constants,hooks,pages,redux,styles,test,types,utils}/**",
"pattern": "{api,assets,components,constants,hooks,pages,redux,styles,test,types,utils}/**",
"group": "internal"
}
],
Expand Down
4 changes: 4 additions & 0 deletions locust/webui/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
/node_modules

tsconfig.tsbuildinfo
.rollup.cache
lib
dist
locust-ui-*.tgz
21 changes: 21 additions & 0 deletions locust/webui/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License

Copyright (c) 2024, Andrew Baldwin, Lars Holmberg

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
169 changes: 169 additions & 0 deletions locust/webui/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# Locust UI

The Locust UI is used for viewing stats, reports, and information on your current Locust test from the browser.

## Locust UI as a Library

**Using the Locust UI as a library should be considered an experimental feature**

The Locust UI may be extended to fit your needs. If you only need limited extensibility, you may do so in your Locustfile, see the [extend_web_ui example](https://github.com/locustio/locust/blob/master/examples/extend_web_ui.py).

However, you may want to further extend certain functionalities. To do so, you may replace the default Locust UI with your own React application. Start by installing the locust-ui in your React application:
```sh
npm install locust-ui
```
or
```sh
yarn add locust-ui
```

## Usage

```js
import LocustUi from "locust-ui";

function App() {
return (
<LocustUi<"content-length", "content_length">
extendedTabs={[
{
title: "Content Length",
key: "content-length",
},
]}
extendedTables={[
{
key: "content-length",
structure: [
{ key: "name", title: "Name" },
{ key: "content_length", title: "Total content length" },
],
},
]}
extendedReports={[
{
href: "/content-length/csv",
title: "Download content length statistics CSV",
},
]}
extendedStats={[
{
key: "content-length",
data: [{ name: "/", safeName: "/", content_length: "123" }],
},
]}
/>
)
}
```

For Locust to be able to pass data to your React frontend, place the following script tag in your html template file:
```html
<script>
window.templateArgs = {{ template_args|tojson }}
</script>
```

To load the favicon, place the link in your head:
```html
<link rel="icon" href="./assets/favicon.ico" />
```

Lastly, you must configure Locust to point to your own React build output. To achieve this, you can use the flag `--build-path` and provide the **absolute** path to your build directory.

```sh
locust -f locust.py --build-path /home/user/custom-webui/dist
```

For more on configuring Locust see [the Locust docs](https://docs.locust.io/en/stable/configuration.html).

### Customizing Tabs
By default, the extended tabs will display the provided data in a table. However you may choose to render any React component in the tab:
```js
import { IRootState } from "locust-webui";
import { useSelector } from "react-redux";

function MyCustomTab() {
const extendedStats = useSelector(
({ ui: { extendedStats } }: IRootState) => extendedStats
);

return <div>{JSON.stringify(extendedStats)}</div>;
}

const extendedTabs = {[
{
title: "Content Length",
key: "content-length",
component: MyCustomTab
},
]};

function App() {
return (
<LocustUi extendedTabs={extendedTabs} />
)
}
```

The `tabs` prop allows for complete control of which tabs are rendered. You can then customize which base tabs are shown or where your new tab should be placed:
```js
import LocustUi, { baseTabs } from "locust-ui";

const tabs = [...baseTabs];
tabs.splice(2, 0, {
title: "Custom Tab",
key: "custom-tab",
component: MyCustomTab,
});

function App() {
return (
<LocustUi tabs={tabs} />
)
}
```

### API
**Tab**
```js
{
title: string; // **Required** Any string for display purposes
key: string; // **Required** Programatic key used in extendedTabs to find corresponding stats and tables
component: // **Optional** React component to render
shouldDisplayTab: // **Optional** Function provided with Locust redux state to output boolean
}
```
**Extended Stat**
```js
{
key: string; // **Required** Programatic key that must correspond to a tab key
data: {
[key: string]: string; // The key must have a corresponding entry in the extended table structure. The value corresponds to the data to be displayed
}[];
}
```
**Extended Table**
```js
{
key: string; // **Required** Programatic key that must correspond to a tab key
structure: {
key: string; // **Required** key that must correspond to a key in the extended stat data object
title: string; // **Required** Corresponds to the title of the column in the table
}[]
}
```
**Locust UI**
```js
// Provide the types for your extended tab and stat keys to get helpful type hints
<LocustUI<ExtendedTabType, StatKey>
extendedTabs={/* Optional array of extended tabs */}
extendedTables={/* Optional array of extended tables */}
extendedReports={/* Optional array of extended reports */}
extendedStats={/* Optional array of extended stats */}
tabs={/* Optional array of tabs that will take precedence over extendedTabs */}
/>
```



Loading
Loading