Event hooks
Locust comes with a number of event hooks that can be used to extend Locust in different ways.
For example, here’s how to set up an event listener that will trigger after a request is completed:
from locust import events
@events.request.add_listener
def my_request_handler(request_type, name, response_time, response_length, response,
context, exception, start_time, url, **kwargs):
if exception:
print(f"Request to {name} failed with exception {exception}")
else:
print(f"Successfully made a request to: {name}")
print(f"The response was {response.text}")
Note
In the above example the wildcard keyword argument (**kwargs) will be empty, because we’re handling all arguments, but it prevents the code from breaking if new arguments are added in some future version of Locust.
Also, it is entirely possible to implement a client that does not supply all parameters for this event. For example, non-HTTP protocols might not even have the a concept of url or response object. Remove any such missing field from your listener function definition or use default arguments.
When running locust in distributed mode, it may be useful to do some setup on worker nodes before running your tests.
You can check to ensure you aren’t running on the master node by checking the type of the node’s runner
:
from locust import events
from locust.runners import MasterRunner
@events.test_start.add_listener
def on_test_start(environment, **kwargs):
if not isinstance(environment.runner, MasterRunner):
print("Beginning test setup")
else:
print("Started test from Master node")
@events.test_stop.add_listener
def on_test_stop(environment, **kwargs):
if not isinstance(environment.runner, MasterRunner):
print("Cleaning up test data")
else:
print("Stopped test from Master node")
You can also use events to add custom command line arguments.
To see a full list of available events see Event hooks.
Request context
The request event
has a context parameter that enable you to pass data about the request (things like username, tags etc). It can be set directly in the call to the request method or at the User level, by overriding the User.context() method.
Context from request method:
class MyUser(HttpUser):
@task
def t(self):
self.client.post("/login", json={"username": "foo"})
self.client.get("/other_request", context={"username": "foo"})
@events.request.add_listener
def on_request(context, **kwargs):
if context:
print(context["username"])
Context from User instance:
class MyUser(HttpUser):
def context(self):
return {"username": self.username}
@task
def t(self):
self.username = "foo"
self.client.post("/login", json={"username": self.username})
@events.request.add_listener
def on_request(context, **kwargs):
print(context["username"])
Context from a value in the response, using catch_response:
with self.client.get("/", catch_response=True) as resp:
resp.request_meta["context"]["requestId"] = resp.json()["requestId"]
Note
Request context doesn’t change how Locust’s regular statistics are calculated. Logging/reporting solutions like locust.cloud use the above mechanic to save the context to a database.
Adding Web Routes
Locust uses Flask to serve the web UI and therefore it is easy to add web end-points to the web UI.
By listening to the init
event, we can retrieve a reference
to the Flask app instance and use that to set up a new route:
from locust import events
@events.init.add_listener
def on_locust_init(environment, **kw):
@environment.web_ui.app.route("/added_page")
def my_added_page():
return "Another page"
You should now be able to start locust and browse to http://127.0.0.1:8089/added_page. Note that it doesn’t get automatically added as a new tab - you’ll need to enter the URL directly.
Extending Web UI
As an alternative to adding simple web routes, you can use Flask Blueprints and 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 but can greatly enhance the utility and customizability of the web UI.
Working examples of extending the web UI can be found in the examples directory of the Locust source code.
extend_modern_web_ui.py
: Display a table with content-length for each call.web_ui_cache_stats.py
: Display Varnish Hit/Miss stats for each call. This could easily be extended to other CDN or cache proxies and gather other cache statistics such as cache age, control, …
Adding Authentication to the Web UI
Locust uses Flask-Login to handle authentication when the --web-login
flag is present.
The login_manager
is exposed on environment.web_ui.app
, allowing the flexibility for you to implement any kind of auth that
you would like!
To use username / password authentication, simply provide a username_password_callback
to the environment.web_ui.auth_args
.
You are responsible for defining the route for the callback and implementing the authentication.
Authentication providers can additionally be configured to allow authentication from 3rd parties such as GitHub or an SSO provider.
Simply provide a list of desired auth_providers
. You may specify the label
and icon
for display on the button.
The callback_url
will be the url that the button directs to. You will be responsible for defining the callback route as
well as the authentication with the 3rd party.
Whether you are using username / password authentication, an auth provider, or both, a user_loader
needs to be proivded
to the login_manager
. The user_loader
should return None
to deny authentication or return a User object when
authentication to the app should be granted.
To display errors on the login page, such as an incorrect username / password combination, you may store the auth_error
on the session object: session["auth_error"] = "Incorrect username or password"
. If you have non-erroneous information
you would like to display to the user, you can opt instead to set auth_info
on the session object:
session["auth_info"] = "Successfully created new user!"
A full example can be seen in the auth example.
In certain situations you may wish to further extend the fields present in the auth form. To achieve this, pass a custom_form
dict
to the environment.web_ui.auth_args
. In this case, the fields will be represented by a list of inputs
, the callback url is
configured by the custom_form.callback_url
, and the submit button may optionally be configured using the custom_form.submit_button_text
.
The fields in the auth form may be a text, select, checkbox, or secret password field.
For a full example see configuring the custom_form in the auth example.
Run a background greenlet
Because a locust file is “just code”, there is nothing preventing you from spawning your own greenlet to run in parallel with your actual load/Users.
For example, you can monitor the fail ratio of your test and stop the run if it goes above some threshold:
import gevent
from locust import events
from locust.runners import STATE_STOPPING, STATE_STOPPED, STATE_CLEANUP, MasterRunner, LocalRunner
def checker(environment):
while not environment.runner.state in [STATE_STOPPING, STATE_STOPPED, STATE_CLEANUP]:
time.sleep(1)
if environment.runner.stats.total.fail_ratio > 0.2:
print(f"fail ratio was {environment.runner.stats.total.fail_ratio}, quitting")
environment.runner.quit()
return
@events.init.add_listener
def on_locust_init(environment, **_kwargs):
# dont run this on workers, we only care about the aggregated numbers
if isinstance(environment.runner, MasterRunner) or isinstance(environment.runner, LocalRunner):
gevent.spawn(checker, environment)
Parametrizing locustfiles
There are two main ways to parametrize your locustfile.
Basic environment variables
Like with any program, you can use environment variables:
On linux/mac:
MY_FUNKY_VAR=42 locust ...
On windows:
SET MY_FUNKY_VAR=42
locust ...
… and then access them in your locustfile.
import os
print(os.environ['MY_FUNKY_VAR'])
Custom arguments
You can add your own command line arguments to Locust, using the init_command_line_parser
Event. Custom arguments are also presented and editable in the web UI. If choices are specified for the argument, they will be presented as a dropdown in the web UI.
from locust import HttpUser, events, task
@events.init_command_line_parser.add_listener
def _(parser):
parser.add_argument("--my-argument", type=str, env_var="LOCUST_MY_ARGUMENT", default="", help="It's working")
# Choices will validate command line input and show a dropdown in the web UI
parser.add_argument("--env", choices=["dev", "staging", "prod"], default="dev", help="Environment")
# Set `include_in_web_ui` to False if you want to hide from the web UI
parser.add_argument("--my-ui-invisible-argument", include_in_web_ui=False, default="I am invisible")
# Set `is_secret` to True if you want the text input to be password masked in the web UI
parser.add_argument("--my-ui-password-argument", is_secret=True, default="I am a secret")
@events.test_start.add_listener
def _(environment, **kw):
print(f"Custom argument supplied: {environment.parsed_options.my_argument}")
class WebsiteUser(HttpUser):
@task
def my_task(self):
print(f"my_argument={self.environment.parsed_options.my_argument}")
print(f"my_ui_invisible_argument={self.environment.parsed_options.my_ui_invisible_argument}")
When running Locust distributed, custom arguments are automatically forwarded to workers when the run is started (but not before then, so you cannot rely on forwarded arguments before the test has actually started).
Test data management
There are a number of ways to get test data into your tests (after all, your test is just a Python program and it can do whatever Python can). Locust’s events give you fine-grained control over when to fetch/release test data. You can find a detailed example here.
More examples
See locust-plugins