#46 support JSON snapshot
This commit is contained in:
parent
72b10d10a8
commit
cf5e6eef85
|
@ -99,7 +99,7 @@ A high-performance HTTP benchmarking tool with real-time web UI and terminal dis
|
|||
Examples:
|
||||
|
||||
plow http://127.0.0.1:8080/ -c 20 -n 100000
|
||||
plow https://httpbin.org/post -c 20 -d 5m --body @file.json -T 'application/json' -m POST
|
||||
plow https://httpbin.org/post -c 20 -d 5m --body @file.jsonFormat -T 'application/jsonFormat' -m POST
|
||||
|
||||
Flags:
|
||||
--help Show context-sensitive help.
|
||||
|
@ -109,6 +109,7 @@ Flags:
|
|||
-d, --duration=DURATION Duration of test, examples: -d 10s -d 3m
|
||||
-i, --interval=200ms Print snapshot result every interval, use 0 to print once at the end
|
||||
--seconds Use seconds as time unit to print
|
||||
--json Print snapshot result as JSON
|
||||
-b, --body=BODY HTTP request body, if start the body with @, the rest should be a filename to read
|
||||
--stream Specify whether to stream file specified by '--body @file' using chunked encoding or to read into memory
|
||||
-m, --method="GET" HTTP method
|
||||
|
@ -126,7 +127,7 @@ Flags:
|
|||
--socks5=ip:port Socks5 proxy
|
||||
--auto-open-browser Specify whether auto open browser to show Web charts
|
||||
--[no-]clean Clean the histogram bar once its finished. Default is true
|
||||
--[no-]summary Only print the summary without realtime reports
|
||||
--summary Only print the summary without realtime reports
|
||||
--version Show application version.
|
||||
|
||||
Flags default values also read from env PLOW_SOME_FLAG, such as PLOW_TIMEOUT=5s equals to --timeout=5s
|
||||
|
|
|
@ -42,7 +42,7 @@ function {{ .ViewID }}_sync() {
|
|||
$.ajax({
|
||||
type: "GET",
|
||||
url: "{{ .APIPath }}{{ .Route }}",
|
||||
dataType: "json",
|
||||
dataType: "jsonFormat",
|
||||
success: function (result) {
|
||||
let opt = goecharts_{{ .ViewID }}.getOption();
|
||||
let x = opt.xAxis[0].data;
|
||||
|
@ -141,8 +141,8 @@ func (c *Charts) newRPSView() components.Charter {
|
|||
}
|
||||
|
||||
type Metrics struct {
|
||||
Values []interface{} `json:"values"`
|
||||
Time string `json:"time"`
|
||||
Values []interface{} `jsonFormat:"values"`
|
||||
Time string `jsonFormat:"time"`
|
||||
}
|
||||
|
||||
type Charts struct {
|
||||
|
|
26
main.go
26
main.go
|
@ -5,6 +5,8 @@ import (
|
|||
"golang.org/x/time/rate"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
@ -20,6 +22,7 @@ var (
|
|||
duration = kingpin.Flag("duration", "Duration of test, examples: -d 10s -d 3m").Short('d').PlaceHolder("DURATION").Duration()
|
||||
interval = kingpin.Flag("interval", "Print snapshot result every interval, use 0 to print once at the end").Short('i').Default("200ms").Duration()
|
||||
seconds = kingpin.Flag("seconds", "Use seconds as time unit to print").Bool()
|
||||
jsonFormat = kingpin.Flag("json", "Print snapshot result as JSON").Bool()
|
||||
|
||||
body = kingpin.Flag("body", "HTTP request body, if start the body with @, the rest should be a filename to read").Short('b').String()
|
||||
stream = kingpin.Flag("stream", "Specify whether to stream file specified by '--body @file' using chunked encoding or to read into memory").Default("false").Bool()
|
||||
|
@ -40,7 +43,8 @@ var (
|
|||
|
||||
autoOpenBrowser = kingpin.Flag("auto-open-browser", "Specify whether auto open browser to show Web charts").Bool()
|
||||
clean = kingpin.Flag("clean", "Clean the histogram bar once its finished. Default is true").Default("true").NegatableBool()
|
||||
summary = kingpin.Flag("summary", "Only print the summary without realtime reports").Default("false").NegatableBool()
|
||||
summary = kingpin.Flag("summary", "Only print the summary without realtime reports").Default("false").Bool()
|
||||
pprofAddr = kingpin.Flag("pprof", "Enable pprof at special address").Hidden().String()
|
||||
url = kingpin.Arg("url", "request url").Required().String()
|
||||
)
|
||||
|
||||
|
@ -79,7 +83,7 @@ var CompactUsageTemplate = `{{define "FormatCommand" -}}
|
|||
Examples:
|
||||
|
||||
plow http://127.0.0.1:8080/ -c 20 -n 100000
|
||||
plow https://httpbin.org/post -c 20 -d 5m --body @file.json -T 'application/json' -m POST
|
||||
plow https://httpbin.org/post -c 20 -d 5m --body @file.jsonFormat -T 'application/jsonFormat' -m POST
|
||||
|
||||
{{if .Context.Flags -}}
|
||||
{{T "Flags:"}}
|
||||
|
@ -182,6 +186,10 @@ func main() {
|
|||
return
|
||||
}
|
||||
|
||||
if *pprofAddr != "" {
|
||||
go http.ListenAndServe(*pprofAddr, nil)
|
||||
}
|
||||
|
||||
var err error
|
||||
var bodyBytes []byte
|
||||
var bodyFile string
|
||||
|
@ -232,11 +240,6 @@ func main() {
|
|||
return
|
||||
}
|
||||
|
||||
outStream := os.Stdout
|
||||
if *summary {
|
||||
outStream = os.Stderr
|
||||
isTerminal = false
|
||||
}
|
||||
// description
|
||||
var desc string
|
||||
desc = fmt.Sprintf("Benchmarking %s", *url)
|
||||
|
@ -247,7 +250,7 @@ func main() {
|
|||
desc += fmt.Sprintf(" for %s", duration.String())
|
||||
}
|
||||
desc += fmt.Sprintf(" using %d connection(s).", *concurrency)
|
||||
fmt.Fprintln(outStream, desc)
|
||||
fmt.Fprintln(os.Stderr, desc)
|
||||
|
||||
// charts listener
|
||||
var ln net.Listener
|
||||
|
@ -257,9 +260,9 @@ func main() {
|
|||
errAndExit(err.Error())
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(outStream, "@ Real-time charts is listening on http://%s\n", ln.Addr().String())
|
||||
fmt.Fprintf(os.Stderr, "@ Real-time charts is listening on http://%s\n", ln.Addr().String())
|
||||
}
|
||||
fmt.Fprintln(outStream, "")
|
||||
fmt.Fprintln(os.Stderr, "")
|
||||
|
||||
// do request
|
||||
go requester.Run()
|
||||
|
@ -280,6 +283,5 @@ func main() {
|
|||
|
||||
// terminal printer
|
||||
printer := NewPrinter(*requests, *duration, !*clean, *summary)
|
||||
printer.PrintLoop(report.Snapshot, *interval, *seconds, report.Done())
|
||||
|
||||
printer.PrintLoop(report.Snapshot, *interval, *seconds, *jsonFormat, report.Done())
|
||||
}
|
||||
|
|
167
print.go
167
print.go
|
@ -2,6 +2,7 @@ package main
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/mattn/go-runewidth"
|
||||
|
@ -63,12 +64,12 @@ func (p *Printer) updateProgressValue(rs *SnapshotReport) {
|
|||
}
|
||||
}
|
||||
|
||||
func (p *Printer) PrintLoop(snapshot func() *SnapshotReport, interval time.Duration, useSeconds bool, doneChan <-chan struct{}) {
|
||||
func (p *Printer) PrintLoop(snapshot func() *SnapshotReport, interval time.Duration, useSeconds bool, json bool, doneChan <-chan struct{}) {
|
||||
var buf bytes.Buffer
|
||||
|
||||
var backCursor string
|
||||
cl := clearLine
|
||||
if p.summary || interval == 0 {
|
||||
if p.summary || interval == 0 || !isTerminal {
|
||||
cl = nil
|
||||
}
|
||||
echo := func(isFinal bool) {
|
||||
|
@ -76,7 +77,11 @@ func (p *Printer) PrintLoop(snapshot func() *SnapshotReport, interval time.Durat
|
|||
p.updateProgressValue(report)
|
||||
os.Stdout.WriteString(backCursor)
|
||||
buf.Reset()
|
||||
p.formatTableReports(&buf, report, isFinal, useSeconds)
|
||||
if json {
|
||||
p.formatJSONReports(&buf, report, isFinal, useSeconds)
|
||||
} else {
|
||||
p.formatTableReports(&buf, report, isFinal, useSeconds)
|
||||
}
|
||||
result := buf.Bytes()
|
||||
n := 0
|
||||
for {
|
||||
|
@ -93,7 +98,9 @@ func (p *Printer) PrintLoop(snapshot func() *SnapshotReport, interval time.Durat
|
|||
result = result[i+1:]
|
||||
}
|
||||
os.Stdout.Sync()
|
||||
backCursor = fmt.Sprintf("\033[%dA", n)
|
||||
if isTerminal {
|
||||
backCursor = fmt.Sprintf("\033[%dA", n)
|
||||
}
|
||||
}
|
||||
|
||||
if interval > 0 {
|
||||
|
@ -185,6 +192,24 @@ func formatFloat64(f float64) string {
|
|||
return strconv.FormatFloat(f, 'f', -1, 64)
|
||||
}
|
||||
|
||||
func (p *Printer) formatJSONReports(writer *bytes.Buffer, snapshot *SnapshotReport, isFinal bool, useSeconds bool) {
|
||||
indent := 0
|
||||
writer.WriteString("{\n")
|
||||
indent++
|
||||
p.buildJSONSummary(writer, snapshot, indent)
|
||||
if len(snapshot.Errors) != 0 {
|
||||
writer.WriteString(",\n")
|
||||
p.buildJSONErrors(writer, snapshot, indent)
|
||||
}
|
||||
writer.WriteString(",\n")
|
||||
p.buildJSONStats(writer, snapshot, useSeconds, indent)
|
||||
writer.WriteString(",\n")
|
||||
p.buildJSONPercentile(writer, snapshot, useSeconds, indent)
|
||||
writer.WriteString(",\n")
|
||||
p.buildJSONHistogram(writer, snapshot, useSeconds, indent)
|
||||
writer.WriteString("\n}\n")
|
||||
}
|
||||
|
||||
func (p *Printer) formatTableReports(writer *bytes.Buffer, snapshot *SnapshotReport, isFinal bool, useSeconds bool) {
|
||||
summaryBulk := p.buildSummary(snapshot, isFinal)
|
||||
errorsBulks := p.buildErrors(snapshot)
|
||||
|
@ -213,6 +238,30 @@ func (p *Printer) formatTableReports(writer *bytes.Buffer, snapshot *SnapshotRep
|
|||
writeBulk(writer, hisBulk)
|
||||
}
|
||||
|
||||
func (p *Printer) buildJSONHistogram(writer *bytes.Buffer, snapshot *SnapshotReport, useSeconds bool, indent int) {
|
||||
tab0 := strings.Repeat(" ", indent)
|
||||
writer.WriteString(tab0 + "\"Histograms\": [\n")
|
||||
tab1 := strings.Repeat(" ", indent+1)
|
||||
|
||||
maxCount := 0
|
||||
hisSum := 0
|
||||
for _, bin := range snapshot.Histograms {
|
||||
if maxCount < bin.Count {
|
||||
maxCount = bin.Count
|
||||
}
|
||||
hisSum += bin.Count
|
||||
}
|
||||
for i, bin := range snapshot.Histograms {
|
||||
writer.WriteString(fmt.Sprintf(`%s[ "%s", %d ]`, tab1,
|
||||
durationToString(bin.Mean, useSeconds), bin.Count))
|
||||
if i != len(snapshot.Histograms)-1 {
|
||||
writer.WriteString(",")
|
||||
}
|
||||
writer.WriteString("\n")
|
||||
}
|
||||
writer.WriteString(tab0 + "]")
|
||||
}
|
||||
|
||||
func (p *Printer) buildHistogram(snapshot *SnapshotReport, useSeconds bool, isFinal bool) [][]string {
|
||||
hisBulk := make([][]string, 0, 8)
|
||||
maxCount := 0
|
||||
|
@ -245,6 +294,22 @@ func (p *Printer) buildHistogram(snapshot *SnapshotReport, useSeconds bool, isFi
|
|||
return hisBulk
|
||||
}
|
||||
|
||||
func (p *Printer) buildJSONPercentile(writer *bytes.Buffer, snapshot *SnapshotReport, useSeconds bool, indent int) {
|
||||
tab0 := strings.Repeat(" ", indent)
|
||||
writer.WriteString(tab0 + "\"Percentiles\": {\n")
|
||||
tab1 := strings.Repeat(" ", indent+1)
|
||||
for i, percentile := range snapshot.Percentiles {
|
||||
perc := formatFloat64(percentile.Percentile * 100)
|
||||
writer.WriteString(fmt.Sprintf(`%s"%s": "%s"`, tab1, "P"+perc,
|
||||
durationToString(percentile.Latency, useSeconds)))
|
||||
if i != len(snapshot.Percentiles)-1 {
|
||||
writer.WriteString(",")
|
||||
}
|
||||
writer.WriteString("\n")
|
||||
}
|
||||
writer.WriteString(tab0 + "}")
|
||||
}
|
||||
|
||||
func (p *Printer) buildPercentile(snapshot *SnapshotReport, useSeconds bool) [][]string {
|
||||
percBulk := make([][]string, 2)
|
||||
percAligns := make([]int, 0, len(snapshot.Percentiles))
|
||||
|
@ -259,6 +324,30 @@ func (p *Printer) buildPercentile(snapshot *SnapshotReport, useSeconds bool) [][
|
|||
return percBulk
|
||||
}
|
||||
|
||||
func (p *Printer) buildJSONStats(writer *bytes.Buffer, snapshot *SnapshotReport, useSeconds bool, indent int) {
|
||||
tab0 := strings.Repeat(" ", indent)
|
||||
writer.WriteString(tab0 + "\"Statistics\": {\n")
|
||||
tab1 := strings.Repeat(" ", indent+1)
|
||||
writer.WriteString(fmt.Sprintf(`%s"Latency": { "Min": "%s", "Mean": "%s", "StdDev": "%s", "Max": "%s" }`,
|
||||
tab1,
|
||||
durationToString(snapshot.Stats.Min, useSeconds),
|
||||
durationToString(snapshot.Stats.Mean, useSeconds),
|
||||
durationToString(snapshot.Stats.StdDev, useSeconds),
|
||||
durationToString(snapshot.Stats.Max, useSeconds),
|
||||
))
|
||||
if snapshot.RpsStats != nil {
|
||||
writer.WriteString(",\n")
|
||||
writer.WriteString(fmt.Sprintf(`%s"RPS": { "Min": %s, "Mean": %s, "StdDev": %s, "Max": %s }`,
|
||||
tab1,
|
||||
formatFloat64(math.Trunc(snapshot.RpsStats.Min*100)/100.0),
|
||||
formatFloat64(math.Trunc(snapshot.RpsStats.Mean*100)/100.0),
|
||||
formatFloat64(math.Trunc(snapshot.RpsStats.StdDev*100)/100.0),
|
||||
formatFloat64(math.Trunc(snapshot.RpsStats.Max*100)/100.0),
|
||||
))
|
||||
}
|
||||
writer.WriteString("\n" + tab0 + "}")
|
||||
}
|
||||
|
||||
func (p *Printer) buildStats(snapshot *SnapshotReport, useSeconds bool) [][]string {
|
||||
var statsBulk [][]string
|
||||
statsBulk = append(statsBulk,
|
||||
|
@ -286,6 +375,23 @@ func (p *Printer) buildStats(snapshot *SnapshotReport, useSeconds bool) [][]stri
|
|||
return statsBulk
|
||||
}
|
||||
|
||||
func (p *Printer) buildJSONErrors(writer *bytes.Buffer, snapshot *SnapshotReport, indent int) {
|
||||
tab0 := strings.Repeat(" ", indent)
|
||||
writer.WriteString(tab0 + "\"Error\": {\n")
|
||||
tab1 := strings.Repeat(" ", indent+1)
|
||||
errors := sortMapStrInt(snapshot.Errors)
|
||||
for i, v := range errors {
|
||||
v[1] = colorize(v[1], FgRedColor)
|
||||
vb, _ := json.Marshal(v[0])
|
||||
writer.WriteString(fmt.Sprintf(`%s%s: %s`, tab1, vb, v[1]))
|
||||
if i != len(errors)-1 {
|
||||
writer.WriteString(",")
|
||||
}
|
||||
writer.WriteString("\n")
|
||||
}
|
||||
writer.WriteString(tab0 + "}")
|
||||
}
|
||||
|
||||
func (p *Printer) buildErrors(snapshot *SnapshotReport) [][]string {
|
||||
var errorsBulks [][]string
|
||||
for k, v := range snapshot.Errors {
|
||||
|
@ -299,9 +405,47 @@ func (p *Printer) buildErrors(snapshot *SnapshotReport) [][]string {
|
|||
return errorsBulks
|
||||
}
|
||||
|
||||
func sortMapStrInt(m map[string]int64) (ret [][]string) {
|
||||
for k, v := range m {
|
||||
ret = append(ret, []string{k, strconv.FormatInt(v, 10)})
|
||||
}
|
||||
sort.Slice(ret, func(i, j int) bool { return ret[i][0] < ret[j][0] })
|
||||
return
|
||||
}
|
||||
|
||||
func (p *Printer) buildJSONSummary(writer *bytes.Buffer, snapshot *SnapshotReport, indent int) {
|
||||
tab0 := strings.Repeat(" ", indent)
|
||||
writer.WriteString(tab0 + "\"Summary\": {\n")
|
||||
{
|
||||
tab1 := strings.Repeat(" ", indent+1)
|
||||
writer.WriteString(fmt.Sprintf("%s\"Elapsed\": \"%s\",\n", tab1, snapshot.Elapsed.Truncate(100*time.Millisecond).String()))
|
||||
writer.WriteString(fmt.Sprintf("%s\"Count\": %d,\n", tab1, snapshot.Count))
|
||||
writer.WriteString(fmt.Sprintf("%s\"Counts\": {\n", tab1))
|
||||
i := 0
|
||||
tab2 := strings.Repeat(" ", indent+2)
|
||||
codes := sortMapStrInt(snapshot.Codes)
|
||||
for _, v := range codes {
|
||||
i++
|
||||
if v[0] != "2xx" {
|
||||
v[1] = colorize(v[1], FgMagentaColor)
|
||||
}
|
||||
writer.WriteString(fmt.Sprintf(`%s"%s": %s`, tab2, v[0], v[1]))
|
||||
if i != len(snapshot.Codes) {
|
||||
writer.WriteString(",")
|
||||
}
|
||||
writer.WriteString("\n")
|
||||
}
|
||||
writer.WriteString(tab1 + "},\n")
|
||||
writer.WriteString(fmt.Sprintf("%s\"RPS\": %.3f,\n", tab1, snapshot.RPS))
|
||||
writer.WriteString(fmt.Sprintf("%s\"Reads\": \"%.3fMB/s\",\n", tab1, snapshot.ReadThroughput))
|
||||
writer.WriteString(fmt.Sprintf("%s\"Writes\": \"%.3fMB/s\"\n", tab1, snapshot.WriteThroughput))
|
||||
}
|
||||
writer.WriteString(tab0 + "}")
|
||||
}
|
||||
|
||||
func (p *Printer) buildSummary(snapshot *SnapshotReport, isFinal bool) [][]string {
|
||||
summarybulk := make([][]string, 0, 8)
|
||||
elapsedLine := []string{"Elapsed", snapshot.Elapsed.Truncate(time.Millisecond).String()}
|
||||
elapsedLine := []string{"Elapsed", snapshot.Elapsed.Truncate(100 * time.Millisecond).String()}
|
||||
if p.maxDuration > 0 && !isFinal {
|
||||
elapsedLine = append(elapsedLine, p.pbDurStr)
|
||||
}
|
||||
|
@ -315,16 +459,13 @@ func (p *Printer) buildSummary(snapshot *SnapshotReport, isFinal bool) [][]strin
|
|||
countLine,
|
||||
)
|
||||
|
||||
codesBulks := make([][]string, 0, len(snapshot.Codes))
|
||||
for k, v := range snapshot.Codes {
|
||||
vs := strconv.FormatInt(v, 10)
|
||||
if k != "2xx" {
|
||||
vs = colorize(vs, FgMagentaColor)
|
||||
codes := sortMapStrInt(snapshot.Codes)
|
||||
for _, v := range codes {
|
||||
if v[0] != "2xx" {
|
||||
v[1] = colorize(v[1], FgMagentaColor)
|
||||
}
|
||||
codesBulks = append(codesBulks, []string{" " + k, vs})
|
||||
summarybulk = append(summarybulk, []string{" " + v[0], v[1]})
|
||||
}
|
||||
sort.Slice(codesBulks, func(i, j int) bool { return codesBulks[i][0] < codesBulks[j][0] })
|
||||
summarybulk = append(summarybulk, codesBulks...)
|
||||
summarybulk = append(summarybulk,
|
||||
[]string{"RPS", fmt.Sprintf("%.3f", snapshot.RPS)},
|
||||
[]string{"Reads", fmt.Sprintf("%.3fMB/s", snapshot.ReadThroughput)},
|
||||
|
|
Loading…
Reference in New Issue