5 Commits
v1.0.0 ... main

Author SHA1 Message Date
a512f3605d Merge pull request #163 from aykhans/dependabot/go_modules/golang.org/x/net-0.49.0
Bump golang.org/x/net from 0.48.0 to 0.49.0
2026-01-13 11:24:54 +04:00
dependabot[bot]
635c33008b Bump golang.org/x/net from 0.48.0 to 0.49.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.48.0 to 0.49.0.
- [Commits](https://github.com/golang/net/compare/v0.48.0...v0.49.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.49.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-13 04:22:03 +00:00
3f2147ec6c Merge pull request #162 from aykhans/feat/url-path-templating
Add URL path templating support with validation and documentation
2026-01-11 21:42:37 +04:00
92d0c5e003 Revise feature support table in README
Updated the supported and not supported features table for clarity.
2026-01-11 21:40:38 +04:00
27bc8f2e96 Add URL path templating support with validation and documentation 2026-01-11 19:05:58 +04:00
8 changed files with 208 additions and 142 deletions

View File

@@ -19,11 +19,11 @@
Sarin is designed for efficient HTTP load testing with minimal resource consumption. It prioritizes simplicity—features like templating add zero overhead when unused. 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 | | ✅ Supported | ❌ Not Supported |
| ---------------------------------------------------- | --------------------------------- | | ---------------------------------------------------------- | --------------------------------- |
| High-performance with low memory footprint | Detailed response body analysis | | High-performance with low memory footprint | Detailed response body analysis |
| Long-running duration/count based tests | Extensive response statistics | | Long-running duration/count based tests | Extensive response statistics |
| Dynamic requests via 320+ template functions | Web UI or complex TUI | | Dynamic requests via 320+ template functions | Web UI or complex TUI |
| Multiple proxy protocols (HTTP/HTTPS/SOCKS5/SOCKS5H) | Scripting or multi-step scenarios | | Multiple proxy protocols<br>(HTTP, HTTPS, SOCKS5, SOCKS5H) | Scripting or multi-step scenarios |
| Flexible config (CLI, ENV, YAML) | HTTP/2, HTTP/3, WebSocket, gRPC | | Flexible config (CLI, ENV, YAML) | HTTP/2, HTTP/3, WebSocket, gRPC |
## Installation ## 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 }}" 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: Run a 5-minute duration-based test:
```sh ```sh
@@ -112,15 +100,15 @@ For detailed documentation on all configuration options (URL, method, timeout, c
## Templating ## 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:** **Example:**
```sh ```sh
sarin -U http://example.com/users \ sarin -U "http://example.com/users/{{ fakeit_UUID }}" -r 1000 -c 10 \
-V "ID={{ fakeit_UUID }}" \ -V "RequestID={{ fakeit_UUID }}" \
-H "X-Request-ID: {{ .Values.ID }}" \ -H "X-Request-ID: {{ .Values.RequestID }}" \
-B '{"id": "{{ .Values.ID }}"}' -B '{"request_id": "{{ .Values.RequestID }}"}'
``` ```
For the complete templating guide and functions reference, see the **[Templating Guide](docs/templating.md)**. For the complete templating guide and functions reference, see the **[Templating Guide](docs/templating.md)**.

View File

@@ -76,7 +76,21 @@ config3.yaml > config2.yaml > config4.yaml > config1.yaml
## URL ## 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 ## Method

View File

@@ -6,8 +6,8 @@ This guide provides practical examples for common Sarin use cases.
- [Basic Usage](#basic-usage) - [Basic Usage](#basic-usage)
- [Request-Based vs Duration-Based Tests](#request-based-vs-duration-based-tests) - [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) - [Headers, Cookies, and Parameters](#headers-cookies-and-parameters)
- [Dynamic Requests with Templating](#dynamic-requests-with-templating)
- [Request Bodies](#request-bodies) - [Request Bodies](#request-bodies)
- [Using Proxies](#using-proxies) - [Using Proxies](#using-proxies)
- [Output Formats](#output-formats) - [Output Formats](#output-formats)
@@ -108,104 +108,6 @@ concurrency: 100
</details> </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 ## Headers, Cookies, and Parameters
**Custom headers:** **Custom headers:**
@@ -323,6 +225,140 @@ cookies:
</details> </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 ## Request Bodies
**Simple JSON body:** **Simple JSON body:**

View File

@@ -1,6 +1,8 @@
# Templating # 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 ## Table of Contents

6
go.mod
View File

@@ -13,7 +13,7 @@ require (
github.com/valyala/fasthttp v1.69.0 github.com/valyala/fasthttp v1.69.0
go.aykhans.me/utils v1.0.7 go.aykhans.me/utils v1.0.7
go.yaml.in/yaml/v4 v4.0.0-rc.3 go.yaml.in/yaml/v4 v4.0.0-rc.3
golang.org/x/net v0.48.0 golang.org/x/net v0.49.0
) )
require ( require (
@@ -50,6 +50,6 @@ require (
github.com/yuin/goldmark v1.7.16 // indirect github.com/yuin/goldmark v1.7.16 // indirect
github.com/yuin/goldmark-emoji v1.0.6 // indirect github.com/yuin/goldmark-emoji v1.0.6 // indirect
golang.org/x/sys v0.40.0 // indirect golang.org/x/sys v0.40.0 // indirect
golang.org/x/term v0.38.0 // indirect golang.org/x/term v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect golang.org/x/text v0.33.0 // indirect
) )

12
go.sum
View File

@@ -101,15 +101,15 @@ go.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go=
go.yaml.in/yaml/v4 v4.0.0-rc.3/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= go.yaml.in/yaml/v4 v4.0.0-rc.3/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -180,6 +180,15 @@ func validateTemplateValues(values []string, funcMap template.FuncMap) []types.F
return validationErrors 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 { func ValidateTemplates(config *Config) []types.FieldValidationError {
// Create template function map using the same functions as sarin package // Create template function map using the same functions as sarin package
randSource := sarin.NewDefaultRandSource() randSource := sarin.NewDefaultRandSource()
@@ -190,6 +199,11 @@ func ValidateTemplates(config *Config) []types.FieldValidationError {
var allErrors []types.FieldValidationError var allErrors []types.FieldValidationError
// Validate URL path
if config.URL != nil {
allErrors = append(allErrors, validateTemplateURLPath(config.URL.Path, funcMap)...)
}
// Validate methods // Validate methods
allErrors = append(allErrors, validateTemplateMethods(config.Methods, funcMap)...) allErrors = append(allErrors, validateTemplateMethods(config.Methods, funcMap)...)

View File

@@ -40,6 +40,7 @@ func NewRequestGenerator(
localRand := rand.New(randSource) localRand := rand.New(randSource)
templateFuncMap := NewDefaultTemplateFuncMap(randSource) templateFuncMap := NewDefaultTemplateFuncMap(randSource)
pathGenerator, isPathGeneratorDynamic := createTemplateFunc(requestURL.Path, templateFuncMap)
methodGenerator, isMethodGeneratorDynamic := NewMethodGeneratorFunc(localRand, methods, templateFuncMap) methodGenerator, isMethodGeneratorDynamic := NewMethodGeneratorFunc(localRand, methods, templateFuncMap)
paramsGenerator, isParamsGeneratorDynamic := NewParamsGeneratorFunc(localRand, params, templateFuncMap) paramsGenerator, isParamsGeneratorDynamic := NewParamsGeneratorFunc(localRand, params, templateFuncMap)
headersGenerator, isHeadersGeneratorDynamic := NewHeadersGeneratorFunc(localRand, headers, templateFuncMap) headersGenerator, isHeadersGeneratorDynamic := NewHeadersGeneratorFunc(localRand, headers, templateFuncMap)
@@ -51,35 +52,45 @@ func NewRequestGenerator(
valuesGenerator := NewValuesGeneratorFunc(values, templateFuncMap) valuesGenerator := NewValuesGeneratorFunc(values, templateFuncMap)
var (
data valuesData
path string
err error
)
return func(req *fasthttp.Request) error { return func(req *fasthttp.Request) error {
req.SetRequestURI(requestURL.Path)
req.Header.SetHost(requestURL.Host) req.Header.SetHost(requestURL.Host)
data, err := valuesGenerator() data, err = valuesGenerator()
if err != nil { if err != nil {
return err 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 return err
} }
bodyTemplateFuncMapData.ClearFormDataContenType() bodyTemplateFuncMapData.ClearFormDataContenType()
if err := bodyGenerator(req, data); err != nil { if err = bodyGenerator(req, data); err != nil {
return err return err
} }
if err := headersGenerator(req, data); err != nil { if err = headersGenerator(req, data); err != nil {
return err return err
} }
if bodyTemplateFuncMapData.GetFormDataContenType() != "" { if bodyTemplateFuncMapData.GetFormDataContenType() != "" {
req.Header.Add("Content-Type", 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 return err
} }
if err := cookiesGenerator(req, data); err != nil { if err = cookiesGenerator(req, data); err != nil {
return err return err
} }
@@ -87,7 +98,8 @@ func NewRequestGenerator(
req.URI().SetScheme("https") req.URI().SetScheme("https")
} }
return nil return nil
}, isMethodGeneratorDynamic || }, isPathGeneratorDynamic ||
isMethodGeneratorDynamic ||
isParamsGeneratorDynamic || isParamsGeneratorDynamic ||
isHeadersGeneratorDynamic || isHeadersGeneratorDynamic ||
isCookiesGeneratorDynamic || isCookiesGeneratorDynamic ||