locust-cloud 1.7.0__py3-none-any.whl → 1.9.0__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 +4 -19
- locust_cloud/auth.py +177 -14
- locust_cloud/cloud.py +28 -55
- locust_cloud/credential_manager.py +5 -2
- locust_cloud/idle_exit.py +2 -2
- locust_cloud/timescale/exporter.py +43 -25
- locust_cloud/timescale/queries.py +32 -21
- locust_cloud/timescale/query.py +10 -4
- locust_cloud/webui/.eslintrc +1 -1
- locust_cloud/webui/dist/assets/{index-zX0XW_CD.js → index-DVpRWzFO.js} +89 -89
- locust_cloud/webui/dist/index.html +1 -1
- locust_cloud/webui/package.json +2 -2
- locust_cloud/webui/tsconfig.tsbuildinfo +1 -1
- locust_cloud/webui/yarn.lock +6 -131
- locust_cloud-1.9.0.dist-info/METADATA +15 -0
- locust_cloud-1.9.0.dist-info/RECORD +23 -0
- locust_cloud/constants.py +0 -5
- locust_cloud-1.7.0.dist-info/METADATA +0 -52
- locust_cloud-1.7.0.dist-info/RECORD +0 -24
- {locust_cloud-1.7.0.dist-info → locust_cloud-1.9.0.dist-info}/WHEEL +0 -0
- {locust_cloud-1.7.0.dist-info → locust_cloud-1.9.0.dist-info}/entry_points.txt +0 -0
locust_cloud/__init__.py
CHANGED
@@ -19,22 +19,16 @@ from locust_cloud.timescale.query import register_query
|
|
19
19
|
from psycopg.conninfo import make_conninfo
|
20
20
|
from psycopg_pool import ConnectionPool
|
21
21
|
|
22
|
-
PG_USER = os.environ.get("PG_USER")
|
23
|
-
PG_HOST = os.environ.get("PG_HOST")
|
24
|
-
PG_PASSWORD = os.environ.get("PG_PASSWORD")
|
25
|
-
PG_DATABASE = os.environ.get("PG_DATABASE")
|
26
|
-
PG_PORT = os.environ.get("PG_PORT", 5432)
|
27
22
|
GRAPH_VIEWER = os.environ.get("GRAPH_VIEWER")
|
28
|
-
MAX_USER_COUNT = os.environ.get("MAX_USER_COUNT")
|
29
23
|
logger = logging.getLogger(__name__)
|
30
24
|
|
31
25
|
|
32
26
|
@events.init_command_line_parser.add_listener
|
33
27
|
def add_arguments(parser: LocustArgumentParser):
|
34
|
-
if not (
|
28
|
+
if not (os.environ.get("PGHOST") or GRAPH_VIEWER):
|
35
29
|
parser.add_argument_group(
|
36
30
|
"locust-cloud",
|
37
|
-
"locust-cloud disabled, because
|
31
|
+
"locust-cloud disabled, because PGHOST was not set - this is normal for local runs",
|
38
32
|
)
|
39
33
|
return
|
40
34
|
|
@@ -73,30 +67,23 @@ def set_autocommit(conn: psycopg.Connection):
|
|
73
67
|
|
74
68
|
@events.init.add_listener
|
75
69
|
def on_locust_init(environment: locust.env.Environment, **_args):
|
76
|
-
if not (
|
70
|
+
if not (os.environ.get("PGHOST")):
|
77
71
|
return
|
78
72
|
|
79
73
|
try:
|
80
74
|
conninfo = make_conninfo(
|
81
|
-
dbname=PG_DATABASE,
|
82
|
-
user=PG_USER,
|
83
|
-
port=PG_PORT,
|
84
|
-
password=PG_PASSWORD,
|
85
|
-
host=PG_HOST,
|
86
75
|
sslmode="require",
|
87
|
-
# options="-c statement_timeout=55000",
|
88
76
|
)
|
89
77
|
pool = ConnectionPool(
|
90
78
|
conninfo,
|
91
79
|
min_size=1,
|
92
|
-
max_size=
|
80
|
+
max_size=20,
|
93
81
|
configure=set_autocommit,
|
94
82
|
check=ConnectionPool.check_connection,
|
95
83
|
)
|
96
84
|
pool.wait()
|
97
85
|
except Exception as e:
|
98
86
|
logger.exception(e)
|
99
|
-
logger.error(f"{PG_HOST=}")
|
100
87
|
raise
|
101
88
|
|
102
89
|
if not GRAPH_VIEWER:
|
@@ -106,8 +93,6 @@ def on_locust_init(environment: locust.env.Environment, **_args):
|
|
106
93
|
Exporter(environment, pool)
|
107
94
|
|
108
95
|
if environment.web_ui:
|
109
|
-
environment.web_ui.template_args["maxUserCount"] = MAX_USER_COUNT
|
110
|
-
|
111
96
|
if GRAPH_VIEWER:
|
112
97
|
environment.web_ui.template_args["isGraphViewer"] = True
|
113
98
|
|
locust_cloud/auth.py
CHANGED
@@ -1,17 +1,20 @@
|
|
1
1
|
import logging
|
2
2
|
import os
|
3
3
|
from datetime import UTC, datetime, timedelta
|
4
|
-
from typing import TypedDict
|
4
|
+
from typing import Any, TypedDict, cast
|
5
5
|
|
6
6
|
import locust.env
|
7
7
|
import requests
|
8
8
|
import werkzeug
|
9
|
-
from flask import redirect, request, url_for
|
9
|
+
from flask import Blueprint, redirect, request, session, url_for
|
10
10
|
from flask_login import UserMixin, login_user
|
11
|
+
from locust.html import render_template_from
|
11
12
|
from locust_cloud import __version__
|
12
|
-
from locust_cloud.constants import DEFAULT_DEPLOYER_URL
|
13
13
|
|
14
|
-
|
14
|
+
REGION = os.environ.get("AWS_DEFAULT_REGION")
|
15
|
+
DEPLOYER_URL = f"https://api.{REGION}.locust.cloud/1"
|
16
|
+
ALLOW_SIGNUP = os.environ.get("ALLOW_SIGNUP", True)
|
17
|
+
|
15
18
|
|
16
19
|
logger = logging.getLogger(__name__)
|
17
20
|
|
@@ -47,6 +50,9 @@ def register_auth(environment: locust.env.Environment):
|
|
47
50
|
environment.web_ui.app.config["SECRET_KEY"] = os.getenv("SECRET_KEY")
|
48
51
|
environment.web_ui.app.debug = False
|
49
52
|
|
53
|
+
web_base_path = environment.parsed_options.web_base_path
|
54
|
+
auth_blueprint = Blueprint("locust_cloud_auth", __name__, url_prefix=web_base_path)
|
55
|
+
|
50
56
|
def load_user(user_sub_id: str):
|
51
57
|
username = request.cookies.get("username")
|
52
58
|
refresh_token = request.cookies.get("user_token")
|
@@ -58,11 +64,19 @@ def register_auth(environment: locust.env.Environment):
|
|
58
64
|
return None
|
59
65
|
|
60
66
|
environment.web_ui.login_manager.user_loader(load_user)
|
61
|
-
environment.web_ui.auth_args =
|
62
|
-
|
63
|
-
|
67
|
+
environment.web_ui.auth_args = cast(
|
68
|
+
Any,
|
69
|
+
{
|
70
|
+
"username_password_callback": f"{web_base_path}/authenticate",
|
71
|
+
},
|
72
|
+
)
|
73
|
+
|
74
|
+
if ALLOW_SIGNUP:
|
75
|
+
environment.web_ui.auth_args["auth_providers"] = [
|
76
|
+
{"label": "Sign Up", "callback_url": f"{web_base_path}/signup"}
|
77
|
+
]
|
64
78
|
|
65
|
-
@
|
79
|
+
@auth_blueprint.route("/authenticate", methods=["POST"])
|
66
80
|
def login_submit():
|
67
81
|
username = request.form.get("username", "")
|
68
82
|
password = request.form.get("password")
|
@@ -77,19 +91,168 @@ def register_auth(environment: locust.env.Environment):
|
|
77
91
|
auth_response.raise_for_status()
|
78
92
|
|
79
93
|
credentials = auth_response.json()
|
80
|
-
response = redirect(url_for("index"))
|
94
|
+
response = redirect(url_for("locust.index"))
|
81
95
|
response = set_credentials(username, credentials, response)
|
82
96
|
login_user(AuthUser(credentials["user_sub_id"]))
|
83
97
|
|
84
98
|
return response
|
85
99
|
except requests.exceptions.HTTPError as e:
|
86
100
|
if e.response.status_code == 401:
|
87
|
-
|
101
|
+
session["auth_error"] = "Invalid username or password"
|
88
102
|
else:
|
89
103
|
logger.error(f"Unknown response from auth: {e.response.status_code} {e.response.text}")
|
90
104
|
|
91
|
-
|
92
|
-
|
93
|
-
|
105
|
+
session["auth_error"] = "Unknown error during authentication, check logs and/or contact support"
|
106
|
+
|
107
|
+
return redirect(url_for("locust.login"))
|
108
|
+
|
109
|
+
@auth_blueprint.route("/signup")
|
110
|
+
def signup():
|
111
|
+
if not ALLOW_SIGNUP:
|
112
|
+
return redirect(url_for("locust.login"))
|
113
|
+
|
114
|
+
if session.get("username"):
|
115
|
+
sign_up_args = {
|
116
|
+
"custom_form": {
|
117
|
+
"inputs": [
|
118
|
+
{
|
119
|
+
"label": "Confirmation Code",
|
120
|
+
"name": "confirmation_code",
|
121
|
+
},
|
122
|
+
],
|
123
|
+
"callback_url": f"{web_base_path}/confirm-signup",
|
124
|
+
"submit_button_text": "Confirm Email",
|
125
|
+
},
|
126
|
+
}
|
127
|
+
else:
|
128
|
+
sign_up_args = {
|
129
|
+
"custom_form": {
|
130
|
+
"inputs": [
|
131
|
+
{
|
132
|
+
"label": "Username",
|
133
|
+
"name": "username",
|
134
|
+
},
|
135
|
+
{
|
136
|
+
"label": "Password",
|
137
|
+
"name": "password",
|
138
|
+
"is_secret": True,
|
139
|
+
},
|
140
|
+
{
|
141
|
+
"label": "Access Code",
|
142
|
+
"name": "access_code",
|
143
|
+
},
|
144
|
+
{
|
145
|
+
"label": "I consent to:\n\n1. Only test your own website/service or our example example target\n\n2. Only use locust-cloud for its intended purpose: to load test other sites/services.\n\n3. Not attempt to circumvent your account limitations (e.g. max user count or max request count)\n\n4. Not use personal data (real names, addresses etc) in your tests.",
|
146
|
+
"name": "consent",
|
147
|
+
"default_value": False,
|
148
|
+
},
|
149
|
+
],
|
150
|
+
"callback_url": f"{web_base_path}/create-account",
|
151
|
+
"submit_button_text": "Sign Up",
|
152
|
+
},
|
153
|
+
}
|
154
|
+
|
155
|
+
if session.get("auth_info"):
|
156
|
+
sign_up_args["info"] = session["auth_info"]
|
157
|
+
if session.get("auth_sign_up_error"):
|
158
|
+
sign_up_args["error"] = session["auth_sign_up_error"]
|
159
|
+
|
160
|
+
return render_template_from(
|
161
|
+
"auth.html",
|
162
|
+
auth_args=sign_up_args,
|
163
|
+
)
|
164
|
+
|
165
|
+
@auth_blueprint.route("/create-account", methods=["POST"])
|
166
|
+
def create_account():
|
167
|
+
if not ALLOW_SIGNUP:
|
168
|
+
return redirect(url_for("locust.login"))
|
169
|
+
|
170
|
+
session["auth_sign_up_error"] = ""
|
171
|
+
session["auth_info"] = ""
|
172
|
+
|
173
|
+
username = request.form.get("username", "")
|
174
|
+
password = request.form.get("password")
|
175
|
+
access_code = request.form.get("access_code")
|
176
|
+
has_consented = request.form.get("consent")
|
177
|
+
|
178
|
+
if not has_consented:
|
179
|
+
session["auth_sign_up_error"] = "Please accept the terms and conditions to create an account."
|
180
|
+
|
181
|
+
return redirect(url_for("locust_cloud_auth.signup"))
|
182
|
+
|
183
|
+
try:
|
184
|
+
auth_response = requests.post(
|
185
|
+
f"{DEPLOYER_URL}/auth/signup",
|
186
|
+
json={"username": username, "password": password, "access_code": access_code},
|
187
|
+
)
|
188
|
+
|
189
|
+
auth_response.raise_for_status()
|
190
|
+
|
191
|
+
session["user_sub"] = auth_response.json().get("user_sub")
|
192
|
+
session["username"] = username
|
193
|
+
session["auth_info"] = (
|
194
|
+
"Please check your email and enter the confirmation code. If you didn't get a code after one minute, you can [request a new one](/resend-code)"
|
195
|
+
)
|
196
|
+
|
197
|
+
return redirect(url_for("locust_cloud_auth.signup"))
|
198
|
+
except requests.exceptions.HTTPError as e:
|
199
|
+
message = e.response.json().get("Message", "An unexpected error occured. Please try again.")
|
200
|
+
session["auth_info"] = ""
|
201
|
+
session["auth_sign_up_error"] = message
|
202
|
+
|
203
|
+
return redirect(url_for("locust_cloud_auth.signup"))
|
204
|
+
|
205
|
+
@auth_blueprint.route("/resend-code")
|
206
|
+
def resend_code():
|
207
|
+
try:
|
208
|
+
auth_response = requests.post(
|
209
|
+
"http://localhost:8000/1/auth/resend-confirmation",
|
210
|
+
json={"username": session["username"]},
|
211
|
+
)
|
212
|
+
|
213
|
+
auth_response.raise_for_status()
|
214
|
+
|
215
|
+
session["auth_sign_up_error"] = ""
|
216
|
+
session["auth_info"] = "Confirmation code sent, please check your email."
|
217
|
+
|
218
|
+
return redirect(url_for("locust_cloud_auth.signup"))
|
219
|
+
except requests.exceptions.HTTPError as e:
|
220
|
+
message = e.response.json().get("Message", "An unexpected error occured. Please try again.")
|
221
|
+
session["auth_info"] = ""
|
222
|
+
session["auth_sign_up_error"] = message
|
223
|
+
|
224
|
+
return redirect(url_for("locust_cloud_auth.signup"))
|
225
|
+
|
226
|
+
@auth_blueprint.route("/confirm-signup", methods=["POST"])
|
227
|
+
def confirm_signup():
|
228
|
+
if not ALLOW_SIGNUP:
|
229
|
+
return redirect(url_for("locust.login"))
|
230
|
+
|
231
|
+
session["auth_sign_up_error"] = ""
|
232
|
+
confirmation_code = request.form.get("confirmation_code")
|
233
|
+
|
234
|
+
try:
|
235
|
+
auth_response = requests.post(
|
236
|
+
f"{DEPLOYER_URL}/auth/confirm-signup",
|
237
|
+
json={
|
238
|
+
"username": session.get("username"),
|
239
|
+
"user_sub": session["user_sub"],
|
240
|
+
"confirmation_code": confirmation_code,
|
241
|
+
},
|
242
|
+
)
|
243
|
+
|
244
|
+
auth_response.raise_for_status()
|
245
|
+
|
246
|
+
session["username"] = None
|
247
|
+
session["auth_info"] = "Account created successfully!"
|
248
|
+
session["auth_sign_up_error"] = ""
|
249
|
+
|
250
|
+
return redirect(url_for("locust.login"))
|
251
|
+
except requests.exceptions.HTTPError as e:
|
252
|
+
message = e.response.json().get("Message", "An unexpected error occured. Please try again.")
|
253
|
+
session["auth_info"] = ""
|
254
|
+
session["auth_sign_up_error"] = message
|
255
|
+
|
256
|
+
return redirect(url_for("locust_cloud_auth.signup"))
|
94
257
|
|
95
|
-
|
258
|
+
environment.web_ui.app.register_blueprint(auth_blueprint)
|
locust_cloud/cloud.py
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
import json
|
2
2
|
import logging
|
3
|
-
import math
|
4
3
|
import os
|
5
4
|
import sys
|
6
5
|
import time
|
@@ -14,13 +13,6 @@ import configargparse
|
|
14
13
|
import requests
|
15
14
|
from botocore.exceptions import ClientError
|
16
15
|
from locust_cloud import __version__
|
17
|
-
from locust_cloud.constants import (
|
18
|
-
DEFAULT_CLUSTER_NAME,
|
19
|
-
DEFAULT_DEPLOYER_URL,
|
20
|
-
DEFAULT_NAMESPACE,
|
21
|
-
DEFAULT_REGION_NAME,
|
22
|
-
USERS_PER_WORKER,
|
23
|
-
)
|
24
16
|
from locust_cloud.credential_manager import CredentialError, CredentialManager
|
25
17
|
|
26
18
|
|
@@ -110,29 +102,9 @@ advanced.add_argument(
|
|
110
102
|
advanced.add_argument(
|
111
103
|
"--region",
|
112
104
|
type=str,
|
113
|
-
default=os.environ.get("AWS_DEFAULT_REGION"
|
105
|
+
default=os.environ.get("AWS_DEFAULT_REGION"),
|
114
106
|
help="Sets the AWS region to use for the deployed cluster, e.g. us-east-1. It defaults to use AWS_DEFAULT_REGION env var, like AWS tools.",
|
115
107
|
)
|
116
|
-
advanced.add_argument(
|
117
|
-
"--kube-cluster-name",
|
118
|
-
type=str,
|
119
|
-
default=DEFAULT_CLUSTER_NAME,
|
120
|
-
help=configargparse.SUPPRESS,
|
121
|
-
env_var="KUBE_CLUSTER_NAME",
|
122
|
-
)
|
123
|
-
advanced.add_argument(
|
124
|
-
"--kube-namespace",
|
125
|
-
type=str,
|
126
|
-
default=DEFAULT_NAMESPACE,
|
127
|
-
help=configargparse.SUPPRESS,
|
128
|
-
env_var="KUBE_NAMESPACE",
|
129
|
-
)
|
130
|
-
parser.add_argument(
|
131
|
-
"--deployer-url",
|
132
|
-
type=str,
|
133
|
-
default=DEFAULT_DEPLOYER_URL,
|
134
|
-
help=configargparse.SUPPRESS,
|
135
|
-
)
|
136
108
|
parser.add_argument(
|
137
109
|
"--aws-access-key-id",
|
138
110
|
type=str,
|
@@ -162,7 +134,7 @@ parser.add_argument(
|
|
162
134
|
parser.add_argument(
|
163
135
|
"--workers",
|
164
136
|
type=int,
|
165
|
-
help=
|
137
|
+
help="Number of workers to use for the deployment. Defaults to number of users divided by 500, but the default may be customized for your account.",
|
166
138
|
default=None,
|
167
139
|
)
|
168
140
|
parser.add_argument(
|
@@ -194,30 +166,30 @@ logging.getLogger("requests").setLevel(logging.INFO)
|
|
194
166
|
logging.getLogger("urllib3").setLevel(logging.INFO)
|
195
167
|
|
196
168
|
|
169
|
+
api_url = f"https://api.{options.region}.locust.cloud/1"
|
170
|
+
|
171
|
+
|
197
172
|
def main() -> None:
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
if options.users > 5000000:
|
203
|
-
logger.error("You asked for more than 5000000 Users, that isn't allowed.")
|
173
|
+
if not options.region:
|
174
|
+
logger.error(
|
175
|
+
"Setting a region is required to use Locust Cloud. Please ensure the AWS_DEFAULT_REGION env variable or the --region flag is set."
|
176
|
+
)
|
204
177
|
sys.exit(1)
|
205
|
-
|
206
|
-
|
178
|
+
|
179
|
+
s3_bucket = "dmdb-default" if options.region == "us-east-1" else "locust-default"
|
180
|
+
deployments: list[Any] = []
|
181
|
+
|
182
|
+
if not ((options.username and options.password) or (options.aws_access_key_id and options.aws_secret_access_key)):
|
183
|
+
logger.error(
|
184
|
+
"Authentication is required to use Locust Cloud. Please ensure the LOCUST_CLOUD_USERNAME and LOCUST_CLOUD_PASSWORD environment variables are set."
|
185
|
+
)
|
207
186
|
sys.exit(1)
|
208
|
-
try:
|
209
|
-
if not (
|
210
|
-
(options.username and options.password) or (options.aws_access_key_id and options.aws_secret_access_key)
|
211
|
-
):
|
212
|
-
logger.error(
|
213
|
-
"Authentication is required to use Locust Cloud. Please ensure the LOCUST_CLOUD_USERNAME and LOCUST_CLOUD_PASSWORD environment variables are set."
|
214
|
-
)
|
215
|
-
sys.exit(1)
|
216
187
|
|
217
|
-
|
218
|
-
logger.
|
188
|
+
try:
|
189
|
+
logger.info(f"Authenticating ({options.region}, v{__version__})")
|
190
|
+
logger.debug(f"Lambda url: {api_url}")
|
219
191
|
credential_manager = CredentialManager(
|
220
|
-
lambda_url=
|
192
|
+
lambda_url=api_url,
|
221
193
|
access_key=options.aws_access_key_id,
|
222
194
|
secret_key=options.aws_secret_access_key,
|
223
195
|
username=options.username,
|
@@ -284,19 +256,21 @@ def main() -> None:
|
|
284
256
|
]
|
285
257
|
and os.environ[env_variable]
|
286
258
|
]
|
287
|
-
deploy_endpoint = f"{
|
259
|
+
deploy_endpoint = f"{api_url}/deploy"
|
288
260
|
payload = {
|
289
261
|
"locust_args": [
|
290
262
|
{"name": "LOCUST_LOCUSTFILE", "value": locustfile_url},
|
291
263
|
{"name": "LOCUST_USERS", "value": str(options.users)},
|
292
264
|
{"name": "LOCUST_FLAGS", "value": " ".join(locust_options)},
|
293
265
|
{"name": "LOCUSTCLOUD_REQUIREMENTS_URL", "value": requirements_url},
|
294
|
-
{"name": "LOCUSTCLOUD_DEPLOYER_URL", "value":
|
266
|
+
{"name": "LOCUSTCLOUD_DEPLOYER_URL", "value": api_url},
|
295
267
|
*locust_env_variables,
|
296
268
|
],
|
297
|
-
"
|
269
|
+
"user_count": options.users,
|
298
270
|
"image_tag": options.image_tag,
|
299
271
|
}
|
272
|
+
if options.workers is not None:
|
273
|
+
payload["worker_count"] = options.workers
|
300
274
|
headers = {
|
301
275
|
"Authorization": f"Bearer {cognito_client_id_token}",
|
302
276
|
"Content-Type": "application/json",
|
@@ -329,7 +303,7 @@ def main() -> None:
|
|
329
303
|
logger.debug("Interrupted by user")
|
330
304
|
sys.exit(0)
|
331
305
|
|
332
|
-
log_group_name =
|
306
|
+
log_group_name = "/eks/dmdb-default" if options.region == "us-east-1" else "/eks/locust-default"
|
333
307
|
master_pod_name = next((deployment for deployment in deployments if "master" in deployment), None)
|
334
308
|
|
335
309
|
if not master_pod_name:
|
@@ -424,9 +398,8 @@ def delete(s3_bucket, credential_manager):
|
|
424
398
|
headers["AWS_SESSION_TOKEN"] = token
|
425
399
|
|
426
400
|
response = requests.delete(
|
427
|
-
f"{
|
401
|
+
f"{api_url}/teardown",
|
428
402
|
headers=headers,
|
429
|
-
params={"namespace": options.kube_namespace} if options.kube_namespace else {},
|
430
403
|
)
|
431
404
|
|
432
405
|
if response.status_code != 200:
|
@@ -100,13 +100,16 @@ class CredentialManager:
|
|
100
100
|
|
101
101
|
except requests.exceptions.HTTPError as http_err:
|
102
102
|
response = http_err.response
|
103
|
-
if response is
|
103
|
+
if response is None:
|
104
|
+
raise CredentialError("Response was None?!") from http_err
|
105
|
+
|
106
|
+
if response.status_code == 401:
|
104
107
|
raise CredentialError("Incorrect username or password.") from http_err
|
105
108
|
else:
|
106
109
|
if js := response.json():
|
107
110
|
if message := js.get("Message"):
|
108
111
|
raise CredentialError(message)
|
109
|
-
error_info = f"HTTP {response.status_code} {response.reason}"
|
112
|
+
error_info = f"HTTP {response.status_code} {response.reason}"
|
110
113
|
raise CredentialError(f"HTTP error occurred while obtaining credentials: {error_info}") from http_err
|
111
114
|
except requests.exceptions.RequestException as req_err:
|
112
115
|
raise CredentialError(f"Request exception occurred while obtaining credentials: {req_err}") from req_err
|
locust_cloud/idle_exit.py
CHANGED
@@ -30,9 +30,9 @@ class IdleExit:
|
|
30
30
|
if self.environment.web_ui.greenlet.started:
|
31
31
|
sys.exit(1)
|
32
32
|
|
33
|
-
def on_test_stop(self, **
|
33
|
+
def on_test_stop(self, **kwargs):
|
34
34
|
self._destroy_task = gevent.spawn(self._destroy)
|
35
35
|
|
36
|
-
def on_locust_state_change(self, **
|
36
|
+
def on_locust_state_change(self, **kwargs):
|
37
37
|
if self._destroy_task:
|
38
38
|
self._destroy_task.kill()
|
@@ -38,6 +38,7 @@ class Exporter:
|
|
38
38
|
self._background = gevent.spawn(self._run)
|
39
39
|
self._hostname = socket.gethostname()
|
40
40
|
self._finished = False
|
41
|
+
self._has_logged_test_stop = False
|
41
42
|
self._pid = os.getpid()
|
42
43
|
self.pool = pool
|
43
44
|
|
@@ -58,11 +59,13 @@ class Exporter:
|
|
58
59
|
message = f"High CPU usage ({cpu_usage}%)"
|
59
60
|
with self.pool.connection() as conn:
|
60
61
|
conn.execute(
|
61
|
-
"INSERT INTO events (time, text, run_id) VALUES (%s, %s, %s)",
|
62
|
+
"INSERT INTO events (time, text, run_id, customer) VALUES (%s, %s, %s, current_user)",
|
63
|
+
(timestamp, message, self._run_id),
|
62
64
|
)
|
63
65
|
|
64
66
|
def on_test_start(self, environment: locust.env.Environment):
|
65
67
|
if not self.env.parsed_options or not self.env.parsed_options.worker:
|
68
|
+
self._has_logged_test_stop = False
|
66
69
|
self._run_id = environment._run_id = datetime.now(UTC) # type: ignore
|
67
70
|
self.env.parsed_options.run_id = format_datetime(environment._run_id) # type: ignore
|
68
71
|
self.log_start_testrun()
|
@@ -78,7 +81,7 @@ class Exporter:
|
|
78
81
|
try:
|
79
82
|
with self.pool.connection() as conn:
|
80
83
|
conn.execute(
|
81
|
-
"""INSERT INTO number_of_users(time, run_id, user_count) VALUES (%s, %s, %s)""",
|
84
|
+
"""INSERT INTO number_of_users(time, run_id, user_count, customer) VALUES (%s, %s, %s, current_user)""",
|
82
85
|
(datetime.now(UTC).isoformat(), self._run_id, self.env.runner.user_count),
|
83
86
|
)
|
84
87
|
except psycopg.Error as error:
|
@@ -136,10 +139,11 @@ class Exporter:
|
|
136
139
|
self._user_count_logger.kill()
|
137
140
|
with self.pool.connection() as conn:
|
138
141
|
conn.execute(
|
139
|
-
"""INSERT INTO number_of_users(time, run_id, user_count) VALUES (%s, %s, %s)""",
|
142
|
+
"""INSERT INTO number_of_users(time, run_id, user_count, customer) VALUES (%s, %s, %s, current_user)""",
|
140
143
|
(datetime.now(UTC).isoformat(), self._run_id, 0),
|
141
144
|
)
|
142
145
|
self.log_stop_test_run()
|
146
|
+
self._has_logged_test_stop = True
|
143
147
|
|
144
148
|
def on_quit(self, exit_code, **kwargs):
|
145
149
|
self._finished = True
|
@@ -149,7 +153,10 @@ class Exporter:
|
|
149
153
|
self._update_end_time_task.kill()
|
150
154
|
if getattr(self, "_user_count_logger", False):
|
151
155
|
self._user_count_logger.kill()
|
152
|
-
self.
|
156
|
+
if not self._has_logged_test_stop:
|
157
|
+
self.log_stop_test_run()
|
158
|
+
if not self.env.parsed_options.worker:
|
159
|
+
self.log_exit_code(exit_code)
|
153
160
|
|
154
161
|
def on_request(
|
155
162
|
self,
|
@@ -175,11 +182,6 @@ class Exporter:
|
|
175
182
|
time = datetime.now(UTC) - timedelta(milliseconds=response_time or 0)
|
176
183
|
greenlet_id = getattr(greenlet.getcurrent(), "minimal_ident", 0) # if we're debugging there is no greenlet
|
177
184
|
|
178
|
-
if response_length >= 0:
|
179
|
-
response_length = response_length
|
180
|
-
else:
|
181
|
-
response_length = None
|
182
|
-
|
183
185
|
if exception:
|
184
186
|
if isinstance(exception, CatchResponseError):
|
185
187
|
exception = str(exception)
|
@@ -188,6 +190,8 @@ class Exporter:
|
|
188
190
|
exception = repr(exception)
|
189
191
|
except AttributeError:
|
190
192
|
exception = f"{exception.__class__} (and it has no string representation)"
|
193
|
+
|
194
|
+
exception = exception[:300]
|
191
195
|
else:
|
192
196
|
exception = None
|
193
197
|
|
@@ -213,7 +217,7 @@ class Exporter:
|
|
213
217
|
cmd = sys.argv[1:]
|
214
218
|
with self.pool.connection() as conn:
|
215
219
|
conn.execute(
|
216
|
-
"INSERT INTO testruns (id, num_users, worker_count, username, locustfile, description, arguments) VALUES (%s,%s,%s,%s,%s,%s,%s)",
|
220
|
+
"INSERT INTO testruns (id, num_users, worker_count, username, locustfile, description, arguments, customer) VALUES (%s,%s,%s,%s,%s,%s,%s,current_user)",
|
217
221
|
(
|
218
222
|
self._run_id,
|
219
223
|
self.env.runner.target_user_count if self.env.runner else 1,
|
@@ -230,7 +234,7 @@ class Exporter:
|
|
230
234
|
),
|
231
235
|
)
|
232
236
|
conn.execute(
|
233
|
-
"INSERT INTO events (time, text, run_id) VALUES (%s, %s, %s)",
|
237
|
+
"INSERT INTO events (time, text, run_id, customer) VALUES (%s, %s, %s, current_user)",
|
234
238
|
(datetime.now(UTC).isoformat(), "Test run started", self._run_id),
|
235
239
|
)
|
236
240
|
|
@@ -240,7 +244,7 @@ class Exporter:
|
|
240
244
|
try:
|
241
245
|
with self.pool.connection() as conn:
|
242
246
|
conn.execute(
|
243
|
-
"INSERT INTO events (time, text, run_id) VALUES (%s, %s, %s)",
|
247
|
+
"INSERT INTO events (time, text, run_id, customer) VALUES (%s, %s, %s, current_user)",
|
244
248
|
(end_time, f"Rampup complete, {user_count} users spawned", self._run_id),
|
245
249
|
)
|
246
250
|
except psycopg.Error as error:
|
@@ -248,7 +252,7 @@ class Exporter:
|
|
248
252
|
"Failed to insert rampup complete event time to Postgresql timescale database: " + repr(error)
|
249
253
|
)
|
250
254
|
|
251
|
-
def log_stop_test_run(self
|
255
|
+
def log_stop_test_run(self):
|
252
256
|
logging.debug(f"Test run id {self._run_id} stopping")
|
253
257
|
if self.env.parsed_options.worker:
|
254
258
|
return # only run on master or standalone
|
@@ -256,17 +260,14 @@ class Exporter:
|
|
256
260
|
try:
|
257
261
|
with self.pool.connection() as conn:
|
258
262
|
conn.execute(
|
259
|
-
"UPDATE testruns SET end_time = %s
|
260
|
-
(end_time,
|
261
|
-
)
|
262
|
-
conn.execute(
|
263
|
-
"INSERT INTO events (time, text, run_id) VALUES (%s, %s, %s)",
|
264
|
-
(end_time, f"Finished with exit code: {exit_code}", self._run_id),
|
263
|
+
"UPDATE testruns SET end_time = %s WHERE id = %s",
|
264
|
+
(end_time, self._run_id),
|
265
265
|
)
|
266
|
-
|
267
|
-
# We dont use start_time / end_time to calculate RPS, instead we use the time between the actual first and last request
|
268
|
-
# (as this is a more accurate measurement of the actual test)
|
266
|
+
|
269
267
|
try:
|
268
|
+
# The AND time > run_id clause in the following statements are there to help Timescale performance
|
269
|
+
# We dont use start_time / end_time to calculate RPS, instead we use the time between the actual first and last request
|
270
|
+
# (as this is a more accurate measurement of the actual test)
|
270
271
|
conn.execute(
|
271
272
|
"""
|
272
273
|
UPDATE testruns
|
@@ -275,12 +276,12 @@ SET (requests, resp_time_avg, rps_avg, fail_ratio) =
|
|
275
276
|
(SELECT
|
276
277
|
COUNT(*)::numeric AS reqs,
|
277
278
|
AVG(response_time)::numeric as resp_time
|
278
|
-
FROM
|
279
|
+
FROM requests_view WHERE run_id = %(run_id)s AND time > %(run_id)s) AS _,
|
279
280
|
(SELECT
|
280
|
-
EXTRACT(epoch FROM (SELECT MAX(time)-MIN(time) FROM
|
281
|
+
EXTRACT(epoch FROM (SELECT MAX(time)-MIN(time) FROM requests_view WHERE run_id = %(run_id)s AND time > %(run_id)s))::numeric AS duration) AS __,
|
281
282
|
(SELECT
|
282
283
|
COUNT(*)::numeric AS fails
|
283
|
-
FROM
|
284
|
+
FROM requests_view WHERE run_id = %(run_id)s AND time > %(run_id)s AND success = 0) AS ___
|
284
285
|
WHERE id = %(run_id)s""",
|
285
286
|
{"run_id": self._run_id},
|
286
287
|
)
|
@@ -293,3 +294,20 @@ WHERE id = %(run_id)s""",
|
|
293
294
|
"Failed to update testruns record (or events) with end time to Postgresql timescale database: "
|
294
295
|
+ repr(error)
|
295
296
|
)
|
297
|
+
|
298
|
+
def log_exit_code(self, exit_code=None):
|
299
|
+
try:
|
300
|
+
with self.pool.connection() as conn:
|
301
|
+
conn.execute(
|
302
|
+
"UPDATE testruns SET exit_code = %s WHERE id = %s",
|
303
|
+
(exit_code, self._run_id),
|
304
|
+
)
|
305
|
+
conn.execute(
|
306
|
+
"INSERT INTO events (time, text, run_id, customer) VALUES (%s, %s, %s, current_user)",
|
307
|
+
(datetime.now(UTC).isoformat(), f"Finished with exit code: {exit_code}", self._run_id),
|
308
|
+
)
|
309
|
+
except psycopg.Error as error:
|
310
|
+
logging.error(
|
311
|
+
"Failed to update testruns record (or events) with end time to Postgresql timescale database: "
|
312
|
+
+ repr(error)
|
313
|
+
)
|