Skip to content

Commit

Permalink
Merge pull request #2669 from obriat/feature-2644-web-ui-cache-stats
Browse files Browse the repository at this point in the history
Add an example that displays cache stats using the new UI.
  • Loading branch information
cyberw authored Apr 9, 2024
2 parents a7ca357 + 2fe2e0c commit 31d957e
Show file tree
Hide file tree
Showing 4 changed files with 231 additions and 3 deletions.
17 changes: 14 additions & 3 deletions docs/extending-locust.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ To see a full list of available events see :ref:`events`.

.. _request_context:


Request context
===============

Expand Down Expand Up @@ -110,6 +111,10 @@ to the Flask app instance and use that to set up a new route::

You should now be able to start locust and browse to http://127.0.0.1:8089/added_page

.. note::

Please note that at the moment Locust does not show the extended Web UI under the default root path `"/"`.
To view your extensions, navigate to the newly added web route. In this example, `"/added_page"`.


Extending Web UI
Expand All @@ -123,9 +128,14 @@ as it involves also writing and including HTML and Javascript files to be served
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
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.

* ``web_ui_cache_stats.py``: Display Varnish Hit/ Miss stats for each call. Could be easly extended to other CDN or cache proxies and gather other cache statistics such as cache age, control, ...

.. image:: images/extend_modern_web_ui_cache_stats.png


Adding Authentication to the Web UI
Expand All @@ -149,8 +159,6 @@ authentication to the app should be granted.
A full example can be seen `in the auth example <https://github.com/locustio/locust/tree/master/examples/web_ui_auth.py>`_.




Run a background greenlet
=========================

Expand Down Expand Up @@ -182,6 +190,7 @@ For example, you can monitor the fail ratio of your test and stop the run if it
.. _parametrizing-locustfiles:


Parametrizing locustfiles
=========================

Expand Down Expand Up @@ -224,11 +233,13 @@ You can add your own command line arguments to Locust, using the :py:attr:`init_

When running Locust :ref:`distributed <running-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 <https://github.com/locustio/locust/tree/master/examples/test_data_management.py>`_.


More examples
=============

Expand Down
Binary file added docs/images/extend_modern_web_ui_cache_stats.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions examples/extend_web_ui/extend.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,16 @@
import os
from html import escape
from time import time
from warnings import warn

from flask import Blueprint, jsonify, make_response, render_template

warn(
"This UI example is deprecated, please downgrade to Locust 2.21.0 or use --legacy-ui flag ; version=2.22.0",
DeprecationWarning,
stacklevel=2,
)


class MyTaskSet(TaskSet):
@task(2)
Expand Down
210 changes: 210 additions & 0 deletions examples/web_ui_cache_stats.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
"""
This is an example of a locustfile that uses Locust's built in event and web
UI extension hooks to track the sum of Varnish cache hit/miss headers
and display them in the web UI.
"""

from locust import HttpUser, TaskSet, between, events, task, web

import json
import os
from html import escape
from time import time

from flask import Blueprint, jsonify, make_response, render_template, request


class MyTaskSet(TaskSet):
@task(1)
def miss(l):
"""MISS X-Cache header"""
l.client.get("/response-headers?X-Cache=MISS")

@task(2)
def hit(l):
"""HIT X-Cache header"""
l.client.get("/response-headers?X-Cache=HIT")

@task(1)
def noinfo(l):
"""No X-Cache header (noinfo counter)"""
l.client.get("/")


class WebsiteUser(HttpUser):
host = "http://httpbin.org"
wait_time = between(2, 5)
tasks = [MyTaskSet]


# This example is based on the Varnish hit/miss headers (https://docs.varnish-software.com/tutorials/hit-miss-logging/).
# It could easly be customised for matching other caching sytems, CDN or custom headers.
CACHE_HEADER = "X-Cache"

cache_stats = {}

page_stats = {"hit": 0, "miss": 0, "noinfo": 0}

path = os.path.dirname(os.path.abspath(__file__))
extend = Blueprint(
"extend",
"extend_web_ui",
static_folder=f"{path}/static/",
static_url_path="/extend/static/",
template_folder=f"{path}/templates/",
)


@events.init.add_listener
def locust_init(environment, **kwargs):
"""
Load data on locust init.
:param environment:
:param kwargs:
:return:
"""

if environment.web_ui:
# this code is only run on the master node (the web_ui instance doesn't exist on workers)

def get_cache_stats():
"""
This is used by the Cache tab in the
extended web UI to show the stats.
"""
if cache_stats:
stats_tmp = []

for name, inner_stats in cache_stats.items():
stats_tmp.append(
{
"name": name,
"safe_name": escape(name, quote=False),
"hit": inner_stats["hit"],
"miss": inner_stats["miss"],
"noinfo": inner_stats["noinfo"],
}
)

# Truncate the total number of stats and errors displayed since a large number
# of rows will cause the app to render extremely slowly.
return stats_tmp[:500]
return cache_stats

@environment.web_ui.app.after_request
def extend_stats_response(response):
if request.path != "/stats/requests":
return response

# extended_stats contains the data where extended_tables looks for its data: "cache-statistics"
response.set_data(
json.dumps(
{**response.json, "extended_stats": [{"key": "cache-statistics", "data": get_cache_stats()}]}
)
)

return response

@extend.route("/extend")
def extend_web_ui():
"""
Add route to access the extended web UI with our new tab.
"""
# ensure the template_args are up to date before using them
environment.web_ui.update_template_args()
# set the static paths to use the modern ui
environment.web_ui.set_static_modern_ui()

return render_template(
"index.html",
template_args={
**environment.web_ui.template_args,
# extended_tabs and extended_tables keys must match.
"extended_tabs": [{"title": "Cache statistics", "key": "cache-statistics"}],
"extended_tables": [
{
"key": "cache-statistics",
"structure": [
{"key": "name", "title": "Name"},
{"key": "hit", "title": "Hit"},
{"key": "miss", "title": "Miss"},
{"key": "noinfo", "title": "No Info"},
],
}
],
"extended_csv_files": [{"href": "/cache/csv", "title": "Download Cache statistics CSV"}],
},
)

@extend.route("/cache/csv")
def request_cache_csv():
"""
Add route to enable downloading of cache stats as CSV
"""
response = make_response(cache_csv())
file_name = f"cache-{time()}.csv"
disposition = f"attachment;filename={file_name}"
response.headers["Content-type"] = "text/csv"
response.headers["Content-disposition"] = disposition
return response

def cache_csv():
"""Returns the cache stats as CSV."""
rows = [",".join(['"Name"', '"hit"', '"miss"', '"noinfo"'])]

if cache_stats:
for name, stats in cache_stats.items():
rows.append(f'"{name}",' + ",".join(str(v) for v in stats.values()))
return "\n".join(rows)

# register our new routes and extended UI with the Locust web UI
environment.web_ui.app.register_blueprint(extend)


@events.request.add_listener
def on_request(name, response, exception, **kwargs):
"""
Event handler that get triggered on every request
"""

cache_stats.setdefault(name, page_stats.copy())

if CACHE_HEADER not in response.headers:
cache_stats[name]["noinfo"] += 1
elif response.headers[CACHE_HEADER] == "HIT":
cache_stats[name]["hit"] += 1
elif response.headers[CACHE_HEADER] == "MISS":
cache_stats[name]["miss"] += 1


@events.report_to_master.add_listener
def on_report_to_master(client_id, data):
"""
This event is triggered on the worker instances every time a stats report is
to be sent to the locust master. It will allow us to add our extra cache
data to the dict that is being sent, and then we clear the local stats in the worker.
"""
global cache_stats
data["cache_stats"] = cache_stats
cache_stats = {}


@events.worker_report.add_listener
def on_worker_report(client_id, data):
"""
This event is triggered on the master instance when a new stats report arrives
from a worker. Here we just add the cache to the master's aggregated stats dict.
"""
for name in data["cache_stats"]:
cache_stats.setdefault(name, page_stats.copy())
for stat_name, value in data["cache_stats"][name].items():
cache_stats[name][stat_name] += value


@events.reset_stats.add_listener
def on_reset_stats():
"""
Event handler that get triggered on click of web UI Reset Stats button
"""
global cache_stats
cache_stats = {}

0 comments on commit 31d957e

Please sign in to comment.