locust 2.28.1.dev54__py3-none-any.whl → 2.28.1.dev66__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/_version.py CHANGED
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '2.28.1.dev54'
16
- __version_tuple__ = version_tuple = (2, 28, 1, 'dev54')
15
+ __version__ = version = '2.28.1.dev66'
16
+ __version_tuple__ = version_tuple = (2, 28, 1, 'dev66')
locust/env.py CHANGED
@@ -99,6 +99,8 @@ class Environment:
99
99
  """List of the available Tasks per User Classes to pick from in the Task Picker"""
100
100
  self.dispatcher_class = dispatcher_class
101
101
  """A user dispatcher class that decides how users are spawned, default :class:`UsersDispatcher <locust.dispatch.UsersDispatcher>`"""
102
+ self.worker_logs: dict[str, list[str]] = {}
103
+ """Captured logs from all connected workers"""
102
104
 
103
105
  self._remove_user_classes_with_weight_zero()
104
106
  self._validate_user_class_name_uniqueness()
@@ -209,6 +211,10 @@ class Environment:
209
211
  if key == "tasks":
210
212
  user_class.tasks = [task for task in user_tasks if task.__name__ in value]
211
213
 
214
+ def update_worker_logs(self, worker_log_report):
215
+ if worker_log_report.get("worker_id", None):
216
+ self.worker_logs[worker_log_report.get("worker_id")] = worker_log_report.get("logs", [])
217
+
212
218
  def _filter_tasks_by_tags(self) -> None:
213
219
  """
214
220
  Filter the tasks on all the user_classes recursively, according to the tags and
locust/log.py CHANGED
@@ -2,6 +2,7 @@ import logging
2
2
  import logging.config
3
3
  import re
4
4
  import socket
5
+ from collections import deque
5
6
 
6
7
  HOSTNAME = re.sub(r"\..*", "", socket.gethostname())
7
8
 
@@ -13,7 +14,7 @@ unhandled_greenlet_exception = False
13
14
  class LogReader(logging.Handler):
14
15
  def __init__(self):
15
16
  super().__init__()
16
- self.logs = []
17
+ self.logs = deque(maxlen=500)
17
18
 
18
19
  def emit(self, record):
19
20
  self.logs.append(self.format(record))
@@ -75,6 +76,15 @@ def setup_logging(loglevel, logfile=None):
75
76
  logging.config.dictConfig(LOGGING_CONFIG)
76
77
 
77
78
 
79
+ def get_logs():
80
+ log_reader_handler = [handler for handler in logging.getLogger("root").handlers if handler.name == "log_reader"]
81
+
82
+ if log_reader_handler:
83
+ return list(log_reader_handler[0].logs)
84
+
85
+ return []
86
+
87
+
78
88
  def greenlet_exception_logger(logger, level=logging.CRITICAL):
79
89
  """
80
90
  Return a function that can be used as argument to Greenlet.link_exception() that will log the
locust/runners.py CHANGED
@@ -39,7 +39,7 @@ from gevent.pool import Group
39
39
  from . import argument_parser
40
40
  from .dispatch import UsersDispatcher
41
41
  from .exception import RPCError, RPCReceiveError, RPCSendError
42
- from .log import greenlet_exception_logger
42
+ from .log import get_logs, greenlet_exception_logger
43
43
  from .rpc import (
44
44
  Message,
45
45
  rpc,
@@ -66,6 +66,7 @@ STATE_INIT, STATE_SPAWNING, STATE_RUNNING, STATE_CLEANUP, STATE_STOPPING, STATE_
66
66
  "missing",
67
67
  ]
68
68
  WORKER_REPORT_INTERVAL = 3.0
69
+ WORKER_LOG_REPORT_INTERVAL = 10
69
70
  CPU_MONITOR_INTERVAL = 5.0
70
71
  CPU_WARNING_THRESHOLD = 90
71
72
  HEARTBEAT_INTERVAL = 1
@@ -1116,6 +1117,8 @@ class MasterRunner(DistributedRunner):
1116
1117
  # a worker finished spawning (this happens multiple times during rampup)
1117
1118
  self.clients[msg.node_id].state = STATE_RUNNING
1118
1119
  self.clients[msg.node_id].user_classes_count = msg.data["user_classes_count"]
1120
+ elif msg.type == "logs":
1121
+ self.environment.update_worker_logs(msg.data)
1119
1122
  elif msg.type == "quit":
1120
1123
  if msg.node_id in self.clients:
1121
1124
  client = self.clients[msg.node_id]
@@ -1212,6 +1215,7 @@ class WorkerRunner(DistributedRunner):
1212
1215
  self.client_id = socket.gethostname() + "_" + uuid4().hex
1213
1216
  self.master_host = master_host
1214
1217
  self.master_port = master_port
1218
+ self.logs: list[str] = []
1215
1219
  self.worker_cpu_warning_emitted = False
1216
1220
  self._users_dispatcher: UsersDispatcher | None = None
1217
1221
  self.client = rpc.Client(master_host, master_port, self.client_id)
@@ -1220,6 +1224,7 @@ class WorkerRunner(DistributedRunner):
1220
1224
  self.greenlet.spawn(self.heartbeat).link_exception(greenlet_exception_handler)
1221
1225
  self.greenlet.spawn(self.heartbeat_timeout_checker).link_exception(greenlet_exception_handler)
1222
1226
  self.greenlet.spawn(self.stats_reporter).link_exception(greenlet_exception_handler)
1227
+ self.greenlet.spawn(self.logs_reporter).link_exception(greenlet_exception_handler)
1223
1228
 
1224
1229
  # register listener that adds the current number of spawned users to the report that is sent to the master node
1225
1230
  def on_report_to_master(client_id: str, data: dict[str, Any]):
@@ -1417,6 +1422,25 @@ class WorkerRunner(DistributedRunner):
1417
1422
  logger.error(f"Temporary connection lost to master server: {e}, will retry later.")
1418
1423
  gevent.sleep(WORKER_REPORT_INTERVAL)
1419
1424
 
1425
+ def logs_reporter(self) -> None:
1426
+ if WORKER_LOG_REPORT_INTERVAL < 0:
1427
+ return
1428
+
1429
+ while True:
1430
+ current_logs = get_logs()
1431
+
1432
+ if (len(current_logs) - len(self.logs)) > 10:
1433
+ logger.warning(
1434
+ "The worker attempted to send more than 10 log lines in one interval. Further log sending was disabled for this worker."
1435
+ )
1436
+ self._send_logs(get_logs())
1437
+ break
1438
+ if len(current_logs) > len(self.logs):
1439
+ self._send_logs(current_logs)
1440
+
1441
+ self.logs = current_logs
1442
+ gevent.sleep(WORKER_LOG_REPORT_INTERVAL)
1443
+
1420
1444
  def send_message(self, msg_type: str, data: dict[str, Any] | None = None, client_id: str | None = None) -> None:
1421
1445
  """
1422
1446
  Sends a message to master node
@@ -1433,6 +1457,9 @@ class WorkerRunner(DistributedRunner):
1433
1457
  self.environment.events.report_to_master.fire(client_id=self.client_id, data=data)
1434
1458
  self.client.send(Message("stats", data, self.client_id))
1435
1459
 
1460
+ def _send_logs(self, current_logs) -> None:
1461
+ self.send_message("logs", {"worker_id": self.client_id, "logs": current_logs})
1462
+
1436
1463
  def connect_to_master(self):
1437
1464
  self.retry += 1
1438
1465
  self.client.send(Message("client_ready", __version__, self.client_id))
@@ -11,6 +11,7 @@ from locust.argument_parser import parse_options
11
11
  from locust.dispatch import UsersDispatcher
12
12
  from locust.env import Environment
13
13
  from locust.exception import RPCError, RPCReceiveError, StopUser
14
+ from locust.log import LogReader
14
15
  from locust.main import create_environment
15
16
  from locust.rpc import Message
16
17
  from locust.runners import (
@@ -32,6 +33,7 @@ from locust.user import (
32
33
  )
33
34
 
34
35
  import json
36
+ import logging
35
37
  import random
36
38
  import time
37
39
  import unittest
@@ -80,11 +82,12 @@ def mocked_rpc(raise_on_close=True):
80
82
  self.outbox.append(message)
81
83
 
82
84
  def send_to_client(self, message):
83
- self.outbox.append((message.node_id, message))
85
+ print(message)
86
+ self.outbox.append(message)
84
87
 
85
88
  @classmethod
86
89
  def get_messages(cls, message_type=None) -> list:
87
- return [message[1] for message in cls.outbox if message_type is None or message[1].type == message_type]
90
+ return [message for message in cls.outbox if message_type is None or message.type == message_type]
88
91
 
89
92
  def recv_from_client(self):
90
93
  results = self.queue.get()
@@ -2601,9 +2604,9 @@ class TestMasterRunner(LocustRunnerTestCase):
2601
2604
  server.mocked_send(Message("client_ready", __version__, "zeh_fake_client2"))
2602
2605
  self.assertEqual(2, len(master.clients))
2603
2606
  sleep(0.1) # give time for messages to be sent to clients
2604
- spawn_messages = [message for message in server.outbox if message[1].type == "spawn"]
2605
- self.assertEqual({"TestUser": 50}, spawn_messages[-1][1].data["user_classes_count"])
2606
- self.assertEqual({"TestUser": 50}, spawn_messages[-2][1].data["user_classes_count"])
2607
+ spawn_messages = server.get_messages("spawn")
2608
+ self.assertEqual({"TestUser": 50}, spawn_messages[-1].data["user_classes_count"])
2609
+ self.assertEqual({"TestUser": 50}, spawn_messages[-2].data["user_classes_count"])
2607
2610
 
2608
2611
  def test_sends_spawn_data_to_ready_running_spawning_workers(self):
2609
2612
  """Sends spawn job to running, ready, or spawning workers"""
@@ -2648,7 +2651,7 @@ class TestMasterRunner(LocustRunnerTestCase):
2648
2651
  server.mocked_send(Message("client_ready", __version__, "fake_client%i" % i))
2649
2652
 
2650
2653
  master.start(7, 7)
2651
- self.assertEqual(15, len(server.outbox))
2654
+ self.assertEqual(15, len(server.get_messages()))
2652
2655
  self.assertEqual(1, run_count[0])
2653
2656
 
2654
2657
  # change number of users and check that test_start isn't fired again
@@ -2687,7 +2690,7 @@ class TestMasterRunner(LocustRunnerTestCase):
2687
2690
  server.mocked_send(Message("client_ready", __version__, "fake_client%i" % i))
2688
2691
 
2689
2692
  master.start(7, 7)
2690
- self.assertEqual(15, len(server.outbox))
2693
+ self.assertEqual(15, len(server.get_messages()))
2691
2694
  master.stop()
2692
2695
  self.assertTrue(self.runner_stopping)
2693
2696
  self.assertTrue(self.runner_stopped)
@@ -2726,7 +2729,7 @@ class TestMasterRunner(LocustRunnerTestCase):
2726
2729
  server.mocked_send(Message("client_ready", __version__, "fake_client%i" % i))
2727
2730
 
2728
2731
  master.start(7, 7)
2729
- self.assertEqual(15, len(server.outbox))
2732
+ self.assertEqual(15, len(server.get_messages()))
2730
2733
  master.quit()
2731
2734
  self.assertTrue(self.runner_stopping)
2732
2735
  self.assertTrue(self.runner_stopped)
@@ -2819,9 +2822,8 @@ class TestMasterRunner(LocustRunnerTestCase):
2819
2822
  self.assertEqual(USERS_COUNT * 3, len(server.outbox))
2820
2823
 
2821
2824
  indexes = []
2822
- for _, msg in server.outbox:
2823
- if msg.type == "ack":
2824
- indexes.append(msg.data["index"])
2825
+ for msg in server.get_messages("ack"):
2826
+ indexes.append(msg.data["index"])
2825
2827
  self.assertEqual(USERS_COUNT, len(indexes), "Total number of locusts/workers is not 5")
2826
2828
 
2827
2829
  indexes.sort()
@@ -3110,8 +3112,10 @@ class TestMasterRunner(LocustRunnerTestCase):
3110
3112
  master.clients[i] = WorkerNode(str(i))
3111
3113
  master.send_message("test_custom_msg", {"test_data": 123})
3112
3114
 
3113
- self.assertEqual(5, len(server.outbox))
3114
- for _, msg in server.outbox:
3115
+ messages = server.get_messages()
3116
+
3117
+ self.assertEqual(5, len(messages))
3118
+ for msg in messages:
3115
3119
  self.assertEqual("test_custom_msg", msg.type)
3116
3120
  self.assertEqual(123, msg.data["test_data"])
3117
3121
 
@@ -3198,10 +3202,12 @@ class TestMasterRunner(LocustRunnerTestCase):
3198
3202
  master = self.get_runner()
3199
3203
  server.mocked_send(Message("client_ready", __version__, "dummy_client"))
3200
3204
 
3205
+ messages = server.get_messages()
3206
+
3201
3207
  self.assertEqual(1, len(master.clients))
3202
- self.assertEqual("ack", server.outbox[0][1].type)
3203
- self.assertEqual(1, len(server.outbox))
3204
- self.assertEqual(0, server.outbox[0][1].data["index"])
3208
+ self.assertEqual("ack", messages[0].type)
3209
+ self.assertEqual(1, len(messages))
3210
+ self.assertEqual(0, messages[0].data["index"])
3205
3211
 
3206
3212
  def test_worker_sends_bad_message_to_master(self):
3207
3213
  """
@@ -3224,11 +3230,12 @@ class TestMasterRunner(LocustRunnerTestCase):
3224
3230
  master.start(10, 10)
3225
3231
  sleep(0.1)
3226
3232
  server.mocked_send(Message("stats", BAD_MESSAGE, "zeh_fake_client1"))
3227
- self.assertEqual(5, len(server.outbox))
3233
+ messages = server.get_messages()
3234
+ self.assertEqual(5, len(messages))
3228
3235
 
3229
3236
  # Expected message order in outbox: ack, spawn, reconnect, ack
3230
3237
  self.assertEqual(
3231
- "reconnect", server.outbox[3][1].type, "Master didn't send worker reconnect message when expected."
3238
+ "reconnect", messages[3].type, "Master didn't send worker reconnect message when expected."
3232
3239
  )
3233
3240
 
3234
3241
  def test_worker_sends_unrecognized_message_to_master(self):
@@ -3252,7 +3259,7 @@ class TestMasterRunner(LocustRunnerTestCase):
3252
3259
  master.start(10, 10)
3253
3260
  sleep(0.1)
3254
3261
  server.mocked_send(Message("stats", UNRECOGNIZED_MESSAGE, "zeh_fake_client1"))
3255
- self.assertEqual(3, len(server.outbox))
3262
+ self.assertEqual(3, len(server.get_messages()))
3256
3263
 
3257
3264
  def test_unknown_host_sends_message_to_master(self):
3258
3265
  """
@@ -3275,7 +3282,7 @@ class TestMasterRunner(LocustRunnerTestCase):
3275
3282
  master.start(10, 10)
3276
3283
  sleep(0.1)
3277
3284
  server.mocked_send(Message("stats", UNRECOGNIZED_HOST_MESSAGE, "unknown_host"))
3278
- self.assertEqual(3, len(server.outbox))
3285
+ self.assertEqual(3, len(server.get_messages()))
3279
3286
 
3280
3287
 
3281
3288
  class TestWorkerRunner(LocustTestCase):
@@ -3308,8 +3315,9 @@ class TestWorkerRunner(LocustTestCase):
3308
3315
 
3309
3316
  with mock.patch("locust.rpc.rpc.Client", mocked_rpc()) as client:
3310
3317
  worker = self.get_runner(environment=Environment(), user_classes=[MyTestUser], client=client)
3311
- self.assertEqual(1, len(client.outbox))
3312
- self.assertEqual("client_ready", client.outbox[0].type)
3318
+ messages = client.get_messages()
3319
+ self.assertEqual(1, len(messages))
3320
+ self.assertEqual("client_ready", messages[0].type)
3313
3321
  client.mocked_send(
3314
3322
  Message(
3315
3323
  "spawn",
@@ -3323,8 +3331,9 @@ class TestWorkerRunner(LocustTestCase):
3323
3331
  "dummy_client_id",
3324
3332
  )
3325
3333
  )
3334
+ self.assertTrue(client.get_messages("spawning"))
3326
3335
  # wait for worker to spawn locusts
3327
- self.assertIn("spawning", [m.type for m in client.outbox])
3336
+ # self.assertIn("spawning", [m.type for m in messages])
3328
3337
  worker.spawning_greenlet.join()
3329
3338
  self.assertEqual(1, len(worker.user_greenlets))
3330
3339
  # check that locust has started running
@@ -3348,8 +3357,9 @@ class TestWorkerRunner(LocustTestCase):
3348
3357
 
3349
3358
  with mock.patch("locust.rpc.rpc.Client", mocked_rpc()) as client:
3350
3359
  worker = self.get_runner(environment=Environment(), user_classes=[MyTestUser], client=client)
3351
- self.assertEqual(1, len(client.outbox))
3352
- self.assertEqual("client_ready", client.outbox[0].type)
3360
+ messages = client.get_messages()
3361
+ self.assertEqual(1, len(messages))
3362
+ self.assertEqual("client_ready", messages[0].type)
3353
3363
  client.mocked_send(
3354
3364
  Message(
3355
3365
  "spawn",
@@ -3365,7 +3375,7 @@ class TestWorkerRunner(LocustTestCase):
3365
3375
  )
3366
3376
  # print("outbox:", client.outbox)
3367
3377
  # wait for worker to spawn locusts
3368
- self.assertIn("spawning", [m.type for m in client.outbox])
3378
+ self.assertTrue(client.get_messages("spawning"))
3369
3379
  worker.spawning_greenlet.join()
3370
3380
  self.assertEqual(1, len(worker.user_greenlets))
3371
3381
  # check that locust has started running
@@ -3509,14 +3519,14 @@ class TestWorkerRunner(LocustTestCase):
3509
3519
 
3510
3520
  sleep(2)
3511
3521
 
3512
- message = next((m for m in reversed(client.outbox) if m.type == "stats"), None)
3522
+ message = client.get_messages("stats")[-1]
3513
3523
  self.assertIsNotNone(message)
3514
3524
  self.assertIn("user_count", message.data)
3515
3525
  self.assertIn("user_classes_count", message.data)
3516
3526
  self.assertEqual(message.data["user_count"], 10)
3517
3527
  self.assertEqual(message.data["user_classes_count"]["MyUser"], 10)
3518
3528
 
3519
- message = next((m for m in client.outbox if m.type == "spawning_complete"), None)
3529
+ message = client.get_messages("spawning_complete")[0]
3520
3530
  self.assertIsNotNone(message)
3521
3531
  self.assertIn("user_count", message.data)
3522
3532
  self.assertIn("user_classes_count", message.data)
@@ -3541,11 +3551,11 @@ class TestWorkerRunner(LocustTestCase):
3541
3551
  worker = self.get_runner(environment=Environment(), user_classes=[MyUser], client=client)
3542
3552
 
3543
3553
  t0 = time.perf_counter()
3544
- while len([m for m in client.outbox if m.type == "heartbeat"]) == 0:
3554
+ while len(client.get_messages("heartbeat")) == 0:
3545
3555
  self.assertLessEqual(time.perf_counter() - t0, 3)
3546
3556
  sleep(0.1)
3547
3557
 
3548
- message = next(m for m in reversed(client.outbox) if m.type == "heartbeat")
3558
+ message = client.get_messages("heartbeat")[-1]
3549
3559
  self.assertEqual(len(message.data), 3)
3550
3560
  self.assertIn("state", message.data)
3551
3561
  self.assertIn("current_cpu_usage", message.data)
@@ -3718,8 +3728,9 @@ class TestWorkerRunner(LocustTestCase):
3718
3728
  worker = self.get_runner(environment=Environment(), user_classes=[MyUser], client=client)
3719
3729
  client.outbox.clear()
3720
3730
  worker.send_message("test_custom_msg", {"test_data": 123})
3721
- self.assertEqual("test_custom_msg", client.outbox[0].type)
3722
- self.assertEqual(123, client.outbox[0].data["test_data"])
3731
+ messages = client.get_messages()
3732
+ self.assertEqual("test_custom_msg", messages[0].type)
3733
+ self.assertEqual(123, messages[0].data["test_data"])
3723
3734
  worker.quit()
3724
3735
 
3725
3736
  def test_custom_message_receive(self):
@@ -3790,8 +3801,9 @@ class TestWorkerRunner(LocustTestCase):
3790
3801
  run_count[0] += 1
3791
3802
 
3792
3803
  worker = self.get_runner(environment=environment, user_classes=[MyTestUser], client=client)
3793
- self.assertEqual(1, len(client.outbox))
3794
- self.assertEqual("client_ready", client.outbox[0].type)
3804
+ messages = client.get_messages()
3805
+ self.assertEqual(1, len(messages))
3806
+ self.assertEqual("client_ready", messages[0].type)
3795
3807
  client.mocked_send(
3796
3808
  Message(
3797
3809
  "spawn",
@@ -3808,7 +3820,7 @@ class TestWorkerRunner(LocustTestCase):
3808
3820
  )
3809
3821
  )
3810
3822
  # wait for worker to spawn locusts
3811
- self.assertIn("spawning", [m.type for m in client.outbox])
3823
+ self.assertTrue(client.get_messages("spawning"))
3812
3824
  worker.spawning_greenlet.join()
3813
3825
  self.assertEqual(1, len(worker.user_greenlets))
3814
3826
  self.assertEqual(1, run_count[0])
@@ -3876,8 +3888,9 @@ class TestWorkerRunner(LocustTestCase):
3876
3888
  run_count[0] += 1
3877
3889
 
3878
3890
  worker = self.get_runner(environment=environment, user_classes=[MyTestUser], client=client)
3879
- self.assertEqual(1, len(client.outbox))
3880
- self.assertEqual("client_ready", client.outbox[0].type)
3891
+ messages = client.get_messages()
3892
+ self.assertEqual(1, len(messages))
3893
+ self.assertEqual("client_ready", messages[0].type)
3881
3894
  client.mocked_send(
3882
3895
  Message(
3883
3896
  "spawn",
@@ -3895,7 +3908,7 @@ class TestWorkerRunner(LocustTestCase):
3895
3908
  )
3896
3909
 
3897
3910
  # wait for worker to spawn locusts
3898
- self.assertIn("spawning", [m.type for m in client.outbox])
3911
+ self.assertTrue(client.get_messages("spawning"))
3899
3912
  worker.spawning_greenlet.join()
3900
3913
  self.assertEqual(1, len(worker.user_greenlets))
3901
3914
 
@@ -3942,9 +3955,10 @@ class TestWorkerRunner(LocustTestCase):
3942
3955
  with mock.patch("locust.runners.CONNECT_TIMEOUT", new=1):
3943
3956
  with mock.patch("locust.rpc.rpc.Client", mocked_rpc()) as client:
3944
3957
  worker = self.get_runner(environment=Environment(), user_classes=[MyTestUser], client=client)
3958
+ messages = client.get_messages()
3945
3959
 
3946
- self.assertEqual("client_ready", client.outbox[0].type)
3947
- self.assertEqual(1, len(client.outbox))
3960
+ self.assertEqual("client_ready", messages[0].type)
3961
+ self.assertEqual(1, len(messages))
3948
3962
  self.assertTrue(worker.connected)
3949
3963
 
3950
3964
  def test_worker_connect_failure(self):
@@ -3962,6 +3976,71 @@ class TestWorkerRunner(LocustTestCase):
3962
3976
  )
3963
3977
  self.assertEqual(2, len(client.outbox))
3964
3978
 
3979
+ def test_send_logs(self):
3980
+ class MyUser(User):
3981
+ wait_time = constant(1)
3982
+
3983
+ @task
3984
+ def my_task(self):
3985
+ pass
3986
+
3987
+ with mock.patch("locust.rpc.rpc.Client", mocked_rpc()) as client:
3988
+ short_time = 0.05
3989
+
3990
+ log_handler = LogReader()
3991
+ log_handler.name = "log_reader"
3992
+ log_handler.setLevel(logging.INFO)
3993
+ logger = logging.getLogger("root")
3994
+ logger.addHandler(log_handler)
3995
+ log_line = "some log info"
3996
+ logger.info(log_line)
3997
+
3998
+ worker = self.get_runner(environment=Environment(), user_classes=[MyUser], client=client)
3999
+
4000
+ gevent.sleep(short_time)
4001
+
4002
+ messages = client.get_messages()
4003
+
4004
+ self.assertEqual("logs", messages[3].type)
4005
+ self.assertEqual(log_line, messages[3].data.get("logs", [])[0])
4006
+ self.assertEqual(worker.client_id, messages[3].data.get("worker_id"))
4007
+ worker.quit()
4008
+
4009
+ def test_quit_worker_logs(self):
4010
+ class MyUser(User):
4011
+ wait_time = constant(1)
4012
+
4013
+ @task
4014
+ def my_task(self):
4015
+ pass
4016
+
4017
+ with mock.patch("locust.rpc.rpc.Client", mocked_rpc()) as client:
4018
+ short_time = 0.05
4019
+
4020
+ log_handler = LogReader()
4021
+ log_handler.name = "log_reader"
4022
+ log_handler.setLevel(logging.INFO)
4023
+ logger = logging.getLogger("root")
4024
+ logger.addHandler(log_handler)
4025
+ log_line = "spamming log"
4026
+
4027
+ for _ in range(11):
4028
+ logger.info(log_line)
4029
+
4030
+ worker = self.get_runner(environment=Environment(), user_classes=[MyUser], client=client)
4031
+
4032
+ gevent.sleep(short_time)
4033
+
4034
+ message = client.get_messages("logs")[0]
4035
+
4036
+ self.assertEqual(
4037
+ "The worker attempted to send more than 10 log lines in one interval. Further log sending was disabled for this worker.",
4038
+ message.data.get("logs", [])[-1],
4039
+ )
4040
+ self.assertEqual(worker.client_id, message.data.get("worker_id"))
4041
+ worker.quit()
4042
+ logger.removeHandler(log_handler)
4043
+
3965
4044
 
3966
4045
  class TestMessageSerializing(unittest.TestCase):
3967
4046
  def test_message_serialize(self):
locust/test/test_web.py CHANGED
@@ -1020,7 +1020,25 @@ class TestWebUI(LocustTestCase, _HeaderCheckMixin):
1020
1020
 
1021
1021
  response = requests.get("http://127.0.0.1:%i/logs" % self.web_port)
1022
1022
 
1023
- self.assertIn(log_line, response.json().get("logs"))
1023
+ self.assertIn(log_line, response.json().get("master"))
1024
+
1025
+ def test_worker_logs(self):
1026
+ log_handler = LogReader()
1027
+ log_handler.name = "log_reader"
1028
+ log_handler.setLevel(logging.INFO)
1029
+ logger = logging.getLogger("root")
1030
+ logger.addHandler(log_handler)
1031
+ log_line = "some log info"
1032
+ logger.info(log_line)
1033
+
1034
+ worker_id = "123"
1035
+ worker_log_line = "worker log"
1036
+ self.environment.update_worker_logs({"worker_id": worker_id, "logs": [worker_log_line]})
1037
+
1038
+ response = requests.get("http://127.0.0.1:%i/logs" % self.web_port)
1039
+
1040
+ self.assertIn(log_line, response.json().get("master"))
1041
+ self.assertIn(worker_log_line, response.json().get("workers").get(worker_id))
1024
1042
 
1025
1043
  def test_template_args(self):
1026
1044
  class MyUser(User):
locust/web.py CHANGED
@@ -33,7 +33,7 @@ from . import __version__ as version
33
33
  from . import argument_parser
34
34
  from . import stats as stats_module
35
35
  from .html import BUILD_PATH, ROOT_PATH, STATIC_PATH, get_html_report
36
- from .log import greenlet_exception_logger
36
+ from .log import get_logs, greenlet_exception_logger
37
37
  from .runners import STATE_MISSING, STATE_RUNNING, MasterRunner
38
38
  from .stats import StatsCSV, StatsCSVFileWriter, StatsErrorDict, sort_stats
39
39
  from .user.inspectuser import get_ratio
@@ -490,16 +490,7 @@ class WebUI:
490
490
  @app.route("/logs")
491
491
  @self.auth_required_if_enabled
492
492
  def logs():
493
- log_reader_handler = [
494
- handler for handler in logging.getLogger("root").handlers if handler.name == "log_reader"
495
- ]
496
-
497
- if log_reader_handler:
498
- logs = log_reader_handler[0].logs
499
- else:
500
- logs = []
501
-
502
- return jsonify({"logs": logs})
493
+ return jsonify({"master": get_logs(), "workers": self.environment.worker_logs})
503
494
 
504
495
  @app.route("/login")
505
496
  def login():