Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: relocate cli code #71

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Binary files
/bin

# IDE files
/.vscode
/.idea
Expand Down
173 changes: 113 additions & 60 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,93 +16,146 @@

## About

Benchttp engine is a Go library providing a way to perform benchmarks and tests
on HTTP endpoints.
Benchttp is a command line tool for end-to-end performance testing of HTTP endpoints.

![Benchttp demo](doc/demo.gif)

You can define performance targets for an endpoint in a declarative way and run them from the command line.

The test suite will exits with a status code 0 if successful or 1 if any test failed. This makes Benchttp very interoperable with a CI.
You can making sure your changes do not introduce any perfomance regressions.

![Benchttp test suite](doc/test-suite.png)

## Installation

### Prerequisites
### Manual download

Download the latest release from the [releases page](https://github.com/benchttp/engine/releases) that matches your operating system and architecture (format is `benchttp_<os>_<architecture>`).

Rename the binary to `benchttp` and move it to a directory in your `PATH`.

```bash
mv benchttp_<os>_<architecture> benchttp

export PATH=$PATH:/path/to/benchttp

Go1.17 environment or higher is required.
benchttp version
```

Install.
## Command line usage

```txt
go get github.com/benchttp/engine
```bash
# Command syntax
benchttp run [options]
```

## Usage
If no options are provided, benchttp will use a default configuration with minimal options.
Only the URL is always required.

### Basic usage
```bash
benchttp run -url https://example.com
```

```go
package main
### Configuration

import (
"context"
"fmt"
You can override the default configuration and fine tune the Benchttp runner by either providing a configuration file (YAML or JSON) with the `-configFile` flag, or by passing individual flags to the `run` command.

"github.com/benchttp/engine/benchttp"
)
Mixing configuration file and flags is possible, the flags will override the configuration file.

func main() {
report, _ := benchttp.
DefaultRunner(). // Default runner with safe configuration
WithNewRequest("GET", "http://localhost:3000", nil). // Attach request
Run(context.Background()) // Run benchmark, retrieve report
### Specification

fmt.Println(report.Metrics.ResponseTimes.Mean)
}
```
Every option can be set either via command line flags or a configuration file, expect for command line only options (see below). Option names always match between the two.

### Usage with JSON config via `configio`
#### HTTP request options

```go
package main
| CLI flag | File option | Description | Usage example |
| --------- | --------------------- | ------------------------- | ----------------------------------------- |
| `-url` | `request.url` | Target URL (**Required**) | `-url http://localhost:8080/users?page=3` |
| `-method` | `request.method` | HTTP Method | `-method POST` |
| - | `request.queryParams` | Added query params to URL | - |
| `-header` | `request.header` | Request headers | `-header 'key0:val0' -header 'key1:val1'` |
| `-body` | `request.body` | Raw request body | `-body 'raw:{"id":"abc"}'` |

import (
"context"
"fmt"
#### Runner options

"github.com/benchttp/engine/benchttp"
"github.com/benchttp/engine/configio"
)
| CLI flag | File option | Description | Usage example |
| ----------------- | ----------------------- | -------------------------------------------------------------------- | -------------------- |
| `-requests` | `runner.requests` | Number of requests to run (-1 means infinite, stop on globalTimeout) | `-requests 100` |
| `-concurrency` | `runner.concurrency` | Maximum concurrent requests | `-concurrency 10` |
| `-interval` | `runner.interval` | Minimum duration between two non-concurrent requests | `-interval 200ms` |
| `-requestTimeout` | `runner.requestTimeout` | Timeout for every single request | `-requestTimeout 5s` |
| `-globalTimeout` | `runner.globalTimeout` | Timeout for the whole benchmark | `-globalTimeout 30s` |

func main() {
// JSON configuration obtained via e.g. a file or HTTP call
jsonConfig := []byte(`
{
"request": {
"url": "http://localhost:3000"
}
}`)
Note: the expected format for durations is `<int><unit>`, with `unit` being any of `ns`, `µs`, `ms`, `s`, `m`, `h`.

// Instantiate a base Runner (here the default with a safe configuration)
runner := benchttp.DefaultRunner()
#### Test suite options

// Parse the json configuration into the Runner
_ = configio.UnmarshalJSONRunner(jsonConfig, &runner)
Test suite options are only available via configuration file.
They must be declared in a configuration file. There is currently no way to set these via cli options.

// Run benchmark, retrieve report
report, _ := runner.Run(context.Background())
Refer to [our Wiki](https://github.com/benchttp/engine/wiki/IO-Structures#yaml) for how to configure test suite.
You can define a test for every available [fields](https://github.com/benchttp/engine/wiki/Fields).

fmt.Println(report.Metrics.ResponseTimes.Mean)
}
```
#### Command line only options

| CLI flag | Description | Usage example |
| ------------- | ---------------------------- | ---------------------------------- |
| `-silent` | Remove convenience prints | `-silent` / `-silent=false` |
| `-configFile` | Path to benchttp config file | `-configFile=path/to/benchttp.yml` |

## Use in CI

Benchttp can aslo be used in CI. A GitHub action is available [here](https://github.com/benchttp/action).

## How does the configuration work?

📄 Please refer to [our Wiki](https://github.com/benchttp/engine/wiki/IO-Structures) for exhaustive `Runner` and `Report` structures (and more!)
The runner uses a default configuration that can be overridden by a configuration file and/or flags. To determine the final configuration of a benchmark and which options take predecence over the others, the runner follows this flow:

1. It starts with a [default configuration](./examples/config/default.yml)
2. Then it tries to find a configuration file and overrides the defaults with the values set in it

- If flag `-configFile` is set, it resolves its value as a path
- Else, it tries to find a config file in the working directory, by priority order:
`.benchttp.yml` > `.benchttp.yaml` > `.benchttp.json`

The configuration file is _optional_: if none is found, this step is ignored.
If a configuration file has an option `extends`, it resolves all files recursively until the root is reached and overrides the values from parent to child.

3. Then it overrides the current config values with any value set via command line flags
4. Finally, it performs a validation on the resulting config (not before!).
This allows composed configurations for better granularity.

## Development

### Prerequisites
Requires Go version 1.17 or higher.

Build for all platforms:

```bash
./script/build
```

```bash
./script/build
```

Test:

```bash
./script/test
```

1. Go 1.17 or higher is required
1. Golangci-lint for linting files
Lint:

### Main commands
Requires [golangci-lint](https://golangci-lint.run/).

| Command | Description |
| --------------- | ------------------------------------------------- |
| `./script/lint` | Runs lint on the codebase |
| `./script/test` | Runs tests suites from all packages |
| `./script/doc` | Serves Go doc for this module at `localhost:9995` |
```bash
./script/lint
```

Serve Go doc:

```bash
./script/doc
```
139 changes: 139 additions & 0 deletions cli/configflag/bind.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package configflag

import (
"bytes"
"errors"
"flag"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"

"github.com/benchttp/engine/configio"
)

// Bind reads arguments provided to flagset as config fields
// and binds their value to the appropriate fields of dst.
// The provided *flag.Flagset must not have been parsed yet, otherwise
// bindings its values would fail.
func Bind(flagset *flag.FlagSet, dst *configio.Builder) {
for field, bind := range bindings {
flagset.Func(field, flagsUsage[field], bind(dst))
}
}

type setter = func(string) error

var bindings = map[string]func(*configio.Builder) setter{
flagMethod: func(b *configio.Builder) setter {
return func(in string) error {
b.SetRequestMethod(in)
return nil
}
},
flagURL: func(b *configio.Builder) setter {
return func(in string) error {
u, err := url.ParseRequestURI(in)
if err != nil {
return err
}
b.SetRequestURL(u)
return nil
}
},
flagHeader: func(b *configio.Builder) setter {
return func(in string) error {
keyval := strings.SplitN(in, ":", 2)
if len(keyval) != 2 {
return errors.New(`-header: expect format "<key>:<value>"`)
}
key, val := keyval[0], keyval[1]
b.SetRequestHeaderFunc(func(h http.Header) http.Header {
if h == nil {
h = http.Header{}
}
h[key] = append(h[key], val)
return h
})
return nil
}
},
flagBody: func(b *configio.Builder) setter {
return func(in string) error {
errFormat := fmt.Errorf(`expect format "<type>:<content>", got %q`, in)
if in == "" {
return errFormat
}
split := strings.SplitN(in, ":", 2)
if len(split) != 2 {
return errFormat
}
btype, bcontent := split[0], split[1]
if bcontent == "" {
return errFormat
}
switch btype {
case "raw":
b.SetRequestBody(io.NopCloser(bytes.NewBufferString(bcontent)))
// case "file":
// // TODO
default:
return fmt.Errorf(`unsupported type: %s (only "raw" accepted)`, btype)
}
return nil
}
},
flagRequests: func(b *configio.Builder) setter {
return func(in string) error {
n, err := strconv.Atoi(in)
if err != nil {
return err
}
b.SetRequests(n)
return nil
}
},
flagConcurrency: func(b *configio.Builder) setter {
return func(in string) error {
n, err := strconv.Atoi(in)
if err != nil {
return err
}
b.SetConcurrency(n)
return nil
}
},
flagInterval: func(b *configio.Builder) setter {
return func(in string) error {
d, err := time.ParseDuration(in)
if err != nil {
return err
}
b.SetInterval(d)
return nil
}
},
flagRequestTimeout: func(b *configio.Builder) setter {
return func(in string) error {
d, err := time.ParseDuration(in)
if err != nil {
return err
}
b.SetRequestTimeout(d)
return nil
}
},
flagGlobalTimeout: func(b *configio.Builder) setter {
return func(in string) error {
d, err := time.ParseDuration(in)
if err != nil {
return err
}
b.SetGlobalTimeout(d)
return nil
}
},
}
Loading