locust-cloud 1.5.3__py3-none-any.whl → 1.5.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- locust_cloud/__init__.py +25 -11
- locust_cloud/auth.py +25 -30
- locust_cloud/timescale/exporter.py +2 -2
- locust_cloud/timescale/queries.py +8 -0
- locust_cloud/timescale/query.py +14 -0
- locust_cloud/webui/dist/assets/{index-BeDy6pr-.js → index-DUDp7J7F.js} +80 -80
- locust_cloud/webui/dist/index.html +1 -1
- locust_cloud/webui/tsconfig.tsbuildinfo +1 -1
- {locust_cloud-1.5.3.dist-info → locust_cloud-1.5.4.dist-info}/METADATA +1 -1
- {locust_cloud-1.5.3.dist-info → locust_cloud-1.5.4.dist-info}/RECORD +12 -12
- {locust_cloud-1.5.3.dist-info → locust_cloud-1.5.4.dist-info}/WHEEL +0 -0
- {locust_cloud-1.5.3.dist-info → locust_cloud-1.5.4.dist-info}/entry_points.txt +0 -0
locust_cloud/__init__.py
CHANGED
@@ -3,8 +3,10 @@ import os
|
|
3
3
|
os.environ["LOCUST_SKIP_MONKEY_PATCH"] = "1"
|
4
4
|
|
5
5
|
import argparse
|
6
|
+
import logging
|
6
7
|
import sys
|
7
8
|
|
9
|
+
import locust.env
|
8
10
|
import psycopg
|
9
11
|
from locust import events
|
10
12
|
from locust.argument_parser import LocustArgumentParser
|
@@ -19,6 +21,7 @@ PG_PASSWORD = os.environ.get("PG_PASSWORD")
|
|
19
21
|
PG_DATABASE = os.environ.get("PG_DATABASE")
|
20
22
|
PG_PORT = os.environ.get("PG_PORT", 5432)
|
21
23
|
GRAPH_VIEWER = os.environ.get("GRAPH_VIEWER")
|
24
|
+
logger = logging.getLogger(__name__)
|
22
25
|
|
23
26
|
|
24
27
|
@events.init_command_line_parser.add_listener
|
@@ -55,7 +58,9 @@ def set_autocommit(conn: psycopg.Connection):
|
|
55
58
|
conn.autocommit = True
|
56
59
|
|
57
60
|
|
58
|
-
def create_connection_pool(
|
61
|
+
def create_connection_pool(
|
62
|
+
pg_user: str, pg_host: str, pg_password: str, pg_database: str, pg_port: str | int
|
63
|
+
) -> ConnectionPool:
|
59
64
|
try:
|
60
65
|
return ConnectionPool(
|
61
66
|
conninfo=f"postgres://{pg_user}:{pg_password}@{pg_host}:{pg_port}/{pg_database}?sslmode=require",
|
@@ -69,19 +74,28 @@ def create_connection_pool(pg_user, pg_host, pg_password, pg_database, pg_port):
|
|
69
74
|
|
70
75
|
|
71
76
|
@events.init.add_listener
|
72
|
-
def on_locust_init(environment, **_args):
|
73
|
-
if not (PG_HOST
|
77
|
+
def on_locust_init(environment: locust.env.Environment, **_args):
|
78
|
+
if not (PG_HOST and PG_USER and PG_PASSWORD and PG_DATABASE and PG_PORT):
|
74
79
|
return
|
75
80
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
81
|
+
if GRAPH_VIEWER:
|
82
|
+
environment.runner.state = "STOPPED"
|
83
|
+
|
84
|
+
try:
|
85
|
+
pool = create_connection_pool(
|
86
|
+
pg_user=PG_USER,
|
87
|
+
pg_host=PG_HOST,
|
88
|
+
pg_password=PG_PASSWORD,
|
89
|
+
pg_database=PG_DATABASE,
|
90
|
+
pg_port=PG_PORT,
|
91
|
+
)
|
92
|
+
pool.wait()
|
93
|
+
except Exception as e:
|
94
|
+
logger.exception(e)
|
95
|
+
logger.error(f"{PG_HOST=}")
|
96
|
+
raise
|
83
97
|
|
84
|
-
if not GRAPH_VIEWER and environment.parsed_options.exporter:
|
98
|
+
if not GRAPH_VIEWER and environment.parsed_options and environment.parsed_options.exporter:
|
85
99
|
Exporter(environment, pool)
|
86
100
|
|
87
101
|
if environment.web_ui:
|
locust_cloud/auth.py
CHANGED
@@ -1,71 +1,66 @@
|
|
1
1
|
import os
|
2
2
|
from datetime import UTC, datetime, timedelta
|
3
|
+
from typing import TypedDict
|
3
4
|
|
5
|
+
import locust.env
|
4
6
|
import requests
|
7
|
+
import werkzeug
|
5
8
|
from flask import redirect, request, url_for
|
6
|
-
from flask_login import UserMixin,
|
9
|
+
from flask_login import UserMixin, login_user
|
7
10
|
from locust_cloud.constants import DEFAULT_LAMBDA_URL
|
8
11
|
|
9
12
|
LAMBDA = os.environ.get("LOCUST_API_BASE_URL", DEFAULT_LAMBDA_URL)
|
10
13
|
|
11
14
|
|
15
|
+
class Credentials(TypedDict):
|
16
|
+
user_sub_id: str
|
17
|
+
refresh_token: str
|
18
|
+
|
19
|
+
|
12
20
|
class AuthUser(UserMixin):
|
13
|
-
def __init__(self, user_sub_id):
|
21
|
+
def __init__(self, user_sub_id: str):
|
14
22
|
self.user_sub_id = user_sub_id
|
15
23
|
|
16
24
|
def get_id(self):
|
17
25
|
return self.user_sub_id
|
18
26
|
|
19
27
|
|
20
|
-
def set_credentials(credentials, response):
|
21
|
-
if not credentials.get("
|
28
|
+
def set_credentials(username: str, credentials: Credentials, response: werkzeug.wrappers.response.Response):
|
29
|
+
if not credentials.get("user_sub_id"):
|
22
30
|
return response
|
23
31
|
|
24
|
-
id_token = credentials["cognito_client_id_token"]
|
25
32
|
user_sub_id = credentials["user_sub_id"]
|
26
33
|
refresh_token = credentials["refresh_token"]
|
27
34
|
|
28
|
-
response.set_cookie("
|
35
|
+
response.set_cookie("username", username, expires=datetime.now(tz=UTC) + timedelta(days=365))
|
29
36
|
response.set_cookie("user_token", refresh_token, expires=datetime.now(tz=UTC) + timedelta(days=365))
|
30
37
|
response.set_cookie("user_sub_id", user_sub_id, expires=datetime.now(tz=UTC) + timedelta(days=365))
|
31
38
|
|
32
39
|
return response
|
33
40
|
|
34
41
|
|
35
|
-
def
|
36
|
-
|
42
|
+
def register_auth(environment: locust.env.Environment):
|
43
|
+
environment.web_ui.app.config["SECRET_KEY"] = os.getenv("SECRET_KEY")
|
44
|
+
environment.web_ui.app.debug = False
|
37
45
|
|
38
|
-
|
39
|
-
|
46
|
+
def load_user(user_sub_id: str):
|
47
|
+
username = request.cookies.get("username")
|
48
|
+
refresh_token = request.cookies.get("user_token")
|
40
49
|
|
41
|
-
|
50
|
+
if refresh_token:
|
51
|
+
environment.web_ui.template_args["username"] = username
|
52
|
+
return AuthUser(user_sub_id)
|
42
53
|
|
54
|
+
return None
|
43
55
|
|
44
|
-
def register_auth(environment):
|
45
|
-
environment.web_ui.app.config["SECRET_KEY"] = os.getenv("SECRET_KEY")
|
46
|
-
environment.web_ui.app.debug = False
|
47
56
|
environment.web_ui.login_manager.user_loader(load_user)
|
48
57
|
environment.web_ui.auth_args = {
|
49
58
|
"username_password_callback": "/authenticate",
|
50
59
|
}
|
51
60
|
|
52
|
-
@environment.web_ui.app.after_request
|
53
|
-
def refresh_handler(response):
|
54
|
-
if request.path == "/" and current_user:
|
55
|
-
refresh_token = request.cookies.get("user_token")
|
56
|
-
user_sub_id = request.cookies.get("user_sub_id")
|
57
|
-
if user_sub_id and refresh_token:
|
58
|
-
auth_response = requests.post(
|
59
|
-
f"{LAMBDA}/auth/login", json={"user_sub_id": user_sub_id, "refresh_token": refresh_token}
|
60
|
-
)
|
61
|
-
credentials = auth_response.json()
|
62
|
-
response = set_credentials(credentials, response)
|
63
|
-
|
64
|
-
return response
|
65
|
-
|
66
61
|
@environment.web_ui.app.route("/authenticate", methods=["POST"])
|
67
62
|
def login_submit():
|
68
|
-
username = request.form.get("username")
|
63
|
+
username = request.form.get("username", "")
|
69
64
|
password = request.form.get("password")
|
70
65
|
|
71
66
|
try:
|
@@ -74,7 +69,7 @@ def register_auth(environment):
|
|
74
69
|
if auth_response.status_code == 200:
|
75
70
|
credentials = auth_response.json()
|
76
71
|
response = redirect(url_for("index"))
|
77
|
-
response = set_credentials(credentials, response)
|
72
|
+
response = set_credentials(username, credentials, response)
|
78
73
|
login_user(AuthUser(credentials["user_sub_id"]))
|
79
74
|
|
80
75
|
return response
|
@@ -235,7 +235,7 @@ class Exporter:
|
|
235
235
|
"""
|
236
236
|
UPDATE testruns
|
237
237
|
SET (requests, resp_time_avg, rps_avg, fail_ratio) =
|
238
|
-
(SELECT reqs, resp_time, reqs / GREATEST(duration, 1), fails / reqs) FROM
|
238
|
+
(SELECT reqs, resp_time, reqs / GREATEST(duration, 1), fails / GREATEST(reqs, 1)) FROM
|
239
239
|
(SELECT
|
240
240
|
COUNT(*)::numeric AS reqs,
|
241
241
|
AVG(response_time)::numeric as resp_time
|
@@ -248,7 +248,7 @@ SET (requests, resp_time_avg, rps_avg, fail_ratio) =
|
|
248
248
|
WHERE id = %(run_id)s""",
|
249
249
|
{"run_id": self._run_id},
|
250
250
|
)
|
251
|
-
except psycopg.errors.DivisionByZero:
|
251
|
+
except psycopg.errors.DivisionByZero: # remove this except later, because it shouldnt happen any more
|
252
252
|
logging.info(
|
253
253
|
"Got DivisionByZero error when trying to update testruns, most likely because there were no requests logged"
|
254
254
|
)
|
@@ -255,6 +255,13 @@ JOIN avg_response_time_failed f ON a.time = f.time
|
|
255
255
|
ORDER BY a.time
|
256
256
|
"""
|
257
257
|
|
258
|
+
total_runtime = """
|
259
|
+
SELECT
|
260
|
+
SUM((end_time - id) * num_users) AS "totalVuh"
|
261
|
+
FROM testruns
|
262
|
+
WHERE id >= date_trunc('month', NOW())
|
263
|
+
"""
|
264
|
+
|
258
265
|
queries: dict["str", LiteralString] = {
|
259
266
|
"request-names": request_names,
|
260
267
|
"requests": requests_query,
|
@@ -273,4 +280,5 @@ queries: dict["str", LiteralString] = {
|
|
273
280
|
"testruns-table": testruns_table,
|
274
281
|
"testruns-rps": testruns_rps,
|
275
282
|
"testruns-response-time": testruns_response_time,
|
283
|
+
"total-runtime": total_runtime,
|
276
284
|
}
|
locust_cloud/timescale/query.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
import logging
|
2
2
|
|
3
3
|
from flask import make_response, request
|
4
|
+
from flask_login import login_required
|
4
5
|
from locust_cloud.timescale.queries import queries
|
5
6
|
|
6
7
|
logger = logging.getLogger(__name__)
|
@@ -12,6 +13,7 @@ def adapt_timestamp(result):
|
|
12
13
|
|
13
14
|
def register_query(environment, pool):
|
14
15
|
@environment.web_ui.app.route("/cloud-stats/<query>", methods=["POST"])
|
16
|
+
@login_required
|
15
17
|
def query(query):
|
16
18
|
results = []
|
17
19
|
try:
|
@@ -21,6 +23,18 @@ def register_query(environment, pool):
|
|
21
23
|
# get_conn_time = (time.perf_counter() - start_time) * 1000
|
22
24
|
sql_params = request.get_json()
|
23
25
|
# start_time = time.perf_counter()
|
26
|
+
from datetime import datetime, timedelta
|
27
|
+
|
28
|
+
if "start" in sql_params:
|
29
|
+
# protect the database against huge queries
|
30
|
+
start_time = datetime.fromisoformat(sql_params["start"])
|
31
|
+
end_time = datetime.fromisoformat(sql_params["end"])
|
32
|
+
if end_time >= start_time + timedelta(hours=6):
|
33
|
+
logger.warning(
|
34
|
+
f"UI asked for too long time interval. Start was {sql_params['start']}, end was {sql_params['end']}"
|
35
|
+
)
|
36
|
+
return []
|
37
|
+
|
24
38
|
cursor = conn.execute(queries[query], sql_params)
|
25
39
|
# exec_time = (time.perf_counter() - start_time) * 1000
|
26
40
|
assert cursor
|