diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..520ddfe --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,9 @@ +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.12" + +mkdocs: + configuration: mkdocs.yml diff --git a/docs/cli.md b/docs/cli.md new file mode 100644 index 0000000..234c056 --- /dev/null +++ b/docs/cli.md @@ -0,0 +1,72 @@ +# CLI Reference + +## loadforge run + +Run a load test from a scenario file. + +```bash +loadforge run [scenario.yaml] [flags] +``` + +| Flag | Short | Description | +|------|-------|-------------| +| `--workers` | `-w` | Override worker count from config | +| `--duration` | `-d` | Override duration (e.g. `30s`, `2m`) | +| `--output` | `-o` | Save report to a JSON file | +| `--var` | | Set a variable (`key=value`) | +| `--env-file` | | Load variables from a `.env` file | +| `--no-ui` | | Plain text output instead of the terminal UI | +| `--verbose` | `-v` | Enable verbose logging | + +### Examples + +```bash +# Run with defaults from the file +loadforge run scenario.yaml + +# Override workers and duration +loadforge run scenario.yaml --workers 50 --duration 2m + +# Inject a variable +loadforge run scenario.yaml --var BASE_URL=https://staging.example.com + +# Load variables from .env +loadforge run scenario.yaml --env-file .env +``` + +--- + +## loadforge validate + +Validate a scenario file without running it. + +```bash +loadforge validate [scenario.yaml] +``` + +--- + +## loadforge version + +Print the installed version. + +```bash +loadforge version +``` + +--- + +## loadforge --uninstall + +Stop the web service and remove all LoadForge files from the system. + +```bash +sudo loadforge --uninstall +``` + +This removes: + +- `/usr/local/bin/loadforge` +- `/usr/local/bin/loadforge-web` +- `~/.loadforge/` (config, history, logs) +- The background service (systemd or launchd) diff --git a/docs/configuration/assertions.md b/docs/configuration/assertions.md new file mode 100644 index 0000000..2c3ffcf --- /dev/null +++ b/docs/configuration/assertions.md @@ -0,0 +1,66 @@ +# Assertions + +Assertions define SLA thresholds that automatically mark a test as passed or failed. + +## Configuration + +```yaml +assertions: + - metric: p95_latency + operator: less_than + value: 500 + enabled: true +``` + +## Available metrics + +| Metric | Description | +|--------|-------------| +| `p50_latency` | 50th percentile latency (ms) | +| `p90_latency` | 90th percentile latency (ms) | +| `p95_latency` | 95th percentile latency (ms) | +| `p99_latency` | 99th percentile latency (ms) | +| `avg_latency` | Average latency (ms) | +| `max_latency` | Maximum latency (ms) | +| `rps` | Requests per second | +| `error_rate` | Percentage of failed requests | +| `success_rate` | Percentage of successful requests | +| `total_requests` | Total requests made | +| `total_errors` | Total failed requests | + +## Available operators + +| Operator | Meaning | +|----------|---------| +| `less_than` | metric < value | +| `less_than_or_equal` | metric <= value | +| `greater_than` | metric > value | +| `greater_than_or_equal` | metric >= value | +| `equal` | metric == value | + +## Example + +```yaml +assertions: + - metric: p95_latency + operator: less_than + value: 500 + enabled: true + + - metric: p99_latency + operator: less_than + value: 1000 + enabled: true + + - metric: error_rate + operator: less_than + value: 1 + enabled: true + + - metric: rps + operator: greater_than + value: 100 + enabled: true +``` + +Use `enabled: false` to define an assertion without enforcing it — useful for tracking a metric without failing the test. diff --git a/docs/configuration/auth.md b/docs/configuration/auth.md new file mode 100644 index 0000000..338837d --- /dev/null +++ b/docs/configuration/auth.md @@ -0,0 +1,46 @@ +# Auth + +Authentication can be set per step in the scenario file. + +## Basic auth + +```yaml +steps: + - method: GET + url: /secure + auth: + basic: + username: admin + password: secret +``` + +## Bearer token + +```yaml +steps: + - method: GET + url: /secure + auth: + bearer: your-token-here +``` + +## Custom header + +```yaml +steps: + - method: GET + url: /secure + auth: + header: + key: X-API-Key + value: your-api-key +``` + +Or use it for any arbitrary header-based auth scheme: + +```yaml +auth: + header: + key: Authorization + value: "Token abc123" +``` diff --git a/docs/configuration/load-profiles.md b/docs/configuration/load-profiles.md new file mode 100644 index 0000000..b505faf --- /dev/null +++ b/docs/configuration/load-profiles.md @@ -0,0 +1,77 @@ +# Load Profiles + +A load profile controls how worker concurrency changes over time. + +## constant + +Fixed number of workers for the full duration. Use this for steady-state benchmarks. + +```yaml +load: + profile: constant + workers: 20 + duration: 60s +``` + +--- + +## ramp + +Linearly increases workers from `start_workers` to `end_workers` over the ramp duration. Use this to find the degradation point without shocking the system. + +```yaml +load: + profile: ramp + duration: 60s + ramp_up: + start_workers: 1 + end_workers: 50 + duration: 30s +``` + +--- + +## step + +Adds workers in fixed increments at regular intervals. Use this to observe how latency changes as load increases in discrete steps. + +```yaml +load: + profile: step + duration: 60s + step: + start_workers: 5 + step_size: 5 + step_duration: 10s + max_workers: 50 +``` + +--- + +## spike + +Runs a base level of workers with periodic traffic bursts. Use this to test how your system recovers after a sudden surge. + +```yaml +load: + profile: spike + duration: 60s + spike: + base_workers: 10 + spike_workers: 50 + spike_duration: 5s + spike_every: 15s +``` + +--- + +## Stopping conditions + +Use `duration` to run for a fixed time, or `max_requests` to stop after a total request count: + +```yaml +load: + profile: constant + workers: 10 + max_requests: 1000 +``` diff --git a/docs/configuration/scenario.md b/docs/configuration/scenario.md new file mode 100644 index 0000000..ba76255 --- /dev/null +++ b/docs/configuration/scenario.md @@ -0,0 +1,88 @@ +# Scenario File + +LoadForge scenarios are defined in YAML. A scenario file describes the requests to send, how to authenticate, and how much load to apply. + +## Full reference + +```yaml +name: Test Name +base_url: https://api.example.com + +scenarios: + - name: Scenario Name + weight: 100 # Relative probability when multiple scenarios are defined + steps: + - name: Step Name + method: GET # GET, POST, PUT, PATCH, DELETE, HEAD + url: /path # Relative (uses base_url) or absolute URL + + headers: + X-Custom-Header: value + + body: + raw: "raw string body" + json: + key: value + form: + field: value + + auth: + basic: + username: user + password: pass + bearer: your-token + header: + key: Authorization + value: Bearer your-token + + options: + timeout: 10s + follow_redirects: true + tls_skip_verify: false + http2: true + + think: 100ms # Pause after this step + +load: + profile: constant # constant | ramp | step | spike + workers: 10 + duration: 30s + +assertions: + - metric: p95_latency + operator: less_than + value: 500 + enabled: true +``` + +## Multiple scenarios + +When multiple scenarios are defined, workers pick one at random weighted by the `weight` field. + +```yaml +scenarios: + - name: Read path + weight: 80 + steps: + - method: GET + url: /items + + - name: Write path + weight: 20 + steps: + - method: POST + url: /items + body: + json: + name: test +``` + +## Body types + +Only one body type is used per step. Priority: `json` → `form` → `raw`. + +| Type | Content-Type set automatically | +|------|-------------------------------| +| `json` | `application/json` | +| `form` | `application/x-www-form-urlencoded` | +| `raw` | none (set manually via `headers`) | diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..0e82789 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,49 @@ +# LoadForge + +> Developer-first HTTP load testing — run from the terminal or a browser-based UI. + +LoadForge is an open-source HTTP load testing tool built for developers and QA engineers who want to stress-test their APIs without fighting complex configuration or expensive SaaS platforms. + +Write a simple YAML scenario, point it at your API, and LoadForge hammers it with concurrent workers while giving you live metrics in a clean terminal UI or a browser dashboard. + +--- + +## Why LoadForge? + +Most load testing tools are either too heavy (JMeter, Gatling) or too limited (ab, wrk). LoadForge sits in the middle: + +- **Zero dependencies** — a single binary, no JVM, no Node, no Docker required +- **Two interfaces** — use the CLI for scripts and CI pipelines, or the web UI for interactive testing +- **OpenAPI / Swagger support** — point at an API spec URL and LoadForge generates and runs the scenario for you +- **Multiple load profiles** — constant, ramp, step, and spike traffic patterns out of the box +- **Assertions & SLA thresholds** — define pass/fail criteria and catch regressions automatically +- **History tracking** — every run is persisted so you can compare results over time + +--- + +## Features + +| Feature | Description | +|---|---| +| **Web UI** | Browser-based dashboard to trigger, monitor, and review tests | +| **OpenAPI / Swagger import** | Auto-generate scenarios from OpenAPI 3.x and Swagger 2.0 specs | +| **HAR file support** | Convert or replay browser HAR recordings as load tests | +| **Load profiles** | `constant`, `ramp`, `step`, `spike` — choose how traffic behaves | +| **Assertions** | Define SLA thresholds (p95 latency, error rate, RPS) with pass/fail results | +| **Auth support** | Basic auth, Bearer tokens, and custom header auth per request step | +| **Variable injection** | Pass variables via `--var` flags or a `.env` file | +| **History** | Persistent run history with per-request breakdown | +| **Password protection** | Secure the web UI with bcrypt-hashed credentials | +| **Multi-platform** | Linux, macOS, and Windows — amd64 and arm64 | + +--- + +## Get started + +```bash +curl -fsSL https://github.com/farhapartex/loadforge/releases/latest/download/install.sh | sudo bash +``` + +Then open [http://localhost:8090](http://localhost:8090) — the web UI starts automatically. + +See the [Installation](installation.md) guide for full details. diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..a2a357f --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,39 @@ +# Installation + +## Linux / macOS (recommended) + +```bash +curl -fsSL https://github.com/farhapartex/loadforge/releases/latest/download/install.sh | sudo bash +``` + +This single command: + +1. Downloads the latest `loadforge` and `loadforge-web` binaries to `/usr/local/bin/` +2. Creates `~/.loadforge/` with a default configuration +3. Registers and starts the web UI as a background service + +Open [http://localhost:8090](http://localhost:8090) immediately — no extra commands needed. + +Default credentials: **admin / admin** + +--- + +## Manual download + +Pre-built binaries are available on the [releases page](https://github.com/farhapartex/loadforge/releases) for: + +| OS | Architecture | +|---|---| +| Linux | amd64, arm64 | +| macOS | amd64, arm64 | +| Windows | amd64 | + +Download the binary for your platform, make it executable, and move it to a directory on your `PATH`. + +--- + +## Verify installation + +```bash +loadforge version +``` diff --git a/docs/quickstart.md b/docs/quickstart.md new file mode 100644 index 0000000..2d6eb7c --- /dev/null +++ b/docs/quickstart.md @@ -0,0 +1,50 @@ +# Quick Start + +## Web UI + +After installation the web UI starts automatically. Open [http://localhost:8090](http://localhost:8090) in your browser. + +Default credentials: **admin / admin** + +1. Paste an OpenAPI or Swagger spec URL into the input field +2. Set your load parameters (workers, duration, profile) +3. Click **Start Test** + +Live logs stream to the dashboard as the test runs. Past runs appear under **History**. + +--- + +## CLI + +### 1. Write a scenario file + +```yaml title="scenario.yaml" +name: My API Test +base_url: https://api.example.com + +scenarios: + - name: Get users + steps: + - name: List users + method: GET + url: /users + +load: + profile: constant + workers: 10 + duration: 30s +``` + +### 2. Run the test + +```bash +loadforge run scenario.yaml +``` + +A real-time terminal UI shows live metrics. Press `q` to stop early. + +### 3. Validate without running + +```bash +loadforge validate scenario.yaml +``` diff --git a/docs/uninstallation.md b/docs/uninstallation.md new file mode 100644 index 0000000..54fb0e8 --- /dev/null +++ b/docs/uninstallation.md @@ -0,0 +1,11 @@ +# Uninstallation + +```bash +sudo loadforge --uninstall +``` + +This single command: + +1. Stops and removes the background service (systemd on Linux, launchd on macOS) +2. Removes `/usr/local/bin/loadforge` and `/usr/local/bin/loadforge-web` +3. Deletes `~/.loadforge/` including config, run history, and logs diff --git a/docs/web-ui.md b/docs/web-ui.md new file mode 100644 index 0000000..5d6f037 --- /dev/null +++ b/docs/web-ui.md @@ -0,0 +1,71 @@ +# Web UI + +The web UI is a browser-based dashboard for running and reviewing load tests without using the CLI. + +## Accessing the UI + +After installation the web UI starts automatically as a background service. Open [http://localhost:8090](http://localhost:8090). + +Default credentials: **admin / admin** + +Change the credentials in `~/.loadforge/web.yml`: + +```yaml +username: yourname +password: yourpassword +``` + +Then restart the service: + +=== "macOS" + ```bash + sudo launchctl unload /Library/LaunchDaemons/com.loadforge.web.plist + sudo launchctl load /Library/LaunchDaemons/com.loadforge.web.plist + ``` + +=== "Linux" + ```bash + sudo systemctl restart loadforge-web + ``` + +--- + +## Running a test + +1. Paste an OpenAPI or Swagger spec URL into the input field, or select a saved scenario +2. Set load parameters: profile, workers, duration +3. Click **Start Test** + +Live logs stream to the page via Server-Sent Events as the test runs. + +--- + +## History + +Every completed run is saved to `~/.loadforge/load_forge_history.json` (up to 100 entries). The **History** page lists all past runs with: + +- Latency percentiles (P50, P90, P95, P99) +- RPS, error rate, success rate +- Status code distribution +- Assertion pass/fail results + +--- + +## Configuration file + +The web server reads `~/.loadforge/web.yml`: + +```yaml +addr: :8090 +username: admin +password: admin +session_ttl: 24h +log_file: /home/user/.loadforge/loadforge.logs +history_file: /home/user/.loadforge/load_forge_history.json + +assertions: + - metric: p95_latency + operator: less_than + value: 500 + enabled: true +``` diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..a401feb --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,44 @@ +site_name: LoadForge +site_description: Developer-first HTTP load testing — run from the terminal or a browser-based UI +site_url: https://loadforge.readthedocs.io +repo_url: https://github.com/farhapartex/loadforge +repo_name: farhapartex/loadforge +edit_uri: edit/main/docs/ + +theme: + name: material + palette: + scheme: slate + primary: indigo + accent: indigo + features: + - navigation.sections + - navigation.top + - content.code.copy + +nav: + - Home: index.md + - Installation: installation.md + - Quick Start: quickstart.md + - Configuration: + - Scenario File: configuration/scenario.md + - Load Profiles: configuration/load-profiles.md + - Assertions: configuration/assertions.md + - Auth: configuration/auth.md + - Web UI: web-ui.md + - CLI Reference: cli.md + - Uninstallation: uninstallation.md + +markdown_extensions: + - admonition + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.superfences + - pymdownx.tabbed: + alternate_style: true + - tables + - toc: + permalink: true + +plugins: + - search diff --git a/scripts/install.sh b/scripts/install.sh index 8e3301f..000f920 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -219,7 +219,7 @@ remove_service() { do_install() { require curl - local os arch version binary_name web_binary_name download_url web_download_url tmpdir + local os arch version binary_name web_binary_name download_url web_download_url os=$(detect_os) arch=$(detect_arch)