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 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 (PG_HOST or GRAPH_VIEWER):
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 PG_HOST was not set - this is normal for local runs",
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 (PG_HOST and PG_USER and PG_PASSWORD and PG_DATABASE and PG_PORT):
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=10,
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
- DEPLOYER_URL = os.environ.get("LOCUSTCLOUD_DEPLOYER_URL", DEFAULT_DEPLOYER_URL)
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
- "username_password_callback": "/authenticate",
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
- @environment.web_ui.app.route("/authenticate", methods=["POST"])
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
- environment.web_ui.auth_args["error"] = "Invalid username or password"
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
- environment.web_ui.auth_args["error"] = (
92
- "Unknown error during authentication, check logs and/or contact support"
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
- return redirect(url_for("login"))
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", DEFAULT_REGION_NAME),
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=f"Number of workers to use for the deployment. Defaults to number of users divided by {USERS_PER_WORKER}.",
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
- s3_bucket = f"{options.kube_cluster_name}-{options.kube_namespace}"
199
- deployments: list[Any] = []
200
- worker_count: int = max(options.workers or math.ceil(options.users / USERS_PER_WORKER), 2)
201
- os.environ["AWS_DEFAULT_REGION"] = options.region
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
- if worker_count > 1000:
206
- logger.error("You asked for more than 20 workers, that isn't allowed.")
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
- logger.info(f"Authenticating ({os.environ['AWS_DEFAULT_REGION']}, v{__version__})")
218
- logger.debug(f"Lambda url: {options.deployer_url}")
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=options.deployer_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"{options.deployer_url}/{options.kube_cluster_name}"
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": options.deployer_url},
266
+ {"name": "LOCUSTCLOUD_DEPLOYER_URL", "value": api_url},
295
267
  *locust_env_variables,
296
268
  ],
297
- "worker_count": worker_count,
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 = f"/eks/{options.kube_cluster_name}-{options.kube_namespace}"
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"{options.deployer_url}/{options.kube_cluster_name}",
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 not None and response.status_code == 401:
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}" if response else "No response received."
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, **_kwargs):
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, **_kwargs):
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)", (timestamp, message, self._run_id)
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.log_stop_test_run(exit_code)
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, exit_code=None):
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, exit_code = %s where id = %s",
260
- (end_time, exit_code, self._run_id),
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
- # The AND time > run_id clause in the following statements are there to help Timescale performance
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 requests WHERE run_id = %(run_id)s AND time > %(run_id)s) AS _,
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 requests WHERE run_id = %(run_id)s AND time > %(run_id)s))::numeric AS duration) AS __,
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 requests WHERE run_id = %(run_id)s AND time > %(run_id)s AND success = 0) AS ___
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
+ )