locust-cloud 1.5.4__py3-none-any.whl → 1.5.5__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
@@ -79,7 +79,7 @@ def on_locust_init(environment: locust.env.Environment, **_args):
79
79
  return
80
80
 
81
81
  if GRAPH_VIEWER:
82
- environment.runner.state = "STOPPED"
82
+ environment.web_ui.template_args["isGraphViewer"] = True
83
83
 
84
84
  try:
85
85
  pool = create_connection_pool(
locust_cloud/auth.py CHANGED
@@ -7,9 +7,6 @@ import requests
7
7
  import werkzeug
8
8
  from flask import redirect, request, url_for
9
9
  from flask_login import UserMixin, login_user
10
- from locust_cloud.constants import DEFAULT_LAMBDA_URL
11
-
12
- LAMBDA = os.environ.get("LOCUST_API_BASE_URL", DEFAULT_LAMBDA_URL)
13
10
 
14
11
 
15
12
  class Credentials(TypedDict):
@@ -64,7 +61,10 @@ def register_auth(environment: locust.env.Environment):
64
61
  password = request.form.get("password")
65
62
 
66
63
  try:
67
- auth_response = requests.post(f"{LAMBDA}/auth/login", json={"username": username, "password": password})
64
+ auth_response = requests.post(
65
+ f"{environment.parsed_options.deployer_url}/auth/login",
66
+ json={"username": username, "password": password},
67
+ )
68
68
 
69
69
  if auth_response.status_code == 200:
70
70
  credentials = auth_response.json()
locust_cloud/cloud.py CHANGED
@@ -16,14 +16,13 @@ import requests
16
16
  from botocore.exceptions import ClientError
17
17
  from locust_cloud.constants import (
18
18
  DEFAULT_CLUSTER_NAME,
19
- DEFAULT_LAMBDA_URL,
19
+ DEFAULT_DEPLOYER_URL,
20
20
  DEFAULT_NAMESPACE,
21
21
  DEFAULT_REGION_NAME,
22
22
  USERS_PER_WORKER,
23
23
  )
24
24
  from locust_cloud.credential_manager import CredentialError, CredentialManager
25
25
 
26
- LOCUST_ENV_VARIABLE_IGNORE_LIST = ["LOCUST_BUILD_PATH", "LOCUST_SKIP_MONKEY_PATCH"]
27
26
  __version__ = importlib.metadata.version("locust-cloud")
28
27
 
29
28
 
@@ -57,7 +56,7 @@ parser = configargparse.ArgumentParser(
57
56
  "~/.cloud.conf",
58
57
  "cloud.conf",
59
58
  ],
60
- auto_env_var_prefix="LOCUST_",
59
+ auto_env_var_prefix="LOCUSTCLOUD_",
61
60
  formatter_class=configargparse.RawDescriptionHelpFormatter,
62
61
  config_file_parser_class=configargparse.CompositeConfigParser(
63
62
  [
@@ -67,7 +66,7 @@ parser = configargparse.ArgumentParser(
67
66
  ),
68
67
  description="""Launches distributed Locust runs on locust.cloud infrastructure.
69
68
 
70
- Example: locust-cloud -f my_locustfile.py --aws-region-name us-east-1 --users 1000""",
69
+ Example: locust-cloud -f my_locustfile.py --region us-east-1 --users 2000""",
71
70
  epilog="""Any parameters not listed here are forwarded to locust master unmodified, so go ahead and use things like --users, --host, --run-time, ...
72
71
  Locust config can also be set using config file (~/.locust.conf, locust.conf, pyproject.toml, ~/.cloud.conf or cloud.conf).
73
72
  Parameters specified on command line override env vars, which in turn override config files.""",
@@ -88,21 +87,19 @@ parser.add_argument(
88
87
  "--users",
89
88
  type=int,
90
89
  default=1,
91
- help="Number of users to launch. This is the same as the regular Locust argument, but also decides how many workers to launch by default.",
90
+ help="Number of users to launch. This is the same as the regular Locust argument, but also affects how many workers to launch.",
92
91
  env_var="LOCUST_USERS",
93
92
  )
94
93
  parser.add_argument(
95
94
  "--requirements",
96
95
  type=str,
97
96
  help="Optional requirements.txt file that contains your external libraries.",
98
- env_var="LOCUST_REQUIREMENTS",
99
97
  )
100
98
  parser.add_argument(
101
- "--aws-region-name",
99
+ "--region",
102
100
  type=str,
103
- default=DEFAULT_REGION_NAME,
104
- help="Sets the region to use for the deployed cluster",
105
- env_var="AWS_REGION_NAME",
101
+ default=os.environ.get("AWS_DEFAULT_REGION", DEFAULT_REGION_NAME),
102
+ 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.",
106
103
  )
107
104
  parser.add_argument(
108
105
  "--kube-cluster-name",
@@ -119,11 +116,10 @@ parser.add_argument(
119
116
  env_var="KUBE_NAMESPACE",
120
117
  )
121
118
  parser.add_argument(
122
- "--lambda-url",
119
+ "--deployer-url",
123
120
  type=str,
124
- default=DEFAULT_LAMBDA_URL,
125
- help="Sets the namespace for scoping the deployed cluster",
126
- env_var="LOCUST_API_BASE_URL",
121
+ default=DEFAULT_DEPLOYER_URL,
122
+ help=configargparse.SUPPRESS,
127
123
  )
128
124
  parser.add_argument(
129
125
  "--aws-access-key-id",
@@ -143,29 +139,25 @@ parser.add_argument(
143
139
  "--username",
144
140
  type=str,
145
141
  help="Authentication for deploying with Locust Cloud",
146
- env_var="LOCUST_CLOUD_USERNAME",
147
- default=None,
142
+ default=os.getenv("LOCUST_CLOUD_USERNAME", None), # backwards compatitibility for dmdb
148
143
  )
149
144
  parser.add_argument(
150
145
  "--password",
151
146
  type=str,
152
147
  help="Authentication for deploying with Locust Cloud",
153
- env_var="LOCUST_CLOUD_PASSWORD",
154
- default=None,
148
+ default=os.getenv("LOCUST_CLOUD_PASSWORD", None), # backwards compatitibility for dmdb
155
149
  )
156
150
  parser.add_argument(
157
151
  "--loglevel",
158
152
  "-L",
159
153
  type=str,
160
154
  help="Log level",
161
- env_var="LOCUST_CLOUD_LOGLEVEL",
162
155
  default="INFO",
163
156
  )
164
157
  parser.add_argument(
165
158
  "--workers",
166
159
  type=int,
167
- help="Number of workers to use for the deployment",
168
- env_var="LOCUST_CLOUD_WORKERS",
160
+ help=f"Number of workers to use for the deployment. Defaults to number of users divided by {USERS_PER_WORKER}",
169
161
  default=None,
170
162
  )
171
163
  parser.add_argument(
@@ -176,7 +168,6 @@ parser.add_argument(
176
168
  parser.add_argument(
177
169
  "--image-tag",
178
170
  type=str,
179
- env_var="LOCUST_CLOUD_IMAGE_TAG",
180
171
  default="latest",
181
172
  help=configargparse.SUPPRESS, # overrides the locust-cloud docker image tag. for internal use
182
173
  )
@@ -202,6 +193,7 @@ def main() -> None:
202
193
  s3_bucket = f"{options.kube_cluster_name}-{options.kube_namespace}"
203
194
  deployed_pods: list[Any] = []
204
195
  worker_count: int = max(options.workers or math.ceil(options.users / USERS_PER_WORKER), 2)
196
+ os.environ["AWS_DEFAULT_REGION"] = options.region
205
197
  if options.users > 10000:
206
198
  logger.error("You asked for more than 10000 Users, that isn't allowed.")
207
199
  sys.exit(1)
@@ -217,15 +209,14 @@ def main() -> None:
217
209
  )
218
210
  sys.exit(1)
219
211
 
220
- logger.info(f"Authenticating ({options.aws_region_name}, v{__version__})")
221
- logger.debug(f"Lambda url: {options.lambda_url}")
212
+ logger.info(f"Authenticating ({os.environ['AWS_DEFAULT_REGION']}, v{__version__})")
213
+ logger.debug(f"Lambda url: {options.deployer_url}")
222
214
  credential_manager = CredentialManager(
223
- lambda_url=options.lambda_url,
215
+ lambda_url=options.deployer_url,
224
216
  access_key=options.aws_access_key_id,
225
217
  secret_key=options.aws_secret_access_key,
226
218
  username=options.username,
227
219
  password=options.password,
228
- region_name=options.aws_region_name,
229
220
  )
230
221
 
231
222
  credentials = credential_manager.get_current_credentials()
@@ -278,26 +269,23 @@ def main() -> None:
278
269
  {"name": env_variable, "value": str(os.environ[env_variable])}
279
270
  for env_variable in os.environ
280
271
  if env_variable.startswith("LOCUST_")
281
- and not env_variable.startswith("LOCUST_CLOUD")
282
272
  and not env_variable
283
273
  in [
284
274
  "LOCUST_LOCUSTFILE",
285
275
  "LOCUST_USERS",
286
276
  "LOCUST_WEB_HOST_DISPLAY_NAME",
287
- "LOCUST_API_BASE_URL",
288
277
  "LOCUST_SKIP_MONKEY_PATCH",
289
- "LOCUST_REQUIREMENTS_URL",
290
278
  ]
291
279
  and os.environ[env_variable]
292
280
  ]
293
- deploy_endpoint = f"{options.lambda_url}/{options.kube_cluster_name}"
281
+ deploy_endpoint = f"{options.deployer_url}/{options.kube_cluster_name}"
294
282
  payload = {
295
283
  "locust_args": [
296
284
  {"name": "LOCUST_LOCUSTFILE", "value": locustfile_url},
297
285
  {"name": "LOCUST_USERS", "value": str(options.users)},
298
- {"name": "LOCUST_REQUIREMENTS_URL", "value": requirements_url},
299
286
  {"name": "LOCUST_FLAGS", "value": " ".join(locust_options)},
300
- {"name": "LOCUST_API_BASE_URL", "value": DEFAULT_LAMBDA_URL},
287
+ {"name": "LOCUSTCLOUD_REQUIREMENTS_URL", "value": requirements_url},
288
+ {"name": "LOCUSTCLOUD_DEPLOYER_URL", "value": options.deployer_url},
301
289
  *locust_env_variables,
302
290
  ],
303
291
  "worker_count": worker_count,
@@ -416,7 +404,7 @@ def delete(s3_bucket, credential_manager):
416
404
  headers["AWS_SESSION_TOKEN"] = token
417
405
 
418
406
  response = requests.delete(
419
- f"{options.lambda_url}/{options.kube_cluster_name}",
407
+ f"{options.deployer_url}/{options.kube_cluster_name}",
420
408
  headers=headers,
421
409
  params={"namespace": options.kube_namespace} if options.kube_namespace else {},
422
410
  )
@@ -426,7 +414,7 @@ def delete(s3_bucket, credential_manager):
426
414
  f"Could not automatically tear down Locust Cloud: HTTP {response.status_code}/{response.reason} - Response: {response.text} - URL: {response.request.url}"
427
415
  )
428
416
  except Exception as e:
429
- logger.error(f"Could not automatically tear down Locust Cloud: {e}")
417
+ logger.error(f"Could not automatically tear down Locust Cloud: {e.__class__.__name__}:{e}")
430
418
 
431
419
  try:
432
420
  logger.debug("Cleaning up locustfiles")
locust_cloud/constants.py CHANGED
@@ -1,5 +1,5 @@
1
1
  DEFAULT_REGION_NAME = "us-east-1"
2
2
  DEFAULT_CLUSTER_NAME = "dmdb"
3
3
  DEFAULT_NAMESPACE = "default"
4
- DEFAULT_LAMBDA_URL = "https://api.locust.cloud/1"
4
+ DEFAULT_DEPLOYER_URL = "https://api.locust.cloud/1"
5
5
  USERS_PER_WORKER = 500
@@ -8,7 +8,6 @@ import jwt
8
8
  import requests
9
9
  from botocore.credentials import RefreshableCredentials
10
10
  from botocore.session import Session as BotocoreSession
11
- from locust_cloud.constants import DEFAULT_REGION_NAME
12
11
 
13
12
  logger = logging.getLogger(__name__)
14
13
 
@@ -27,7 +26,6 @@ class CredentialManager:
27
26
  password: str | None = None,
28
27
  user_sub_id: str | None = None,
29
28
  refresh_token: str | None = None,
30
- region_name: str = DEFAULT_REGION_NAME,
31
29
  access_key: str | None = None,
32
30
  secret_key: str | None = None,
33
31
  ) -> None:
@@ -36,7 +34,6 @@ class CredentialManager:
36
34
  self.password = password
37
35
  self.user_sub_id = user_sub_id
38
36
  self.refresh_token = refresh_token
39
- self.region_name = region_name
40
37
 
41
38
  self.credentials = {
42
39
  "access_key": access_key,
@@ -56,12 +53,8 @@ class CredentialManager:
56
53
  botocore_session = BotocoreSession()
57
54
  botocore_session._credentials = self.refreshable_credentials # type: ignore
58
55
  botocore_session.set_config_variable("signature_version", "v4")
59
- botocore_session.set_config_variable("region", self.region_name)
60
56
 
61
- self.session = boto3.Session(
62
- botocore_session=botocore_session,
63
- region_name=self.region_name,
64
- )
57
+ self.session = boto3.Session(botocore_session=botocore_session)
65
58
  logger.debug("Boto3 session created with RefreshableCredentials.")
66
59
 
67
60
  def obtain_credentials(self) -> None:
@@ -33,7 +33,7 @@ GROUP BY "name",left(exception,300)
33
33
  requests_per_second = """
34
34
  WITH request_count_agg AS (
35
35
  SELECT
36
- time_bucket_gapfill('%(resolution)ss', bucket) AS time,
36
+ time_bucket_gapfill(%(resolution)s * interval '1 second', bucket) AS time,
37
37
  COALESCE(SUM(count)/%(resolution)s, 0) as rps
38
38
  FROM requests_summary
39
39
  WHERE bucket BETWEEN %(start)s AND %(end)s
@@ -43,7 +43,7 @@ WITH request_count_agg AS (
43
43
  ),
44
44
  user_count_agg AS (
45
45
  SELECT
46
- time_bucket_gapfill('%(resolution)ss', time) AS time,
46
+ time_bucket_gapfill(%(resolution)s * interval '1 second', time) AS time,
47
47
  COALESCE(avg(user_count), 0) as users
48
48
  FROM number_of_users
49
49
  WHERE time BETWEEN %(start)s AND %(end)s
@@ -53,7 +53,7 @@ user_count_agg AS (
53
53
  ),
54
54
  errors_per_s_agg AS (
55
55
  SELECT
56
- time_bucket_gapfill('%(resolution)ss', bucket) AS time,
56
+ time_bucket_gapfill(%(resolution)s * interval '1 second', bucket) AS time,
57
57
  COALESCE(SUM(failed_count)/%(resolution)s, 0) as error_rate
58
58
  FROM requests_summary
59
59
  WHERE bucket BETWEEN %(start)s AND %(end)s
@@ -101,7 +101,7 @@ AND run_id = %(testrun)s
101
101
 
102
102
  rps_per_request = """
103
103
  SELECT
104
- time_bucket_gapfill('%(resolution)ss', bucket) AS time,
104
+ time_bucket_gapfill(%(resolution)s * interval '1 second', bucket) AS time,
105
105
  name,
106
106
  COALESCE(SUM(count)/%(resolution)s, 0) as throughput
107
107
  FROM requests_summary
@@ -114,7 +114,7 @@ ORDER BY 1,2
114
114
 
115
115
  avg_response_times = """
116
116
  SELECT
117
- time_bucket_gapfill('%(resolution)ss', bucket) as time,
117
+ time_bucket_gapfill(%(resolution)s * interval '1 second', bucket) as time,
118
118
  name,
119
119
  avg(average) as "responseTime"
120
120
  FROM requests_summary
@@ -126,7 +126,7 @@ ORDER BY 1, 2
126
126
 
127
127
  errors_per_request = """
128
128
  SELECT
129
- time_bucket_gapfill('%(resolution)ss', bucket) AS time,
129
+ time_bucket_gapfill(%(resolution)s * interval '1 second', bucket) AS time,
130
130
  name,
131
131
  SUM(failed_count)/%(resolution)s as "errorRate"
132
132
  FROM requests_summary
@@ -138,19 +138,20 @@ ORDER BY 1
138
138
 
139
139
 
140
140
  perc99_response_times = """
141
- SELECT time_bucket('%(resolution)ss', bucket) AS time,
141
+ SELECT time_bucket(%(resolution)s * interval '1 second', bucket) AS time,
142
142
  name,
143
143
  perc99
144
144
  FROM requests_summary
145
145
  WHERE bucket BETWEEN %(start)s AND %(end)s
146
146
  AND run_id = %(testrun)s
147
147
  GROUP BY 1, name, perc99
148
+ ORDER BY 1
148
149
  """
149
150
 
150
151
 
151
152
  response_length = """
152
153
  SELECT
153
- time_bucket('%(resolution)ss', bucket) as time,
154
+ time_bucket(%(resolution)s * interval '1 second', bucket) as time,
154
155
  response_length as "responseLength",
155
156
  name
156
157
  FROM requests_summary
@@ -59,6 +59,5 @@ def register_query(environment, pool):
59
59
  logger.warning(f"Received invalid query key: '{query}'")
60
60
  return make_response("Invalid query key", 401)
61
61
  except Exception as e:
62
- print(e)
63
62
  logger.error(f"Error executing query '{query}': {e}", exc_info=True)
64
63
  return make_response("Error executing query", 401)