Skip to content

Commit

Permalink
Merge pull request #8 from rogerwelin/refactor-project-structure
Browse files Browse the repository at this point in the history
Refactor project structure
  • Loading branch information
rogerwelin committed Jan 16, 2020
2 parents d937504 + f2cab42 commit aa7459b
Show file tree
Hide file tree
Showing 17 changed files with 381 additions and 278 deletions.
2 changes: 1 addition & 1 deletion .gitignore
@@ -1,3 +1,3 @@
apa*
cassowary
out.json
go-dist
5 changes: 3 additions & 2 deletions .goreleaser.yml
@@ -1,14 +1,15 @@
# This is an example goreleaser.yaml file with some sane defaults.
# Make sure to check the documentation at http://goreleaser.com
before:
hooks:
# you may remove this if you don't use vgo
- go mod tidy
# you may remove this if you don't need go generate
- go generate ./...
dist: go-dist
builds:
- env:
- CGO_ENABLED=0
main: ./cmd/cassowary
binary: cassowary
goos:
- linux
- darwin
Expand Down
66 changes: 62 additions & 4 deletions README.md
Expand Up @@ -20,6 +20,7 @@ Features
- **Flexible metrics**: Prometheus metrics (pushing metrics to Prometheus PushGateway), JSON file
- **Configurable**: Able to pass in arbitrary HTTP headers
- **Cross Platform**: One single pre-built binary for Linux, Mac OSX and Windows
- **Importable** - Besides the CLI tool cassowary can be imported as a module in your Go app

<img src="https://i.imgur.com/geJykYH.gif" />

Expand All @@ -41,7 +42,7 @@ Running Cassowary

Example running **cassowary** against www.example.com with 100 requests spread out over 10 concurrent users:

```
```bash
$ ./cassowary run -u http://www.example.com -c 10 -n 100

Starting Load Test with 100 requests using 10 concurrent users
Expand All @@ -62,7 +63,7 @@ Summary:

Example running **cassowary** in file slurp mode where all URL paths are specified from an external file (which can also be fetched from http if specified):

```
```bash
$ ./cassowary run-file -u http://localhost:8000 -c 10 -f urlpath.txt

Starting Load Test with 3925 requests using 10 concurrent users
Expand All @@ -83,8 +84,8 @@ Summary:

Example exporting **cassowary** json metrics to a file:

```sh
$ ./cassowary run --json-metrics --json-metrics-file=metrics.json -u http://localhost:5000 -c 125 -n 100000
```bash
$ ./cassowary run --json-metrics --json-metrics-file=metrics.json -u http://localhost:8000 -c 125 -n 100000

Starting Load Test with 100000 requests using 125 concurrent users

Expand All @@ -104,6 +105,63 @@ Summary:

> If `json-metrics-file` flag is missing then the default filename is `out.json`.

Example adding an HTTP header when running **cassowary**

```bash
$ ./cassowary run -u http://localhost:8000 -c 10 -n 1000 -H 'Host: www.example.com'

Starting Load Test with 1000 requests using 10 concurrent users

[ omitted for brevity ]

```

**Importing cassowary as a module/library**

Cassowary can be imported and used as a module in your Go app. Start by fetching the dependency by using go mod:

```bash
$ go mod init test && go get github.com/rogerwelin/cassowary/pkg/client
```

And below show a simple example on how to trigger a load test from your code and printing the results:

```go
package main

import (
"fmt"

"github.com/rogerwelin/cassowary/pkg/client"
)

func main() {
cass := &client.Cassowary{
BaseURL: "http://www.example.com",
ConcurrencyLevel: 1,
Requests: 10,
DisableTerminalOutput: true,
}
metrics, err := cass.Coordinate()
if err != nil {
panic(err)
}

// print results
fmt.Printf("%+v\n", metrics)

// or print as json
jsonMetrics, err := json.Marshal(metrics)
if err != nil {
panic(err)
}

fmt.Println(string(jsonMetrics))
}
```


Project Status & Contribute
--------

Expand Down
141 changes: 83 additions & 58 deletions cli.go → cmd/cassowary/cli.go
@@ -1,41 +1,72 @@
package main

import (
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
"strconv"

"github.com/schollz/progressbar"
"github.com/fatih/color"
"github.com/rogerwelin/cassowary/pkg/client"
"github.com/urfave/cli"
)

var (
version = "dev"
errConcurrencyLevel = errors.New("Error: Concurrency level cannot be set to: 0")
errRequestNo = errors.New("Error: No. of request cannot be set to: 0")
errNotValidURL = errors.New("Error: Not a valid URL. Must have the following format: http{s}://{host}")
errNotValidHeader = errors.New("Error: Not a valid header value. Did you forget : ?")
)

type cassowary struct {
fileMode bool
isTLS bool
inputFile string
baseURL string
concurrencyLevel int
requests int
exportMetrics bool
// The filename which json metrics should written
// to if `exportMetrics` is true, otherwise it defaults to "out.json".
exportMetricsFile string
promExport bool
promURL string
requestHeader []string
client *http.Client
bar *progressbar.ProgressBar
func outPutResults(metrics client.ResultMetrics) {
printf(summaryTable,
color.CyanString(fmt.Sprintf("%.2f", metrics.TCPStats.TCPMean)),
color.CyanString(fmt.Sprintf("%.2f", metrics.TCPStats.TCPMedian)),
color.CyanString(fmt.Sprintf("%.2f", metrics.TCPStats.TCP95p)),
color.CyanString(fmt.Sprintf("%.2f", metrics.ProcessingStats.ServerProcessingMean)),
color.CyanString(fmt.Sprintf("%.2f", metrics.ProcessingStats.ServerProcessingMedian)),
color.CyanString(fmt.Sprintf("%.2f", metrics.ProcessingStats.ServerProcessing95p)),
color.CyanString(fmt.Sprintf("%.2f", metrics.ContentStats.ContentTransferMean)),
color.CyanString(fmt.Sprintf("%.2f", metrics.ContentStats.ContentTransferMedian)),
color.CyanString(fmt.Sprintf("%.2f", metrics.ContentStats.ContentTransfer95p)),
color.CyanString(strconv.Itoa(metrics.TotalRequests)),
color.CyanString(strconv.Itoa(metrics.FailedRequests)),
color.CyanString(fmt.Sprintf("%.2f", metrics.DNSMedian)),
color.CyanString(fmt.Sprintf("%.2f", metrics.RequestsPerSecond)),
)
}

func validateRun(c *cli.Context) error {
func outPutJSON(fileName string, metrics client.ResultMetrics) error {
if fileName == "" {
// default filename for json metrics output.
fileName = "out.json"
}
f, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return err
}
defer f.Close()

enc := json.NewEncoder(f)
return enc.Encode(metrics)
}

func runLoadTest(c *client.Cassowary) error {
metrics, err := c.Coordinate()
if err != nil {
return err
}
outPutResults(metrics)

if c.ExportMetrics {
return outPutJSON(c.ExportMetricsFile, metrics)
}
return nil
}

func validateCLI(c *cli.Context) error {

prometheusEnabled := false
var header []string
Expand All @@ -48,7 +79,7 @@ func validateRun(c *cli.Context) error {
return errRequestNo
}

if isValidURL(c.String("url")) == false {
if client.IsValidURL(c.String("url")) == false {
return errNotValidURL
}

Expand All @@ -58,43 +89,36 @@ func validateRun(c *cli.Context) error {

if c.String("header") != "" {
length := 0
length, header = splitHeader(c.String("header"))
length, header = client.SplitHeader(c.String("header"))
if length != 2 {
return errNotValidHeader
}
}

cass := &cassowary{
fileMode: false,
baseURL: c.String("url"),
concurrencyLevel: c.Int("concurrency"),
requests: c.Int("requests"),
requestHeader: header,
promExport: prometheusEnabled,
promURL: c.String("prompushgwurl"),
exportMetrics: c.Bool("json-metrics"),
// could have a single --json-output=file.json
// as it is not even documented (yet)
// but don't break existing usage.
// However, the author should make any necessary changes (even breaking ones)
// as soon as possible before huge amount of users.
exportMetricsFile: c.String("json-metrics-file"),
}

//fmt.Printf("%+v\n", cass)
return cass.coordinate()
}
cass := &client.Cassowary{
FileMode: false,
BaseURL: c.String("url"),
ConcurrencyLevel: c.Int("concurrency"),
Requests: c.Int("requests"),
RequestHeader: header,
PromExport: prometheusEnabled,
PromURL: c.String("prompushgwurl"),
ExportMetrics: c.Bool("json-metrics"),
ExportMetricsFile: c.String("json-metrics-file"),
}

func validateRunFile(c *cli.Context) error {
return runLoadTest(cass)
}

func validateCLIFile(c *cli.Context) error {
prometheusEnabled := false
var header []string

if c.Int("concurrency") == 0 {
return errConcurrencyLevel
}

if isValidURL(c.String("url")) == false {
if client.IsValidURL(c.String("url")) == false {
return errNotValidURL
}

Expand All @@ -104,34 +128,35 @@ func validateRunFile(c *cli.Context) error {

if c.String("header") != "" {
length := 0
length, header = splitHeader(c.String("header"))
length, header = client.SplitHeader(c.String("header"))
if length != 2 {
return errNotValidHeader
}
}

cass := &cassowary{
fileMode: true,
inputFile: c.String("file"),
baseURL: c.String("url"),
concurrencyLevel: c.Int("concurrency"),
requestHeader: header,
promExport: prometheusEnabled,
promURL: c.String("prompushgwurl"),
exportMetrics: c.Bool("json-metrics"),
exportMetricsFile: c.String("json-metrics-file"),
cass := &client.Cassowary{
FileMode: true,
InputFile: c.String("file"),
BaseURL: c.String("url"),
ConcurrencyLevel: c.Int("concurrency"),
RequestHeader: header,
PromExport: prometheusEnabled,
PromURL: c.String("prompushgwurl"),
ExportMetrics: c.Bool("json-metrics"),
ExportMetricsFile: c.String("json-metrics-file"),
}

return cass.coordinate()
return runLoadTest(cass)
}

func runCLI(args []string) {
app := cli.NewApp()
app.Name = "cassowary - 鹤鸵"
app.Name = "cassowary - 學名"
app.HelpName = "cassowary"
app.UsageText = "cassowary [command] [command options] [arguments...]"
app.EnableBashCompletion = true
app.Usage = ""
app.Version = version
app.Commands = []cli.Command{
{
Name: "run-file",
Expand Down Expand Up @@ -169,7 +194,7 @@ func runCLI(args []string) {
Usage: "outputs metrics to a custom json filepath, if json-metrics is set to true",
},
},
Action: validateRunFile,
Action: validateCLIFile,
},
{
Name: "run",
Expand Down Expand Up @@ -207,7 +232,7 @@ func runCLI(args []string) {
Usage: "outputs metrics to a custom json filepath, if json-metrics is set to true",
},
},
Action: validateRun,
Action: validateCLI,
},
}

Expand Down
File renamed without changes.
24 changes: 24 additions & 0 deletions cmd/cassowary/output.go
@@ -0,0 +1,24 @@
package main

import (
"fmt"

"github.com/fatih/color"
)

const (
summaryTable = `` + "\n\n" +
` TCP Connect.....................: Avg/mean=%sms ` + "\t" + `Median=%sms` + "\t" + `p(95)=%sms` + "\n" +
` Server Processing...............: Avg/mean=%sms ` + "\t" + `Median=%sms` + "\t" + `p(95)=%sms` + "\n" +
` Content Transfer................: Avg/mean=%sms ` + "\t" + `Median=%sms` + "\t" + `p(95)=%sms` + "\n" +
`` + "\n" +
`Summary: ` + "\n" +
` Total Req.......................: %s` + "\n" +
` Failed Req......................: %s` + "\n" +
` DNS Lookup......................: %sms` + "\n" +
` Req/s...........................: %s` + "\n\n"
)

func printf(format string, a ...interface{}) {
fmt.Fprintf(color.Output, format, a...)
}
2 changes: 1 addition & 1 deletion fileops.go → pkg/client/fileops.go
@@ -1,4 +1,4 @@
package main
package client

import (
"bufio"
Expand Down

0 comments on commit aa7459b

Please sign in to comment.