locust-cloud 1.5.2__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 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(pg_user, pg_host, pg_password, pg_database, pg_port):
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 or GRAPH_VIEWER):
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
- pool = create_connection_pool(
77
- pg_user=PG_USER,
78
- pg_host=PG_HOST,
79
- pg_password=PG_PASSWORD,
80
- pg_database=PG_DATABASE,
81
- pg_port=PG_PORT,
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, current_user, login_user
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("cognito_client_id_token"):
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("cognito_token", id_token, expires=datetime.now(tz=UTC) + timedelta(days=1))
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 load_user(user_sub_id):
36
- refresh_token = request.cookies.get("user_token")
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
- if refresh_token:
39
- return AuthUser(user_sub_id)
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
- return None
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
locust_cloud/cloud.py CHANGED
@@ -202,7 +202,12 @@ def main() -> None:
202
202
  s3_bucket = f"{options.kube_cluster_name}-{options.kube_namespace}"
203
203
  deployed_pods: list[Any] = []
204
204
  worker_count: int = max(options.workers or math.ceil(options.users / USERS_PER_WORKER), 2)
205
-
205
+ if options.users > 10000:
206
+ logger.error("You asked for more than 10000 Users, that isn't allowed.")
207
+ sys.exit(1)
208
+ if worker_count > 20:
209
+ logger.error("You asked for more than 20 workers, that isn't allowed.")
210
+ sys.exit(1)
206
211
  try:
207
212
  if not (
208
213
  (options.username and options.password) or (options.aws_access_key_id and options.aws_secret_access_key)
@@ -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
  }
@@ -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