mirror of
https://github.com/aykhans/sarin.git
synced 2026-01-13 20:11:21 +00:00
Merge pull request #162 from aykhans/feat/url-path-templating
Add URL path templating support with validation and documentation
This commit is contained in:
36
README.md
36
README.md
@@ -18,13 +18,13 @@
|
||||
|
||||
Sarin is designed for efficient HTTP load testing with minimal resource consumption. It prioritizes simplicity—features like templating add zero overhead when unused.
|
||||
|
||||
| ✅ Supported | ❌ Not Supported |
|
||||
| ---------------------------------------------------- | --------------------------------- |
|
||||
| High-performance with low memory footprint | Detailed response body analysis |
|
||||
| Long-running duration/count based tests | Extensive response statistics |
|
||||
| Dynamic requests via 320+ template functions | Web UI or complex TUI |
|
||||
| Multiple proxy protocols (HTTP/HTTPS/SOCKS5/SOCKS5H) | Scripting or multi-step scenarios |
|
||||
| Flexible config (CLI, ENV, YAML) | HTTP/2, HTTP/3, WebSocket, gRPC |
|
||||
| ✅ Supported | ❌ Not Supported |
|
||||
| ---------------------------------------------------------- | --------------------------------- |
|
||||
| High-performance with low memory footprint | Detailed response body analysis |
|
||||
| Long-running duration/count based tests | Extensive response statistics |
|
||||
| Dynamic requests via 320+ template functions | Web UI or complex TUI |
|
||||
| Multiple proxy protocols<br>(HTTP, HTTPS, SOCKS5, SOCKS5H) | Scripting or multi-step scenarios |
|
||||
| Flexible config (CLI, ENV, YAML) | HTTP/2, HTTP/3, WebSocket, gRPC |
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -74,18 +74,6 @@ Send 10,000 GET requests with 50 concurrent connections and a random User-Agent
|
||||
sarin -U http://example.com -r 10_000 -c 50 -H "User-Agent: {{ fakeit_UserAgent }}"
|
||||
```
|
||||
|
||||
Example output:
|
||||
|
||||
```
|
||||
┌──────────┬───────┬──────────┬───────────┬─────────┬───────────┬──────────┬───────────┐
|
||||
│ Response │ Count │ Min │ Max │ Average │ P90 │ P95 │ P99 │
|
||||
├──────────┼───────┼──────────┼───────────┼─────────┼───────────┼──────────┼───────────┤
|
||||
│ 200 │ 10000 │ 78.038ms │ 288.153ms │ 94.71ms │ 103.078ms │ 131.08ms │ 269.218ms │
|
||||
├──────────┼───────┼──────────┼───────────┼─────────┼───────────┼──────────┼───────────┤
|
||||
│ Total │ 10000 │ 78.038ms │ 288.153ms │ 94.71ms │ 103.078ms │ 131.08ms │ 269.218ms │
|
||||
└──────────┴───────┴──────────┴───────────┴─────────┴───────────┴──────────┴───────────┘
|
||||
```
|
||||
|
||||
Run a 5-minute duration-based test:
|
||||
|
||||
```sh
|
||||
@@ -112,15 +100,15 @@ For detailed documentation on all configuration options (URL, method, timeout, c
|
||||
|
||||
## Templating
|
||||
|
||||
Sarin supports Go templates in methods, bodies, headers, params, cookies, and values. Use the 320+ built-in functions to generate dynamic data for each request.
|
||||
Sarin supports Go templates in URL paths, methods, bodies, headers, params, cookies, and values. Use the 320+ built-in functions to generate dynamic data for each request.
|
||||
|
||||
**Example:**
|
||||
|
||||
```sh
|
||||
sarin -U http://example.com/users \
|
||||
-V "ID={{ fakeit_UUID }}" \
|
||||
-H "X-Request-ID: {{ .Values.ID }}" \
|
||||
-B '{"id": "{{ .Values.ID }}"}'
|
||||
sarin -U "http://example.com/users/{{ fakeit_UUID }}" -r 1000 -c 10 \
|
||||
-V "RequestID={{ fakeit_UUID }}" \
|
||||
-H "X-Request-ID: {{ .Values.RequestID }}" \
|
||||
-B '{"request_id": "{{ .Values.RequestID }}"}'
|
||||
```
|
||||
|
||||
For the complete templating guide and functions reference, see the **[Templating Guide](docs/templating.md)**.
|
||||
|
||||
@@ -76,7 +76,21 @@ config3.yaml > config2.yaml > config4.yaml > config1.yaml
|
||||
|
||||
## URL
|
||||
|
||||
Target URL. Must be HTTP or HTTPS.
|
||||
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
|
||||
|
||||
|
||||
234
docs/examples.md
234
docs/examples.md
@@ -6,8 +6,8 @@ This guide provides practical examples for common Sarin use cases.
|
||||
|
||||
- [Basic Usage](#basic-usage)
|
||||
- [Request-Based vs Duration-Based Tests](#request-based-vs-duration-based-tests)
|
||||
- [Dynamic Requests with Templating](#dynamic-requests-with-templating)
|
||||
- [Headers, Cookies, and Parameters](#headers-cookies-and-parameters)
|
||||
- [Dynamic Requests with Templating](#dynamic-requests-with-templating)
|
||||
- [Request Bodies](#request-bodies)
|
||||
- [Using Proxies](#using-proxies)
|
||||
- [Output Formats](#output-formats)
|
||||
@@ -108,104 +108,6 @@ concurrency: 100
|
||||
|
||||
</details>
|
||||
|
||||
## Dynamic Requests with Templating
|
||||
|
||||
Generate a random User-Agent for each request:
|
||||
|
||||
```sh
|
||||
sarin -U http://example.com -r 1000 -c 10 \
|
||||
-H "User-Agent: {{ fakeit_UserAgent }}"
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>YAML equivalent</summary>
|
||||
|
||||
```yaml
|
||||
url: http://example.com
|
||||
requests: 1000
|
||||
concurrency: 10
|
||||
headers:
|
||||
User-Agent: "{{ fakeit_UserAgent }}"
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
Send requests with random user data:
|
||||
|
||||
```sh
|
||||
sarin -U http://example.com/api/users -r 1000 -c 10 \
|
||||
-M POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-B '{"name": "{{ fakeit_Name }}", "email": "{{ fakeit_Email }}"}'
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>YAML equivalent</summary>
|
||||
|
||||
```yaml
|
||||
url: http://example.com/api/users
|
||||
requests: 1000
|
||||
concurrency: 10
|
||||
method: POST
|
||||
headers:
|
||||
Content-Type: application/json
|
||||
body: '{"name": "{{ fakeit_Name }}", "email": "{{ fakeit_Email }}"}'
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
Use values to share generated data across headers and body:
|
||||
|
||||
```sh
|
||||
sarin -U http://example.com/api/users -r 1000 -c 10 \
|
||||
-M POST \
|
||||
-V "ID={{ fakeit_UUID }}" \
|
||||
-H "X-Request-ID: {{ .Values.ID }}" \
|
||||
-B '{"id": "{{ .Values.ID }}", "name": "{{ fakeit_Name }}"}'
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>YAML equivalent</summary>
|
||||
|
||||
```yaml
|
||||
url: http://example.com/api/users
|
||||
requests: 1000
|
||||
concurrency: 10
|
||||
method: POST
|
||||
values: "ID={{ fakeit_UUID }}"
|
||||
headers:
|
||||
X-Request-ID: "{{ .Values.ID }}"
|
||||
body: '{"id": "{{ .Values.ID }}", "name": "{{ fakeit_Name }}"}'
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
Generate random IPs and timestamps:
|
||||
|
||||
```sh
|
||||
sarin -U http://example.com/api/logs -r 500 -c 20 \
|
||||
-M POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-B '{"ip": "{{ fakeit_IPv4Address }}", "timestamp": "{{ fakeit_Date }}", "action": "{{ fakeit_HackerVerb }}"}'
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>YAML equivalent</summary>
|
||||
|
||||
```yaml
|
||||
url: http://example.com/api/logs
|
||||
requests: 500
|
||||
concurrency: 20
|
||||
method: POST
|
||||
headers:
|
||||
Content-Type: application/json
|
||||
body: '{"ip": "{{ fakeit_IPv4Address }}", "timestamp": "{{ fakeit_Date }}", "action": "{{ fakeit_HackerVerb }}"}'
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
> For the complete list of 320+ template functions, see the **[Templating Guide](templating.md)**.
|
||||
|
||||
## Headers, Cookies, and Parameters
|
||||
|
||||
**Custom headers:**
|
||||
@@ -323,6 +225,140 @@ cookies:
|
||||
|
||||
</details>
|
||||
|
||||
## Dynamic Requests with Templating
|
||||
|
||||
**Dynamic URL paths:**
|
||||
|
||||
Test different resource endpoints with random IDs:
|
||||
|
||||
```sh
|
||||
sarin -U "http://example.com/users/{{ fakeit_UUID }}/profile" -r 1000 -c 10
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>YAML equivalent</summary>
|
||||
|
||||
```yaml
|
||||
url: http://example.com/users/{{ fakeit_UUID }}/profile
|
||||
requests: 1000
|
||||
concurrency: 10
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
Test with random numeric IDs:
|
||||
|
||||
```sh
|
||||
sarin -U "http://example.com/products/{{ fakeit_Number 1 10000 }}" -r 1000 -c 10
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>YAML equivalent</summary>
|
||||
|
||||
```yaml
|
||||
url: http://example.com/products/{{ fakeit_Number 1 10000 }}
|
||||
requests: 1000
|
||||
concurrency: 10
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
**Generate a random User-Agent for each request:**
|
||||
|
||||
```sh
|
||||
sarin -U http://example.com -r 1000 -c 10 \
|
||||
-H "User-Agent: {{ fakeit_UserAgent }}"
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>YAML equivalent</summary>
|
||||
|
||||
```yaml
|
||||
url: http://example.com
|
||||
requests: 1000
|
||||
concurrency: 10
|
||||
headers:
|
||||
User-Agent: "{{ fakeit_UserAgent }}"
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
Send requests with random user data:
|
||||
|
||||
```sh
|
||||
sarin -U http://example.com/api/users -r 1000 -c 10 \
|
||||
-M POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-B '{"name": "{{ fakeit_Name }}", "email": "{{ fakeit_Email }}"}'
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>YAML equivalent</summary>
|
||||
|
||||
```yaml
|
||||
url: http://example.com/api/users
|
||||
requests: 1000
|
||||
concurrency: 10
|
||||
method: POST
|
||||
headers:
|
||||
Content-Type: application/json
|
||||
body: '{"name": "{{ fakeit_Name }}", "email": "{{ fakeit_Email }}"}'
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
Use values to share generated data across headers and body:
|
||||
|
||||
```sh
|
||||
sarin -U http://example.com/api/users -r 1000 -c 10 \
|
||||
-M POST \
|
||||
-V "ID={{ fakeit_UUID }}" \
|
||||
-H "X-Request-ID: {{ .Values.ID }}" \
|
||||
-B '{"id": "{{ .Values.ID }}", "name": "{{ fakeit_Name }}"}'
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>YAML equivalent</summary>
|
||||
|
||||
```yaml
|
||||
url: http://example.com/api/users
|
||||
requests: 1000
|
||||
concurrency: 10
|
||||
method: POST
|
||||
values: "ID={{ fakeit_UUID }}"
|
||||
headers:
|
||||
X-Request-ID: "{{ .Values.ID }}"
|
||||
body: '{"id": "{{ .Values.ID }}", "name": "{{ fakeit_Name }}"}'
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
Generate random IPs and timestamps:
|
||||
|
||||
```sh
|
||||
sarin -U http://example.com/api/logs -r 500 -c 20 \
|
||||
-M POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-B '{"ip": "{{ fakeit_IPv4Address }}", "timestamp": "{{ fakeit_Date }}", "action": "{{ fakeit_HackerVerb }}"}'
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>YAML equivalent</summary>
|
||||
|
||||
```yaml
|
||||
url: http://example.com/api/logs
|
||||
requests: 500
|
||||
concurrency: 20
|
||||
method: POST
|
||||
headers:
|
||||
Content-Type: application/json
|
||||
body: '{"ip": "{{ fakeit_IPv4Address }}", "timestamp": "{{ fakeit_Date }}", "action": "{{ fakeit_HackerVerb }}"}'
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
> For the complete list of 320+ template functions, see the **[Templating Guide](templating.md)**.
|
||||
|
||||
## Request Bodies
|
||||
|
||||
**Simple JSON body:**
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Templating
|
||||
|
||||
Sarin supports Go templates in methods, bodies, headers, params, cookies, and values.
|
||||
Sarin supports Go templates in URL paths, methods, bodies, headers, params, cookies, and values.
|
||||
|
||||
> **Note:** Templating in URL host and scheme is not supported. Only the path portion of the URL can contain templates.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
|
||||
@@ -180,6 +180,15 @@ func validateTemplateValues(values []string, funcMap template.FuncMap) []types.F
|
||||
return validationErrors
|
||||
}
|
||||
|
||||
func validateTemplateURLPath(urlPath string, funcMap template.FuncMap) []types.FieldValidationError {
|
||||
if err := validateTemplateString(urlPath, funcMap); err != nil {
|
||||
return []types.FieldValidationError{
|
||||
types.NewFieldValidationError("URL.Path", urlPath, err),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidateTemplates(config *Config) []types.FieldValidationError {
|
||||
// Create template function map using the same functions as sarin package
|
||||
randSource := sarin.NewDefaultRandSource()
|
||||
@@ -190,6 +199,11 @@ func ValidateTemplates(config *Config) []types.FieldValidationError {
|
||||
|
||||
var allErrors []types.FieldValidationError
|
||||
|
||||
// Validate URL path
|
||||
if config.URL != nil {
|
||||
allErrors = append(allErrors, validateTemplateURLPath(config.URL.Path, funcMap)...)
|
||||
}
|
||||
|
||||
// Validate methods
|
||||
allErrors = append(allErrors, validateTemplateMethods(config.Methods, funcMap)...)
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ func NewRequestGenerator(
|
||||
localRand := rand.New(randSource)
|
||||
templateFuncMap := NewDefaultTemplateFuncMap(randSource)
|
||||
|
||||
pathGenerator, isPathGeneratorDynamic := createTemplateFunc(requestURL.Path, templateFuncMap)
|
||||
methodGenerator, isMethodGeneratorDynamic := NewMethodGeneratorFunc(localRand, methods, templateFuncMap)
|
||||
paramsGenerator, isParamsGeneratorDynamic := NewParamsGeneratorFunc(localRand, params, templateFuncMap)
|
||||
headersGenerator, isHeadersGeneratorDynamic := NewHeadersGeneratorFunc(localRand, headers, templateFuncMap)
|
||||
@@ -51,35 +52,45 @@ func NewRequestGenerator(
|
||||
|
||||
valuesGenerator := NewValuesGeneratorFunc(values, templateFuncMap)
|
||||
|
||||
var (
|
||||
data valuesData
|
||||
path string
|
||||
err error
|
||||
)
|
||||
return func(req *fasthttp.Request) error {
|
||||
req.SetRequestURI(requestURL.Path)
|
||||
req.Header.SetHost(requestURL.Host)
|
||||
|
||||
data, err := valuesGenerator()
|
||||
data, err = valuesGenerator()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := methodGenerator(req, data); err != nil {
|
||||
path, err = pathGenerator(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.SetRequestURI(path)
|
||||
|
||||
if err = methodGenerator(req, data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bodyTemplateFuncMapData.ClearFormDataContenType()
|
||||
if err := bodyGenerator(req, data); err != nil {
|
||||
if err = bodyGenerator(req, data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := headersGenerator(req, data); err != nil {
|
||||
if err = headersGenerator(req, data); err != nil {
|
||||
return err
|
||||
}
|
||||
if bodyTemplateFuncMapData.GetFormDataContenType() != "" {
|
||||
req.Header.Add("Content-Type", bodyTemplateFuncMapData.GetFormDataContenType())
|
||||
}
|
||||
|
||||
if err := paramsGenerator(req, data); err != nil {
|
||||
if err = paramsGenerator(req, data); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cookiesGenerator(req, data); err != nil {
|
||||
if err = cookiesGenerator(req, data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -87,7 +98,8 @@ func NewRequestGenerator(
|
||||
req.URI().SetScheme("https")
|
||||
}
|
||||
return nil
|
||||
}, isMethodGeneratorDynamic ||
|
||||
}, isPathGeneratorDynamic ||
|
||||
isMethodGeneratorDynamic ||
|
||||
isParamsGeneratorDynamic ||
|
||||
isHeadersGeneratorDynamic ||
|
||||
isCookiesGeneratorDynamic ||
|
||||
|
||||
Reference in New Issue
Block a user