diff --git a/args_parser.go b/args_parser.go index e604bd4..2edf10d 100644 --- a/args_parser.go +++ b/args_parser.go @@ -39,6 +39,7 @@ type kingpinParser struct { printSpec *nullableString noPrint bool + detailed bool formatSpec string } @@ -63,6 +64,7 @@ func newKingpinParser() argsParser { clientType: fhttp, printSpec: new(nullableString), noPrint: false, + detailed: false, formatSpec: "plain-text", } @@ -164,6 +166,9 @@ func newKingpinParser() argsParser { Short('q'). BoolVar(&kparser.noPrint) + app.Flag("detailed", "Print detailed statistics"). + BoolVar(&kparser.detailed) + app.Flag("format", "Which format to use to output the result. "+ " is either a name (or its shorthand) of some format "+ "understood by bombardier or a path to the user-defined template, "+ @@ -231,6 +236,7 @@ func (k *kingpinParser) parse(args []string) (config, error) { printIntro: pi, printProgress: pp, printResult: pr, + printDetailed: k.detailed, format: format, }, nil } diff --git a/bombardier.go b/bombardier.go index 7a57728..0ca46f1 100644 --- a/bombardier.go +++ b/bombardier.go @@ -6,9 +6,9 @@ import ( "io/ioutil" "os" "os/signal" + "sort" "strings" "sync" - "sync/atomic" "text/template" "time" @@ -24,12 +24,8 @@ type bombardier struct { bytesRead, bytesWritten int64 // HTTP codes - req1xx uint64 - req2xx uint64 - req3xx uint64 - req4xx uint64 - req5xx uint64 - others uint64 + statsMu sync.Mutex + statusCodes map[int]uint64 conf config barrier completionBarrier @@ -67,6 +63,7 @@ func newBombardier(c config) (*bombardier, error) { b.conf = c b.latencies = uhist.Default() b.requests = fhist.Default() + b.statusCodes = make(map[int]uint64) if b.conf.testType() == counted { b.bar = pb.New64(int64(*b.conf.numReqs)) @@ -196,8 +193,12 @@ func (b *bombardier) prepareTemplate() (*template.Template, error) { "WithLatencies": func() bool { return b.conf.printLatencies }, - "FormatBinary": formatBinary, - "FormatTimeUs": formatTimeUs, + "WithDetailedStats": func() bool { + return b.conf.printDetailed + }, + "SortedStatusCodes": sortedStatusCodes, + "FormatBinary": formatBinary, + "FormatTimeUs": formatTimeUs, "FormatTimeUsUint64": func(us uint64) string { return formatTimeUs(float64(us)) }, @@ -230,22 +231,10 @@ func (b *bombardier) writeStatistics( b.rpl.Lock() b.reqs++ b.rpl.Unlock() - var counter *uint64 - switch code / 100 { - case 1: - counter = &b.req1xx - case 2: - counter = &b.req2xx - case 3: - counter = &b.req3xx - case 4: - counter = &b.req4xx - case 5: - counter = &b.req5xx - default: - counter = &b.others - } - atomic.AddUint64(counter, 1) + + b.statsMu.Lock() + b.statusCodes[code]++ + b.statsMu.Unlock() } func (b *bombardier) performSingleRequest() { @@ -381,13 +370,6 @@ func (b *bombardier) gatherInfo() internal.TestInfo { BytesWritten: b.bytesWritten, TimeTaken: b.timeTaken, - Req1XX: b.req1xx, - Req2XX: b.req2xx, - Req3XX: b.req3xx, - Req4XX: b.req4xx, - Req5XX: b.req5xx, - Others: b.others, - Latencies: b.latencies, Requests: b.requests, }, @@ -401,6 +383,37 @@ func (b *bombardier) gatherInfo() internal.TestInfo { info.Spec.NumberOfRequests = *b.conf.numReqs } + sc := make(map[int]uint64, len(b.statusCodes)) + var req1xx, req2xx, req3xx, req4xx, req5xx, others uint64 + + b.statsMu.Lock() + for k, v := range b.statusCodes { + sc[k] = v + switch k / 100 { + case 1: + req1xx += v + case 2: + req2xx += v + case 3: + req3xx += v + case 4: + req4xx += v + case 5: + req5xx += v + default: + others += v + } + } + b.statsMu.Unlock() + + info.Result.Req1XX = req1xx + info.Result.Req2XX = req2xx + info.Result.Req3XX = req3xx + info.Result.Req4XX = req4xx + info.Result.Req5XX = req5xx + info.Result.Others = others + info.Result.StatusCodes = sc + if b.conf.headers != nil { for _, h := range *b.conf.headers { info.Spec.Headers = append(info.Spec.Headers, @@ -440,6 +453,24 @@ func (b *bombardier) disableOutput() { b.bar.NotPrint = true } +type statusCodePair struct { + Code int + Count uint64 +} + +func sortedStatusCodes(m map[int]uint64) []statusCodePair { + keys := make([]int, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Ints(keys) + res := make([]statusCodePair, len(keys)) + for i, k := range keys { + res[i] = statusCodePair{Code: k, Count: m[k]} + } + return res +} + func main() { cfg, err := parser.parse(os.Args) if err != nil { diff --git a/bombardier_test.go b/bombardier_test.go index de895bd..e9b5e41 100644 --- a/bombardier_test.go +++ b/bombardier_test.go @@ -193,16 +193,17 @@ func testBombardierHTTPCodeRecording(clientType clientTyp, t *testing.T) { } b.disableOutput() b.bombard() + info := b.gatherInfo() expectation := []struct { name string reqsGot uint64 expected uint64 }{ - {"errored", b.others, eachCodeCount * 2}, - {"2xx", b.req2xx, eachCodeCount}, - {"3xx", b.req3xx, eachCodeCount}, - {"4xx", b.req4xx, eachCodeCount}, - {"5xx", b.req5xx, eachCodeCount}, + {"errored", info.Result.Others, eachCodeCount * 2}, + {"2xx", info.Result.Req2XX, eachCodeCount}, + {"3xx", info.Result.Req3XX, eachCodeCount}, + {"4xx", info.Result.Req4XX, eachCodeCount}, + {"5xx", info.Result.Req5XX, eachCodeCount}, } for _, e := range expectation { if e.reqsGot != e.expected { @@ -421,8 +422,11 @@ func testBombardierClientCerts(clientType clientTyp, t *testing.T) { b.disableOutput() b.bombard() - if b.req2xx != 1 { - t.Error("no 2xx responses, total =", b.reqs, ", 1xx/2xx/3xx/4xx/5xx =", b.req1xx, b.req2xx, b.req3xx, b.req4xx, b.req5xx) + info := b.gatherInfo() + if info.Result.Req2XX != 1 { + t.Error("no 2xx responses, total =", b.reqs, ", 1xx/2xx/3xx/4xx/5xx =", + info.Result.Req1XX, info.Result.Req2XX, info.Result.Req3XX, + info.Result.Req4XX, info.Result.Req5XX) } server.Close() @@ -464,9 +468,10 @@ func testBombardierRateLimiting(clientType clientTyp, t *testing.T) { } b.disableOutput() b.bombard() - if float64(b.req2xx) < float64(rate)*0.75 || - float64(b.req2xx) > float64(rate)*1.25 { - t.Error(rate, b.req2xx) + info := b.gatherInfo() + if float64(info.Result.Req2XX) < float64(rate)*0.75 || + float64(info.Result.Req2XX) > float64(rate)*1.25 { + t.Error(rate, info.Result.Req2XX) } } diff --git a/config.go b/config.go index 9dbbc8d..5b8b6cd 100644 --- a/config.go +++ b/config.go @@ -26,7 +26,7 @@ type config struct { rate *uint64 clientType clientTyp - printIntro, printProgress, printResult bool + printIntro, printProgress, printResult, printDetailed bool format format } diff --git a/internal/test_info.go b/internal/test_info.go index 1c0faf5..3e9c109 100644 --- a/internal/test_info.go +++ b/internal/test_info.go @@ -87,6 +87,8 @@ type Results struct { Req1XX, Req2XX, Req3XX, Req4XX, Req5XX uint64 Others uint64 + StatusCodes map[int]uint64 + Errors []ErrorWithCount Latencies ReadonlyUint64Histogram diff --git a/templates.go b/templates.go index dd83772..09a93c2 100644 --- a/templates.go +++ b/templates.go @@ -57,6 +57,12 @@ const ( {{ " HTTP codes:" }} {{ printf " 1xx - %v, 2xx - %v, 3xx - %v, 4xx - %v, 5xx - %v" .Req1XX .Req2XX .Req3XX .Req4XX .Req5XX }} {{- printf "\n others - %v" .Others }} + {{- if WithDetailedStats }} + {{- "\n Detailed status codes:" }} + {{- range $pair := SortedStatusCodes .StatusCodes }} + {{- printf "\n %d - %d" $pair.Code $pair.Count }} + {{- end -}} + {{- end }} {{- with .Errors }} {{- "\n Errors:"}} {{- range . }} @@ -129,6 +135,13 @@ const ( ,"req5xx":{{ .Req5XX -}} ,"others":{{ .Others -}} +,"statusCodes":{ +{{- range $index, $pair := SortedStatusCodes .StatusCodes -}} +{{- if ne $index 0 -}},{{- end -}} +{{- printf "\"%d\":%d" $pair.Code $pair.Count -}} +{{- end -}} +} + {{- with .Errors -}} ,"errors":[ {{- range $index, $error := . -}}