Files
sarin/docs/configuration.md
T

682 lines
27 KiB
Markdown

# Configuration
Sarin supports environment variables, CLI flags, and YAML files. However, they are not exactly equivalent: YAML files have the most configuration options, followed by CLI flags, and then environment variables.
When the same option is specified in multiple sources, the following priority order applies:
```
CLI Flags (Highest) > YAML > Environment Variables (Lowest)
```
Use `-s` or `--show-config` to see the final merged configuration before sending requests.
## Properties
> **Note:** For CLI flags with `string / []string` type, the flag can be used once with a single value or multiple times to provide multiple values.
| Name | YAML | CLI | ENV | Default | Description |
| --------------------------------------- | ----------------------------------- | -------------------------------------------- | ----------------------------------- | ------- | ----------------------------- |
| [Help](#help) | - | `-help` / `-h` | - | - | Show help message |
| [Version](#version) | - | `-version` / `-v` | - | - | Show version and build info |
| [Show Config](#show-config) | `showConfig`<br>(boolean) | `-show-config` / `-s`<br>(boolean) | `SARIN_SHOW_CONFIG`<br>(boolean) | `false` | Show merged configuration |
| [Config File](#config-file) | `configFile`<br>(string / []string) | `-config-file` / `-f`<br>(string / []string) | `SARIN_CONFIG_FILE`<br>(string) | - | Path to config file(s) |
| [URL](#url) | `url`<br>(string) | `-url` / `-U`<br>(string) | `SARIN_URL`<br>(string) | - | Target URL (HTTP/HTTPS) |
| [Method](#method) | `method`<br>(string / []string) | `-method` / `-M`<br>(string / []string) | `SARIN_METHOD`<br>(string) | `GET` | HTTP method(s) |
| [Timeout](#timeout) | `timeout`<br>(duration) | `-timeout` / `-T`<br>(duration) | `SARIN_TIMEOUT`<br>(duration) | `10s` | Request timeout |
| [Max Response Body](#max-response-body) | `maxResponseBody`<br>(size) | `-max-response-body` / `-S`<br>(size) | `SARIN_MAX_RESPONSE_BODY`<br>(size) | `10MiB` | Largest response body to read |
| [Concurrency](#concurrency) | `concurrency`<br>(number) | `-concurrency` / `-c`<br>(number) | `SARIN_CONCURRENCY`<br>(number) | `1` | Number of concurrent workers |
| [Requests](#requests) | `requests`<br>(number) | `-requests` / `-r`<br>(number) | `SARIN_REQUESTS`<br>(number) | - | Total requests to send |
| [Duration](#duration) | `duration`<br>(duration) | `-duration` / `-d`<br>(duration) | `SARIN_DURATION`<br>(duration) | - | Test duration |
| [Log Level](#log-level) | `logLevel`<br>(string) | `-log-level` / `-l`<br>(string) | `SARIN_LOG_LEVEL`<br>(string) | `error` | Runtime log levels to emit |
| [Log File](#log-file) | `logFile`<br>(string) | `-log-file` / `-w`<br>(string) | `SARIN_LOG_FILE`<br>(string) | - | Write runtime logs to a file |
| [Progress](#progress) | `progress`<br>(string) | `-progress` / `-p`<br>(string) | `SARIN_PROGRESS`<br>(string) | `bar` | Progress display (bar/none) |
| [Output](#output) | `output`<br>(string) | `-output` / `-o`<br>(string) | `SARIN_OUTPUT`<br>(string) | `table` | Output format for stats |
| [Dry Run](#dry-run) | `dryRun`<br>(boolean) | `-dry-run` / `-z`<br>(boolean) | `SARIN_DRY_RUN`<br>(boolean) | `false` | Generate without sending |
| [Insecure](#insecure) | `insecure`<br>(boolean) | `-insecure` / `-I`<br>(boolean) | `SARIN_INSECURE`<br>(boolean) | `false` | Skip TLS verification |
| [Body](#body) | `body`<br>(string / []string) | `-body` / `-B`<br>(string / []string) | `SARIN_BODY`<br>(string) | - | Request body |
| [Params](#params) | `params`<br>(object) | `-param` / `-P`<br>(string / []string) | `SARIN_PARAM`<br>(string) | - | URL query parameters |
| [Headers](#headers) | `headers`<br>(object) | `-header` / `-H`<br>(string / []string) | `SARIN_HEADER`<br>(string) | - | HTTP headers |
| [Cookies](#cookies) | `cookies`<br>(object) | `-cookie` / `-C`<br>(string / []string) | `SARIN_COOKIE`<br>(string) | - | HTTP cookies |
| [Proxy](#proxy) | `proxy`<br>(string / []string) | `-proxy` / `-X`<br>(string / []string) | `SARIN_PROXY`<br>(string) | - | Proxy URL(s) |
| [Values](#values) | `values`<br>(string / []string) | `-values` / `-V`<br>(string / []string) | `SARIN_VALUES`<br>(string) | - | Template values (key=value) |
| [Lua](#lua) | `lua`<br>(string / []string) | `-lua`<br>(string / []string) | `SARIN_LUA`<br>(string) | - | Lua script(s) |
| [Js](#js) | `js`<br>(string / []string) | `-js`<br>(string / []string) | `SARIN_JS`<br>(string) | - | JavaScript script(s) |
---
## Help
Show help message.
```sh
sarin -help
```
## Version
Show version and build information.
```sh
sarin -version
```
## Show Config
Show the final merged configuration before sending requests.
```sh
sarin -show-config
```
## Config File
Path to configuration file(s). Supports local paths and remote URLs.
**Priority Rules:**
1. **CLI flags** (`-f`) have highest priority, processed left to right (rightmost wins)
2. **Included files** (via `configFile` property) are processed with lower priority than their parent
3. **Environment variable** (`SARIN_CONFIG_FILE`) has lowest priority
**Example:**
```yaml
# config2.yaml
configFile: /config4.yaml
url: http://from-config2.com
```
```sh
SARIN_CONFIG_FILE=/config1.yaml sarin -f /config2.yaml -f https://example.com/config3.yaml
```
**Resolution order (lowest to highest priority):**
| Source | File | Priority |
| ------------------------ | ------------ | -------- |
| ENV (SARIN_CONFIG_FILE) | config1.yaml | Lowest |
| Included by config2.yaml | config4.yaml | ↑ |
| CLI -f (first) | config2.yaml | ↑ |
| CLI -f (second) | config3.yaml | Highest |
**Why this order?**
- `config1.yaml` comes from ENV → lowest priority
- `config2.yaml` comes from CLI → higher than ENV
- `config4.yaml` is included BY `config2.yaml` → inherits position below its parent
- `config3.yaml` comes from CLI after `config2.yaml` → highest priority
If all four files define `url`, the value from `config3.yaml` wins.
**Merge behavior by field:**
- **Scalar fields** (`url`, `requests`, `duration`, `timeout`, `concurrency`, etc.): higher priority overrides lower priority
- **Method and Body**: higher priority overrides lower priority (no merging)
- **Headers, Params, Cookies, Proxies, Values, Lua, and Js**: accumulated across all config files
## URL
Target URL. Must be HTTP or HTTPS. The URL path supports [templating](templating.md), allowing dynamic path generation per request.
> **Note:** Templating is only supported in the URL path. Host and scheme must be static.
**Example with dynamic path:**
```yaml
url: http://example.com/users/{{ fakeit_UUID }}/profile
```
**CLI example with dynamic path:**
```sh
sarin -U "http://example.com/users/{{ fakeit_UUID }}" -r 1000 -c 10
```
## Method
HTTP method(s). Defaults to `GET`. If multiple values are provided, Sarin starts at a random index and cycles through them in order. Once the cycle completes, it picks a new random starting point. Supports [templating](templating.md).
**YAML example:**
```yaml
method: GET
# OR
method:
- GET
- POST
- PUT
```
**CLI example:**
```sh
-method GET -method POST -method PUT
```
**ENV example:**
```sh
SARIN_METHOD=GET
```
## Timeout
Request timeout. Must be greater than 0. Defaults to `10s`.
Valid time units: `ns`, `us` (or `µs`), `ms`, `s`, `m`, `h`
**Examples:** `5s`, `300ms`, `1m20s`
## Max Response Body
Largest response body sarin reads. Defaults to `10MiB`, and `0` removes the limit.
The value is a whole number of bytes, with or without a unit: `B`, `KB`, `MB`, `GB`, `TB` are powers of 1000, `KiB`, `MiB`, `GiB`, `TiB` are powers of 1024. Case is ignored and a space before the unit is allowed. The largest accepted value is `1GiB`, because one response can take a few times the limit in memory.
**Examples:** `1048576`, `10MiB`, `2MB`, `512KiB`
A server can declare a `Content-Length` far larger than it sends, and the body is allocated from that declared length, so without a limit a single response can exhaust memory. A response above the limit fails with `response body exceeds the body limit` and is counted like any other error.
The limit applies per response in flight, and a declared Content-Length is allocated up to it, so the worst case is a few times the limit times the concurrency. It covers the requests scripts make as well.
A compressed response in a script is decoded first, so the limit covers the decoded size as well as the bytes that arrived. Decoding holds both at once, so a brotli or zstd body peaks at over ten times the limit. Ratios are unbounded, and 106 bytes of brotli can hold 64MiB, so with `0` a tiny download can still exhaust memory.
## Concurrency
Number of concurrent workers. Must be between 1 and 100,000,000. Defaults to `1`.
## Requests
Total number of requests to send. At least one of `requests` or `duration` must be specified. If both are provided, the test stops when either limit is reached first.
## Duration
Test duration. At least one of `requests` or `duration` must be specified. If both are provided, the test stops when either limit is reached first.
Valid time units: `ns`, `us` (or `µs`), `ms`, `s`, `m`, `h`
**Examples:** `1m30s`, `25s`, `1h`
## Log Level
Runtime log levels to emit, comma-separated. Valid levels: `info`, `error`. Defaults to `error`.
- `error`: errors that occur while generating a request. A request that fails to send is counted in the results instead
- `info`: every completed response
Leave empty to disable logging entirely.
**Examples:** `error` (only errors), `info` (only responses), `info,error` (both)
## Log File
Write runtime logs to this file instead of the terminal or stderr. The parent directory must exist.
```sh
sarin -U http://example.com -r 1000 --log-file ./run.log
```
## Progress
Progress display. Valid values: `bar` (default), `none`. Use `none` to hide the progress bar.
## Output
Output format for response statistics.
Valid formats: `table` (default), `json`, `yaml`, `none`
Using `none` disables output and reduces memory usage since response statistics are not stored.
## Dry Run
Generate requests without sending them. Useful for testing templates.
A script's own `http` calls are still sent, since a script needs them to build the request you are inspecting. Only the requests sarin generates are held back.
The duration in the results is how long generating each request took, templates and scripts included, so it is a different measurement from the response time a real run reports. A request with neither renders once before the run and reports `0s`.
## Insecure
Skip TLS certificate verification.
## Body
Request body. If multiple values are provided, Sarin starts at a random index and cycles through them in order. Once the cycle completes, it picks a new random starting point. Supports [templating](templating.md).
**YAML example:**
```yaml
body: '{"product": "car"}'
# OR
body:
- '{"product": "car"}'
- '{"product": "phone"}'
- '{"product": "watch"}'
```
**CLI example:**
```sh
-body '{"product": "car"}' -body '{"product": "phone"}' -body '{"product": "watch"}'
```
**ENV example:**
```sh
SARIN_BODY='{"product": "car"}'
```
## Params
URL query parameters. Supports [templating](templating.md).
When the same key appears as **separate entries** (in CLI or config file), all values are sent in every request. When multiple values are specified as an **array on a single key** (config file only), Sarin cycles through them.
**YAML example:**
```yaml
params:
key1: value1
key2: [value2, value3] # cycles between value2 and value3
# OR
params:
- key1: value1
- key2: [value2, value3] # cycles between value2 and value3
# To send both values in every request, use separate entries:
params:
- key2: value2
- key2: value3 # both sent in every request
```
**CLI example:**
```sh
-param "key1=value1" -param "key2=value2" -param "key2=value3" # sends both value2 and value3
```
**ENV example:**
```sh
SARIN_PARAM="key1=value1"
```
## Headers
HTTP headers. Supports [templating](templating.md).
When the same key appears as **separate entries** (in CLI or config file), all values are sent in every request. When multiple values are specified as an **array on a single key** (config file only), Sarin cycles through them.
**YAML example:**
```yaml
headers:
key1: value1
key2: [value2, value3] # cycles between value2 and value3
# OR
headers:
- key1: value1
- key2: [value2, value3] # cycles between value2 and value3
# To send both values in every request, use separate entries:
headers:
- key2: value2
- key2: value3 # both sent in every request
```
**CLI example:**
```sh
-header "key1: value1" -header "key2: value2" -header "key2: value3" # sends both value2 and value3
```
**ENV example:**
```sh
SARIN_HEADER="key1: value1"
```
## Cookies
HTTP cookies. Supports [templating](templating.md).
When the same key appears as **separate entries** (in CLI or config file), all values are sent in every request. When multiple values are specified as an **array on a single key** (config file only), Sarin cycles through them.
**YAML example:**
```yaml
cookies:
key1: value1
key2: [value2, value3] # cycles between value2 and value3
# OR
cookies:
- key1: value1
- key2: [value2, value3] # cycles between value2 and value3
# To send both values in every request, use separate entries:
cookies:
- key2: value2
- key2: value3 # both sent in every request
```
**CLI example:**
```sh
-cookie "key1=value1" -cookie "key2=value2" -cookie "key2=value3" # sends both value2 and value3
```
**ENV example:**
```sh
SARIN_COOKIE="key1=value1"
```
## Proxy
Proxy URL(s). If multiple values are provided, Sarin starts at a random index and cycles through them in order. Once the cycle completes, it picks a new random starting point.
Supported protocols: `http`, `https`, `socks5`, `socks5h`
**YAML example:**
```yaml
proxy: http://proxy1.com
# OR
proxy:
- http://proxy1.com
- socks5://proxy2.com
- socks5h://proxy3.com
```
**CLI example:**
```sh
-proxy http://proxy1.com -proxy socks5://proxy2.com -proxy socks5h://proxy3.com
```
**ENV example:**
```sh
SARIN_PROXY="http://proxy1.com"
```
## Values
Template values in key=value format. Supports [templating](templating.md). Multiple values can be specified and all are rendered for each request.
See the [Templating Guide](templating.md) for more details on using values and available template functions.
**YAML example:**
```yaml
values: "key=value"
# OR
values: |
key1=value1
key2=value2
key3=value3
```
**CLI example:**
```sh
-values "key1=value1" -values "key2=value2" -values "key3=value3"
```
**ENV example:**
```sh
SARIN_VALUES="key1=value1"
```
## Lua
Lua script(s) for request transformation. Each script must define a global `transform` function that receives a request object and returns the modified request object. Scripts run after template rendering, before the request is sent.
If multiple Lua scripts are provided, they are chained in order-the output of one becomes the input to the next. When both Lua and JavaScript scripts are specified, all Lua scripts run first, then all JavaScript scripts.
**Script sources:**
Scripts can be provided as:
- **Inline script:** Direct script code
- **File reference:** `@/path/to/script.lua` or `@./relative/path.lua`
- **URL reference:** `@http://...` or `@https://...`
- **Escaped `@`:** `@@...` for inline scripts that start with a literal `@`
**The `transform` function:**
```lua
function transform(req)
-- req.method (string) - HTTP method (e.g. "GET", "POST")
-- req.path (string) - URL path (e.g. "/api/users")
-- req.body (string) - Request body
-- req.headers (table of string/arrays) - HTTP headers (e.g. {["X-Key"] = "value"})
-- req.params (table of string/arrays) - Query parameters (e.g. {["id"] = "123"})
-- req.cookies (table of string/arrays) - Cookies (e.g. {["session"] = "abc"})
req.headers["X-Custom"] = "my-value"
return req
end
```
> **Note:** Header, parameter, and cookie values can be a single string or a table (array) for multiple values per key (e.g. `{"val1", "val2"}`). Numbers and booleans are converted to text, other values are skipped.
> **Note:** Lua scripts can send their own HTTP requests with `http` and work with JSON through the global `json` table. See [HTTP Requests in Scripts](#http-requests-in-scripts).
**YAML example:**
```yaml
lua: |
function transform(req)
req.headers["X-Custom"] = "my-value"
return req
end
# OR
lua:
- "@/path/to/script1.lua"
- "@/path/to/script2.lua"
```
**CLI example:**
```sh
-lua 'function transform(req) req.headers["X-Custom"] = "my-value" return req end'
# OR
-lua @/path/to/script1.lua -lua @/path/to/script2.lua
```
**ENV example:**
```sh
SARIN_LUA='function transform(req) req.headers["X-Custom"] = "my-value" return req end'
```
## Js
JavaScript script(s) for request transformation. Each script must define a global `transform` function that receives a request object and returns the modified request object. Scripts run after template rendering, before the request is sent.
If multiple JavaScript scripts are provided, they are chained in order-the output of one becomes the input to the next. When both Lua and JavaScript scripts are specified, all Lua scripts run first, then all JavaScript scripts.
**Script sources:**
Scripts can be provided as:
- **Inline script:** Direct script code
- **File reference:** `@/path/to/script.js` or `@./relative/path.js`
- **URL reference:** `@http://...` or `@https://...`
- **Escaped `@`:** `@@...` for inline scripts that start with a literal `@`
**The `transform` function:**
```javascript
function transform(req) {
// req.method (string) - HTTP method (e.g. "GET", "POST")
// req.path (string) - URL path (e.g. "/api/users")
// req.body (string) - Request body
// req.headers (object of string/arrays) - HTTP headers (e.g. {"X-Key": "value"})
// req.params (object of string/arrays) - Query parameters (e.g. {"id": "123"})
// req.cookies (object of string/arrays) - Cookies (e.g. {"session": "abc"})
req.headers["X-Custom"] = "my-value";
return req;
}
```
> **Note:** Header, parameter, and cookie values can be a single string or an array for multiple values per key (e.g. `["val1", "val2"]`). Numbers and booleans are converted to text, other values are skipped.
> **Note:** JavaScript scripts can send their own HTTP requests with `http`. See [HTTP Requests in Scripts](#http-requests-in-scripts).
**YAML example:**
```yaml
js: |
function transform(req) {
req.headers["X-Custom"] = "my-value";
return req;
}
# OR
js:
- "@/path/to/script1.js"
- "@/path/to/script2.js"
```
**CLI example:**
```sh
-js 'function transform(req) { req.headers["X-Custom"] = "my-value"; return req; }'
# OR
-js @/path/to/script1.js -js @/path/to/script2.js
```
**ENV example:**
```sh
SARIN_JS='function transform(req) { req.headers["X-Custom"] = "my-value"; return req; }'
```
## HTTP Requests in Scripts
Lua and JavaScript scripts can send their own HTTP requests and use the response in the main request, for example to fetch a fresh token or CSRF value from another endpoint.
**Function:**
```
http(url, opts)
```
`url` must be an absolute `http` or `https` URL. `opts` is optional.
**Options:**
| Option | Type | Default | Description |
| -------------- | --------------------------- | ------- | ------------------------------------------------------------------------------------------------------ |
| `method` | string | `GET` | HTTP method, sent as given |
| `headers` | table/object | - | Request headers, same shape as `req.headers` |
| `params` | table/object | - | Query parameters, added to any already in the URL |
| `cookies` | table/object | - | Cookies, sent as a single `Cookie` header |
| `body` | string | `""` | Request body |
| `timeout` | duration string (`"500ms"`) | `10s` | Request timeout (the `timeout` config does not apply), applied to each hop when redirects are followed |
| `insecure` | boolean | `false` | Skip TLS verification (the `insecure` config does not apply) |
| `maxRedirects` | number | `0` | How many redirects to follow, `0` follows none, maximum 100 |
Unknown options and values of the wrong type raise an error.
**Response:**
| Field / Method | Description |
| -------------- | ------------------------------------------------------------------ |
| `status` | Status code (number) |
| `headers` | Response headers, each key holding an array of values |
| `body` | Response body (string) |
| `header(name)` | First value of a header, case-insensitive; `nil`/`null` if missing |
| `cookie(name)` | Value of a cookie from `Set-Cookie`; `nil`/`null` if missing |
| `json()` | JavaScript only: parses the body as JSON |
| `bytes()` | JavaScript only: the body as a `Uint8Array` |
In Lua, call the helpers as `res:header("name")` or `res.header("name")`.
> **Note:** JavaScript strings hold UTF-16, so `body` replaces bytes that are not valid UTF-8 and `body.length` counts code units rather than bytes. Use `bytes()` for binary responses and for the real byte count. Lua strings keep the bytes as they arrived.
**Behavior:**
- **Runs on every request:** the requests are sent each time `transform` runs, so every main request triggers them. Script globals persist across requests within a worker, so a script can cache a value itself (e.g. `if not token then token = login() end`).
- **Same proxy:** when proxies are configured, a script's requests go through the same proxy as the main request they belong to.
- **Status codes:** non-2xx responses are returned normally; check `status` in the script.
- **Errors:** network failures and timeouts raise a script error. The main request is not sent and the error is counted in the results. Scripts can catch it with `pcall` (Lua) or `try`/`catch` (JavaScript).
- **Response bodies:** the [Max Response Body](#max-response-body) config bounds these requests too, and a body above it raises an error the script can catch.
- **Compressed responses:** sarin asks for no encoding, so this only applies when a script sends `Accept-Encoding` itself or the server compresses unasked. A `gzip`, `deflate`, `br` or `zstd` body is then decoded before the script sees it, and `Content-Encoding` and `Content-Length` are both dropped from `headers` because neither describes what the script receives. Anything else is left raw with its header intact: an empty body, `identity`, a `Content-Encoding` sarin does not recognise, and two or more codings at once. A body that fails to decode raises an error the script can catch, though `br` carries no checksum, so damage to a brotli body can go unnoticed.
- **Only inside `transform`:** `http` works anywhere while `transform` runs, including in helper functions it calls. Calling it at the top level of a script raises an error, because that code also runs during config validation and when each worker starts.
- **Dry run:** script requests are still sent in dry-run mode.
- **Not measured:** script requests are not included in the results.
**Lua JSON:**
Lua has no built-in JSON support, so Lua scripts get a global `json` table:
- `json.decode(string)`: arrays become 1-indexed tables and `null` becomes `nil` (so `null` array items leave holes).
- `json.encode(value)`: a table with keys exactly `1..n` becomes an array, any other table becomes an object, and an empty table becomes `{}`.
JavaScript scripts use the built-in `JSON` object.
**Lua example:**
```lua
function transform(req)
local res = http("https://api.example.com/login", {
method = "POST",
headers = { ["Content-Type"] = "application/json" },
body = json.encode({ user = "test", password = "secret" }),
timeout = "2s",
})
if res.status ~= 200 then
error("login failed: " .. res.status)
end
req.headers["Authorization"] = "Bearer " .. json.decode(res.body).token
return req
end
```
**JavaScript example:**
```javascript
function transform(req) {
const res = http("https://api.example.com/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ user: "test", password: "secret" }),
timeout: "2s",
});
if (res.status !== 200) {
throw new Error("login failed: " + res.status);
}
req.headers["Authorization"] = "Bearer " + res.json().token;
return req;
}
```