Ядро
Встроенные действия
Действия (actions) — это единицы, из которых состоят steps нативного теста.
Каждый шаг указывает действие в use, передаёт параметры в with и может
проверять результат в check.
Полные идентификаторы содержат пространство имён и версию (std/http@v1);
короткие псевдонимы (http, graphql, tcp, udp, pubsub, llm, ws,
ws-connect, ws-send, ws-recv, ws-ping,
ws-close, grpc, grpc-connect, grpc-call, grpc-stream-open,
grpc-stream-send, grpc-stream-recv, grpc-stream-close, db-connect,
db-query, db-tx-begin, db-tx-commit, db-tx-rollback, db-close,
check, random, sleep,
log, file-read, read-json, pick, file-write, child_process, kill_process,
set_shared_variable, get_shared_variable) разрешаются в те же
реализации.
Ещё два пространства имён находятся за пределами встроенных: действия pro/* —
проприетарные модули, регистрируемые сборкой агента, а lib/<alias>/<step>
(RFC 009) вызывает пользовательский шаг, экспортированный WASM-библиотекой,
объявленной в libraries: — компонент получает интерполированные параметры
with: и возвращает тот же формат success/output/logs/metrics, что и
встроенные. См. Библиотеки.
std/http@v1
Выполняет один HTTP-запрос за итерацию. Время выполнения попадает в метрики прогона.
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
url | string | обязателен | Абсолютный URL |
method | string | GET | Любой HTTP-метод, включая расширенные методы вроде QUERY (безопасный метод с телом, draft-ietf-httpbis-safe-method-w-body) |
headers | object | — | { "Name": "Value" }, значения — только строки |
body | string | object | — | Строка → text/plain; объект → сериализованный JSON с application/json. Взаимоисключающий с multipart |
multipart | array | — | Отправка multipart/form-data — см. Multipart-загрузки. Взаимоисключающий с body |
timeout | integer (мс) | 10000 | Таймаут на запрос |
insecure | boolean | false | Пропустить проверку TLS-сертификата — для целей с самоподписанными сертификатами, например perfscale serve --tls. Никогда не используйте против хостов, которые вы не контролируете |
pool | string | per-vu | per-vu привязывает шаг к шарду HTTP-клиента виртуального пользователя; shared переводит всех VU на один глобальный для процесса клиент; dedicated выдаёт каждому VU собственный клиент, живущий до выхода VU — см. Выделенные соединения на VU |
source_ips | string | array | — | Локальные исходные адреса для исходящих соединений — см. Исходные IP. VU i привязывается к ips[(i-1) % n] |
Результат (доступен через outputs / __last__):
{
"status": 200,
"body": "...",
"duration_ms": 42.37,
"headers": { "content-type": "application/json", "x-request-id": "abc-123" }
}
Имена заголовков приводятся к нижнему регистру; повторяющиеся заголовки
объединяются через ", ". Используйте их в последующих шагах через
${{ resp.headers.x-request-id }}.
Ответы с текстовым типом содержимого (text/*, application/json,
application/*+json, application/*+xml, …) приходят в body, как и раньше.
Бинарные данные (например, application/octet-stream) вместо этого возвращают
пустое body плюс поле body_base64 — именно так полученный protobuf
FileDescriptorSet передаётся в gRPC-шаг:
descriptor_set: "${{ fetch.body_base64 }}". Отсутствующий тип содержимого
определяется эвристически: корректный UTF-8 обрабатывается как текст, всё
остальное — как бинарные данные.
Сжатые сервером ответы (Content-Encoding: gzip) прозрачно декодируются до
всего этого, поэтому body и пути вида ${{ feed.body.posts }} видят
исходные данные — чтобы включить сжатие, явно передайте
Accept-Encoding: gzip в headers. Декодирование применяется только к
ответам: тела запросов никогда не сжимаются.
Статусы ≥ 400, транспортные ошибки и таймауты считаются неудачными запросами в
http_req_failed. Таймаут логируется отдельно (→ TIMEOUT after ...ms).
Выделенные соединения на VU
pool: dedicated выдаёт каждому VU собственный HTTP-клиент, созданный при
первом запросе VU и живущий до его выхода — время обдумывания
(std/sleep@v1) между шагами соединение не закрывает. Это режим для
сценариев «N виртуальных пользователей = N открытых keep-alive соединений»:
шардовые пулы по умолчанию держат не более 64 простаивающих соединений на
хост на шард, поэтому после ~шарды × 64 VU простаивающее соединение VU
вытесняется между запросами, и каждый запрос переподключается (connect churn,
накопление TIME_WAIT). Режим dedicated снимает этот предел.
with:
url: http://target:8080/api
pool: dedicated
Семантика и стоимость:
- Клиент каждого VU хранится в реестре прогона с ключом
(vu_id, insecure, source_ip)и удаляется при выходе VU (фиксированные, staged ramp-down и arrival-rate воркеры одинаково) — соединения закрываются при выходе VU, а не в конце итерации и не при выходе процесса. - Совместим с
source_ips: клиент VU привязан к его закреплённому исходному адресу, обе привязки действуют одновременно. - Семантика
http_req_durationне меняется; учтите, что с выделенным клиентом TCP/TLS connect происходит один раз на VU — первый запрос VU включает его, последующие нет. - Соединения, простаивающие дольше 60 с, пересоздаются, как и в других режимах пула — цель, рвущая простаивающие сокеты раньше, проявится как переподключения.
- Память: клиент с одним простаивающим сокетом стоит заметно меньше 100 КБ;
5 000 VU в режиме dedicated добавляют суммарно несколько сотен МБ RSS
процесса. Первый запрос прогона логирует
pool: dedicated — one HTTP client per VU. pool: dedicated— режимstd/http@v1;std/graphql@v1принимает толькоper-vu/shared.
Исходные IP
source_ips привязывает исходящие соединения к локальным исходным адресам —
это множитель бюджета ~64k одновременных соединений на endpoint: пространство
эфемерных портов выделяется на кортеж (исходный IP, IP назначения, порт
назначения), поэтому одна машина с одним адресом упирается в предел ниже 64k
сокетов на один endpoint. Элементы — отдельные IP или CIDR-префиксы, IPv4 и
IPv6:
with:
url: http://target:8080/api
source_ips: ["10.0.0.2", "10.0.0.16/28", "fd00::/120"]
CIDR раскрывается во все адреса префикса (включая network/broadcast — адрес,
который нельзя привязать, явно падает на connect); суммарный список ограничен
4096 адресами, при превышении lint и валидация прогона падают с ошибкой
«сузьте префикс». Виртуальный пользователь i привязывается к
ips[(i-1) % n] — стабильно между итерациями, поэтому каждый VU держит один
тёплый keep-alive пул в своём клиенте (шард, исходный IP). Клиенты создаются
лениво — большой список не выделяет заранее тысячи пулов: прогон создаёт не
больше source-IP-клиентов, чем реально использованных пар (шард, адрес).
Адреса должны существовать на интерфейсах генератора нагрузки (например,
дополнительные loopback-алиасы: sudo ifconfig lo0 alias 127.0.0.2 up на
macOS; Linux по умолчанию маршрутизирует весь 127/8). Первый запрос прогона
логирует, сколько исходных адресов активно. source_ips не поддерживает
интерполяцию ${{ … }} — список валидируется на lint и кешируется. В режиме
pool: dedicated собственный клиент VU привязывается к его закреплённому
адресу — ключ становится (vu_id, адрес) вместо (шард, адрес).
Эффект измеряет набор sourceips в bench (см.
Бенчмарки): VU держат постоянные
keep-alive соединения при суженном диапазоне эфемерных портов, с одного IP
источника против нескольких loopback-адресов — один IP упирается в бюджет
портов, и остальные VU не могут подключиться (EADDRNOTAVAIL), несколько IP
держат всех.
Multipart-загрузки
multipart отправляет multipart/form-data (загрузка файлов, эндпоинты в
стиле HTML-форм). Каждый элемент массива — одна часть:
| Поле части | Обязательно | Описание |
|---|---|---|
name | да | Имя поля формы |
value | одно из value/file | Содержимое текстового поля |
file | одно из value/file | Путь к файлу на диске (относительно рабочего каталога) |
filename | нет | Имя файла, отправляемое серверу; по умолчанию — базовое имя файла |
content_type | нет | MIME-тип части (например, application/octet-stream) |
steps:
- name: upload report
use: std/http@v1
with:
method: POST
url: "https://api.example.com/upload"
multipart:
- name: file
file: ./fixtures/report.csv
content_type: text/csv
- name: description
value: "uploaded by ${{ __last__.status }} check run"
check:
status: 201
Примечания:
- Заголовок
Content-Type: multipart/form-data; boundary=…выставляется автоматически — не добавляйте его вheaders. - Плейсхолдеры
${{ ... }}работают в значениях частей и путях, как и везде вwith:. - Файлы читаются с диска на каждой итерации: страничный кэш ОС делает повторные чтения дешёвыми, а файл, изменённый между прогонами, подхватывается. При высоком RPS предпочитайте небольшие файлы-фикстуры.
- Отсутствующий или недоступный для чтения файл приводит к ошибке шага до отправки любого запроса.
std/graphql@v1
Отправляет одну GraphQL-операцию (запрос или мутацию) за итерацию по HTTP, с опциональной валидацией схемы по схеме, полученной интроспекцией эндпоинта. Полное пошаговое руководство — в Нагрузочное тестирование GraphQL.
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
url | string | обязателен | GraphQL-эндпоинт, например https://api.example.com/graphql |
query | string | одно из query/query_file | Встроенный GraphQL-документ |
query_file | string (путь) | одно из query/query_file | Прочитать документ из файла .graphql. Доступ к файловой системе: требует allow_file_actions, учитывает fs_root |
variables | object | — | Переменные запроса. Применяется интерполяция ${{ … }}, а токены-генераторы с одинарными фигурными скобками ${…} (${uuid}, ${rand}, ${now}) раскрываются при каждом выполнении |
operation | string | — | operationName для выполнения. Обязателен, когда документ содержит несколько операций; также служит тегом метрик по операциям |
method | string | POST | POST (тело JSON) или GET (параметры в URL-запросе — для чтений, кэшируемых CDN) |
headers | object | — | { "Name": "Value" }, значения — только строки |
timeout | integer (мс) | 10000 | Таймаут на запрос |
insecure | boolean | false | Пропустить проверку TLS-сертификата |
introspection | boolean | true | Один раз за прогон получить схему эндпоинта и валидировать каждый запрос перед отправкой. Ошибка получения схемы деградирует до прогона без валидации (логируется один раз) |
schema_file | string (путь) | — | Валидировать по локальному SDL-файлу вместо интроспекции (запасной вариант для эндпоинтов с отключённой интроспекцией). Те же правила файловой системы, что и для query_file |
pool | string | per-vu | per-vu привязывает шаг к шарду HTTP-клиента виртуального пользователя (поведение std/http@v1); shared переводит всех VU на один глобальный для процесса клиент |
Результат (доступен через outputs / __last__):
{
"status": 200,
"data": { "viewer": { "id": "u-1" } },
"errors": [ { "message": "widgets timed out" } ],
"body": "{\"data\":…}",
"duration_ms": 12.31,
"headers": { "content-type": "application/json" }
}
data и errors — декодированная полезная нагрузка GraphQL (errors
присутствует, только когда сервер её прислал); body хранит сырой текст для
проверок body_contains. Извлечение читает структурированную нагрузку:
${{ create.data.createWidget.id }}.
Семантика ответов соответствует реальности GraphQL-over-HTTP: статусы ≥ 400
приводят к ошибке шага; 200 с errors и без data — тоже ошибка; частичные
data плюс errors считаются успехом (сервер разрешил то, что смог), а ошибки
учитываются в метрике graphql_errors.
Синтаксическая ошибка, ошибка валидации схемы или нечитаемый query_file
приводят к ошибке шага до отправки запроса — цель никогда не увидит
некорректный запрос.
steps:
- name: fetch viewer
use: std/graphql@v1
with:
url: https://api.example.com/graphql
query: |
query GetViewer($id: ID!) {
viewer(id: $id) { id name }
}
variables: { "id": "${{ vars.viewer_id }}" }
check:
status: 200
outputs: viewer
- name: rename widget
use: std/graphql@v1
with:
url: https://api.example.com/graphql
query_file: ./queries/rename-widget.graphql
variables: { "id": "${{ viewer.data.viewer.id }}", "name": "w-${seq}" }
Метрики: graphql_req_duration (гистограмма с производной частотой
graphql_req_failed), graphql_errors (счётчик) и
graphql_op_<operationName>_duration, когда операция именована, — плюс
стандартные агрегаты http_req_*, которые пополняет каждый HTTP-шаг.
std/tcp@v1
Открывает «сырое» TCP-соединение, опционально отправляет данные, опционально читает ответ и измеряет время всего обмена. Без протокольного фрейминга — строительный блок для проверки произвольных строковых/бинарных сервисов (Redis, SMTP, собственные шлюзы).
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
host | string | обязателен* | Целевой хост (вместе с port) |
port | integer | обязателен* | Целевой порт |
address | string | — | Сокращение host:port, вместо host+port |
send | string | — | Текстовые данные для отправки после подключения |
send_base64 | string | — | Данные в Base64 для бинарных протоколов. Взаимоисключающий с send |
read | boolean | true, если задан expect, иначе false | Прочитать один фрагмент ответа |
read_bytes | integer | 65536 | Ограничение на число прочитанных байт |
expect | string | — | Подстрока, которую должен содержать ответ (подразумевает read) |
timeout | integer (мс) | 10000 | Таймаут на подключение + обмен |
* Укажите либо address, либо host + port.
Результат (доступен через outputs / __last__):
{ "connected": true, "sent": 4, "received": 4, "response": "pong", "duration_ms": 0.63 }
response — UTF-8 с потерями (lossy), поэтому для бинарных сервисов проверяйте
received, а не строку. Ошибки подключения, таймауты и несовпадение expect
помечают шаг неудачным и учитываются в http_req_failed; время попадает в
http_req_duration наравне с HTTP и UDP, так что перцентили сопоставимы между
транспортами.
std/udp@v1
Отправляет UDP-датаграмму цели и опционально ждёт ответ. Задержка «туда и
обратно» измеряется от отправки до датаграммы-ответа (или только сама отправка,
когда ответ не ожидается). Те же поля with, что у std/tcp@v1, за
исключением того, что send (или send_base64) обязателен.
UDP не устанавливает соединения: «успешная» отправка означает лишь, что
датаграмма покинула хост. Задайте read (или expect), чтобы действительно
проверить ответ.
Результат:
{ "sent": 4, "received": 4, "response": "pong", "duration_ms": 0.21 }
std/pubsub@v1
Пошаговое описание на уровне концепций (драйверы, шаблоны producer/consumer, метрики, пороги): руководство по Pub/Sub.
Публикует сообщения в subject и/или ждёт сообщения из него — разовый pub/sub обмен, измеряющий задержку публикации и сквозную (end-to-end) задержку. Транспорт подключается через драйвер:
| Драйвер | Транспорт |
|---|---|
memory (по умолчанию) | Внутрипроцессная шина рассылки — брокер не нужен. Шина глобальна для процесса: один канал на subject, общий для всех VU, поэтому публикация одного VU доставляется каждому VU, подписанному на этот subject (fan-out между VU — это особенность) |
nats | Настоящий сервер NATS (core NATS, без JetStream) через async-nats |
Проприетарные драйверы (Kafka, Redis, …) регистрируются через ту же точку
подключения в pro-сборке; неизвестное значение driver приводит к ошибке шага
со списком зарегистрированных драйверов — так вы узнаёте, что в вашей сборке их
нет.
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
driver | string | memory | memory, nats или драйвер, зарегистрированный downstream |
subject | string | обязателен | Subject NATS / имя топика в памяти |
url | string | — | URL брокера; обязателен для nats, игнорируется memory |
publish | string | array | — | Одно сообщение или список; не-строки сериализуются в текст JSON |
subscribe | object | — | { count, until_contains, timeout_ms } — ждать count сообщений (по умолчанию 1), каждое из которых содержит until_contains (опционально), в течение timeout_ms (по умолчанию 5000) |
options | object | — | Специфичная для драйвера настройка, передаваемая как есть — встроенные драйверы её игнорируют; pro-драйверы (Kafka, Redis, MQTT) используют для QoS, consumer-групп, аутентификации и т.д. |
scope | string | exchange | Время жизни подключения: exchange открывает свежее подключение к брокеру на каждый обмен (стоимость подключения включена, см. pubsub_connect_ms); vu держит одно подключение на VU и переиспользует его между обменами. Только брокерные драйверы; memory игнорирует |
flush | string | message | Режим ack публикации: message делает flush и ждёт ack брокера после публикации (pubsub_publish_ms = round trip); none — fire-and-forget (pubsub_publish_ms = постановка в очередь). Сочетается со scope: vu; при scope: exchange перед разрывом подключения всё равно выполняется один flush вне замеров |
Требуется хотя бы один из publish / subscribe. Когда заданы оба, подписка
устанавливается первой, поэтому roundtrip на одном и том же subject видит
свои же сообщения. Только публикация — чистый шаг-производитель (успех = все
публикации приняты); только подписка — чистый шаг-потребитель. Шаг завершается
ошибкой при сбое подключения, ошибке публикации или таймауте подписки — в
ошибке указано, сколько сообщений из count пришло и сколько отклонил
сопоставитель until_contains.
Результат:
{ "driver": "memory", "subject": "orders.created", "published": 2,
"received": 1, "duration_ms": 3.21,
"body": "<joined received payloads>",
"metrics": { "pubsub_msgs_published": 2, "pubsub_msgs_received": 1,
"pubsub_connect_ms": [2.1], "pubsub_publish_ms": [0.8],
"pubsub_errors": 0, "pubsub_e2e_ms": [1.2] } }
received / body и метрики pubsub_msgs_received / pubsub_e2e_ms
появляются, только когда задан subscribe. pubsub_e2e_ms содержит по одному
замеру на сопоставленное сообщение: начало фазы публикации → получено.
pubsub_connect_ms содержит один замер на обмен, открывший подключение к
брокеру — каждый обмен при scope: exchange, только первый на VU при
scope: vu. pubsub_publish_ms содержит по замеру на опубликованное сообщение —
латентность вызова publish транспорта (у nats с flush: message замер
последнего сообщения включает flush пачки и ожидание ack). pubsub_errors
эмитируется на каждый обмен и складывает синхронный признак ошибки с
асинхронными ошибками драйвера (ошибки доставки Kafka, таймауты ack MQTT,
ошибочные ответы Redis pipeline), поэтому значение может быть больше 1 за
обмен; асинхронные ошибки не роняют шаг. При успехе значение 0 — конвенция
db_errors. ${…}-токены
генераторов раскрываются на каждое выполнение в subject, publish и
subscribe.until_contains; одно выполнение — одно сообщение, поэтому
нагрузка и матчер делят одно значение ${seq} / ${vu}. body —
это сопоставленные нагрузки, объединённые переводами строк, поэтому работает
check: { body_contains: … }.
Примеры. Roundtrip в памяти (без брокера — обе стороны в одном шаге):
steps:
- name: order events roundtrip
use: std/pubsub@v1
with:
subject: orders.created
publish: ["order-1 created", "order-2 created"]
subscribe: { count: 2, until_contains: created, timeout_ms: 2000 }
check:
body_contains: order-2
Производитель NATS, только публикация:
steps:
- name: produce order events
use: std/pubsub@v1
with:
driver: nats
url: nats://127.0.0.1:4222
subject: orders.created
publish:
- '{"id":"ord-1","total":42.50}'
- '{"id":"ord-2","total":17.00}'
Потребитель NATS с сопоставителем, таймаутом и блоком check:
steps:
- name: await shipment event
use: std/pubsub@v1
with:
driver: nats
url: nats://127.0.0.1:4222
subject: orders.shipped
subscribe: { count: 1, until_contains: '"id":"ord-1"', timeout_ms: 3000 }
check:
body_contains: ord-1
outputs: shipment
std/set_shared_variable@v1
Пошаговое описание на уровне концепций (объявление, шаблоны, драйверы): руководство по разделяемым переменным.
Атомарно изменяет разделяемую переменную — единое хранилище «ключ/значение» в
рамках прогона, общее для всех VU процесса, для координации между VU
(разделяемые счётчики, очереди producer/consumer, барьеры), которую не могут
выразить неизменяемые variables отдельного VU. Имена должны быть
объявлены в блоке
shared_variables: конфигурационного
файла; шаг, ссылающийся на необъявленное имя, или op, несовместимая с
объявленным типом (increment над списком), завершится ошибкой валидации
конфигурации до старта любого VU.
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
driver | string | memory | memory (глобальное для процесса хранилище) или драйвер, зарегистрированный downstream (pro-сборки: Redis, …). Неизвестное значение приводит к ошибке шага со списком зарегистрированных драйверов |
name | string | обязателен | Имя объявленной разделяемой переменной |
op | string | set | set (любой тип), increment (число — value это приращение), append (список — value это элемент) |
value | any | обязателен | Новое значение / приращение / добавляемый элемент |
increment возвращает новое значение, append — новую длину списка, set —
сохранённое значение. Каждая операция — одно атомарное захватывание: пары
«прочитал-изменил-записал», которая могла бы породить гонку, нет.
Результат:
{ "driver": "memory", "name": "approved_count", "op": "increment",
"value": 3, "duration_ms": 0.03 }
std/get_shared_variable@v1
Атомарно читает разделяемую переменную, опционально блокируясь до выполнения условия.
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
driver | string | memory | memory или драйвер, зарегистрированный downstream |
name | string | обязателен | Имя объявленной разделяемой переменной |
op | string | get | get (любой тип) или pop (список: удалить и вернуть первый элемент FIFO; null, когда пусто) |
wait_for | object | — | { exists | equals: <json> | length_gte: <int>, timeout_ms } — блокироваться, пока условие не выполнится; ровно один ключ условия, timeout_ms по умолчанию 5000 |
extract | object | — | { key: dotted-path } — извлечь поля из значения; тот же синтаксис $.a.b[0], что в extract у std/llm@v1 |
wait_for опрашивает условие с небольшой асинхронной паузой (без активного
ожидания). По таймауту шаг завершается ошибкой (строка [err], тот же
контракт, что у subscribe в std/pubsub@v1) и сообщает последнее наблюдавшееся
значение — медленный производитель это провал проверки, а не молчаливый null.
Результат:
{ "driver": "memory", "name": "pending_orders", "op": "pop",
"value": {"id": "ord-1"}, "duration_ms": 0.05 }
С extract каждый извлечённый ключ добавляется на верхний уровень вместо
value (неразрешимые пути превращаются в null). Ожидание wait_for
добавляет в результат waited_ms и замер метрики shared_variable_wait_ms.
Примеры. Очередь producer/consumer поверх разделяемого списка:
# config.yaml
shared_variables:
pending_orders: [] # type inferred: list
# producer step
- name: enqueue order
use: std/set_shared_variable@v1
with: { name: pending_orders, op: append, value: { id: "ord-${seq}" } }
# consumer step — block until something is there, then take it
- name: dequeue order
use: std/get_shared_variable@v1
with:
name: pending_orders
op: pop
wait_for: { length_gte: 1, timeout_ms: 10000 }
extract: { order_id: $.id }
Разделяемый счётчик:
# config.yaml
shared_variables:
approved_count: 0 # type inferred: number
- name: count approval
use: std/set_shared_variable@v1
with: { name: approved_count, op: increment, value: 1 }
std/llm@v1
Пошаговое описание на уровне концепций (эндпоинты, примеры, метрики, пороги): руководство по LLM.
Отправляет один запрос LLM chat-completion за итерацию, по умолчанию в потоковом режиме, измеряя время до первого токена (TTFT), пропускную способность генерации и расход токенов. Три проводных формата эндпоинтов:
| Эндпоинт | Проводной формат |
|---|---|
openai (по умолчанию) | OpenAI chat completions — а также OpenAI-совместимые серверы (Ollama, vLLM, LM Studio, …). При стриминге отправляет stream_options: { include_usage: true } |
anthropic | Anthropic messages API; отправляет anthropic-version: 2023-06-01, ключ — в x-api-key |
generic | Тело — объект params как есть; поля ответа извлекаются через extract |
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
endpoint | string | openai | openai, anthropic или generic |
url | string | обязателен | URL эндпоинта completion |
model | string | — | Имя модели; обязательно для openai / anthropic |
prompt | string | одно из prompt/messages | Сахар для одного сообщения user (взаимоисключающий с messages) |
messages | array | одно из prompt/messages | [{ role, content }] — сообщения чата |
max_tokens | integer | 256 | Ограничение на токены ответа |
stream | boolean | true (openai/anthropic), false (generic) | Стримить ответ как SSE |
api_key | string | — | Authorization: Bearer (openai/generic) или x-api-key (anthropic) |
headers | object | — | Дополнительные заголовки запроса, строковые значения |
params | object | — | Сквозные поля тела (temperature, …); для generic это и ЕСТЬ тело |
extract | object | — | Только generic: { text, prompt_tokens, completion_tokens } — каждое значение это точечный путь ($.usage.completion_tokens, $.choices[0].text) или регулярное выражение с одной группой захвата |
timeout_ms | integer (мс) | 120000 | Таймаут на весь запрос (подключение → последний фрагмент) |
Статус не 2xx приводит к ошибке шага с указанием статуса и первых ~500 символов тела ошибки.
Результат:
{ "endpoint": "openai", "model": "gpt-4o-mini", "status": 200,
"ttft_ms": 120.31, "duration_ms": 850.02,
"prompt_tokens": 12, "completion_tokens": 96,
"tokens_per_sec": 131.5, "chunks": 34, "text": "…",
"metrics": { "llm_ttft_ms": [120.31], "llm_tokens_per_sec": [131.5],
"llm_prompt_tokens": 12, "llm_completion_tokens": 96,
"llm_chunks": 34 } }
ttft_ms появляется только для потоковых ответов (TTFT = начало запроса →
первый фрагмент контента); tokens_per_sec делит токены ответа на время
генерации после первого токена, а если TTFT не был измерен — на время всего
запроса. text обрезается до ~4 КиБ. llm_ttft_ms / llm_tokens_per_sec —
тренды (доступны перцентили в std/thresholds@v1); llm_prompt_tokens /
llm_completion_tokens / llm_chunks — счётчики.
Примеры. Локальный Ollama (OpenAI-совместимый, без ключа):
steps:
- name: llama completion
use: std/llm@v1
with:
url: http://127.0.0.1:11434/v1/chat/completions
model: llama3.1
prompt: "Summarize the CAP theorem in two sentences."
max_tokens: 128
check:
status: 200
Стриминг Anthropic с ключом из окружения:
steps:
- name: claude completion
use: std/llm@v1
with:
endpoint: anthropic
url: https://api.anthropic.com/v1/messages
model: claude-sonnet-4-5
api_key: ${{ env.ANTHROPIC_API_KEY }}
prompt: "Explain backpressure in one paragraph."
Эндпоинт generic с извлечением полей:
steps:
- name: tgi generate
use: std/llm@v1
with:
endpoint: generic
url: http://127.0.0.1:8080/generate
params:
inputs: "Tell me a joke."
parameters: { max_new_tokens: 64 }
extract:
text: "$.generated_text"
completion_tokens: '"generated_tokens": (\d+)'
WebSocket: std/ws@v1 и семейство std/ws-*@v1
Пошаговое описание на уровне концепций (стили, динамические сообщения, проверки, метрики): руководство по WebSocket.
Два способа нагрузочного тестирования WebSocket-эндпоинта:
- Разовая сессия —
std/ws@v1открывает соединение, обменивается сообщениями и закрывает его, всё в одном шаге (как FIX-сессия). Самый простой вариант; вся сессия измеряется как один замерhttp_req_duration. - Живое соединение —
std/ws-connect@v1открывает соединение, которое остаётся активным между шагами внутри итерации;ws-send/ws-recv/ws-ping/ws-closeобращаются к нему по id, который вернул шаг connect. Используйте это, чтобы чередовать WS-трафик с другими шагами (например, подписаться по WS, инициировать событие по HTTP, проверить, что push пришёл).
Соединение, оставленное открытым в конце итерации, разрывается аварийно (без
Close-рукопожатия) — вызывайте std/ws-close@v1 для корректного завершения.
Живые соединения никогда не переживают переход в следующую итерацию, а
ws-connect внутри before: setup бесполезен (контекст setup исчезает до
старта VU).
Профиль соединения
Все шаги, умеющие подключаться (std/ws@v1, std/ws-connect@v1), принимают
одни и те же параметры цели — инлайн или собранные в объект-профиль под
connection (инлайн-поля побеждают). Профиль, определённый в шаге before:
конфига, передаётся как connection: "${{ config.<name> }}".
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
url | string | обязателен | Цель ws:// или wss:// |
headers | object | — | Дополнительные заголовки рукопожатия (токены авторизации и т.п.) |
subprotocols | array | string | — | Предлагаемые значения Sec-WebSocket-Protocol (например, graphql-ws) |
skipTLSVerify | boolean | false | Принимать любой сертификат сервера (только staging с самоподписанными сертификатами) |
connection | object | string | — | Профиль, задающий значения по умолчанию для любого поля выше |
timeout (integer, мс, по умолчанию 10000) принимается обоими шагами инлайн,
но не является частью профиля — timeout внутри connection
игнорируется. Для std/ws-connect@v1 он ограничивает рукопожатие; для
std/ws@v1 — всю сессию.
std/ws-connect@v1
Открывает живое соединение. Результат:
{ "id": "ws-1", "connected": true, "subprotocol": "graphql-ws", "duration_ms": 3.1 }
subprotocol — согласованный с сервером протокол или null, когда сервер не
выбрал ни одного. Рукопожатие попадает в http_req_duration; неудачное
рукопожатие учитывается в http_req_failed, а результат выглядит как
{ "connected": false, "error": "…", "duration_ms": … }.
Сохраните результат (outputs: feed) и передавайте id: "${{ feed.id }}"
остальным шагам ws-*. Идентификаторы выдаются на каждый VU (ws-1, ws-2,
…) и действительны только внутри текущей итерации этого VU.
std/ws-send@v1
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
id | string | обязателен | Идентификатор соединения от std/ws-connect@v1 |
send | string | одно из send/send_base64 | Текстовая нагрузка; токены ${…} раскрываются при каждой отправке (см. ниже) |
send_base64 | string | одно из send/send_base64 | Бинарная нагрузка |
repeat | integer | 1 | Отправить N сообщений из одного шаблона |
interval_ms | integer | 0 | Пауза между повторными отправками |
timeout | integer (мс) | 10000 | На весь цикл отправки |
Результат: { "sent": N, "bytes": B, "duration_ms": …, "metrics": { "ws_msgs_sent": N } }.
Учитывается в частоте ws_msgs_sent. Транспортная ошибка приводит к ошибке
шага и разрывает соединение — последующие шаги по этому id получают ошибку
«unknown connection id». Ошибка параметров (например, заданы и send, и
send_base64) оставляет соединение пригодным к использованию.
Текстовые нагрузки могут содержать токены с одинарными фигурными скобками
${…}, раскрываемые заново при каждой отправке, — в отличие от ${{ … }}
движка, которые разрешаются один раз до запуска действия:
| Токен | Раскрывается в |
|---|---|
${seq} | Монотонный счётчик, уникальный для каждого сообщения (продолжает считать между отправками по одному соединению) |
${uuid} | Случайный 32-символьный hex-идентификатор |
${now} | UTC YYYYMMDD-HH:MM:SS.sss (формат FIX SendingTime) |
${now_ms} | Unix-миллисекунды |
${now_iso} | UTC RFC 3339 YYYY-MM-DDTHH:MM:SS.sssZ |
${rand(a,b)} | Случайное целое в [a,b] |
${randf(a,b[,dp])} | Случайное число с плавающей точкой, dp знаков после запятой (по умолчанию 2) |
${choice(x|y|z)} | Случайный выбор |
Неизвестные токены остаются как есть. Нагрузки send_base64 декодируются один
раз и отправляются без изменений — раскрытия токенов нет.
std/ws-recv@v1
Читает, пока не сработает правило остановки — если оно не сработало за
timeout, шаг завершается ошибкой:
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
id | string | обязателен | Идентификатор соединения |
until_contains | string | — | Остановиться, когда сообщение содержит эту подстроку (взаимоисключающий с until_json) |
until_json | object | — | Остановиться, когда сообщение совпадает с JSON-подмножеством (поля шаблона должны быть равны; лишние поля игнорируются) |
count | integer | 1 | Без правила until_*: остановиться после N сообщений с данными |
timeout | integer (мс) | 10000 | Дедлайн для правила остановки |
Результат:
{ "messages": ["…"], "body": "…", "count": 2, "matched": true, "duration_ms": 8.4,
"metrics": { "ws_msgs_received": 2, "ws_msg_rtt": [7.9] } }
matched показывает, было ли достигнуто правило остановки (в режиме count:
достигнуто ли количество). Текстовые фреймы приходят строками, бинарные —
строками base64; body — текстовая форма, объединённая переводами строк
(поэтому работает check: { body_contains: … }). Полученные сообщения
учитываются в частоте ws_msgs_received. Когда правило until_* совпало и
ранее по этому соединению был ws-send, время «отправка → совпадение»
записывается как замер гистограммы ws_msg_rtt — круговой путь сообщения на
уровне приложения.
Каждое прочитанное по пути сообщение остаётся в messages, каким бы ни был
итог. Если пир закрыл соединение или транспорт умер до срабатывания правила,
шаг завершается ошибкой, результат получает поле error, а соединение
разрывается; обычный таймаут тоже приводит к ошибке шага, но оставляет
соединение пригодным для последующих шагов. Ping/pong-фреймы, приходящие во
время чтения, игнорируются как транспортный шум.
Собственный duration_ms шага намеренно не попадает в
http_req_duration: то, как долго сервер решит ждать перед отправкой push, —
не задержка цели, и такие замеры отравили бы общие перцентили.
std/ws-ping@v1
Круговой путь ping→pong на уровне транспорта: { "pong": true, "duration_ms": 0.4 }.
Принимает id и timeout. RTT не агрегируется ни в одну гистограмму —
ограничивайте его через check: { duration_ms_lt: … }, когда нужно. Сообщения
с данными, приходящие во время ожидания pong, буферизуются для следующего
ws-recv. Отсутствие pong за timeout (или закрытое соединение) приводит к
ошибке шага и разрыву соединения.
std/ws-close@v1
Корректное Close-рукопожатие. Принимает id, code (по умолчанию 1000,
нормальное закрытие), reason (по умолчанию пусто) — оба отправляются в
Close-фрейме — и timeout. Результат: { "closed": true, "duration_ms": … },
возвращается, даже когда пир не подтвердил закрытие за timeout: сокет в любом
случае исчезает, а id освобождается.
std/ws@v1 — разовая сессия
Параметры профиля как выше, плюс messages — список, где каждый элемент это
строка (шаблон ${…} для отправки) или объект:
| Поле элемента | Описание |
|---|---|
send / send_base64 | Нагрузка, как в ws-send |
repeat / interval_ms | Развёртывание потока, как в ws-send |
until_contains / until_json | Ждать подходящий ответ перед следующим элементом; даёт замер ws_msg_rtt |
steps:
- name: subscribe and await first trade
use: std/ws@v1
with:
url: wss://stream.example.com/feed
messages:
- send: '{"op":"subscribe","channel":"trades","id":"sub-${seq}"}'
until_json: { type: trade }
check:
message_matches: { type: trade }
outputs: feed
Результат: { "connected": true, "sent": N, "received": M, "messages": […], "body": "…", "subprotocol": …, "duration_ms": …, "metrics": { "ws_msgs_sent": N, "ws_msgs_received": M, "ws_msg_rtt": […] } }.
Вся сессия — один замер http_req_duration; шаг завершается ошибкой при ошибках
рукопожатия/транспорта или любом элементе, чьё правило until_* не совпало
вовремя. Ошибка в середине сессии всё равно сообщает всё, что было передано до
этого момента, плюс поле error с именем проблемного элемента (message[i]: …);
ошибка рукопожатия даёт { "connected": false, "error": "…", "duration_ms": … }
и учитывается в http_req_failed.
Ограничения
Входящие протокольные ограничения берутся из значений по умолчанию
WebSocket-библиотеки: сообщения до 64 МиБ, отдельные фреймы до 16 МиБ — более
крупное входящее сообщение приводит к ошибке соединения (а значит, и читающего
его шага). Значения таймаутов, счётчики repeat и список messages встроенных
ограничений не имеют.
gRPC: std/grpc@v1 и семейство std/grpc-*@v1
Пошаговое описание на уровне концепций (источники схемы, стримы, проверки, метрики): руководство по gRPC.
Два способа нагрузочного тестирования gRPC-эндпоинта:
- Разовый вызов —
std/grpc@v1открывает канал, загружает схему, делает один унарный вызов и закрывает, всё в одном шаге. Самый простой вариант для эпизодических проверок. - Живой канал —
std/grpc-connect@v1открывает HTTP/2-канал, который остаётся активным между шагами внутри итерации;grpc-call(унарный) и семействоgrpc-stream-*обращаются к нему по id, который вернул шаг connect. Используйте это для любой серьёзной нагрузки: соединение и загрузка схемы оплачиваются один раз на итерацию, а не на вызов.
Вызовы динамические: никакой кодогенерации protobuf — схема приходит во
время выполнения, а запросы/ответы представлены в JSON (правила protobuf-JSON:
имена полей принимают и proto-имя, и его camelCase-вариант json_name; 64-битные
целые — строки; enum — имена).
Канал, оставленный открытым в конце итерации, разрывается — живые каналы и
стримы никогда не переживают переход в следующую итерацию, а grpc-connect
внутри before: setup бесполезен (контекст setup исчезает до старта VU).
Профиль канала
std/grpc@v1 и std/grpc-connect@v1 принимают одни и те же параметры цели —
инлайн или собранные в объект-профиль под connection (инлайн-поля побеждают).
Профиль, определённый в шаге before: конфига, передаётся как
connection: "${{ config.<name> }}".
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
url | string | обязателен | Цель grpc:// (открытый текст) или grpcs:// (TLS); хост без схемы означает grpcs:// |
metadata | object | — | Метаданные вызовов по умолчанию (токены авторизации и т.п.); metadata на вызов переопределяет их по ключу |
skipTLSVerify | boolean | false | Принимать любой сертификат сервера (только staging с самоподписанными сертификатами) |
descriptor_set | string | один источник схемы | Base64 сериализованного FileDescriptorSet (взаимоисключающий с reflection) |
reflection | boolean | один источник схемы | true: получить схему через сервис reflection сервера (v1) |
max_recv_size | integer (байты) | 16777216 | Ограничение входящего сообщения (16 МиБ) |
connection | object | string | — | Профиль, задающий значения по умолчанию для любого поля выше |
Требуется ровно один источник схемы. descriptor_set — это base64
FileDescriptorSet — создайте его через protoc --descriptor_set_out или
получите по HTTP и сошлитесь на ${{ fetch.body_base64 }}. С reflection: true
сервер должен включить gRPC reflection; полученный пул кэшируется по URL до
конца прогона, так что повторные подключения к одному серверу оплачивают один
круговой путь.
timeout (integer, мс, по умолчанию 10000) принимается обоими шагами инлайн,
но не является частью профиля. Для std/grpc-connect@v1 он ограничивает
подключение + загрузку схемы; для std/grpc@v1 — весь шаг (подключение → схема
→ вызов; заголовок grpc-timeout вызова получает оставшийся бюджет).
Методы именуются как "package.Service/Method". Опечатка приводит к ошибке с
подсказкой «did you mean», когда известный метод находится в расстоянии
редактирования 2.
Нагрузки
Запросы принимают payload (JSON → динамическое protobuf-сообщение) или
payload_base64 (base64 сериализованных байт protobuf) — взаимоисключающие.
Строковые листья payload могут содержать токены с одинарными фигурными
скобками ${…}, описанные в std/ws-send@v1, раскрываемые на
каждый вызов/отправку (${seq} продолжает считать в рамках канала/стрима).
Ответы появляются в body как JSON по тем же правилам отображения.
std/grpc-connect@v1
Открывает живой канал и загружает схему. Результат:
{ "id": "grpc-1", "connected": true, "duration_ms": 4.8 }
Сохраните результат (outputs: conn) и передавайте id: "${{ conn.id }}"
остальным шагам grpc-*. Идентификаторы выдаются на каждый VU (grpc-1,
grpc-2, …) и действительны только внутри текущей итерации этого VU. Неудачное
подключение или загрузка схемы даёт
{ "connected": false, "error": "…", "duration_ms": … }.
std/grpc-call@v1
Один унарный вызов по живому каналу.
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
id | string | обязателен | Идентификатор канала от std/grpc-connect@v1 |
method | string | обязателен | "package.Service/Method" (только унарные методы) |
payload | object | array | string | одно из payload/payload_base64 | JSON-сообщение запроса; токены ${…} раскрываются на каждый вызов |
payload_base64 | string | одно из payload/payload_base64 | Сериализованные байты protobuf |
metadata | object | — | Метаданные вызова (переопределяют значения канала по ключу) |
expect_status | integer | 0 | Ожидаемый код статуса gRPC — любой другой приводит к ошибке шага |
timeout | integer (мс) | 10000 | Отправляется как grpc-timeout и контролируется локально |
Результат:
{ "status": 0, "body": { "message": "hello" }, "duration_ms": 2.4,
"metrics": { "grpc_req_duration": [2.4], "grpc_msgs_sent": 1,
"grpc_msgs_received": 1, "grpc_msg_rtt": [2.3], "grpc_req_failed": 0 } }
При ненулевом статусе результат содержит error (текст статуса) вместо body.
expect_status делает тесты ошибочных веток естественными: expect_status: 5
проходит, когда сервер возвращает NOT_FOUND, и падает на OK. Неудачный RPC
приводит к ошибке шага, но канал остаётся пригодным (HTTP/2-каналы
восстанавливаются; мёртвый WebSocket — нет). Унарные метрики: гистограммы
grpc_req_duration и grpc_msg_rtt, счётчики grpc_msgs_sent /
grpc_msgs_received, grpc_req_failed для RPC, не удовлетворивших
expect_status.
std/grpc@v1 — разовый вызов
Параметры профиля канала и вызова в одном шаге; подключается, загружает схему,
вызывает, закрывает. Результат тот же, что у std/grpc-call@v1.
steps:
- name: fetch schema
use: std/http@v1
with: { url: "https://schema.example.com/echo.pb" }
outputs: fetch
- name: unary probe
use: std/grpc@v1
with:
url: grpcs://api.example.com:443
descriptor_set: "${{ fetch.body_base64 }}"
method: "echo.v1.Echo/Unary"
payload: { message: "ping ${seq}" }
check:
duration_ms_lt: 500
std/grpc-stream-open@v1
Запускает client-streaming, bidi или server-streaming вызов по живому каналу и
возвращает id стрима (grpcs-1, …).
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
id | string | обязателен | Идентификатор канала |
method | string | обязателен | "package.Service/Method" (только стриминговые методы) |
payload | object | server-streaming: обязателен | Единственное сообщение запроса для server-streaming методов |
payload_base64 | string | — | Сериализованная форма вышесказанного |
metadata | object | — | Метаданные вызова |
Результат: { "id": "grpcs-1", "kind": "server"|"client"|"bidi", "open": true, "duration_ms": … }.
Для server-streaming единственный запрос уходит при открытии; для
client-streaming/bidi сообщения отправляются через std/grpc-stream-send@v1
(передача payload при открытии — ошибка). Open возвращается немедленно —
вызов выполняется в задаче-ретрансляторе, потому что client-streaming сервер
отправляет свои начальные метаданные только после half-close со стороны
клиента. Ошибка на стороне сервера (UNIMPLEMENTED, авторизация, …) поэтому
проявляется при первом recv/close, а не при open.
std/grpc-stream-send@v1
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
id | string | обязателен | Идентификатор стрима |
payload | object | одно из payload/payload_base64 | JSON-сообщение; токены ${…} раскрываются на каждую отправку |
payload_base64 | string | одно из payload/payload_base64 | Сериализованные байты protobuf |
repeat | integer | 1 | Отправить N сообщений из одного шаблона |
interval_ms | integer | 0 | Пауза между повторными отправками |
timeout | integer (мс) | 10000 | На весь цикл отправки |
Результат: { "sent": N, "duration_ms": …, "metrics": { "grpc_msgs_sent": N } }.
Отправка в server-streaming стрим — ошибка параметров (стрим остаётся
пригодным). Отправка, завершившаяся ошибкой из-за того, что пир завершил вызов,
приводит к ошибке шага и аннулирует id стрима.
std/grpc-stream-recv@v1
Читает, пока не сработает правило остановки — если оно не сработало за
timeout, шаг завершается ошибкой:
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
id | string | обязателен | Идентификатор стрима |
until_contains | string | — | Остановиться, когда сообщение содержит эту подстроку (объекты: компактная JSON-форма) |
until_json | object | — | Остановиться, когда сообщение совпадает с JSON-подмножеством (взаимоисключающий с until_contains) |
count | integer | 1 | Без правила until_*: остановиться после N сообщений |
timeout | integer (мс) | 10000 | Дедлайн для правила остановки |
Результат:
{ "messages": [ { "message": "hello" } ], "count": 1, "matched": true,
"duration_ms": 3.1,
"metrics": { "grpc_msgs_received": 1, "grpc_msg_rtt": [2.9] } }
grpc_msg_rtt появляется, только когда правило until_* совпало и ранее по
этому стриму был grpc-stream-send — прикладной RTT «отправка → совпадение».
Обычный таймаут приводит к ошибке шага, но оставляет стрим пригодным;
завершение стрима (корректно или со статусом) до срабатывания правила приводит
к ошибке шага и разрыву стрима.
std/grpc-stream-close@v1
Выполняет half-close стороны запроса (client-streaming/bidi: сервер видит конец
ввода) и дочитывает оставшиеся сообщения сервера до финального статуса, в
пределах timeout.
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
id | string | обязателен | Идентификатор стрима |
expect_status | integer | 0 | Ожидаемый финальный код статуса gRPC |
timeout | integer (мс) | 10000 | Дедлайн дочитывания |
Результат: { "closed": true, "status": 0, "received": N, "messages": […], "duration_ms": …, "metrics": { "grpc_msgs_received": N, "grpc_req_failed": 0|1 } }.
Для client-streaming метода дочитанное единственное сообщение — это ответ
вызова. Id стрима освобождается в любом случае.
steps:
- name: open channel
use: std/grpc-connect@v1
with:
url: grpcs://api.example.com:443
reflection: true
metadata: { authorization: "Bearer ${{ vars.token }}" }
outputs: conn
- name: open bidi stream
use: std/grpc-stream-open@v1
with:
id: "${{ conn.id }}"
method: "echo.v1.Echo/Bidi"
outputs: stream
- name: send events
use: std/grpc-stream-send@v1
with:
id: "${{ stream.id }}"
payload: { message: "evt-${seq}" }
repeat: 5
interval_ms: 20
- name: await echoes
use: std/grpc-stream-recv@v1
with:
id: "${{ stream.id }}"
until_contains: "evt-5"
timeout: 5000
outputs: got
- name: close stream
use: std/grpc-stream-close@v1
with: { id: "${{ stream.id }}" }
Ограничения gRPC
Входящие сообщения ограничены max_recv_size (по умолчанию 16 МиБ);
превышающее лимит сообщение приводит к ошибке вызова с RESOURCE_EXCEEDED.
Бинарные (-bin) ключи метаданных не поддерживаются — значения метаданных это
строки. Время жизни стримов охватывает пользовательские шаги, поэтому стримы
намеренно не попадают в grpc_req_duration (только унарные вызовы); именно
grpc-stream-close превращает финальный статус стрима в grpc_req_failed.
Таймауты, счётчики repeat и длины дочитывания встроенных ограничений не имеют.
База данных: семейство std/db-*@v1
Нагрузочные тесты SQL для PostgreSQL, MySQL/MariaDB и SQLite.
Два стиля, по аналогии с семейством WebSocket: живое соединение, удерживаемое
между шагами (db-connect → db-query / транзакции → db-close), и режим
per-query, где каждый запрос оплачивает подключение + запрос (см.
Режимы подключения).
steps:
- name: open db
use: std/db-connect@v1
with:
driver: postgres
dsn: "${{ vars.db_dsn }}" # keep the password out of the file
outputs: db
- name: begin tx
use: std/db-tx-begin@v1
with: { id: "${{ db.id }}" }
- name: write
use: std/db-query@v1
with:
id: "${{ db.id }}"
query: INSERT INTO hits (path, status) VALUES ($1, $2)
params: ["/api/checkout", 200]
- name: commit
use: std/db-tx-commit@v1
with: { id: "${{ db.id }}" }
- name: hang up
use: std/db-close@v1
with: { id: "${{ db.id }}" }
std/db-connect@v1
Открывает пул соединений (режим persistent) или сохраняет разобранную конфигурацию подключения (режим per-query) и возвращает Connection ID.
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
driver | string | обязателен | postgres, mysql (включая MariaDB) или sqlite |
dsn | string | обязателен | Родная для драйвера строка подключения, например postgres://user:pass@host:5432/db, mysql://user:pass@host:3306/db, sqlite://data.db?mode=rwc, sqlite::memory:. Может использовать интерполяцию ${{ … }} для секретов |
tls | bool | string | true | true проверяет сертификат + имя хоста; false — открытый текст; "skip-verify" шифрует без проверки. Игнорируется для sqlite |
mode | string | persistent | persistent (пул удерживается между шагами) или per-query (только конфигурация — каждый запрос подключается заново) |
scope | string | iteration | iteration (соединение закрывается в конце итерации) или vu (живёт до конца VU; повторный идентичный db-connect в том же VU переиспользует припаркованное соединение — одно тёплое соединение на VU) |
prepared | bool | false | Кэшировать именованные операторы на припаркованном соединении — повторные запросы пропускают Parse-раундтрип (PostgreSQL: один RTT вместо двух). Несовместимо с пулерами в режиме транзакций (PgBouncer/Supavisor); в режиме per-query не действует |
pool_size | integer | 1 | Размер пула в режиме persistent (максимум 1024) |
timeout_ms | integer | 30000 | Таймаут подключения |
Результат: { "id": "db-1", "driver": "postgres", "mode": "persistent", "prepared": false, "scope": "iteration", "connected": true, "duration_ms": …, "metrics": … }.
Переиспользованное vu-scoped подключение дополнительно несёт "reused": true.
В режиме per-query connected равен false — пока ничего не открыто. DSN и
его пароль никогда не логируются; в строках лога используется очищенная метка
host:port/database.
std/db-query@v1
Выполняет один параметризованный оператор. INSERT/UPDATE/DELETE/DDL разрешены по умолчанию.
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
id | string | обязателен | Connection ID от std/db-connect@v1 (id: "${{ db.id }}") |
query | string | обязателен | SQL с родными для драйвера плейсхолдерами ($1, $2, … для postgres; ? для mysql/sqlite). Никогда не интерполируется — ${{ в SQL доходит до базы как есть. Жёсткий лимит 64 КиБ (max_query_bytes) |
params | array | [] | Позиционные значения привязки; каждый элемент интерполируется. Строки → text, числа → i64/f64, булевы → bool, null → типизированный NULL (текстового типа на postgres — приводите в SQL, например $1::int). Массивы/объекты привязать нельзя |
max_rows | integer | 10000 | Жёсткое ограничение на строки, читаемые в память; лишние строки только устанавливают truncated: true |
store_rows | bool | true | false считает строки (rows) без материализации: data пуст, построчного JSON-декода нет — для сценариев, не читающих data. truncated по-прежнему отражает max_rows |
timeout_ms | integer | 30000 | Таймаут запроса (режим per-query: подключение + запрос) |
Результат: { "rows": <u64>, "rows_affected": <u64>, "truncated": <bool>, "data": [ … ], "duration_ms": …, "metrics": … }.
Ячейки строк декодируются как booleans/integers/floats/text/JSON/base64
(бинарные); неотображённые типы (NUMERIC, temporal, UUID, массивы) дают null
— строка всё равно учитывается. При открытой на соединении транзакции запрос
выполняется внутри неё. Неудачный запрос приводит к ошибке шага, но оставляет
соединение — и любую открытую транзакцию — в припаркованном состоянии.
std/db-tx-begin@v1, std/db-tx-commit@v1, std/db-tx-rollback@v1
Контекст транзакции на persistent-соединении (параметр id; применяется
timeout_ms). Каждый шаг измеряется как единое целое (duration_ms +
db_query_duration). Одна открытая транзакция на соединение — begin при
уже открытой или commit/rollback при отсутствующей приводят к ошибке шага.
Транзакции отклоняются на per-query идентификаторах.
std/db-close@v1
Закрывает соединение и освобождает его id (параметр id). Открытая транзакция
откатывается при закрытии. Соединения, оставленные открытыми, неявно
закрываются в конце итерации — а при scope: vu при завершении VU.
Режимы подключения к БД
persistent (по умолчанию): db-connect открывает пул (pool_size, по
умолчанию 1 — шаги внутри VU выполняются строго последовательно), удерживаемый
между шагами VU; db_query_duration измеряет только сам запрос.
per-query: шаг connect лишь валидирует и сохраняет конфигурацию; каждый
db-query открывает новое соединение, выполняет запрос и закрывает, поэтому
db_query_duration измеряет подключение + запрос как единое целое — форма
serverless-драйверов и бенчмарков времени подключения. Транзакции недоступны.
sqlite::memory: здесь бессмысленен (каждое новое соединение — пустая база) —
используйте файловый DSN; и наоборот, in-memory пул обязан оставаться на
pool_size: 1.
Скоуп (scope на db-connect): по умолчанию припаркованное соединение
закрывается в конце итерации. С scope: vu оно живёт до конца VU, а
идентичный db-connect позже в том же VU переиспользует его (возвращается
тот же Connection ID, в выводе "reused": true) вместо открытия нового —
одно тёплое соединение на VU через все итерации, форма k6/locust.
db-close освобождает id досрочно; следующий connect тогда открывает
заново. Id никогда не пересекают границы VU.
Пулеры соединений (PgBouncer, Supabase)
Запросы выполняются как неименованные операторы, поэтому пулеры в режиме
транзакций (PgBouncer, Supavisor от Supabase на порту 6543) поддерживаются —
кэшированный именованный оператор конфликтовал бы на разделяемом бэкенде
(prepared statement "sqlx_s_1" already exists). Если такой пулер разбивает
протокольный обмен запроса между бэкендами под конкуренцией, шаг прозрачно
повторяет попытку внутри транзакции (которая закрепляет бэкенд), и с этого
момента соединение продолжает оборачивать запросы; прямые соединения за это
никогда не платят. Для Supabase: используйте DSN пулера (порт 6543, IPv4) с
tls: "skip-verify" (их цепочки CA нет в webpki-roots) или установите их CA.
С prepared: true на db-connect запросы выполняются как кэшированные
именованные операторы — быстрее на прямых соединениях (нет Parse на каждый
запрос), но несовместимо с пулерами в режиме транзакций: кэшированный
оператор конфликтует на разделяемом бэкенде, и именно поэтому по умолчанию
false.
Метрики БД
| Имя | Тип | Источник | Значение |
|---|---|---|---|
db_connect_duration | гистограмма | std/db-connect@v1 (успех) | Задержка подключения + настройки пула |
db_query_duration | гистограмма | std/db-query@v1, шаги транзакций | Задержка запроса; включает новое подключение в режиме per-query |
db_rows | счётчик | std/db-query@v1 | Возвращённые строки, либо затронутые строки, когда оператор ничего не вернул |
db_errors | счётчик | все шаги БД (ошибка) | Неудачные шаги БД, всего |
db_errors_{connection,constraint,deadlock,timeout,other} | счётчик | все шаги БД (ошибка) | То же, разбитое по классам (SQLSTATE / errno / код результата SQLite; также в error_kind результата шага) |
Метки метрик несут только имя шага — никогда сырой SQL.
std/file-read@v1
Читает файл в общий для процесса кэш и делает его содержимое доступным
последующим шагам. Первое обращение оплачивает чтение с диска; каждая
последующая итерация — всех VU — обслуживается из RAM. Кэш перевалидируется по
(mtime, size) файла при каждом обращении, поэтому файл, отредактированный
между прогонами долгоживущего агента, подхватывается автоматически.
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
path | string | обязателен | Файл для чтения |
encoding | string | text | text (файл должен быть корректным UTF-8) или base64 (бинарное содержимое) |
query | string | — | jq-подобный путь (см. std/read-json@v1); содержимое парсится как JSON, и подзначение по пути становится всем результатом шага |
Результат (доступен через outputs / __last__):
{ "content": "...", "size": 1024, "path": "./fixtures/payload.json" }
С query результат — само извлечённое JSON-подзначение (то есть
${{ payload }} и есть значение по пути).
steps:
- name: load payload
use: std/file-read@v1
with: { path: ./fixtures/payload.json }
outputs: payload
- name: send it
use: std/http@v1
with:
method: POST
url: "https://api.example.com/items"
headers: { content-type: application/json }
body: "${{ payload.content }}"
check:
status: 201
Примечания:
- Не-UTF-8 файл с
encoding: textприводит к ошибке шага — используйтеencoding: base64для бинарного содержимого. - С
queryсодержимое, не являющееся корректным JSON, приводит к ошибке шага; отсутствующий путь — тоже, с именем неудавшегося сегмента, если только сегмент не помечен суффиксом?, который превращает промах вnull. - Не пишет строк лога на итерацию: попадания в кэш — горячий путь.
- Обращение к
${{ payload.content }}копирует содержимое в запрос — кэш экономит чтения с диска, а не копирование на каждый запрос. Держите фикстуры небольшими при высоком RPS. - Для загрузок файлов предпочитайте параметр
multipartуstd/http@v1, который отправляет корректныйmultipart/form-data.
std/file-write@v1
Пишет содержимое в файл — обычно чтобы сохранить ответ предыдущего шага.
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
path | string | обязателен | Файл для записи (родительский каталог должен существовать) |
content | string | обязателен | Данные для записи; плейсхолдеры ${{ ... }} делают типичной нагрузку ${{ resp.body }} |
encoding | string | text | text пишет строку как есть; base64 сначала декодирует её (обратное к base64 у file-read) |
append | boolean | false | Дописывать вместо перезаписи |
query | string | — | jq-подобный путь (см. std/read-json@v1): чтение-модификация-запись — загрузить JSON-файл, установить content по пути, записать документ обратно |
Результат: { "path": <string>, "size": <записанные байты> }
steps:
- name: fetch report
use: std/http@v1
with: { url: "https://api.example.com/report" }
outputs: resp
- name: save it
use: std/file-write@v1
with:
path: ./out/report.json
content: "${{ resp.body }}"
Примечания:
- Запись по пути, закэшированному
std/file-read@v1, автоматически инвалидирует эту запись кэша — кэш чтения перевалидируется по(mtime, size). - С
append: trueкаждый вызов — одна записьO_APPEND, поэтому конкурентные VU не перемешиваются внутри содержимого; порядок между VU не определён. - С
querycontent, парсящийся как JSON, сохраняется как JSON (иначе как строка); промежуточные объекты/массивы создаются как в jq, индекс массива должен быть в пределах границ или равен длине (добавление), отсутствующий файл начинается с пустого документа, а существующее не-JSON содержимое приводит к ошибке шага.queryне сочетается сappendиencoding: base64. - Не пишет строк лога на итерацию.
std/read-json@v1
Читает и парсит JSON-файл — структурный собрат std/file-read@v1.
Распарсенный документ кэшируется на весь процесс и перевалидируется по
(mtime, size) файла при каждом обращении, поэтому итерации после первой
пропускают и чтение с диска, и парсинг. Fail-closed, как и остальные файловые
действия: прогон должен включить allow_file_actions: true.
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
file | string | обязателен | JSON-файл для чтения |
query | string | — | jq-подобный путь, выбирающий подзначение (см. ниже) |
Результат: распарсенный JSON-документ или подзначение по query.
Грамматика query — подмножество jq-путей (без пайпов/фильтров/функций):
необязательная ведущая ., затем сегменты .key, ["key"] или [0];
точечный сегмент из одних цифр тоже индексирует массив, так что posts.0.id ≡
.posts[0].id. Суффикс ? на сегменте делает его терпимым — промах
разрешается в null вместо ошибки шага. Та же грамматика работает в
параметре query у std/file-read@v1 / std/file-write@v1 и в путях внутри
интерполяции ${{ ... }}.
steps:
- name: users fixture
use: std/read-json@v1
with: { file: ./fixtures/users.json, query: ".users[0]" }
outputs: user
- name: log in
use: std/http@v1
with:
method: POST
url: "https://api.example.com/login"
body: '{"token": "${{ user.token }}"}'
Примечания:
- Некорректный JSON приводит к ошибке шага с понятным сообщением; как и
отсутствующий путь
query(ошибка называет неудавшийся сегмент), если сегмент не помечен?. - Не пишет строк лога на итерацию: попадания в кэш — горячий путь.
std/child_process@v1
Запускает локальный процесс ОС и держит его живым весь прогон — mock-сервер,
база-фикстура, sidecar, нужный тестируемой системе. Типичное место — блок
before: конфига (запуск) в паре с after: (остановка); можно использовать и
в шагах теста. Fail-closed: прогон должен явно разрешить это через
allow_process_actions: true в конфиге.
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
command | string | обязателен | Исполняемый файл для запуска |
args | array of strings | [] | Аргументы команды |
env | object | — | Дополнительные переменные окружения ({ "NAME": "value" }, значения — только строки) |
cwd | string | — | Рабочий каталог потомка |
port | integer | — | Порт, на котором отвечает процесс (пробрасывается в outputs); 0 автоматически назначает свободный порт и экспортирует его потомку как переменную окружения PORT |
restart | string | never | Политика перезапуска: never, on-failure (ненулевой код выхода или сигнал), always |
max_restarts | integer | 3 | Бюджет перезапусков для политики |
backoff_ms | integer | 1000 | Задержка перед каждым перезапуском |
buffer_kb | integer | 64 | Размер захватываемого хвоста stdout/stderr на поток (КиБ) |
waitUntil | object | string | — | Гейт готовности — см. ниже |
Результат (доступен через outputs / __last__):
{ "pid": 12345, "ppid": 12300, "pgid": 12345, "port": 8080,
"stdout": "<tail>", "stderr": "<tail>", "restart_count": 0 }
ppid — сам процесс perfscale; pgid — группа процессов потомка — каждый
потомок возглавляет собственную группу, поэтому убийство дерева никогда не
заденет perfscale. port присутствует, только когда шаг его объявил.
pid/pgid/restart_count — это снимок: они меняются при перезапусках,
поэтому останавливайте процессы по имени в реестре
(std/kill_process@v1), а не по захваченному pid.
Потомок продолжает работать после возврата шага. Его вывод стримится в лог
прогона с префиксом {step}: (та же форма, что у раннера k6) и накапливается
в ограниченных буферах хвоста за stdout/stderr. Супервизор применяет
политику перезапуска до конца прогона и логирует каждый перезапуск. Всё, что
ещё живо к концу прогона — нормальное завершение, упавший before: или
Ctrl-C — останавливается автоматически (SIGTERM, с эскалацией до SIGKILL
после льготного периода, вся группа процессов), поэтому забытый kill_process
никогда не оставляет утечку сервера.
waitUntil — гейт готовности
Блокирует шаг до готовности процесса; каждый перечисленный сопоставитель должен сработать. Объектная форма:
| Поле | Тип | Описание |
|---|---|---|
stdout_contains | string | Подстрока в захваченном stdout |
stderr_contains | string | Подстрока в захваченном stderr |
stdout_matches | string (regex) | Регулярное выражение по захваченному stdout |
stderr_matches | string (regex) | Регулярное выражение по захваченному stderr |
port_open | integer | TCP-подключение к 127.0.0.1:<port> успешно; 0 проверяет собственный port шага |
timeout | string | Длительность вида 30s/1m (по умолчанию 30s) |
on_timeout | string | fail (по умолчанию) приводит к ошибке шага; continue логирует промах и идёт дальше |
Процесс, завершившийся до достижения готовности, приводит к ошибке шага с его
кодом выхода и хвостом stderr. Строковая форма покрывает частый случай одного
сопоставителя: waitUntil: 'contains(stdout, "Serving HTTP")',
'matches(stderr, "re")', 'port_open(8080)'.
allow_process_actions: true
before:
- name: web
uses: std/child_process@v1
with:
command: python3
args: ["-m", "http.server", "8080"]
waitUntil:
port_open: 8080
timeout: 10s
restart: on-failure
outputs: web # → ${{ config.web.pid }}, ${{ config.web.port }}
std/kill_process@v1
Останавливает процесс, запущенный через std/child_process@v1 — пара к
after:. Тот же гейт allow_process_actions.
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
name | string | одно из name/pid | Имя управляемого процесса в реестре (имя его шага или имя outputs). Предпочтительно: поиск всегда указывает на текущий pid, даже через перезапуски |
pid | integer | одно из name/pid | Сырой pid ОС — запасной вариант «как получится» для процессов вне реестра (только unix) |
signal | string | TERM | TERM, KILL, INT, HUP, QUIT, USR1, USR2 (префикс SIG допускается) |
grace_ms | integer | 5000 | Ждать завершения процесса столько времени перед эскалацией до SIGKILL |
tree | boolean | true | Сигналить всей группе процессов, а не только лидеру |
Результат: { "pid": 12345, "signal": "TERM", "exit_code": null, "waited_ms": 12 }
exit_code равен null, когда процесс погиб от сигнала, и всегда null для
сырых pid (статус завершения не-потомка собрать нельзя). Остановка уже
остановленного процесса — no-op, а не ошибка; автоматическая остановка в конце
прогона покрывает всё, что не было остановлено явно.
after:
- name: stop web
uses: std/kill_process@v1
with: { name: web, signal: TERM }
На не-unix платформах нет POSIX-сигналов и групп процессов:
std/kill_process@v1 по name завершает только непосредственного потомка
(tree не действует), а pid: не поддерживается.
std/thresholds@v1
SLO-гейты уровня прогона поверх метрик, собранных за прогон, — выражения
порогов в стиле k6, вычисляемые один раз, после остановки всех VU. Их дом —
блок after: конфига:
after:
- name: slo gate
use: std/thresholds@v1
with:
db_query_duration: ["p95<500", "max<2000"]
db_query_failed: ["rate<0.05"]
db_errors: ["count==0"]
http_req_duration: ["avg<300"]
severity: fail # fail (default) | warn | info
message: "checkout SLO" # optional, interpolated
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
with.<metric> | string или array of strings | обязателен (≥1) | Выражения порогов для метрики прогона |
severity | string | fail | Поле уровня шага: во что превращается нарушенный гейт — fail (прогон завершается с ненулевым кодом), warn или info (оба с нулевым кодом) |
message | string | — | Поле уровня шага: пользовательская метка, добавляемая к сводке нарушений; интерполируется |
Выражения имеют вид <agg><op><number>: agg ∈ avg, min, max, p50,
p90, p95, p99, count, rate; op ∈ <, <=, >, >=, ==, !=;
число — обычный float (целое или десятичное, без единиц). Пробелы вокруг частей
допустимы (p95 < 500 парсится).
Какие агрегаты применимы — зависит от вида метрики:
- Метрики-замеры (
db_query_duration,http_req_duration,ws_msg_rtt, …):avg/min/max/p50/p90/p95/p99вычисляются по всем замерам, сделанным за прогон, из тех же HDR-гистограмм, что печатает итоговая сводка — числа гейтов совпадают со сводкой.count— число замеров. - Метрики-счётчики (
db_errors,db_rows, …): толькоcount— финальное значение счётчика. - Метрики ошибок (
http_req_failed,db_query_failed, …):rate— доля неудачных вызовов от всех в0.0..=1.0;count— число вызовов. Раннер выводит их автоматически — каждый вызов шага записывает замер 0/1 под<family>_failedдля каждой метрики длительности, которую он выпустил (db_query_duration→db_query_failed). См. metrics.md.
Ошибки жёсткие. Неизвестное имя метрики приводит к ошибке гейта — и
прогона — со списком метрик, которые есть; то же делают пустой with:,
непарсящееся выражение или агрегат, неприменимый к виду метрики. Сломанный гейт
никогда незаметно не пройдёт CI.
Результат (доступен через outputs / __last__):
{ "status": "fail",
"message": "db_query_duration p95=612ms ≥ 500ms; checkout SLO",
"violations": [{ "metric": "db_query_duration", "expr": "p95<500", "actual": 612.0 }] }
status равен pass, когда выполняются все выражения, иначе — значение
severity. message — сводка нарушений, объединённая через ; , плюс
пользовательское сообщение, обрезанная на 200 символах с …. JSON сводки
прогона (perfscale run --summary-export) получает поле thresholds той же
формы (несколько гейтов объединяются: побеждает худший статус).
std/check@v1
Проверяет свойства результата предыдущего шага. Обычно записывается как
инлайн-блок check: шага, который выполняет это действие над результатом того
шага; автономное использование выбирает цель через on.
| Параметр | Тип | Описание |
|---|---|---|
on | string | Что проверять (по умолчанию — результат последнего шага). Точки спускаются вглубь значения: on: got.messages.0 адресует одно сообщение по позиции |
status | integer | HTTP-статус должен быть равен этому значению |
duration_ms_lt | integer | duration_ms должен быть строго меньше |
body_contains | string | Тело ответа должно содержать эту подстроку |
message_contains | string | Какое-то сообщение в списке messages содержит эту подстроку |
message_matches | object | Какое-то сообщение совпадает с этим объектом как JSON-подмножество |
messages_count_gte | integer | Список messages содержит не менее N элементов |
Проверки message_* работают по списку messages, который предоставляют
действия обмена сообщениями (std/ws@v1, std/ws-recv@v1, pro/fix@v1), и
используют квантор any — хотя бы одно сообщение должно совпасть, потому что
потоки несут шум (heartbeats, посторонние события). Текстовые фреймы WS
(строки) парсятся как JSON для message_matches; фреймы FIX (объекты
«тег→значение») совпадают напрямую, поэтому
message_matches: { "35": "8", "150": "F" } проверяет исполненный
ExecutionReport. Для детерминированных обменов адресуйте одно сообщение по
индексу через on (on: got.messages.0) — хрупко на неупорядоченных потоках.
Каждая проверка логирует PASS/FAIL; неудачи идут в stderr, но не
останавливают прогон. Результат: { "passed": true|false }.
std/random@v1
Генерирует одно числовое значение. Результат — само число, доступное для
интерполяции ${{ name }} в следующих шагах.
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
type | string | обязателен | integer (включительно [min, max]) или float ([min, max)) |
min | number | обязателен | Нижняя граница; не больше max |
max | number | обязателен | Верхняя граница; не меньше min |
При одинаковых границах возвращается эта константа. seed: прогона делает
последовательность воспроизводимой; у каждого VU она своя.
steps:
- use: std/random@v1
with: { type: float, min: 0, max: 1 }
outputs: like_roll
- use: std/http@v1
when: "${{ like_roll }} < 0.15"
with: { method: POST, url: "https://api.example.com/posts/1/like" }
- use: std/random@v1
with: { type: float, min: 3, max: 7 }
outputs: think_time
- use: std/sleep@v1
with: { seconds: "${{ think_time }}" }
std/sleep@v1
Приостанавливает текущего VU.
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
ms | integer or string | 1000 | Сколько миллисекунд спать |
seconds | number or string | — | Альтернатива ms (допускаются дробные значения) |
Строковое значение сначала раскрывает ${…}-токены, поэтому
ms: "${rand(3000,7000)}" задаёт случайную паузу. ${{ … }} интерполяция
происходит до запуска действия, поэтому seconds: "${{ think_time }}" может
использовать результат std/random@v1. Нечисловая строка после раскрытия
завершает шаг ошибкой (раньше молча использовалась пауза 1000 мс).
std/pick@v1
Выбирает элемент JSON-массива, подставляет значение по ключу объекта или переименовывает поля объекта — например, выбирает тестового пользователя для конкретного VU из fixture-файла.
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
from | array, object, or string | одно из from/file | Массив/объект или строка с JSON-массивом/объектом ("${{ feed.body.posts }}") |
file | string (path) | одно из from/file | Файл с JSON-массивом/объектом, парсится один раз на процесс и кешируется по (mtime, size), как std/file-read@v1. Требует allow_file_actions, учитывает fs_root |
index | integer or string | random | Индекс элемента, заворачивается по модулю длины. Если опущен — равномерно случайный элемент. Только для массивов |
index_offset | integer or string | 0 | Знаковое смещение к index перед модулем; отрицательные значения тоже заворачиваются. Только для массивов |
key | string or number | — | Для объекта: выбрать значение по ключу (число канонически приводится к строке). Отсутствующий ключ завершает шаг ошибкой |
remap | object | — | Переименование top-level полей результата: source-field: destination-field. Неуказанные поля сохраняются; переименованные source-поля удаляются. Отсутствующий source, нестроковый/пустой или дублирующийся destination, а также destination, конфликтующий с непереименованным полем, завершают шаг ошибкой |
Результат: сам выбранный элемент, с применённым remap, если он задан.
С объектом key выбирает значение — либо опустите key и передайте remap,
чтобы переименовать поля самого объекта:
steps:
- use: std/pick@v1
with:
from: { dev: "https://dev.example.com", stage: "https://stage.example.com" }
key: "${{ inputs.env }}"
outputs: base_url
- use: std/pick@v1
with: { from: "${{ user_row }}", remap: { id: user_id, name: username } }
outputs: user
remap строит результат из исходного объекта, поэтому обмен имён
(remap: { a: b, b: a }) работает без сюрпризов с порядком, а
source: source — no-op.
index: "${{ __vu__ }}" с index_offset: -1 отображает 1-based VU на
0-based элементы массива — точно (VU-1) % length. Для случайного выбора
через явный std/random@v1 передайте целый index из этого шага вместо
случайного выбора внутри pick.
steps:
- use: std/pick@v1
with: { file: fixtures/users.json, index: "${{ __vu__ }}", index_offset: -1 }
outputs: user
- use: std/http@v1
with: { url: "https://api.example.com/feed" }
outputs: feed
- use: std/random@v1
with: { type: integer, min: 0, max: 19 }
outputs: post_index
- use: std/pick@v1
with: { from: "${{ feed.body.posts }}", index: "${{ post_index }}" }
outputs: post
- use: std/http@v1
with:
method: POST
url: "https://api.example.com/posts/${{ post.id }}/like"
headers: { Authorization: "Bearer ${{ user.token }}" }
std/log@v1
Выводит строку в stdout — наиболее полезно с интерполяцией:
| Параметр | Тип | Описание |
|---|---|---|
message | string | Текст для вывода; ссылки ${{ var.field }} разрешаются первыми |
Правила интерполяции
Все строковые листья в with/check интерполируются перед запуском действия:
${{ name }}— сохранённое значение, приведённое к строке${{ name.field }}— одно поле сохранённого JSON-объекта- неизвестные имена разрешаются в
""; незавершённый${{остаётся как есть
Добавление нового действия (для контрибьюторов)
- Реализуйте его в
crates/perfscale-core/src/step/actions.rsи добавьте ветку диспетчеризации вexecute_action. - Возвращайте
ActionOutput: сохраняемоеvalue, строки лога,successиhttp_sample, если действие выполняет HTTP-работу, которая должна учитываться в метриках. - Добавьте юнит-тесты рядом с существующими (для HTTP доступен wiremock).
- Задокументируйте его здесь и, если оно вводит новые поля шагов, перегенерируйте
схемы (
cargo run -p perfscale-core --example gen_schema).
Пользовательские действия из downstream-крейтов
Действия, которым не место в этом OSS-крейте, — протоколы более высоких тарифов
или проприетарные, такие как FIX-действие pro/fix@v1 — подключаются без
форка. Реализуйте трейт perfscale_core::step::actions::ActionHandler в своём
крейте и вызовите register_action(Arc::new(MyHandler)) один раз при старте
процесса (например, в main агента). Зарегистрированные обработчики опрашиваются
только для идентификаторов действий, которым не соответствует ни одно встроенное
std/*, поэтому встроенные действия не платят за поиск. Параметры доходят до
обработчика с уже применённой интерполяцией ${{ }}.