locust-cloud 1.6.0__py3-none-any.whl → 1.8.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 +23 -41
- locust_cloud/auth.py +17 -13
- locust_cloud/cloud.py +11 -6
- locust_cloud/idle_exit.py +38 -0
- locust_cloud/timescale/exporter.py +92 -64
- locust_cloud/timescale/queries.py +32 -21
- locust_cloud/timescale/query.py +1 -1
- locust_cloud/webui/dist/assets/{index-BrOP_HSY.js → index-DQyGe4ep.js} +89 -89
- locust_cloud/webui/dist/index.html +1 -1
- locust_cloud/webui/package.json +1 -1
- locust_cloud/webui/tsconfig.tsbuildinfo +1 -1
- locust_cloud/webui/yarn.lock +4 -4
- locust_cloud-1.8.0.dist-info/METADATA +15 -0
- locust_cloud-1.8.0.dist-info/RECORD +24 -0
- locust_cloud-1.6.0.dist-info/METADATA +0 -52
- locust_cloud-1.6.0.dist-info/RECORD +0 -23
- {locust_cloud-1.6.0.dist-info → locust_cloud-1.8.0.dist-info}/WHEEL +0 -0
- {locust_cloud-1.6.0.dist-info → locust_cloud-1.8.0.dist-info}/entry_points.txt +0 -0
locust_cloud/__init__.py
CHANGED
@@ -6,34 +6,29 @@ __version__ = importlib.metadata.version("locust-cloud")
|
|
6
6
|
|
7
7
|
import argparse
|
8
8
|
import logging
|
9
|
-
import sys
|
10
9
|
|
10
|
+
import configargparse
|
11
11
|
import locust.env
|
12
12
|
import psycopg
|
13
13
|
from locust import events
|
14
14
|
from locust.argument_parser import LocustArgumentParser
|
15
15
|
from locust_cloud.auth import register_auth
|
16
|
+
from locust_cloud.idle_exit import IdleExit
|
16
17
|
from locust_cloud.timescale.exporter import Exporter
|
17
18
|
from locust_cloud.timescale.query import register_query
|
18
19
|
from psycopg.conninfo import make_conninfo
|
19
20
|
from psycopg_pool import ConnectionPool
|
20
21
|
|
21
|
-
PG_USER = os.environ.get("PG_USER")
|
22
|
-
PG_HOST = os.environ.get("PG_HOST")
|
23
|
-
PG_PASSWORD = os.environ.get("PG_PASSWORD")
|
24
|
-
PG_DATABASE = os.environ.get("PG_DATABASE")
|
25
|
-
PG_PORT = os.environ.get("PG_PORT", 5432)
|
26
22
|
GRAPH_VIEWER = os.environ.get("GRAPH_VIEWER")
|
27
|
-
MAX_USER_COUNT = os.environ.get("MAX_USER_COUNT")
|
28
23
|
logger = logging.getLogger(__name__)
|
29
24
|
|
30
25
|
|
31
26
|
@events.init_command_line_parser.add_listener
|
32
27
|
def add_arguments(parser: LocustArgumentParser):
|
33
|
-
if not (
|
28
|
+
if not (os.environ.get("PGHOST") or GRAPH_VIEWER):
|
34
29
|
parser.add_argument_group(
|
35
30
|
"locust-cloud",
|
36
|
-
"locust-cloud disabled, because
|
31
|
+
"locust-cloud disabled, because PGHOST was not set - this is normal for local runs",
|
37
32
|
)
|
38
33
|
return
|
39
34
|
|
@@ -56,61 +51,48 @@ def add_arguments(parser: LocustArgumentParser):
|
|
56
51
|
default="",
|
57
52
|
help="Description of the test being run",
|
58
53
|
)
|
54
|
+
# do not set
|
55
|
+
# used for sending the run id from master to workers
|
56
|
+
locust_cloud.add_argument(
|
57
|
+
"--run-id",
|
58
|
+
type=str,
|
59
|
+
env_var="LOCUSTCLOUD_RUN_ID",
|
60
|
+
help=configargparse.SUPPRESS,
|
61
|
+
)
|
59
62
|
|
60
63
|
|
61
64
|
def set_autocommit(conn: psycopg.Connection):
|
62
65
|
conn.autocommit = True
|
63
66
|
|
64
67
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
+
@events.init.add_listener
|
69
|
+
def on_locust_init(environment: locust.env.Environment, **_args):
|
70
|
+
if not (os.environ.get("PGHOST")):
|
71
|
+
return
|
72
|
+
|
68
73
|
try:
|
69
74
|
conninfo = make_conninfo(
|
70
|
-
dbname=pg_database,
|
71
|
-
user=pg_user,
|
72
|
-
port=pg_port,
|
73
|
-
password=pg_password,
|
74
|
-
host=pg_host,
|
75
75
|
sslmode="require",
|
76
|
-
options="-c statement_timeout=55000",
|
77
76
|
)
|
78
|
-
|
77
|
+
pool = ConnectionPool(
|
79
78
|
conninfo,
|
80
79
|
min_size=1,
|
81
|
-
max_size=
|
80
|
+
max_size=20,
|
82
81
|
configure=set_autocommit,
|
83
|
-
|
84
|
-
except Exception:
|
85
|
-
sys.stderr.write(f"Could not connect to postgres ({pg_user}@{pg_host}:{pg_port}).")
|
86
|
-
sys.exit(1)
|
87
|
-
|
88
|
-
|
89
|
-
@events.init.add_listener
|
90
|
-
def on_locust_init(environment: locust.env.Environment, **_args):
|
91
|
-
if not (PG_HOST and PG_USER and PG_PASSWORD and PG_DATABASE and PG_PORT):
|
92
|
-
return
|
93
|
-
|
94
|
-
try:
|
95
|
-
pool = create_connection_pool(
|
96
|
-
pg_user=PG_USER,
|
97
|
-
pg_host=PG_HOST,
|
98
|
-
pg_password=PG_PASSWORD,
|
99
|
-
pg_database=PG_DATABASE,
|
100
|
-
pg_port=PG_PORT,
|
82
|
+
check=ConnectionPool.check_connection,
|
101
83
|
)
|
102
84
|
pool.wait()
|
103
85
|
except Exception as e:
|
104
86
|
logger.exception(e)
|
105
|
-
logger.error(f"{PG_HOST=}")
|
106
87
|
raise
|
107
88
|
|
89
|
+
if not GRAPH_VIEWER:
|
90
|
+
IdleExit(environment)
|
91
|
+
|
108
92
|
if not GRAPH_VIEWER and environment.parsed_options and environment.parsed_options.exporter:
|
109
93
|
Exporter(environment, pool)
|
110
94
|
|
111
95
|
if environment.web_ui:
|
112
|
-
environment.web_ui.template_args["maxUserCount"] = MAX_USER_COUNT
|
113
|
-
|
114
96
|
if GRAPH_VIEWER:
|
115
97
|
environment.web_ui.template_args["isGraphViewer"] = True
|
116
98
|
|
locust_cloud/auth.py
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import logging
|
1
2
|
import os
|
2
3
|
from datetime import UTC, datetime, timedelta
|
3
4
|
from typing import TypedDict
|
@@ -12,6 +13,8 @@ from locust_cloud.constants import DEFAULT_DEPLOYER_URL
|
|
12
13
|
|
13
14
|
DEPLOYER_URL = os.environ.get("LOCUSTCLOUD_DEPLOYER_URL", DEFAULT_DEPLOYER_URL)
|
14
15
|
|
16
|
+
logger = logging.getLogger(__name__)
|
17
|
+
|
15
18
|
|
16
19
|
class Credentials(TypedDict):
|
17
20
|
user_sub_id: str
|
@@ -71,21 +74,22 @@ def register_auth(environment: locust.env.Environment):
|
|
71
74
|
headers={"X-Client-Version": __version__},
|
72
75
|
)
|
73
76
|
|
74
|
-
|
75
|
-
credentials = auth_response.json()
|
76
|
-
response = redirect(url_for("index"))
|
77
|
-
response = set_credentials(username, credentials, response)
|
78
|
-
login_user(AuthUser(credentials["user_sub_id"]))
|
77
|
+
auth_response.raise_for_status()
|
79
78
|
|
80
|
-
|
79
|
+
credentials = auth_response.json()
|
80
|
+
response = redirect(url_for("index"))
|
81
|
+
response = set_credentials(username, credentials, response)
|
82
|
+
login_user(AuthUser(credentials["user_sub_id"]))
|
81
83
|
|
82
|
-
|
84
|
+
return response
|
85
|
+
except requests.exceptions.HTTPError as e:
|
86
|
+
if e.response.status_code == 401:
|
87
|
+
environment.web_ui.auth_args["error"] = "Invalid username or password"
|
88
|
+
else:
|
89
|
+
logger.error(f"Unknown response from auth: {e.response.status_code} {e.response.text}")
|
83
90
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
**environment.web_ui.auth_args,
|
88
|
-
"error": "An unknown error occured, please try again",
|
89
|
-
}
|
91
|
+
environment.web_ui.auth_args["error"] = (
|
92
|
+
"Unknown error during authentication, check logs and/or contact support"
|
93
|
+
)
|
90
94
|
|
91
95
|
return redirect(url_for("login"))
|
locust_cloud/cloud.py
CHANGED
@@ -199,12 +199,7 @@ def main() -> None:
|
|
199
199
|
deployments: list[Any] = []
|
200
200
|
worker_count: int = max(options.workers or math.ceil(options.users / USERS_PER_WORKER), 2)
|
201
201
|
os.environ["AWS_DEFAULT_REGION"] = options.region
|
202
|
-
|
203
|
-
logger.error("You asked for more than 5000000 Users, that isn't allowed.")
|
204
|
-
sys.exit(1)
|
205
|
-
if worker_count > 1000:
|
206
|
-
logger.error("You asked for more than 20 workers, that isn't allowed.")
|
207
|
-
sys.exit(1)
|
202
|
+
|
208
203
|
try:
|
209
204
|
if not (
|
210
205
|
(options.username and options.password) or (options.aws_access_key_id and options.aws_secret_access_key)
|
@@ -295,6 +290,7 @@ def main() -> None:
|
|
295
290
|
*locust_env_variables,
|
296
291
|
],
|
297
292
|
"worker_count": worker_count,
|
293
|
+
"user_count": options.users,
|
298
294
|
"image_tag": options.image_tag,
|
299
295
|
}
|
300
296
|
headers = {
|
@@ -372,6 +368,7 @@ def main() -> None:
|
|
372
368
|
startTime=timestamp,
|
373
369
|
startFromHead=True,
|
374
370
|
)
|
371
|
+
locust_shutdown = False
|
375
372
|
for event in response.get("events", []):
|
376
373
|
message = event.get("message", "")
|
377
374
|
event_timestamp = event.get("timestamp", timestamp) + 1
|
@@ -379,9 +376,17 @@ def main() -> None:
|
|
379
376
|
message_json = json.loads(message)
|
380
377
|
if "log" in message_json:
|
381
378
|
print(message_json["log"])
|
379
|
+
|
380
|
+
if "Shutting down (exit code" in message_json["log"]:
|
381
|
+
locust_shutdown = True
|
382
|
+
|
382
383
|
except json.JSONDecodeError:
|
383
384
|
print(message)
|
384
385
|
timestamp = event_timestamp
|
386
|
+
|
387
|
+
if locust_shutdown:
|
388
|
+
break
|
389
|
+
|
385
390
|
time.sleep(5)
|
386
391
|
except ClientError as e:
|
387
392
|
error_code = e.response.get("Error", {}).get("Code", "")
|
@@ -0,0 +1,38 @@
|
|
1
|
+
import logging
|
2
|
+
import sys
|
3
|
+
|
4
|
+
import gevent
|
5
|
+
import locust.env
|
6
|
+
from locust import events
|
7
|
+
|
8
|
+
logger = logging.getLogger(__name__)
|
9
|
+
|
10
|
+
|
11
|
+
class IdleExit:
|
12
|
+
def __init__(self, environment: locust.env.Environment):
|
13
|
+
self.environment = environment
|
14
|
+
self._destroy_task: gevent.Greenlet | None = None
|
15
|
+
events.test_start.add_listener(self.on_locust_state_change)
|
16
|
+
events.test_stop.add_listener(self.on_test_stop)
|
17
|
+
events.quit.add_listener(self.on_locust_state_change)
|
18
|
+
|
19
|
+
if not self.environment.parsed_options.autostart:
|
20
|
+
self._destroy_task = gevent.spawn(self._destroy)
|
21
|
+
|
22
|
+
def _destroy(self):
|
23
|
+
gevent.sleep(1800)
|
24
|
+
logger.info("Locust was detected as idle (no test running) for more than 30 minutes")
|
25
|
+
self.environment.runner.quit()
|
26
|
+
|
27
|
+
if self.environment.web_ui:
|
28
|
+
self.environment.web_ui.greenlet.kill(timeout=5)
|
29
|
+
|
30
|
+
if self.environment.web_ui.greenlet.started:
|
31
|
+
sys.exit(1)
|
32
|
+
|
33
|
+
def on_test_stop(self, **kwargs):
|
34
|
+
self._destroy_task = gevent.spawn(self._destroy)
|
35
|
+
|
36
|
+
def on_locust_state_change(self, **kwargs):
|
37
|
+
if self._destroy_task:
|
38
|
+
self._destroy_task.kill()
|
@@ -22,15 +22,23 @@ def safe_serialize(obj):
|
|
22
22
|
return json.dumps(obj, default=default)
|
23
23
|
|
24
24
|
|
25
|
+
def format_datetime(d: datetime):
|
26
|
+
return d.strftime("%Y-%m-%d, %H:%M:%S.%f")
|
27
|
+
|
28
|
+
|
29
|
+
def parse_datetime(s: str):
|
30
|
+
return datetime.strptime(s, "%Y-%m-%d, %H:%M:%S.%f").replace(tzinfo=UTC)
|
31
|
+
|
32
|
+
|
25
33
|
class Exporter:
|
26
34
|
def __init__(self, environment: locust.env.Environment, pool):
|
27
35
|
self.env = environment
|
28
36
|
self._run_id = None
|
29
|
-
self._samples: list[
|
37
|
+
self._samples: list[tuple] = []
|
30
38
|
self._background = gevent.spawn(self._run)
|
31
|
-
self._update_end_time_task = gevent.spawn(self._update_end_time)
|
32
39
|
self._hostname = socket.gethostname()
|
33
40
|
self._finished = False
|
41
|
+
self._has_logged_test_stop = False
|
34
42
|
self._pid = os.getpid()
|
35
43
|
self.pool = pool
|
36
44
|
|
@@ -43,13 +51,6 @@ class Exporter:
|
|
43
51
|
events.spawning_complete.add_listener(self.spawning_complete)
|
44
52
|
atexit.register(self.log_stop_test_run)
|
45
53
|
|
46
|
-
if self.env.runner is not None:
|
47
|
-
self.env.runner.register_message("run_id", self.set_run_id)
|
48
|
-
|
49
|
-
def set_run_id(self, environment, msg, **kwargs):
|
50
|
-
logging.debug(f"run id from master: {msg.data}")
|
51
|
-
self._run_id = datetime.strptime(msg.data, "%Y-%m-%d, %H:%M:%S.%f").replace(tzinfo=UTC)
|
52
|
-
|
53
54
|
def on_cpu_warning(self, environment: locust.env.Environment, cpu_usage, message=None, timestamp=None, **kwargs):
|
54
55
|
# passing a custom message & timestamp to the event is a haxx to allow using this event for reporting generic events
|
55
56
|
if not timestamp:
|
@@ -58,18 +59,20 @@ 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
|
-
|
68
|
-
if environment.runner is not None:
|
69
|
-
logging.debug(f"about to send run_id to workers: {msg}")
|
70
|
-
environment.runner.send_message("run_id", msg)
|
70
|
+
self.env.parsed_options.run_id = format_datetime(environment._run_id) # type: ignore
|
71
71
|
self.log_start_testrun()
|
72
72
|
self._user_count_logger = gevent.spawn(self._log_user_count)
|
73
|
+
self._update_end_time_task = gevent.spawn(self._update_end_time)
|
74
|
+
if self.env.parsed_options.worker:
|
75
|
+
self._run_id = parse_datetime(self.env.parsed_options.run_id)
|
73
76
|
|
74
77
|
def _log_user_count(self):
|
75
78
|
while True:
|
@@ -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:
|
@@ -99,6 +102,10 @@ class Exporter:
|
|
99
102
|
gevent.sleep(0.5)
|
100
103
|
|
101
104
|
def _update_end_time(self):
|
105
|
+
# delay setting first end time
|
106
|
+
# so UI doesn't display temporary value
|
107
|
+
gevent.sleep(5)
|
108
|
+
|
102
109
|
# Regularly update endtime to prevent missing endtimes when a test crashes
|
103
110
|
while True:
|
104
111
|
current_end_time = datetime.now(UTC)
|
@@ -117,35 +124,39 @@ class Exporter:
|
|
117
124
|
try:
|
118
125
|
with self.pool.connection() as conn:
|
119
126
|
conn: psycopg.connection.Connection
|
120
|
-
with conn.cursor()
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
""",
|
126
|
-
samples,
|
127
|
-
)
|
127
|
+
with conn.cursor().copy(
|
128
|
+
"COPY requests (time,run_id,greenlet_id,loadgen,name,request_type,response_time,success,response_length,exception,pid,url,context) FROM STDIN"
|
129
|
+
) as copy:
|
130
|
+
for sample in samples:
|
131
|
+
copy.write_row(sample)
|
128
132
|
except psycopg.Error as error:
|
129
133
|
logging.error("Failed to write samples to Postgresql timescale database: " + repr(error))
|
130
134
|
|
131
135
|
def on_test_stop(self, environment):
|
136
|
+
if getattr(self, "_update_end_time_task", False):
|
137
|
+
self._update_end_time_task.kill()
|
132
138
|
if getattr(self, "_user_count_logger", False):
|
133
139
|
self._user_count_logger.kill()
|
134
140
|
with self.pool.connection() as conn:
|
135
141
|
conn.execute(
|
136
|
-
"""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)""",
|
137
143
|
(datetime.now(UTC).isoformat(), self._run_id, 0),
|
138
144
|
)
|
139
145
|
self.log_stop_test_run()
|
146
|
+
self._has_logged_test_stop = True
|
140
147
|
|
141
148
|
def on_quit(self, exit_code, **kwargs):
|
142
149
|
self._finished = True
|
143
150
|
atexit._clear() # make sure we dont capture additional ctrl-c:s
|
144
151
|
self._background.join(timeout=10)
|
145
|
-
self
|
152
|
+
if getattr(self, "_update_end_time_task", False):
|
153
|
+
self._update_end_time_task.kill()
|
146
154
|
if getattr(self, "_user_count_logger", False):
|
147
155
|
self._user_count_logger.kill()
|
148
|
-
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)
|
149
160
|
|
150
161
|
def on_request(
|
151
162
|
self,
|
@@ -159,6 +170,9 @@ class Exporter:
|
|
159
170
|
url=None,
|
160
171
|
**kwargs,
|
161
172
|
):
|
173
|
+
# handle if a worker connects after test_start
|
174
|
+
if not self._run_id:
|
175
|
+
self._run_id = parse_datetime(self.env.parsed_options.run_id)
|
162
176
|
success = 0 if exception else 1
|
163
177
|
if start_time:
|
164
178
|
time = datetime.fromtimestamp(start_time, tz=UTC)
|
@@ -167,35 +181,35 @@ class Exporter:
|
|
167
181
|
# (which will be horribly wrong if users spend a lot of time in a with/catch_response-block)
|
168
182
|
time = datetime.now(UTC) - timedelta(milliseconds=response_time or 0)
|
169
183
|
greenlet_id = getattr(greenlet.getcurrent(), "minimal_ident", 0) # if we're debugging there is no greenlet
|
170
|
-
sample = {
|
171
|
-
"time": time,
|
172
|
-
"run_id": self._run_id,
|
173
|
-
"greenlet_id": greenlet_id,
|
174
|
-
"loadgen": self._hostname,
|
175
|
-
"name": name,
|
176
|
-
"request_type": request_type,
|
177
|
-
"response_time": response_time,
|
178
|
-
"success": success,
|
179
|
-
"url": url[0:255] if url else None,
|
180
|
-
"pid": self._pid,
|
181
|
-
"context": psycopg.types.json.Json(context, safe_serialize),
|
182
|
-
}
|
183
|
-
|
184
|
-
if response_length >= 0:
|
185
|
-
sample["response_length"] = response_length
|
186
|
-
else:
|
187
|
-
sample["response_length"] = None
|
188
184
|
|
189
185
|
if exception:
|
190
186
|
if isinstance(exception, CatchResponseError):
|
191
|
-
|
187
|
+
exception = str(exception)
|
192
188
|
else:
|
193
189
|
try:
|
194
|
-
|
190
|
+
exception = repr(exception)
|
195
191
|
except AttributeError:
|
196
|
-
|
192
|
+
exception = f"{exception.__class__} (and it has no string representation)"
|
193
|
+
|
194
|
+
exception = exception[:300]
|
197
195
|
else:
|
198
|
-
|
196
|
+
exception = None
|
197
|
+
|
198
|
+
sample = (
|
199
|
+
time,
|
200
|
+
self._run_id,
|
201
|
+
greenlet_id,
|
202
|
+
self._hostname,
|
203
|
+
name,
|
204
|
+
request_type,
|
205
|
+
response_time,
|
206
|
+
success,
|
207
|
+
response_length,
|
208
|
+
exception,
|
209
|
+
self._pid,
|
210
|
+
url[0:255] if url else None,
|
211
|
+
psycopg.types.json.Json(context, safe_serialize),
|
212
|
+
)
|
199
213
|
|
200
214
|
self._samples.append(sample)
|
201
215
|
|
@@ -203,7 +217,7 @@ class Exporter:
|
|
203
217
|
cmd = sys.argv[1:]
|
204
218
|
with self.pool.connection() as conn:
|
205
219
|
conn.execute(
|
206
|
-
"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)",
|
207
221
|
(
|
208
222
|
self._run_id,
|
209
223
|
self.env.runner.target_user_count if self.env.runner else 1,
|
@@ -220,7 +234,7 @@ class Exporter:
|
|
220
234
|
),
|
221
235
|
)
|
222
236
|
conn.execute(
|
223
|
-
"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)",
|
224
238
|
(datetime.now(UTC).isoformat(), "Test run started", self._run_id),
|
225
239
|
)
|
226
240
|
|
@@ -230,7 +244,7 @@ class Exporter:
|
|
230
244
|
try:
|
231
245
|
with self.pool.connection() as conn:
|
232
246
|
conn.execute(
|
233
|
-
"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)",
|
234
248
|
(end_time, f"Rampup complete, {user_count} users spawned", self._run_id),
|
235
249
|
)
|
236
250
|
except psycopg.Error as error:
|
@@ -238,7 +252,7 @@ class Exporter:
|
|
238
252
|
"Failed to insert rampup complete event time to Postgresql timescale database: " + repr(error)
|
239
253
|
)
|
240
254
|
|
241
|
-
def log_stop_test_run(self
|
255
|
+
def log_stop_test_run(self):
|
242
256
|
logging.debug(f"Test run id {self._run_id} stopping")
|
243
257
|
if self.env.parsed_options.worker:
|
244
258
|
return # only run on master or standalone
|
@@ -246,17 +260,14 @@ class Exporter:
|
|
246
260
|
try:
|
247
261
|
with self.pool.connection() as conn:
|
248
262
|
conn.execute(
|
249
|
-
"UPDATE testruns SET end_time = %s
|
250
|
-
(end_time,
|
263
|
+
"UPDATE testruns SET end_time = %s WHERE id = %s",
|
264
|
+
(end_time, self._run_id),
|
251
265
|
)
|
252
|
-
|
253
|
-
"INSERT INTO events (time, text, run_id) VALUES (%s, %s, %s)",
|
254
|
-
(end_time, f"Finished with exit code: {exit_code}", self._run_id),
|
255
|
-
)
|
256
|
-
# The AND time > run_id clause in the following statements are there to help Timescale performance
|
257
|
-
# We dont use start_time / end_time to calculate RPS, instead we use the time between the actual first and last request
|
258
|
-
# (as this is a more accurate measurement of the actual test)
|
266
|
+
|
259
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)
|
260
271
|
conn.execute(
|
261
272
|
"""
|
262
273
|
UPDATE testruns
|
@@ -265,12 +276,12 @@ SET (requests, resp_time_avg, rps_avg, fail_ratio) =
|
|
265
276
|
(SELECT
|
266
277
|
COUNT(*)::numeric AS reqs,
|
267
278
|
AVG(response_time)::numeric as resp_time
|
268
|
-
FROM
|
279
|
+
FROM requests_view WHERE run_id = %(run_id)s AND time > %(run_id)s) AS _,
|
269
280
|
(SELECT
|
270
|
-
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 __,
|
271
282
|
(SELECT
|
272
283
|
COUNT(*)::numeric AS fails
|
273
|
-
FROM
|
284
|
+
FROM requests_view WHERE run_id = %(run_id)s AND time > %(run_id)s AND success = 0) AS ___
|
274
285
|
WHERE id = %(run_id)s""",
|
275
286
|
{"run_id": self._run_id},
|
276
287
|
)
|
@@ -283,3 +294,20 @@ WHERE id = %(run_id)s""",
|
|
283
294
|
"Failed to update testruns record (or events) with end time to Postgresql timescale database: "
|
284
295
|
+ repr(error)
|
285
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
|
+
)
|