From 972fe6925dd12876e8dc963ac245fccbe5c42f8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Senart?= Date: Tue, 10 Sep 2013 20:09:22 +0100 Subject: [PATCH 1/8] Results: Extraction + Encoding, Decoding --- lib/attack.go | 27 +++--------------------- lib/results.go | 46 +++++++++++++++++++++++++++++++++++++++++ lib/results_test.go | 50 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 99 insertions(+), 24 deletions(-) create mode 100644 lib/results.go create mode 100644 lib/results_test.go diff --git a/lib/attack.go b/lib/attack.go index c66fc33..ef671ee 100644 --- a/lib/attack.go +++ b/lib/attack.go @@ -4,29 +4,17 @@ import ( "errors" "io/ioutil" "net/http" - "sort" "time" ) -// Result represents the metrics defined out of an http.Response -// generated by each target hit -type Result struct { - Code uint16 - Timestamp time.Time - Timing time.Duration - BytesOut uint64 - BytesIn uint64 - Error error -} - // Attack hits the passed Targets (http.Requests) at the rate specified for // duration time and then waits for all the requests to come back. // The results of the attack are put into a slice which is returned. -func Attack(targets Targets, rate uint64, duration time.Duration) []Result { +func Attack(targets Targets, rate uint64, duration time.Duration) Results { total := rate * uint64(duration.Seconds()) hits := make(chan *http.Request, total) res := make(chan Result, total) - results := make(results, total) + results := make(Results, total) // Scatter go drill(rate, hits, res) for i := 0; i < cap(hits); i++ { @@ -39,18 +27,9 @@ func Attack(targets Targets, rate uint64, duration time.Duration) []Result { } close(res) - sort.Sort(results) - - return results + return results.Sort() } -// results is a slice of Result defined only to be sortable with sort.Interface -type results []Result - -func (r results) Len() int { return len(r) } -func (r results) Less(i, j int) bool { return r[i].Timestamp.Before(r[j].Timestamp) } -func (r results) Swap(i, j int) { r[i], r[j] = r[j], r[i] } - // drill loops over the passed reqs channel and executes each request. // It is throttled to the rate specified. func drill(rate uint64, reqs chan *http.Request, res chan Result) { diff --git a/lib/results.go b/lib/results.go new file mode 100644 index 0000000..75c84dd --- /dev/null +++ b/lib/results.go @@ -0,0 +1,46 @@ +package vegeta + +import ( + "encoding/gob" + "io" + "sort" + "time" +) + +// Result represents the metrics defined out of an http.Response +// generated by each target hit +type Result struct { + Code uint16 + Timestamp time.Time + Timing time.Duration + BytesOut uint64 + BytesIn uint64 + Error error +} + +// Results is a slice of Result structs with encoding, +// decoding and sorting behavior attached +type Results []Result + +// WriteTo encodes the results and writes it to an io.Writer +// returning an error in case of failure +func (r Results) WriteTo(out io.Writer) error { + return gob.NewEncoder(out).Encode(r) +} + +// ReadFrom reads data from an io.Reader and decodes it into a Results struct +// returning an error in case of failure +func (r *Results) ReadFrom(in io.Reader) error { + return gob.NewDecoder(in).Decode(r) +} + +// Sort sorts Results by Timestamp in ascending order and returns +// the sorted slice +func (r Results) Sort() Results { + sort.Sort(r) + return r +} + +func (r Results) Len() int { return len(r) } +func (r Results) Less(i, j int) bool { return r[i].Timestamp.Before(r[j].Timestamp) } +func (r Results) Swap(i, j int) { r[i], r[j] = r[j], r[i] } diff --git a/lib/results_test.go b/lib/results_test.go new file mode 100644 index 0000000..b0053fe --- /dev/null +++ b/lib/results_test.go @@ -0,0 +1,50 @@ +package vegeta + +import ( + "bytes" + "sort" + "testing" + "time" +) + +func TestEncoding(t *testing.T) { + results := Results{ + Result{200, time.Now(), 100 * time.Millisecond, 10, 30, nil}, + Result{200, time.Now(), 20 * time.Millisecond, 20, 20, nil}, + Result{200, time.Now(), 30 * time.Millisecond, 30, 10, nil}, + } + buffer := &bytes.Buffer{} + + if err := results.WriteTo(buffer); err != nil { + t.Fatalf("Failed WriteTo: %s", err) + } + + decoded := Results{} + if err := decoded.ReadFrom(buffer); err != nil { + t.Fatalf("Failed ReadFrom: %s", err) + } + + if len(decoded) != len(results) { + t.Fatalf("Length mismatch. Want: %d, Got: %d", len(results), len(decoded)) + } + + for i, result := range results { + if decoded[i].Timestamp != result.Timestamp { + t.Fatalf("Expected result with timestamp: %s, got: %s", result.Timestamp, decoded[i].Timestamp) + } + } +} + +func TestSort(t *testing.T) { + results := Results{ + Result{Timestamp: time.Date(2013, 9, 10, 20, 4, 0, 3, time.UTC)}, + Result{Timestamp: time.Date(2013, 9, 10, 20, 4, 0, 2, time.UTC)}, + Result{Timestamp: time.Date(2013, 9, 10, 20, 4, 0, 1, time.UTC)}, + } + + results.Sort() + + if !sort.IsSorted(results) { + t.Fatalf("Sort failed: %v", results) + } +} From 6348a1b7df0e174f3b426231916bba483cae37ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Senart?= Date: Tue, 10 Sep 2013 21:46:28 +0100 Subject: [PATCH 2/8] Make Result.Error a string --- lib/attack.go | 8 ++++---- lib/metrics.go | 4 ++-- lib/metrics_test.go | 7 +++---- lib/results.go | 2 +- lib/results_test.go | 6 +++--- 5 files changed, 13 insertions(+), 14 deletions(-) diff --git a/lib/attack.go b/lib/attack.go index ef671ee..2aa9b9e 100644 --- a/lib/attack.go +++ b/lib/attack.go @@ -1,7 +1,6 @@ package vegeta import ( - "errors" "io/ioutil" "net/http" "time" @@ -50,13 +49,14 @@ func hit(req *http.Request, res chan Result) { Timestamp: began, Timing: time.Since(began), BytesOut: uint64(req.ContentLength), - Error: err, } - if err == nil { + if err != nil { + result.Error = err.Error() + } else { result.Code = uint16(r.StatusCode) if body, err := ioutil.ReadAll(r.Body); err != nil { if result.Code < 200 || result.Code >= 300 { - result.Error = errors.New(string(body)) + result.Error = string(body) } } else { result.BytesIn = uint64(len(body)) diff --git a/lib/metrics.go b/lib/metrics.go index 1588b1f..6592156 100644 --- a/lib/metrics.go +++ b/lib/metrics.go @@ -37,8 +37,8 @@ func NewMetrics(results []Result) *Metrics { if result.Code >= 200 && result.Code < 300 { m.TotalSuccess++ } - if result.Error != nil { - errorSet[result.Error.Error()] = struct{}{} + if result.Error != "" { + errorSet[result.Error] = struct{}{} } } diff --git a/lib/metrics_test.go b/lib/metrics_test.go index 8d17ed5..94571ab 100644 --- a/lib/metrics_test.go +++ b/lib/metrics_test.go @@ -1,16 +1,15 @@ package vegeta import ( - "errors" "testing" "time" ) func TestNewMetrics(t *testing.T) { m := NewMetrics([]Result{ - Result{500, time.Now(), 100 * time.Millisecond, 10, 30, errors.New("Internal server error")}, - Result{200, time.Now(), 20 * time.Millisecond, 20, 20, nil}, - Result{200, time.Now(), 30 * time.Millisecond, 30, 10, nil}, + Result{500, time.Now(), 100 * time.Millisecond, 10, 30, "Internal server error"}, + Result{200, time.Now(), 20 * time.Millisecond, 20, 20, ""}, + Result{200, time.Now(), 30 * time.Millisecond, 30, 10, ""}, }) for field, values := range map[string][]float64{ diff --git a/lib/results.go b/lib/results.go index 75c84dd..5d8a530 100644 --- a/lib/results.go +++ b/lib/results.go @@ -15,7 +15,7 @@ type Result struct { Timing time.Duration BytesOut uint64 BytesIn uint64 - Error error + Error string } // Results is a slice of Result structs with encoding, diff --git a/lib/results_test.go b/lib/results_test.go index b0053fe..c42847a 100644 --- a/lib/results_test.go +++ b/lib/results_test.go @@ -9,9 +9,9 @@ import ( func TestEncoding(t *testing.T) { results := Results{ - Result{200, time.Now(), 100 * time.Millisecond, 10, 30, nil}, - Result{200, time.Now(), 20 * time.Millisecond, 20, 20, nil}, - Result{200, time.Now(), 30 * time.Millisecond, 30, 10, nil}, + Result{200, time.Now(), 100 * time.Millisecond, 10, 30, ""}, + Result{200, time.Now(), 20 * time.Millisecond, 20, 20, ""}, + Result{200, time.Now(), 30 * time.Millisecond, 30, 10, ""}, } buffer := &bytes.Buffer{} From 4bbc7a0296bd12655521f0479aee19cb54c34207 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Senart?= Date: Tue, 10 Sep 2013 21:47:37 +0100 Subject: [PATCH 3/8] Split Attack and Report into sub commands --- attack.go | 67 +++++++++++++++++++ main_test.go => attack_test.go | 41 +++++------- main.go | 114 +++++++++------------------------ report.go | 59 +++++++++++++++++ 4 files changed, 170 insertions(+), 111 deletions(-) create mode 100644 attack.go rename main_test.go => attack_test.go (59%) create mode 100644 report.go diff --git a/attack.go b/attack.go new file mode 100644 index 0000000..b10e5ab --- /dev/null +++ b/attack.go @@ -0,0 +1,67 @@ +package main + +import ( + "fmt" + vegeta "github.com/tsenart/vegeta/lib" + "io" + "log" + "os" + "time" +) + +const ( + errRatePrefix = "Rate: " + errDurationPrefix = "Duration: " + errOutputFilePrefix = "Output file: " + errTargetsFilePrefix = "Targets file: " + errOrderingPrefix = "Ordering: " + errReportingPrefix = "Reporting: " +) + +// attack is the command function that validates the attack arguments, sets up the +// required resources, launches the attack and reports the results +func attack(rate uint64, duration time.Duration, targetsf, ordering, output string) error { + if rate == 0 { + return fmt.Errorf(errRatePrefix + "can't be zero") + } + + if duration == 0 { + return fmt.Errorf(errDurationPrefix + "can't be zero") + } + + targets, err := vegeta.NewTargetsFromFile(targetsf) + if err != nil { + return fmt.Errorf(errTargetsFilePrefix+"(%s): %s", targetsf, err) + } + + switch ordering { + case "random": + targets.Shuffle(time.Now().UnixNano()) + case "sequential": + break + default: + return fmt.Errorf(errOrderingPrefix+"`%s` is invalid", ordering) + } + + var out io.Writer + switch output { + case "stdout": + out = os.Stdout + default: + file, err := os.Create(output) + if err != nil { + return fmt.Errorf(errOutputFilePrefix+"(%s): %s", output, err) + } + defer file.Close() + out = file + } + + log.Printf("Vegeta is attacking %d targets in %s order for %s...\n", len(targets), ordering, duration) + results := vegeta.Attack(targets, rate, duration) + log.Println("Done!") + log.Printf("Writing results to '%s'...", output) + if err := results.WriteTo(out); err != nil { + return err + } + return nil +} diff --git a/main_test.go b/attack_test.go similarity index 59% rename from main_test.go rename to attack_test.go index 9cd75a0..824386d 100644 --- a/main_test.go +++ b/attack_test.go @@ -14,31 +14,31 @@ func init() { } func TestRateValidation(t *testing.T) { - rate, duration, targetsf, ordering, reporter, output := defaultArguments() + rate, duration, targetsf, ordering, output := defaultArguments() rate = 0 - err := run(rate, duration, targetsf, ordering, reporter, output) + err := attack(rate, duration, targetsf, ordering, output) if err == nil || (err != nil && !strings.HasPrefix(err.Error(), errRatePrefix)) { t.Errorf("Rate 0 shouldn't be valid: %s", err) } } func TestDurationValidation(t *testing.T) { - rate, duration, targetsf, ordering, reporter, output := defaultArguments() + rate, duration, targetsf, ordering, output := defaultArguments() duration = 0 - err := run(rate, duration, targetsf, ordering, reporter, output) + err := attack(rate, duration, targetsf, ordering, output) if err == nil || (err != nil && !strings.HasPrefix(err.Error(), errDurationPrefix)) { t.Errorf("Duration 0 shouldn't be valid: %s", err) } } func TestOutputValidation(t *testing.T) { - rate, duration, targetsf, ordering, reporter, _ := defaultArguments() + rate, duration, targetsf, ordering, _ := defaultArguments() // Good cases for _, output := range []string{"stdout", "/dev/null"} { - err := run(rate, duration, targetsf, ordering, reporter, output) + err := attack(rate, duration, targetsf, ordering, output) if err != nil { t.Errorf("Output file `%s` should be valid: %s", output, err) } @@ -46,46 +46,35 @@ func TestOutputValidation(t *testing.T) { // Bad case badOutput := "" - err := run(rate, duration, targetsf, ordering, reporter, badOutput) + err := attack(rate, duration, targetsf, ordering, badOutput) if err == nil || (err != nil && !strings.HasPrefix(err.Error(), errOutputFilePrefix)) { t.Errorf("Output file `%s` shouldn't be valid: %s", badOutput, err) } } -func TestReporter(t *testing.T) { - rate, duration, targetsf, ordering, _, output := defaultArguments() - - for _, reporter := range []string{"text", "plot:timings"} { - err := run(rate, duration, targetsf, ordering, reporter, output) - if err != nil { - t.Errorf("Reporter `%s` shouldn't return an error: %s", reporter, err) - } - } -} - func TestTargetsValidation(t *testing.T) { - rate, duration, goodFile, ordering, reporter, output := defaultArguments() + rate, duration, goodFile, ordering, output := defaultArguments() // Good case - err := run(rate, duration, goodFile, ordering, reporter, output) + err := attack(rate, duration, goodFile, ordering, output) if err != nil { t.Errorf("Targets file `%s` should be valid: %s", goodFile, err) } // Bad case badFile := "randomInexistingFile12345.txt" - err = run(rate, duration, badFile, ordering, reporter, output) + err = attack(rate, duration, badFile, ordering, output) if err == nil || (err != nil && !strings.HasPrefix(err.Error(), errTargetsFilePrefix)) { t.Errorf("Targets file `%s` shouldn't be valid: %s", badFile, err) } } func TestOrderingValidation(t *testing.T) { - rate, duration, targetsf, _, reporter, output := defaultArguments() + rate, duration, targetsf, _, output := defaultArguments() // Good cases for _, ordering := range []string{"random", "sequential"} { - err := run(rate, duration, targetsf, ordering, reporter, output) + err := attack(rate, duration, targetsf, ordering, output) if err != nil { t.Errorf("Ordering `%s` should be valid: %s", ordering, err) } @@ -93,12 +82,12 @@ func TestOrderingValidation(t *testing.T) { // Bad case badOrdering := "lolcat" - err := run(rate, duration, targetsf, badOrdering, reporter, output) + err := attack(rate, duration, targetsf, badOrdering, output) if err == nil || (err != nil && !strings.HasPrefix(err.Error(), errOrderingPrefix)) { t.Errorf("Ordering `%s` shouldn't be valid: %s", badOrdering, err) } } -func defaultArguments() (uint64, time.Duration, string, string, string, string) { - return uint64(1000), 5 * time.Millisecond, ".targets.txt", "random", "text", "/dev/null" +func defaultArguments() (uint64, time.Duration, string, string, string) { + return uint64(1000), 5 * time.Millisecond, ".targets.txt", "random", "/dev/null" } diff --git a/main.go b/main.go index 63363f6..31b62c9 100644 --- a/main.go +++ b/main.go @@ -2,105 +2,49 @@ package main import ( "flag" - "fmt" - vegeta "github.com/tsenart/vegeta/lib" - "io" "log" - "os" "runtime" "time" ) func main() { - var ( - rate = flag.Uint64("rate", 50, "Requests per second") - targetsf = flag.String("targets", "targets.txt", "Targets file") - ordering = flag.String("ordering", "random", "Attack ordering [sequential, random]") - duration = flag.Duration("duration", 10*time.Second, "Duration of the test") - reporter = flag.String("reporter", "text", "Reporter to use [text, json, plot:timings]") - output = flag.String("output", "stdout", "Reporter output file") - cpus = flag.Int("cpus", runtime.NumCPU(), "Number of CPUs to use") - ) + // Global flags + cpus := flag.Int("cpus", runtime.NumCPU(), "Number of CPUs to use") flag.Parse() - - if flag.NFlag() == 0 { - flag.Usage() - return - } - runtime.GOMAXPROCS(*cpus) - if err := run(*rate, *duration, *targetsf, *ordering, *reporter, *output); err != nil { - log.Fatal(err) + args := flag.Args() + if len(args) < 1 { + log.Fatal("Unspecified command") } -} + cmd, cmdf := args[0], flag.NewFlagSet(args[0], flag.ExitOnError) -const ( - errRatePrefix = "Rate: " - errDurationPrefix = "Duration: " - errOutputFilePrefix = "Output file: " - errTargetsFilePrefix = "Targets file: " - errOrderingPrefix = "Ordering: " - errReportingPrefix = "Reporting: " -) + switch cmd { + case "attack": + rate := cmdf.Uint64("rate", 50, "Requests per second") + targetsf := cmdf.String("targets", "targets.txt", "Targets file") + ordering := cmdf.String("ordering", "random", "Attack ordering [sequential, random]") + duration := cmdf.Duration("duration", 10*time.Second, "Duration of the test") + output := cmdf.String("output", "stdout", "Vegeta Results file") -// run is an utility function that validates the attack arguments, sets up the -// required resources, launches the attack and reports the results -func run(rate uint64, duration time.Duration, targetsf, ordering, reporter, output string) error { - if rate == 0 { - return fmt.Errorf(errRatePrefix + "can't be zero") - } - - if duration == 0 { - return fmt.Errorf(errDurationPrefix + "can't be zero") - } - - var out io.Writer - switch output { - case "stdout": - out = os.Stdout - default: - file, err := os.Create(output) - if err != nil { - return fmt.Errorf(errOutputFilePrefix+"(%s): %s", output, err) + if err := cmdf.Parse(args[1:]); err != nil { + log.Fatal(err) } - defer file.Close() - out = file - } + if err := attack(*rate, *duration, *targetsf, *ordering, *output); err != nil { + log.Fatal(err) + } + case "report": + reporter := cmdf.String("reporter", "text", "Reporter [text, json, plot:timings]") + input := cmdf.String("input", "stdin", "Vegeta Results file") + output := cmdf.String("output", "stdout", "Output file") - var rep vegeta.Reporter - switch reporter { - case "text": - rep = vegeta.ReportText - case "json": - rep = vegeta.ReportJSON - case "plot:timings": - rep = vegeta.ReportTimingsPlot + if err := cmdf.Parse(args[1:]); err != nil { + log.Fatal(err) + } + if err := report(*reporter, *input, *output); err != nil { + log.Fatal(err) + } default: - log.Println("Reporter provided is not supported. Using text") - rep = vegeta.ReportText + log.Fatalf("Unknown command: %s", cmd) } - - targets, err := vegeta.NewTargetsFromFile(targetsf) - if err != nil { - return fmt.Errorf(errTargetsFilePrefix+"(%s): %s", targetsf, err) - } - - switch ordering { - case "random": - targets.Shuffle(time.Now().UnixNano()) - case "sequential": - break - default: - return fmt.Errorf(errOrderingPrefix+"`%s` is invalid", ordering) - } - - log.Printf("Vegeta is attacking %d targets in %s order for %s...\n", len(targets), ordering, duration) - results := vegeta.Attack(targets, rate, duration) - log.Println("Done!") - log.Printf("Writing report to '%s'...", output) - if err = rep(results, out); err != nil { - return fmt.Errorf(errReportingPrefix+"%s", err) - } - return nil } diff --git a/report.go b/report.go new file mode 100644 index 0000000..f99eb6d --- /dev/null +++ b/report.go @@ -0,0 +1,59 @@ +package main + +import ( + vegeta "github.com/tsenart/vegeta/lib" + "io" + "log" + "os" +) + +func report(reporter, input, output string) error { + var rep vegeta.Reporter + switch reporter { + case "text": + rep = vegeta.ReportText + case "json": + rep = vegeta.ReportJSON + case "plot:timings": + rep = vegeta.ReportTimingsPlot + default: + log.Println("Reporter provided is not supported. Using text") + rep = vegeta.ReportText + } + + var in io.Reader + switch input { + case "stdin": + in = os.Stdin + default: + file, err := os.Open(input) + if err != nil { + return err + } + defer file.Close() + in = file + } + + var out io.Writer + switch output { + case "stdout": + out = os.Stdout + default: + file, err := os.Create(output) + if err != nil { + return err + } + defer file.Close() + out = file + } + + results := vegeta.Results{} + if err := results.ReadFrom(in); err != nil { + return err + } + if err := rep(results, out); err != nil { + return err + } + + return nil +} From e61ded8e5b37d3c37efd4f17fd4b9f2b08c456b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Senart?= Date: Wed, 11 Sep 2013 00:10:35 +0100 Subject: [PATCH 4/8] Abstract command building and setup with closures --- attack.go | 35 +++++++++++++++++++++++++---------- main.go | 48 ++++++++++++++---------------------------------- report.go | 15 +++++++++++++++ 3 files changed, 54 insertions(+), 44 deletions(-) diff --git a/attack.go b/attack.go index b10e5ab..bceabb7 100644 --- a/attack.go +++ b/attack.go @@ -1,6 +1,7 @@ package main import ( + "flag" "fmt" vegeta "github.com/tsenart/vegeta/lib" "io" @@ -9,17 +10,22 @@ import ( "time" ) -const ( - errRatePrefix = "Rate: " - errDurationPrefix = "Duration: " - errOutputFilePrefix = "Output file: " - errTargetsFilePrefix = "Targets file: " - errOrderingPrefix = "Ordering: " - errReportingPrefix = "Reporting: " -) +func attackCmd(args []string) command { + fs := flag.NewFlagSet("attack", flag.ExitOnError) + rate := fs.Uint64("rate", 50, "Requests per second") + targetsf := fs.String("targets", "targets.txt", "Targets file") + ordering := fs.String("ordering", "random", "Attack ordering [sequential, random]") + duration := fs.Duration("duration", 10*time.Second, "Duration of the test") + output := fs.String("output", "stdout", "Vegeta Results file") + fs.Parse(args) -// attack is the command function that validates the attack arguments, sets up the -// required resources, launches the attack and reports the results + return func() error { + return attack(*rate, *duration, *targetsf, *ordering, *output) + } +} + +// attack validates the attack arguments, sets up the +// required resources, launches the attack and writes the results func attack(rate uint64, duration time.Duration, targetsf, ordering, output string) error { if rate == 0 { return fmt.Errorf(errRatePrefix + "can't be zero") @@ -65,3 +71,12 @@ func attack(rate uint64, duration time.Duration, targetsf, ordering, output stri } return nil } + +const ( + errRatePrefix = "Rate: " + errDurationPrefix = "Duration: " + errOutputFilePrefix = "Output file: " + errTargetsFilePrefix = "Targets file: " + errOrderingPrefix = "Ordering: " + errReportingPrefix = "Reporting: " +) diff --git a/main.go b/main.go index 31b62c9..bd73933 100644 --- a/main.go +++ b/main.go @@ -4,47 +4,27 @@ import ( "flag" "log" "runtime" - "time" ) +// command is a closure function which each command constructor +// builds and returns +type command func() error + func main() { + commands := map[string]func([]string) command{ + "attack": attackCmd, + "report": reportCmd, + } // Global flags cpus := flag.Int("cpus", runtime.NumCPU(), "Number of CPUs to use") flag.Parse() + args := flag.Args() + runtime.GOMAXPROCS(*cpus) - args := flag.Args() - if len(args) < 1 { - log.Fatal("Unspecified command") - } - cmd, cmdf := args[0], flag.NewFlagSet(args[0], flag.ExitOnError) - - switch cmd { - case "attack": - rate := cmdf.Uint64("rate", 50, "Requests per second") - targetsf := cmdf.String("targets", "targets.txt", "Targets file") - ordering := cmdf.String("ordering", "random", "Attack ordering [sequential, random]") - duration := cmdf.Duration("duration", 10*time.Second, "Duration of the test") - output := cmdf.String("output", "stdout", "Vegeta Results file") - - if err := cmdf.Parse(args[1:]); err != nil { - log.Fatal(err) - } - if err := attack(*rate, *duration, *targetsf, *ordering, *output); err != nil { - log.Fatal(err) - } - case "report": - reporter := cmdf.String("reporter", "text", "Reporter [text, json, plot:timings]") - input := cmdf.String("input", "stdin", "Vegeta Results file") - output := cmdf.String("output", "stdout", "Output file") - - if err := cmdf.Parse(args[1:]); err != nil { - log.Fatal(err) - } - if err := report(*reporter, *input, *output); err != nil { - log.Fatal(err) - } - default: - log.Fatalf("Unknown command: %s", cmd) + if cmd, ok := commands[args[0]]; !ok { + log.Fatalf("Unknown command: %s", args[0]) + } else if err := cmd(args[1:])(); err != nil { + log.Fatal(err) } } diff --git a/report.go b/report.go index f99eb6d..69bf5e4 100644 --- a/report.go +++ b/report.go @@ -1,12 +1,27 @@ package main import ( + "flag" vegeta "github.com/tsenart/vegeta/lib" "io" "log" "os" ) +func reportCmd(args []string) command { + fs := flag.NewFlagSet("report", flag.ExitOnError) + reporter := fs.String("reporter", "text", "Reporter [text, json, plot:timings]") + input := fs.String("input", "stdin", "Vegeta Results file") + output := fs.String("output", "stdout", "Output file") + fs.Parse(args) + + return func() error { + return report(*reporter, *input, *output) + } +} + +// report validates the report arguments, sets up the required resources +// and writes the report func report(reporter, input, output string) error { var rep vegeta.Reporter switch reporter { From ea5764489fc4c7be9e70f3b0c9de790f3d3f359b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Senart?= Date: Wed, 11 Sep 2013 00:31:17 +0100 Subject: [PATCH 5/8] Better Usage func --- attack.go | 2 +- main.go | 30 +++++++++++++++++++++++++----- report.go | 2 +- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/attack.go b/attack.go index bceabb7..b9de6ba 100644 --- a/attack.go +++ b/attack.go @@ -16,7 +16,7 @@ func attackCmd(args []string) command { targetsf := fs.String("targets", "targets.txt", "Targets file") ordering := fs.String("ordering", "random", "Attack ordering [sequential, random]") duration := fs.Duration("duration", 10*time.Second, "Duration of the test") - output := fs.String("output", "stdout", "Vegeta Results file") + output := fs.String("output", "stdout", "Vegeta data file") fs.Parse(args) return func() error { diff --git a/main.go b/main.go index bd73933..7184bb2 100644 --- a/main.go +++ b/main.go @@ -2,7 +2,9 @@ package main import ( "flag" + "fmt" "log" + "os" "runtime" ) @@ -10,17 +12,35 @@ import ( // builds and returns type command func() error +var usage = fmt.Sprintf( + `Usage: vegeta [globals] [options] + +Commands: + attack Hit the targets + report Report the results + +Globals: + -cpus=%d Number of CPUs to use +`, runtime.NumCPU()) + +func init() { + flag.Usage = func() { fmt.Print(usage) } + cpus := flag.Int("cpus", runtime.NumCPU(), "Number of CPUs to use") + flag.Parse() + runtime.GOMAXPROCS(*cpus) +} + func main() { commands := map[string]func([]string) command{ "attack": attackCmd, "report": reportCmd, } - // Global flags - cpus := flag.Int("cpus", runtime.NumCPU(), "Number of CPUs to use") - flag.Parse() - args := flag.Args() - runtime.GOMAXPROCS(*cpus) + args := flag.Args() + if len(args) == 0 { + flag.Usage() + os.Exit(1) + } if cmd, ok := commands[args[0]]; !ok { log.Fatalf("Unknown command: %s", args[0]) diff --git a/report.go b/report.go index 69bf5e4..efa953c 100644 --- a/report.go +++ b/report.go @@ -11,7 +11,7 @@ import ( func reportCmd(args []string) command { fs := flag.NewFlagSet("report", flag.ExitOnError) reporter := fs.String("reporter", "text", "Reporter [text, json, plot:timings]") - input := fs.String("input", "stdin", "Vegeta Results file") + input := fs.String("input", "stdin", "Vegeta data file") output := fs.String("output", "stdout", "Output file") fs.Parse(args) From 30598547514a5df77aaa1d54f1e04c690a5dc5ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Senart?= Date: Wed, 11 Sep 2013 12:47:32 +0100 Subject: [PATCH 6/8] Better wording --- attack.go | 2 +- report.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/attack.go b/attack.go index b9de6ba..5752bee 100644 --- a/attack.go +++ b/attack.go @@ -16,7 +16,7 @@ func attackCmd(args []string) command { targetsf := fs.String("targets", "targets.txt", "Targets file") ordering := fs.String("ordering", "random", "Attack ordering [sequential, random]") duration := fs.Duration("duration", 10*time.Second, "Duration of the test") - output := fs.String("output", "stdout", "Vegeta data file") + output := fs.String("output", "stdout", "Output file") fs.Parse(args) return func() error { diff --git a/report.go b/report.go index efa953c..d0a5499 100644 --- a/report.go +++ b/report.go @@ -11,7 +11,7 @@ import ( func reportCmd(args []string) command { fs := flag.NewFlagSet("report", flag.ExitOnError) reporter := fs.String("reporter", "text", "Reporter [text, json, plot:timings]") - input := fs.String("input", "stdin", "Vegeta data file") + input := fs.String("input", "stdin", "Input file") output := fs.String("output", "stdout", "Output file") fs.Parse(args) From 7cd13327c62f78cb475af50d4763386108ae29dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Senart?= Date: Wed, 11 Sep 2013 14:10:45 +0100 Subject: [PATCH 7/8] Default targets to stdin and abstract file handling --- attack.go | 26 +++++++++++--------------- attack_test.go | 19 ------------------- file.go | 26 ++++++++++++++++++++++++++ lib/targets.go | 15 ++------------- lib/targets_test.go | 4 ++-- report.go | 33 +++++++++------------------------ 6 files changed, 50 insertions(+), 73 deletions(-) create mode 100644 file.go diff --git a/attack.go b/attack.go index 5752bee..8d7e83b 100644 --- a/attack.go +++ b/attack.go @@ -4,16 +4,14 @@ import ( "flag" "fmt" vegeta "github.com/tsenart/vegeta/lib" - "io" "log" - "os" "time" ) func attackCmd(args []string) command { fs := flag.NewFlagSet("attack", flag.ExitOnError) rate := fs.Uint64("rate", 50, "Requests per second") - targetsf := fs.String("targets", "targets.txt", "Targets file") + targetsf := fs.String("targets", "stdin", "Targets file") ordering := fs.String("ordering", "random", "Attack ordering [sequential, random]") duration := fs.Duration("duration", 10*time.Second, "Duration of the test") output := fs.String("output", "stdout", "Output file") @@ -35,7 +33,12 @@ func attack(rate uint64, duration time.Duration, targetsf, ordering, output stri return fmt.Errorf(errDurationPrefix + "can't be zero") } - targets, err := vegeta.NewTargetsFromFile(targetsf) + in, err := file(targetsf, false) + if err != nil { + return fmt.Errorf(errTargetsFilePrefix+"(%s): %s", targetsf, err) + } + defer in.Close() + targets, err := vegeta.NewTargetsFrom(in) if err != nil { return fmt.Errorf(errTargetsFilePrefix+"(%s): %s", targetsf, err) } @@ -49,18 +52,11 @@ func attack(rate uint64, duration time.Duration, targetsf, ordering, output stri return fmt.Errorf(errOrderingPrefix+"`%s` is invalid", ordering) } - var out io.Writer - switch output { - case "stdout": - out = os.Stdout - default: - file, err := os.Create(output) - if err != nil { - return fmt.Errorf(errOutputFilePrefix+"(%s): %s", output, err) - } - defer file.Close() - out = file + out, err := file(output, true) + if err != nil { + return fmt.Errorf(errOutputFilePrefix+"(%s): %s", output, err) } + defer out.Close() log.Printf("Vegeta is attacking %d targets in %s order for %s...\n", len(targets), ordering, duration) results := vegeta.Attack(targets, rate, duration) diff --git a/attack_test.go b/attack_test.go index 824386d..f9b1216 100644 --- a/attack_test.go +++ b/attack_test.go @@ -33,25 +33,6 @@ func TestDurationValidation(t *testing.T) { } } -func TestOutputValidation(t *testing.T) { - rate, duration, targetsf, ordering, _ := defaultArguments() - - // Good cases - for _, output := range []string{"stdout", "/dev/null"} { - err := attack(rate, duration, targetsf, ordering, output) - if err != nil { - t.Errorf("Output file `%s` should be valid: %s", output, err) - } - } - - // Bad case - badOutput := "" - err := attack(rate, duration, targetsf, ordering, badOutput) - if err == nil || (err != nil && !strings.HasPrefix(err.Error(), errOutputFilePrefix)) { - t.Errorf("Output file `%s` shouldn't be valid: %s", badOutput, err) - } -} - func TestTargetsValidation(t *testing.T) { rate, duration, goodFile, ordering, output := defaultArguments() diff --git a/file.go b/file.go new file mode 100644 index 0000000..539c313 --- /dev/null +++ b/file.go @@ -0,0 +1,26 @@ +package main + +import ( + "os" +) + +func file(filename string, create bool) (*os.File, error) { + switch filename { + case "stdin": + return os.Stdin, nil + case "stdout": + return os.Stdout, nil + default: + var file *os.File + var err error + if create { + file, err = os.Create(filename) + } else { + file, err = os.Open(filename) + } + if err != nil { + return nil, err + } + return file, nil + } +} diff --git a/lib/targets.go b/lib/targets.go index 467b899..9864f86 100644 --- a/lib/targets.go +++ b/lib/targets.go @@ -6,25 +6,14 @@ import ( "io" "math/rand" "net/http" - "os" "strings" ) // Targets represents the http.Requests which will be issued during the test type Targets []*http.Request -// NewTargetsFromFile reads and parses targets from a text file -func NewTargetsFromFile(filename string) (Targets, error) { - file, err := os.Open(filename) - if err != nil { - return Targets{}, err - } - defer file.Close() - return readTargets(file) -} - -// readTargets reads targets out of a line separated source skipping empty lines -func readTargets(source io.Reader) (Targets, error) { +// NewTargetsFrom reads targets out of a line separated source skipping empty lines +func NewTargetsFrom(source io.Reader) (Targets, error) { scanner := bufio.NewScanner(source) lines := make([]string, 0) for scanner.Scan() { diff --git a/lib/targets_test.go b/lib/targets_test.go index 6b7e99e..9337928 100644 --- a/lib/targets_test.go +++ b/lib/targets_test.go @@ -6,9 +6,9 @@ import ( "testing" ) -func TestReadTargets(t *testing.T) { +func TestNewTargetsFrom(t *testing.T) { lines := bytes.NewBufferString("GET http://lolcathost:9999/\n\n // HEAD http://lolcathost.com this is a comment \nHEAD http://lolcathost:9999/\n") - targets, err := readTargets(lines) + targets, err := NewTargetsFrom(lines) if err != nil { t.Fatalf("Couldn't parse valid source: %s", err) } diff --git a/report.go b/report.go index d0a5499..e428e7b 100644 --- a/report.go +++ b/report.go @@ -3,9 +3,7 @@ package main import ( "flag" vegeta "github.com/tsenart/vegeta/lib" - "io" "log" - "os" ) func reportCmd(args []string) command { @@ -36,36 +34,23 @@ func report(reporter, input, output string) error { rep = vegeta.ReportText } - var in io.Reader - switch input { - case "stdin": - in = os.Stdin - default: - file, err := os.Open(input) - if err != nil { - return err - } - defer file.Close() - in = file + in, err := file(input, false) + if err != nil { + return err } + defer in.Close() - var out io.Writer - switch output { - case "stdout": - out = os.Stdout - default: - file, err := os.Create(output) - if err != nil { - return err - } - defer file.Close() - out = file + out, err := file(output, true) + if err != nil { + return err } + defer out.Close() results := vegeta.Results{} if err := results.ReadFrom(in); err != nil { return err } + if err := rep(results, out); err != nil { return err } From dc2b311144b6ef4fc3464016b0abc1058c9d4db5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Senart?= Date: Wed, 11 Sep 2013 14:35:04 +0100 Subject: [PATCH 8/8] Update README --- README.md | 87 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 61 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index a81af23..ddf9e48 100644 --- a/README.md +++ b/README.md @@ -21,23 +21,42 @@ $ go get github.com/tsenart/vegeta $ go install github.com/tsenart/vegeta ``` -## Usage (CLI) +## Usage examples +```shell +$ echo "GET http://localhost/" | vegeta attack -rate=100 -duration=5s | vegeta report +$ vegeta attack -targets=targets.txt > results.vr +$ vegeta report -input=results.vr -reporter=json > metrics.json +$ vegeta report -input=results.vr -reporter=plot:timings > plot.svg +``` + +## Usage manual ```shell $ vegeta -h -Usage of vegeta: - -cpus=n: Number of CPUs to use - -duration=10s: Duration of the test - -ordering="random": Attack ordering [sequential, random] - -output="stdout": Reporter output file - -rate=50: Requests per second - -reporter="text": Reporter to use [text, json, plot:timings] - -targets="targets.txt": Targets file +Usage: vegeta [globals] [options] + +Commands: + attack Hit the targets + report Report the results + +Globals: + -cpus=8 Number of CPUs to use ``` #### -cpus Specifies the number of CPUs to be used internally. It defaults to the amount of CPUs available in the system. +### attack +```shell +$ vegeta attack -h +Usage of attack: + -duration=10s: Duration of the test + -ordering="random": Attack ordering [sequential, random] + -output="stdout": Output file + -rate=50: Requests per second + -targets="stdin": Targets file +``` + #### -duration Specifies the amount of time to issue request to the targets. The internal concurrency structure's setup has this value as a variable. @@ -52,18 +71,44 @@ The other option is `sequential` and it does what you would expect it to do. #### -output -Specifies the output file to which the report will be written to. -The default is stdout. +Specifies the output file to which the binary results will be written +to. Made to be piped to the report command input. Defaults to stdout. #### -rate Specifies the requests per second rate to issue against the targets. The actual request rate can vary slightly due to things like garbage collection, but overall it should stay very close to the specified. +#### -targets +Specifies the attack targets in a line sepated file, defaulting to stdin. +The format should be as follows. +``` +GET http://goku:9090/path/to/dragon?item=balls +GET http://user:password@goku:9090/path/to +HEAD http://goku:9090/path/to/success +... +``` + +### report +``` +$ vegeta report -h +Usage of report: + -input="stdin": Input file + -output="stdout": Output file + -reporter="text": Reporter [text, json, plot:timings] +``` + +#### -input +Specifies the input file from which the attack command binary results +are saved. Defaults to stdin. + +#### -output +Specifies the output file to which the report will be written to. + #### -reporter -Specifies the reporting type to display the results with. -The default is the text report printed to stdout. -##### -reporter=text +Specifies the kind of report to be generated. It defaults to text. + +##### text ``` Time(avg) Requests Success Bytes(rx/tx) 152.341ms 200 17.00% 251.00/0.00 @@ -75,7 +120,7 @@ Error Set: Server Timeout Page Not Found ``` -##### -reporter=json +##### json ```json { "total_requests": 50, @@ -93,19 +138,9 @@ Page Not Found "errors": [] } ``` -##### -reporter=plot:timings -Plots the request timings in SVG format. +##### plot:timings ![plot](https://dl.dropboxusercontent.com/u/83217940/plot.svg) -#### -targets -Specifies the attack targets in a line sepated file. The format should -be as follows: -``` -GET http://goku:9090/path/to/dragon?item=balls -GET http://user:password@goku:9090/path/to -HEAD http://goku:9090/path/to/success -... -``` ## Usage (Library) ```go