From 27bc8f2e9633cec8c96fc9cd75d5c31de55596f8 Mon Sep 17 00:00:00 2001 From: Aykhan Shahsuvarov Date: Sun, 11 Jan 2026 19:05:58 +0400 Subject: [PATCH 1/2] Add URL path templating support with validation and documentation --- README.md | 22 +-- docs/configuration.md | 16 +- docs/examples.md | 234 +++++++++++++++----------- docs/templating.md | 4 +- internal/config/template_validator.go | 14 ++ internal/sarin/request.go | 28 ++- 6 files changed, 192 insertions(+), 126 deletions(-) diff --git a/README.md b/README.md index 40be526..74287ad 100644 --- a/README.md +++ b/README.md @@ -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)**. diff --git a/docs/configuration.md b/docs/configuration.md index ab0c7f2..69de6d0 100644 --- a/docs/configuration.md +++ b/docs/configuration.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 diff --git a/docs/examples.md b/docs/examples.md index e5d245f..05b3222 100644 --- a/docs/examples.md +++ b/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 -## 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 }}" -``` - -
-YAML equivalent - -```yaml -url: http://example.com -requests: 1000 -concurrency: 10 -headers: - User-Agent: "{{ fakeit_UserAgent }}" -``` - -
- -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 }}"}' -``` - -
-YAML equivalent - -```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 }}"}' -``` - -
- -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 }}"}' -``` - -
-YAML equivalent - -```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 }}"}' -``` - -
- -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 }}"}' -``` - -
-YAML equivalent - -```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 }}"}' -``` - -
- -> 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: +## 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 +``` + +
+YAML equivalent + +```yaml +url: http://example.com/users/{{ fakeit_UUID }}/profile +requests: 1000 +concurrency: 10 +``` + +
+ +Test with random numeric IDs: + +```sh +sarin -U "http://example.com/products/{{ fakeit_Number 1 10000 }}" -r 1000 -c 10 +``` + +
+YAML equivalent + +```yaml +url: http://example.com/products/{{ fakeit_Number 1 10000 }} +requests: 1000 +concurrency: 10 +``` + +
+ +**Generate a random User-Agent for each request:** + +```sh +sarin -U http://example.com -r 1000 -c 10 \ + -H "User-Agent: {{ fakeit_UserAgent }}" +``` + +
+YAML equivalent + +```yaml +url: http://example.com +requests: 1000 +concurrency: 10 +headers: + User-Agent: "{{ fakeit_UserAgent }}" +``` + +
+ +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 }}"}' +``` + +
+YAML equivalent + +```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 }}"}' +``` + +
+ +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 }}"}' +``` + +
+YAML equivalent + +```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 }}"}' +``` + +
+ +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 }}"}' +``` + +
+YAML equivalent + +```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 }}"}' +``` + +
+ +> For the complete list of 320+ template functions, see the **[Templating Guide](templating.md)**. + ## Request Bodies **Simple JSON body:** diff --git a/docs/templating.md b/docs/templating.md index 5a4bf62..05ae26f 100644 --- a/docs/templating.md +++ b/docs/templating.md @@ -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 diff --git a/internal/config/template_validator.go b/internal/config/template_validator.go index 72de180..6618209 100644 --- a/internal/config/template_validator.go +++ b/internal/config/template_validator.go @@ -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)...) diff --git a/internal/sarin/request.go b/internal/sarin/request.go index 96768c9..9b4604e 100644 --- a/internal/sarin/request.go +++ b/internal/sarin/request.go @@ -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 || From 92d0c5e003e9d7a8d940992f6944156563ecc297 Mon Sep 17 00:00:00 2001 From: Aykhan Shahsuvarov Date: Sun, 11 Jan 2026 21:40:38 +0400 Subject: [PATCH 2/2] Revise feature support table in README Updated the supported and not supported features table for clarity. --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 74287ad..1756621 100644 --- a/README.md +++ b/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
(HTTP, HTTPS, SOCKS5, SOCKS5H) | Scripting or multi-step scenarios | +| Flexible config (CLI, ENV, YAML) | HTTP/2, HTTP/3, WebSocket, gRPC | ## Installation