locust 2.43.5.dev25__tar.gz → 2.43.5.dev31__tar.gz
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-2.43.5.dev25 → locust-2.43.5.dev31}/PKG-INFO +1 -1
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/_version.py +2 -2
- locust-2.43.5.dev31/locust/contrib/csv_request_logger.py +187 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/contrib/oai.py +1 -1
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/main.py +20 -12
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/stats.py +1 -1
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/user/inspectuser.py +1 -1
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/web.py +1 -1
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/pyproject.toml +1 -1
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/.gitignore +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/LICENSE +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/README.md +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/hatch_build.py +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/__init__.py +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/__main__.py +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/argument_parser.py +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/clients.py +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/contrib/__init__.py +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/contrib/dns.py +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/contrib/fasthttp.py +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/contrib/milvus.py +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/contrib/mongodb.py +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/contrib/mqtt.py +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/contrib/postgres.py +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/contrib/qdrant.py +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/contrib/socketio.py +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/debug.py +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/dispatch.py +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/env.py +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/event.py +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/exception.py +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/html.py +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/input_events.py +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/log.py +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/opentelemetry.py +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/py.typed +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/rpc/__init__.py +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/rpc/protocol.py +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/rpc/zmqrpc.py +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/runners.py +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/shape.py +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/user/__init__.py +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/user/markov_taskset.py +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/user/sequential_taskset.py +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/user/task.py +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/user/users.py +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/user/wait_time.py +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/util/__init__.py +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/util/cache.py +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/util/date.py +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/util/deprecation.py +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/util/directory.py +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/util/exception_handler.py +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/util/load_locustfile.py +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/util/rounding.py +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/util/timespan.py +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/util/url.py +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/webui/dist/assets/favicon-dark.png +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/webui/dist/assets/favicon-light.png +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/webui/dist/assets/graphs-dark.png +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/webui/dist/assets/graphs-light.png +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/webui/dist/assets/src-lBIzfjkZ.js +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/webui/dist/assets/terminal.gif +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/webui/dist/assets/testruns-dark.png +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/webui/dist/assets/testruns-light.png +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/webui/dist/auth.html +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/webui/dist/index.html +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/webui/dist/report.html +0 -0
- {locust-2.43.5.dev25 → locust-2.43.5.dev31}/pytest_locust/plugin.py +0 -0
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '2.43.5.
|
|
22
|
-
__version_tuple__ = version_tuple = (2, 43, 5, '
|
|
21
|
+
__version__ = version = '2.43.5.dev31'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 43, 5, 'dev31')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Per-request CSV logger for Locust.
|
|
3
|
+
|
|
4
|
+
Hooks into the ``request`` event and appends one row per completed request to a
|
|
5
|
+
CSV file. Useful for post-run analysis that requires individual data-points
|
|
6
|
+
rather than the aggregated statistics provided by the built-in
|
|
7
|
+
``--csv`` flag.
|
|
8
|
+
|
|
9
|
+
Usage::
|
|
10
|
+
|
|
11
|
+
from locust import HttpUser, task, events
|
|
12
|
+
from locust.contrib.csv_request_logger import CsvRequestLogger
|
|
13
|
+
|
|
14
|
+
logger = CsvRequestLogger("results/requests.csv")
|
|
15
|
+
|
|
16
|
+
@events.init.add_listener
|
|
17
|
+
def on_locust_init(environment, **kwargs):
|
|
18
|
+
logger.register(environment)
|
|
19
|
+
|
|
20
|
+
class MyUser(HttpUser):
|
|
21
|
+
@task
|
|
22
|
+
def index(self):
|
|
23
|
+
self.client.get("/")
|
|
24
|
+
|
|
25
|
+
The CSV file is created (or truncated) when :meth:`CsvRequestLogger.register`
|
|
26
|
+
is called and closed when the ``quitting`` event fires.
|
|
27
|
+
|
|
28
|
+
CSV columns
|
|
29
|
+
-----------
|
|
30
|
+
``timestamp``
|
|
31
|
+
Unix timestamp (seconds, float) at which the request was sent.
|
|
32
|
+
``request_type``
|
|
33
|
+
HTTP method or custom protocol name (e.g. ``GET``, ``POST``, ``WS``).
|
|
34
|
+
``name``
|
|
35
|
+
URL path / request name as reported to Locust stats.
|
|
36
|
+
``response_time_ms``
|
|
37
|
+
Response time in **milliseconds** (float, rounded to 2 dp).
|
|
38
|
+
``response_length``
|
|
39
|
+
Response body size in bytes (int).
|
|
40
|
+
``status_code``
|
|
41
|
+
HTTP status code (int). ``0`` when the request failed before a response
|
|
42
|
+
was received (e.g. connection error or custom failure).
|
|
43
|
+
``exception``
|
|
44
|
+
String representation of the exception if the request failed, else empty.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
import csv
|
|
48
|
+
import io
|
|
49
|
+
import logging
|
|
50
|
+
import time
|
|
51
|
+
from typing import TYPE_CHECKING, Any
|
|
52
|
+
|
|
53
|
+
if TYPE_CHECKING:
|
|
54
|
+
from locust.env import Environment
|
|
55
|
+
from locust.stats import CSVWriter
|
|
56
|
+
|
|
57
|
+
logger = logging.getLogger(__name__)
|
|
58
|
+
|
|
59
|
+
#: Column headers written to the CSV file.
|
|
60
|
+
CSV_COLUMNS = (
|
|
61
|
+
"timestamp",
|
|
62
|
+
"request_type",
|
|
63
|
+
"name",
|
|
64
|
+
"response_time_ms",
|
|
65
|
+
"response_length",
|
|
66
|
+
"status_code",
|
|
67
|
+
"exception",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _status_code(response: Any, exception: Any) -> int:
|
|
72
|
+
"""Extract the HTTP status code from a response object.
|
|
73
|
+
|
|
74
|
+
Returns ``0`` when no response is available (connection errors, custom
|
|
75
|
+
``ResponseContextManager`` failures, non-HTTP protocols, etc.).
|
|
76
|
+
"""
|
|
77
|
+
if exception is not None and response is None:
|
|
78
|
+
return 0
|
|
79
|
+
try:
|
|
80
|
+
code = int(response.status_code)
|
|
81
|
+
return code
|
|
82
|
+
except (AttributeError, TypeError, ValueError):
|
|
83
|
+
return 0
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class CsvRequestLogger:
|
|
87
|
+
"""Listens to Locust's ``request`` event and writes one CSV row per request.
|
|
88
|
+
|
|
89
|
+
Parameters
|
|
90
|
+
----------
|
|
91
|
+
filepath:
|
|
92
|
+
Path to the output CSV file. Parent directories must already exist.
|
|
93
|
+
If the file exists it will be **overwritten** at the start of each run.
|
|
94
|
+
flush_interval:
|
|
95
|
+
Number of rows to buffer before flushing to disk. Use ``1`` for
|
|
96
|
+
immediate write-through (safest for crash recovery), or a larger value
|
|
97
|
+
for better performance on high-RPS tests. Defaults to ``1``.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
def __init__(self, filepath: str, *, flush_interval: int = 1) -> None:
|
|
101
|
+
self.filepath = filepath
|
|
102
|
+
self.flush_interval = max(1, flush_interval)
|
|
103
|
+
|
|
104
|
+
self._filehandle: io.TextIOWrapper | None = None
|
|
105
|
+
self._writer: CSVWriter | None = None
|
|
106
|
+
self._pending: int = 0
|
|
107
|
+
|
|
108
|
+
# ------------------------------------------------------------------
|
|
109
|
+
# Public API
|
|
110
|
+
# ------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
def register(self, environment: "Environment") -> None:
|
|
113
|
+
"""Attach this logger to *environment*'s event hooks.
|
|
114
|
+
|
|
115
|
+
Must be called once, typically inside an ``@events.init`` listener.
|
|
116
|
+
"""
|
|
117
|
+
environment.events.request.add_listener(self._on_request)
|
|
118
|
+
environment.events.quitting.add_listener(self._on_quitting)
|
|
119
|
+
self._open()
|
|
120
|
+
logger.debug("CsvRequestLogger: writing per-request log to %s", self.filepath)
|
|
121
|
+
|
|
122
|
+
def close(self) -> None:
|
|
123
|
+
"""Flush and close the underlying file handle.
|
|
124
|
+
|
|
125
|
+
Safe to call multiple times.
|
|
126
|
+
"""
|
|
127
|
+
if self._filehandle is not None and not self._filehandle.closed:
|
|
128
|
+
self._filehandle.flush()
|
|
129
|
+
self._filehandle.close()
|
|
130
|
+
logger.debug("CsvRequestLogger: closed %s", self.filepath)
|
|
131
|
+
self._filehandle = None
|
|
132
|
+
self._writer = None
|
|
133
|
+
self._pending = 0
|
|
134
|
+
|
|
135
|
+
# ------------------------------------------------------------------
|
|
136
|
+
# Internal helpers
|
|
137
|
+
# ------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
def _open(self) -> None:
|
|
140
|
+
"""Open (or re-open) the CSV file and write the header row."""
|
|
141
|
+
self.close()
|
|
142
|
+
self._filehandle = open(self.filepath, "w", newline="", encoding="utf-8")
|
|
143
|
+
self._writer = csv.writer(self._filehandle)
|
|
144
|
+
self._writer.writerow(CSV_COLUMNS)
|
|
145
|
+
self._filehandle.flush()
|
|
146
|
+
|
|
147
|
+
def _on_request(
|
|
148
|
+
self,
|
|
149
|
+
*,
|
|
150
|
+
request_type: str,
|
|
151
|
+
name: str,
|
|
152
|
+
response_time: float,
|
|
153
|
+
response_length: int,
|
|
154
|
+
exception: Any = None,
|
|
155
|
+
response: Any = None,
|
|
156
|
+
start_time: float | None = None,
|
|
157
|
+
context: Any = None,
|
|
158
|
+
**kwargs: Any,
|
|
159
|
+
) -> None:
|
|
160
|
+
"""Event handler — called by Locust for every completed request."""
|
|
161
|
+
if self._writer is None:
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
ts = start_time if start_time is not None else time.time()
|
|
165
|
+
status_code = _status_code(response, exception)
|
|
166
|
+
exc_str = str(exception) if exception is not None else ""
|
|
167
|
+
|
|
168
|
+
self._writer.writerow(
|
|
169
|
+
[
|
|
170
|
+
round(ts, 6),
|
|
171
|
+
request_type,
|
|
172
|
+
name,
|
|
173
|
+
round(response_time, 2),
|
|
174
|
+
response_length,
|
|
175
|
+
status_code,
|
|
176
|
+
exc_str,
|
|
177
|
+
]
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
self._pending += 1
|
|
181
|
+
if self._pending >= self.flush_interval:
|
|
182
|
+
self._filehandle.flush() # type: ignore[union-attr]
|
|
183
|
+
self._pending = 0
|
|
184
|
+
|
|
185
|
+
def _on_quitting(self, **kwargs: Any) -> None:
|
|
186
|
+
"""Flush and close when Locust shuts down."""
|
|
187
|
+
self.close()
|
|
@@ -33,7 +33,7 @@ class OpenAIClient(OpenAI):
|
|
|
33
33
|
exception = e
|
|
34
34
|
request_event.fire(
|
|
35
35
|
request_type=response.request.method,
|
|
36
|
-
name=self.request_name
|
|
36
|
+
name=self.request_name or response.url.path,
|
|
37
37
|
context={},
|
|
38
38
|
response=response,
|
|
39
39
|
exception=exception,
|
|
@@ -587,18 +587,26 @@ See https://github.com/locustio/locust/wiki/Installation#increasing-maximum-numb
|
|
|
587
587
|
input_listener_greenlet = gevent.spawn(
|
|
588
588
|
input_listener(
|
|
589
589
|
{
|
|
590
|
-
"w": lambda:
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
590
|
+
"w": lambda: (
|
|
591
|
+
runner.start(runner.user_count + 1, 100)
|
|
592
|
+
if runner.state != "spawning"
|
|
593
|
+
else logging.warning("Already spawning users, can't spawn more right now")
|
|
594
|
+
),
|
|
595
|
+
"W": lambda: (
|
|
596
|
+
runner.start(runner.user_count + 10, 100)
|
|
597
|
+
if runner.state != "spawning"
|
|
598
|
+
else logging.warning("Already spawning users, can't spawn more right now")
|
|
599
|
+
),
|
|
600
|
+
"s": lambda: (
|
|
601
|
+
runner.start(max(0, runner.user_count - 1), 100)
|
|
602
|
+
if runner.state != "spawning"
|
|
603
|
+
else logging.warning("Spawning users, can't stop right now")
|
|
604
|
+
),
|
|
605
|
+
"S": lambda: (
|
|
606
|
+
runner.start(max(0, runner.user_count - 10), 100)
|
|
607
|
+
if runner.state != "spawning"
|
|
608
|
+
else logging.warning("Spawning users, can't stop right now")
|
|
609
|
+
),
|
|
602
610
|
"\r": lambda: webbrowser.open_new_tab(url),
|
|
603
611
|
"\n": lambda: webbrowser.open_new_tab(url),
|
|
604
612
|
},
|
|
@@ -35,7 +35,7 @@ def _calc_distribution(user_classes, num_users):
|
|
|
35
35
|
user_classes_count = {}
|
|
36
36
|
|
|
37
37
|
for u in user_classes:
|
|
38
|
-
count = u.fixed_count
|
|
38
|
+
count = u.fixed_count or (u.weight / total_weight) * weighted_count
|
|
39
39
|
user_classes_count[u.__name__] = round(count)
|
|
40
40
|
|
|
41
41
|
return user_classes_count
|
|
@@ -726,7 +726,7 @@ class WebUI:
|
|
|
726
726
|
"is_distributed": is_distributed,
|
|
727
727
|
"user_count": self.environment.runner.user_count,
|
|
728
728
|
"version": version,
|
|
729
|
-
"host": host
|
|
729
|
+
"host": host or "",
|
|
730
730
|
"history": request_stats.history if request_stats.num_requests > 0 else [],
|
|
731
731
|
"override_host_warning": override_host_warning,
|
|
732
732
|
"missing_host_warning": missing_host_warning,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|