Skip to content

Commit 31d957e

Browse files
authored
Merge pull request #2669 from obriat/feature-2644-web-ui-cache-stats
Add an example that displays cache stats using the new UI.
2 parents a7ca357 + 2fe2e0c commit 31d957e

File tree

4 files changed

+231
-3
lines changed

4 files changed

+231
-3
lines changed

Diff for: docs/extending-locust.rst

+14-3
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ To see a full list of available events see :ref:`events`.
5353

5454
.. _request_context:
5555

56+
5657
Request context
5758
===============
5859

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

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

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

114119

115120
Extending Web UI
@@ -123,9 +128,14 @@ as it involves also writing and including HTML and Javascript files to be served
123128
greatly enhance the utility and customizability of the web UI.
124129

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

134+
* ``extend_modern_web_ui.py``: Display a table with content-length for each call.
135+
136+
* ``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, ...
137+
138+
.. image:: images/extend_modern_web_ui_cache_stats.png
129139

130140

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

151161

152-
153-
154162
Run a background greenlet
155163
=========================
156164

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

193+
185194
Parametrizing locustfiles
186195
=========================
187196

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

225234
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).
226235

236+
227237
Test data management
228238
====================
229239

230240
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>`_.
231241

242+
232243
More examples
233244
=============
234245

Diff for: docs/images/extend_modern_web_ui_cache_stats.png

51.9 KB
Loading

Diff for: examples/extend_web_ui/extend.py

+7
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,16 @@
99
import os
1010
from html import escape
1111
from time import time
12+
from warnings import warn
1213

1314
from flask import Blueprint, jsonify, make_response, render_template
1415

16+
warn(
17+
"This UI example is deprecated, please downgrade to Locust 2.21.0 or use --legacy-ui flag ; version=2.22.0",
18+
DeprecationWarning,
19+
stacklevel=2,
20+
)
21+
1522

1623
class MyTaskSet(TaskSet):
1724
@task(2)

Diff for: examples/web_ui_cache_stats.py

+210
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
"""
2+
This is an example of a locustfile that uses Locust's built in event and web
3+
UI extension hooks to track the sum of Varnish cache hit/miss headers
4+
and display them in the web UI.
5+
"""
6+
7+
from locust import HttpUser, TaskSet, between, events, task, web
8+
9+
import json
10+
import os
11+
from html import escape
12+
from time import time
13+
14+
from flask import Blueprint, jsonify, make_response, render_template, request
15+
16+
17+
class MyTaskSet(TaskSet):
18+
@task(1)
19+
def miss(l):
20+
"""MISS X-Cache header"""
21+
l.client.get("/response-headers?X-Cache=MISS")
22+
23+
@task(2)
24+
def hit(l):
25+
"""HIT X-Cache header"""
26+
l.client.get("/response-headers?X-Cache=HIT")
27+
28+
@task(1)
29+
def noinfo(l):
30+
"""No X-Cache header (noinfo counter)"""
31+
l.client.get("/")
32+
33+
34+
class WebsiteUser(HttpUser):
35+
host = "http://httpbin.org"
36+
wait_time = between(2, 5)
37+
tasks = [MyTaskSet]
38+
39+
40+
# This example is based on the Varnish hit/miss headers (https://docs.varnish-software.com/tutorials/hit-miss-logging/).
41+
# It could easly be customised for matching other caching sytems, CDN or custom headers.
42+
CACHE_HEADER = "X-Cache"
43+
44+
cache_stats = {}
45+
46+
page_stats = {"hit": 0, "miss": 0, "noinfo": 0}
47+
48+
path = os.path.dirname(os.path.abspath(__file__))
49+
extend = Blueprint(
50+
"extend",
51+
"extend_web_ui",
52+
static_folder=f"{path}/static/",
53+
static_url_path="/extend/static/",
54+
template_folder=f"{path}/templates/",
55+
)
56+
57+
58+
@events.init.add_listener
59+
def locust_init(environment, **kwargs):
60+
"""
61+
Load data on locust init.
62+
:param environment:
63+
:param kwargs:
64+
:return:
65+
"""
66+
67+
if environment.web_ui:
68+
# this code is only run on the master node (the web_ui instance doesn't exist on workers)
69+
70+
def get_cache_stats():
71+
"""
72+
This is used by the Cache tab in the
73+
extended web UI to show the stats.
74+
"""
75+
if cache_stats:
76+
stats_tmp = []
77+
78+
for name, inner_stats in cache_stats.items():
79+
stats_tmp.append(
80+
{
81+
"name": name,
82+
"safe_name": escape(name, quote=False),
83+
"hit": inner_stats["hit"],
84+
"miss": inner_stats["miss"],
85+
"noinfo": inner_stats["noinfo"],
86+
}
87+
)
88+
89+
# Truncate the total number of stats and errors displayed since a large number
90+
# of rows will cause the app to render extremely slowly.
91+
return stats_tmp[:500]
92+
return cache_stats
93+
94+
@environment.web_ui.app.after_request
95+
def extend_stats_response(response):
96+
if request.path != "/stats/requests":
97+
return response
98+
99+
# extended_stats contains the data where extended_tables looks for its data: "cache-statistics"
100+
response.set_data(
101+
json.dumps(
102+
{**response.json, "extended_stats": [{"key": "cache-statistics", "data": get_cache_stats()}]}
103+
)
104+
)
105+
106+
return response
107+
108+
@extend.route("/extend")
109+
def extend_web_ui():
110+
"""
111+
Add route to access the extended web UI with our new tab.
112+
"""
113+
# ensure the template_args are up to date before using them
114+
environment.web_ui.update_template_args()
115+
# set the static paths to use the modern ui
116+
environment.web_ui.set_static_modern_ui()
117+
118+
return render_template(
119+
"index.html",
120+
template_args={
121+
**environment.web_ui.template_args,
122+
# extended_tabs and extended_tables keys must match.
123+
"extended_tabs": [{"title": "Cache statistics", "key": "cache-statistics"}],
124+
"extended_tables": [
125+
{
126+
"key": "cache-statistics",
127+
"structure": [
128+
{"key": "name", "title": "Name"},
129+
{"key": "hit", "title": "Hit"},
130+
{"key": "miss", "title": "Miss"},
131+
{"key": "noinfo", "title": "No Info"},
132+
],
133+
}
134+
],
135+
"extended_csv_files": [{"href": "/cache/csv", "title": "Download Cache statistics CSV"}],
136+
},
137+
)
138+
139+
@extend.route("/cache/csv")
140+
def request_cache_csv():
141+
"""
142+
Add route to enable downloading of cache stats as CSV
143+
"""
144+
response = make_response(cache_csv())
145+
file_name = f"cache-{time()}.csv"
146+
disposition = f"attachment;filename={file_name}"
147+
response.headers["Content-type"] = "text/csv"
148+
response.headers["Content-disposition"] = disposition
149+
return response
150+
151+
def cache_csv():
152+
"""Returns the cache stats as CSV."""
153+
rows = [",".join(['"Name"', '"hit"', '"miss"', '"noinfo"'])]
154+
155+
if cache_stats:
156+
for name, stats in cache_stats.items():
157+
rows.append(f'"{name}",' + ",".join(str(v) for v in stats.values()))
158+
return "\n".join(rows)
159+
160+
# register our new routes and extended UI with the Locust web UI
161+
environment.web_ui.app.register_blueprint(extend)
162+
163+
164+
@events.request.add_listener
165+
def on_request(name, response, exception, **kwargs):
166+
"""
167+
Event handler that get triggered on every request
168+
"""
169+
170+
cache_stats.setdefault(name, page_stats.copy())
171+
172+
if CACHE_HEADER not in response.headers:
173+
cache_stats[name]["noinfo"] += 1
174+
elif response.headers[CACHE_HEADER] == "HIT":
175+
cache_stats[name]["hit"] += 1
176+
elif response.headers[CACHE_HEADER] == "MISS":
177+
cache_stats[name]["miss"] += 1
178+
179+
180+
@events.report_to_master.add_listener
181+
def on_report_to_master(client_id, data):
182+
"""
183+
This event is triggered on the worker instances every time a stats report is
184+
to be sent to the locust master. It will allow us to add our extra cache
185+
data to the dict that is being sent, and then we clear the local stats in the worker.
186+
"""
187+
global cache_stats
188+
data["cache_stats"] = cache_stats
189+
cache_stats = {}
190+
191+
192+
@events.worker_report.add_listener
193+
def on_worker_report(client_id, data):
194+
"""
195+
This event is triggered on the master instance when a new stats report arrives
196+
from a worker. Here we just add the cache to the master's aggregated stats dict.
197+
"""
198+
for name in data["cache_stats"]:
199+
cache_stats.setdefault(name, page_stats.copy())
200+
for stat_name, value in data["cache_stats"][name].items():
201+
cache_stats[name][stat_name] += value
202+
203+
204+
@events.reset_stats.add_listener
205+
def on_reset_stats():
206+
"""
207+
Event handler that get triggered on click of web UI Reset Stats button
208+
"""
209+
global cache_stats
210+
cache_stats = {}

0 commit comments

Comments
 (0)