From 6e9f34846b2e228eb68e812183b587431f4a9712 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Senart?= Date: Tue, 10 Sep 2013 13:32:44 +0100 Subject: [PATCH 1/4] Extract Metrics out of ReportText Metrics will be reused with other Reporters --- lib/metrics.go | 55 +++++++++++++++++++++++++++++++++++++++++++++ lib/metrics_test.go | 54 ++++++++++++++++++++++++++++++++++++++++++++ lib/reporters.go | 39 +++++--------------------------- 3 files changed, 115 insertions(+), 33 deletions(-) create mode 100644 lib/metrics.go create mode 100644 lib/metrics_test.go diff --git a/lib/metrics.go b/lib/metrics.go new file mode 100644 index 0000000..217f149 --- /dev/null +++ b/lib/metrics.go @@ -0,0 +1,55 @@ +package vegeta + +import ( + "time" +) + +// Metrics holds the stats computed out of a slice of Results +// that is used for some of the Reporters +type Metrics struct { + TotalRequests uint64 `json:"total_requests"` + TotalTiming time.Duration `json:"total_timing"` + MeanTiming time.Duration `json:"mean_timing"` + TotalBytesIn uint64 `json:"total_bytes_in"` + MeanBytesIn float64 `json:"mean_bytes_in"` + TotalBytesOut uint64 `json:"total_bytes_out"` + MeanBytesOut float64 `json:"mean_bytes_out"` + TotalSuccess uint64 `json:"total_success"` + MeanSuccess float64 `json:"mean_success"` + StatusCodes map[uint16]uint64 `json:"status_codes"` + Errors []string `json:"errors"` +} + +// NewMetrics computes and returns a Metrics struct out of a slice of Results +func NewMetrics(results []Result) *Metrics { + m := &Metrics{ + TotalRequests: uint64(len(results)), + StatusCodes: map[uint16]uint64{}, + } + errorSet := map[string]struct{}{} + + for _, result := range results { + m.StatusCodes[result.Code]++ + m.TotalTiming += result.Timing + m.TotalBytesOut += result.BytesOut + m.TotalBytesIn += result.BytesIn + if result.Code >= 200 && result.Code < 300 { + m.TotalSuccess++ + } + if result.Error != nil { + errorSet[result.Error.Error()] = struct{}{} + } + } + + m.MeanTiming = time.Duration(float64(m.TotalTiming) / float64(m.TotalRequests)) + m.MeanBytesOut = float64(m.TotalBytesOut) / float64(m.TotalRequests) + m.MeanBytesIn = float64(m.TotalBytesIn) / float64(m.TotalRequests) + m.MeanSuccess = float64(m.TotalSuccess) / float64(m.TotalRequests) + + m.Errors = make([]string, 0, len(errorSet)) + for err, _ := range errorSet { + m.Errors = append(m.Errors, err) + } + + return m +} diff --git a/lib/metrics_test.go b/lib/metrics_test.go new file mode 100644 index 0000000..7fa20c5 --- /dev/null +++ b/lib/metrics_test.go @@ -0,0 +1,54 @@ +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}, + }) + + for field, values := range map[string][]float64{ + "MeanBytesIn": []float64{m.MeanBytesIn, 20.0}, + "MeanBytesOut": []float64{m.MeanBytesOut, 20.0}, + "MeanSuccess": []float64{m.MeanSuccess, 0.6666666666666666}, + } { + if values[0] != values[1] { + t.Errorf("%s: want: %f, got: %f", field, values[1], values[0]) + } + } + + for field, values := range map[string][]time.Duration{ + "TotalTiming": []time.Duration{m.TotalTiming, 150 * time.Millisecond}, + "MeanTiming": []time.Duration{m.MeanTiming, 50 * time.Millisecond}, + } { + if values[0] != values[1] { + t.Errorf("%s: want: %s, got: %s", field, values[1], values[0]) + } + } + + for field, values := range map[string][]uint64{ + "TotalSuccess": []uint64{m.TotalSuccess, 2}, + "TotalBytesOut": []uint64{m.TotalBytesOut, 60}, + "TotalRequests": []uint64{m.TotalRequests, 3}, + "TotalBytesIn": []uint64{m.TotalBytesIn, 60}, + } { + if values[0] != values[1] { + t.Errorf("%s: want: %d, got: %d", field, values[1], values[0]) + } + } + + if len(m.StatusCodes) != 2 || m.StatusCodes[200] != 2 || m.StatusCodes[500] != 1 { + t.Errorf("StatusCodes: want: %v, got: %v", map[int]int{200: 2, 500: 1}, m.StatusCodes) + } + + err := "Internal server error" + if len(m.Errors) != 1 || m.Errors[0] != err { + t.Errorf("Errors: want: %v, got: %v", []string{err}, m.Errors) + } +} diff --git a/lib/reporters.go b/lib/reporters.go index 73b7890..ddfcff5 100644 --- a/lib/reporters.go +++ b/lib/reporters.go @@ -9,7 +9,6 @@ import ( "fmt" "io" "text/tabwriter" - "time" ) // Reporter represents any function which takes a slice of Results and @@ -17,51 +16,25 @@ import ( // in case of failure type Reporter func([]Result, io.Writer) error -// ReportText computes and prints some metrics out of results -// as formatted text. Metrics include avg time per request, success ratio, -// total number of request, avg bytes in and avg bytes out. +// ReportText writes a computed Metrics struct to out as aligned, formatted text func ReportText(results []Result, out io.Writer) error { - totalRequests := float64(len(results)) - totalTime := time.Duration(0) - totalBytesOut := uint64(0) - totalBytesIn := uint64(0) - totalSuccess := uint64(0) - histogram := map[uint16]uint64{} - errors := map[string]struct{}{} - - for _, res := range results { - histogram[res.Code]++ - totalTime += res.Timing - totalBytesOut += res.BytesOut - totalBytesIn += res.BytesIn - if res.Code >= 200 && res.Code < 300 { - totalSuccess++ - } - if res.Error != nil { - errors[res.Error.Error()] = struct{}{} - } - } - - avgTime := time.Duration(float64(totalTime) / totalRequests) - avgBytesOut := float64(totalBytesOut) / totalRequests - avgBytesIn := float64(totalBytesIn) / totalRequests - avgSuccess := float64(totalSuccess) / totalRequests + m := NewMetrics(results) w := tabwriter.NewWriter(out, 0, 8, 2, '\t', tabwriter.StripEscape) fmt.Fprintf(w, "Time(avg)\tRequests\tSuccess\tBytes(rx/tx)\n") - fmt.Fprintf(w, "%s\t%d\t%.2f%%\t%.2f/%.2f\n", avgTime, int(totalRequests), avgSuccess*100, avgBytesIn, avgBytesOut) + fmt.Fprintf(w, "%s\t%d\t%.2f%%\t%.2f/%.2f\n", m.MeanTiming, m.TotalRequests, m.MeanSuccess*100, m.MeanBytesIn, m.MeanBytesOut) fmt.Fprintf(w, "\nCount:\t") - for _, count := range histogram { + for _, count := range m.StatusCodes { fmt.Fprintf(w, "%d\t", count) } fmt.Fprintf(w, "\nStatus:\t") - for code := range histogram { + for code := range m.StatusCodes { fmt.Fprintf(w, "%d\t", code) } fmt.Fprintln(w, "\n\nError Set:") - for err := range errors { + for err := range m.Errors { fmt.Fprintln(w, err) } From ee9a1a09548b4a39d5ad01c9f667983d40476155 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Senart?= Date: Tue, 10 Sep 2013 13:45:01 +0100 Subject: [PATCH 2/4] ReportJSON --- README.md | 20 +++++++++++++++++++- lib/metrics.go | 27 ++++++++++++++------------- lib/reporters.go | 6 ++++++ main.go | 4 +++- 4 files changed, 42 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index aed1637..a81af23 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Usage of vegeta: -ordering="random": Attack ordering [sequential, random] -output="stdout": Reporter output file -rate=50: Requests per second - -reporter="text": Reporter to use [text, plot:timings] + -reporter="text": Reporter to use [text, json, plot:timings] -targets="targets.txt": Targets file ``` @@ -75,6 +75,24 @@ Error Set: Server Timeout Page Not Found ``` +##### -reporter=json +```json +{ + "total_requests": 50, + "total_timing": 34779791, + "mean_timing": 695595, + "total_bytes_in": 272850, + "mean_bytes_in": 5457, + "total_bytes_out": 0, + "mean_bytes_out": 0, + "total_success": 50, + "mean_success": 1, + "status_codes": { + "200": 50 + }, + "errors": [] +} +``` ##### -reporter=plot:timings Plots the request timings in SVG format. ![plot](https://dl.dropboxusercontent.com/u/83217940/plot.svg) diff --git a/lib/metrics.go b/lib/metrics.go index 217f149..a78855f 100644 --- a/lib/metrics.go +++ b/lib/metrics.go @@ -1,35 +1,36 @@ package vegeta import ( + "strconv" "time" ) // Metrics holds the stats computed out of a slice of Results // that is used for some of the Reporters type Metrics struct { - TotalRequests uint64 `json:"total_requests"` - TotalTiming time.Duration `json:"total_timing"` - MeanTiming time.Duration `json:"mean_timing"` - TotalBytesIn uint64 `json:"total_bytes_in"` - MeanBytesIn float64 `json:"mean_bytes_in"` - TotalBytesOut uint64 `json:"total_bytes_out"` - MeanBytesOut float64 `json:"mean_bytes_out"` - TotalSuccess uint64 `json:"total_success"` - MeanSuccess float64 `json:"mean_success"` - StatusCodes map[uint16]uint64 `json:"status_codes"` - Errors []string `json:"errors"` + TotalRequests uint64 `json:"total_requests"` + TotalTiming time.Duration `json:"total_timing"` + MeanTiming time.Duration `json:"mean_timing"` + TotalBytesIn uint64 `json:"total_bytes_in"` + MeanBytesIn float64 `json:"mean_bytes_in"` + TotalBytesOut uint64 `json:"total_bytes_out"` + MeanBytesOut float64 `json:"mean_bytes_out"` + TotalSuccess uint64 `json:"total_success"` + MeanSuccess float64 `json:"mean_success"` + StatusCodes map[string]int `json:"status_codes"` + Errors []string `json:"errors"` } // NewMetrics computes and returns a Metrics struct out of a slice of Results func NewMetrics(results []Result) *Metrics { m := &Metrics{ TotalRequests: uint64(len(results)), - StatusCodes: map[uint16]uint64{}, + StatusCodes: map[string]int{}, } errorSet := map[string]struct{}{} for _, result := range results { - m.StatusCodes[result.Code]++ + m.StatusCodes[strconv.Itoa(int(result.Code))]++ m.TotalTiming += result.Timing m.TotalBytesOut += result.BytesOut m.TotalBytesIn += result.BytesIn diff --git a/lib/reporters.go b/lib/reporters.go index ddfcff5..a7cd30d 100644 --- a/lib/reporters.go +++ b/lib/reporters.go @@ -6,6 +6,7 @@ import ( "code.google.com/p/plotinum/plotutil" "code.google.com/p/plotinum/vg" "code.google.com/p/plotinum/vg/vgsvg" + "encoding/json" "fmt" "io" "text/tabwriter" @@ -41,6 +42,11 @@ func ReportText(results []Result, out io.Writer) error { return w.Flush() } +// ReportJSON writes a computed Metrics struct to out as JSON +func ReportJSON(results []Result, out io.Writer) error { + return json.NewEncoder(out).Encode(NewMetrics(results)) +} + // ReportTimingsPlot builds up a plot of the response times of the requests // in SVG format and writes it to out func ReportTimingsPlot(results []Result, out io.Writer) error { diff --git a/main.go b/main.go index eccd08e..63363f6 100644 --- a/main.go +++ b/main.go @@ -17,7 +17,7 @@ func main() { 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, plot:timings]") + 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") ) @@ -72,6 +72,8 @@ func run(rate uint64, duration time.Duration, targetsf, ordering, reporter, outp switch reporter { case "text": rep = vegeta.ReportText + case "json": + rep = vegeta.ReportJSON case "plot:timings": rep = vegeta.ReportTimingsPlot default: From 6ee46a365eb866ee445a907dc94b261603fbad05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Senart?= Date: Tue, 10 Sep 2013 13:45:23 +0100 Subject: [PATCH 3/4] Go lint --- lib/metrics.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/metrics.go b/lib/metrics.go index a78855f..1588b1f 100644 --- a/lib/metrics.go +++ b/lib/metrics.go @@ -48,7 +48,7 @@ func NewMetrics(results []Result) *Metrics { m.MeanSuccess = float64(m.TotalSuccess) / float64(m.TotalRequests) m.Errors = make([]string, 0, len(errorSet)) - for err, _ := range errorSet { + for err := range errorSet { m.Errors = append(m.Errors, err) } From 2be03e1f24bd61ef7b77c817ae329b130e65ceca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Senart?= Date: Tue, 10 Sep 2013 13:50:46 +0100 Subject: [PATCH 4/4] Fix metrics.StatusCode test --- lib/metrics_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/metrics_test.go b/lib/metrics_test.go index 7fa20c5..8d17ed5 100644 --- a/lib/metrics_test.go +++ b/lib/metrics_test.go @@ -43,7 +43,7 @@ func TestNewMetrics(t *testing.T) { } } - if len(m.StatusCodes) != 2 || m.StatusCodes[200] != 2 || m.StatusCodes[500] != 1 { + if len(m.StatusCodes) != 2 || m.StatusCodes["200"] != 2 || m.StatusCodes["500"] != 1 { t.Errorf("StatusCodes: want: %v, got: %v", map[int]int{200: 2, 500: 1}, m.StatusCodes) }