Extend Metrics with max and 95th, 99th percentiles
Implements a new format for the text reporter with more information than before. Also Rename Result.Timing to Result.Latency and related names. Adapt README.md examples to account new text reporter format and library changes.
This commit is contained in:
30
README.md
30
README.md
@@ -117,16 +117,21 @@ Specifies the kind of report to be generated. It defaults to text.
|
|||||||
|
|
||||||
##### text
|
##### text
|
||||||
```
|
```
|
||||||
Time(avg) Requests Success Bytes(rx/tx)
|
Requests [total] 1200
|
||||||
152.341ms 200 17.00% 251.00/0.00
|
Latencies [mean, max, 95, 99] 223.340085ms, 7.788103259s, 326.913687ms, 416.537743ms
|
||||||
|
Bytes In [total, mean] 3714690, 3095.57
|
||||||
Count: 49 30 39 48 34
|
Bytes Out [total, mean] 0, 0.00
|
||||||
Status: 500 404 409 503 200
|
Success [ratio] 55.42%
|
||||||
|
Status Codes [code:count] 0:535 200:665
|
||||||
Error Set:
|
Error Set:
|
||||||
Server Timeout
|
Get http://localhost:6060: dial tcp 127.0.0.1:6060: connection refused
|
||||||
Page Not Found
|
Get http://localhost:6060: read tcp 127.0.0.1:6060: connection reset by peer
|
||||||
|
Get http://localhost:6060: dial tcp 127.0.0.1:6060: connection reset by peer
|
||||||
|
Get http://localhost:6060: write tcp 127.0.0.1:6060: broken pipe
|
||||||
|
Get http://localhost:6060: net/http: transport closed before response was received
|
||||||
|
Get http://localhost:6060: http: can't write HTTP request on broken connection
|
||||||
```
|
```
|
||||||
|
|
||||||
##### json
|
##### json
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -172,14 +177,9 @@ func main() {
|
|||||||
duration := 4 * time.Second
|
duration := 4 * time.Second
|
||||||
|
|
||||||
results := vegeta.Attack(targets, rate, duration)
|
results := vegeta.Attack(targets, rate, duration)
|
||||||
|
metrics := vegeta.NewMetrics(results)
|
||||||
|
|
||||||
totalTime := time.Duration(0)
|
fmt.Printf("Mean latency: %s", metrics.Latencies.Mean)
|
||||||
for _, result := range results {
|
|
||||||
totalTime += result.Timing
|
|
||||||
}
|
|
||||||
meanTime := time.Duration(float64(totalTime) / float64(len(results)))
|
|
||||||
|
|
||||||
fmt.Printf("Average timing: %s", meanTime)
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ func hit(req *http.Request, res chan Result) {
|
|||||||
r, err := client.Do(req)
|
r, err := client.Do(req)
|
||||||
result := Result{
|
result := Result{
|
||||||
Timestamp: began,
|
Timestamp: began,
|
||||||
Timing: time.Since(began),
|
Latency: time.Since(began),
|
||||||
BytesOut: uint64(req.ContentLength),
|
BytesOut: uint64(req.ContentLength),
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -3,49 +3,70 @@ package vegeta
|
|||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/bmizerany/perks/quantile"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Metrics holds the stats computed out of a slice of Results
|
// Metrics holds the stats computed out of a slice of Results
|
||||||
// that is used for some of the Reporters
|
// that is used for some of the Reporters
|
||||||
type Metrics struct {
|
type Metrics struct {
|
||||||
TotalRequests uint64 `json:"total_requests"`
|
Latencies struct {
|
||||||
TotalTiming time.Duration `json:"total_timing"`
|
Total time.Duration `json:"total"`
|
||||||
MeanTiming time.Duration `json:"mean_timing"`
|
Max time.Duration `json:"max"`
|
||||||
TotalBytesIn uint64 `json:"total_bytes_in"`
|
Mean time.Duration `json:"mean"`
|
||||||
MeanBytesIn float64 `json:"mean_bytes_in"`
|
Mean95 time.Duration `json:"mean_95"`
|
||||||
TotalBytesOut uint64 `json:"total_bytes_out"`
|
Mean99 time.Duration `json:"mean_99"`
|
||||||
MeanBytesOut float64 `json:"mean_bytes_out"`
|
} `json:"latencies"`
|
||||||
TotalSuccess uint64 `json:"total_success"`
|
|
||||||
MeanSuccess float64 `json:"mean_success"`
|
BytesIn struct {
|
||||||
StatusCodes map[string]int `json:"status_codes"`
|
Total uint64 `json:"total"`
|
||||||
Errors []string `json:"errors"`
|
Mean float64 `json:"mean"`
|
||||||
|
} `json:"bytes_in"`
|
||||||
|
|
||||||
|
BytesOut struct {
|
||||||
|
Total uint64 `json:"total"`
|
||||||
|
Mean float64 `json:"mean"`
|
||||||
|
} `json:"bytes_out"`
|
||||||
|
|
||||||
|
Requests uint64 `json:"requests"`
|
||||||
|
Success float64 `json:"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
|
// NewMetrics computes and returns a Metrics struct out of a slice of Results
|
||||||
func NewMetrics(results []Result) *Metrics {
|
func NewMetrics(results []Result) *Metrics {
|
||||||
m := &Metrics{
|
m := &Metrics{
|
||||||
TotalRequests: uint64(len(results)),
|
Requests: uint64(len(results)),
|
||||||
StatusCodes: map[string]int{},
|
StatusCodes: map[string]int{},
|
||||||
}
|
}
|
||||||
errorSet := map[string]struct{}{}
|
errorSet := map[string]struct{}{}
|
||||||
|
quants := quantile.NewTargeted(0.95, 0.99)
|
||||||
|
totalSuccess := 0
|
||||||
|
|
||||||
for _, result := range results {
|
for _, result := range results {
|
||||||
|
quants.Insert(float64(result.Latency))
|
||||||
m.StatusCodes[strconv.Itoa(int(result.Code))]++
|
m.StatusCodes[strconv.Itoa(int(result.Code))]++
|
||||||
m.TotalTiming += result.Timing
|
m.Latencies.Total += result.Latency
|
||||||
m.TotalBytesOut += result.BytesOut
|
m.BytesOut.Total += result.BytesOut
|
||||||
m.TotalBytesIn += result.BytesIn
|
m.BytesIn.Total += result.BytesIn
|
||||||
|
if result.Latency > m.Latencies.Max {
|
||||||
|
m.Latencies.Max = result.Latency
|
||||||
|
}
|
||||||
if result.Code >= 200 && result.Code < 300 {
|
if result.Code >= 200 && result.Code < 300 {
|
||||||
m.TotalSuccess++
|
totalSuccess++
|
||||||
}
|
}
|
||||||
if result.Error != "" {
|
if result.Error != "" {
|
||||||
errorSet[result.Error] = struct{}{}
|
errorSet[result.Error] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m.MeanTiming = time.Duration(float64(m.TotalTiming) / float64(m.TotalRequests))
|
m.Latencies.Mean = time.Duration(float64(m.Latencies.Total) / float64(m.Requests))
|
||||||
m.MeanBytesOut = float64(m.TotalBytesOut) / float64(m.TotalRequests)
|
m.Latencies.Mean95 = time.Duration(quants.Query(0.95))
|
||||||
m.MeanBytesIn = float64(m.TotalBytesIn) / float64(m.TotalRequests)
|
m.Latencies.Mean99 = time.Duration(quants.Query(0.99))
|
||||||
m.MeanSuccess = float64(m.TotalSuccess) / float64(m.TotalRequests)
|
m.BytesIn.Mean = float64(m.BytesIn.Total) / float64(m.Requests)
|
||||||
|
m.BytesOut.Mean = float64(m.BytesOut.Total) / float64(m.Requests)
|
||||||
|
m.Success = float64(totalSuccess) / float64(m.Requests)
|
||||||
|
|
||||||
m.Errors = make([]string, 0, len(errorSet))
|
m.Errors = make([]string, 0, len(errorSet))
|
||||||
for err := range errorSet {
|
for err := range errorSet {
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ func TestNewMetrics(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
for field, values := range map[string][]float64{
|
for field, values := range map[string][]float64{
|
||||||
"MeanBytesIn": []float64{m.MeanBytesIn, 20.0},
|
"BytesIn.Mean": []float64{m.BytesIn.Mean, 20.0},
|
||||||
"MeanBytesOut": []float64{m.MeanBytesOut, 20.0},
|
"BytesOut.Mean": []float64{m.BytesOut.Mean, 20.0},
|
||||||
"MeanSuccess": []float64{m.MeanSuccess, 0.6666666666666666},
|
"Sucess": []float64{m.Success, 0.6666666666666666},
|
||||||
} {
|
} {
|
||||||
if values[0] != values[1] {
|
if values[0] != values[1] {
|
||||||
t.Errorf("%s: want: %f, got: %f", field, values[1], values[0])
|
t.Errorf("%s: want: %f, got: %f", field, values[1], values[0])
|
||||||
@@ -23,8 +23,11 @@ func TestNewMetrics(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for field, values := range map[string][]time.Duration{
|
for field, values := range map[string][]time.Duration{
|
||||||
"TotalTiming": []time.Duration{m.TotalTiming, 150 * time.Millisecond},
|
"Latencies.Total": []time.Duration{m.Latencies.Total, 150 * time.Millisecond},
|
||||||
"MeanTiming": []time.Duration{m.MeanTiming, 50 * time.Millisecond},
|
"Latencies.Mean": []time.Duration{m.Latencies.Mean, 50 * time.Millisecond},
|
||||||
|
"Latencies.Mean95": []time.Duration{m.Latencies.Mean95, 30 * time.Millisecond},
|
||||||
|
"Latencies.Mean99": []time.Duration{m.Latencies.Mean99, 30 * time.Millisecond},
|
||||||
|
"Latencies.Max": []time.Duration{m.Latencies.Max, 100 * time.Millisecond},
|
||||||
} {
|
} {
|
||||||
if values[0] != values[1] {
|
if values[0] != values[1] {
|
||||||
t.Errorf("%s: want: %s, got: %s", field, values[1], values[0])
|
t.Errorf("%s: want: %s, got: %s", field, values[1], values[0])
|
||||||
@@ -32,10 +35,9 @@ func TestNewMetrics(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for field, values := range map[string][]uint64{
|
for field, values := range map[string][]uint64{
|
||||||
"TotalSuccess": []uint64{m.TotalSuccess, 2},
|
"BytesOut.Total": []uint64{m.BytesOut.Total, 60},
|
||||||
"TotalBytesOut": []uint64{m.TotalBytesOut, 60},
|
"BytesIn.Total": []uint64{m.BytesIn.Total, 60},
|
||||||
"TotalRequests": []uint64{m.TotalRequests, 3},
|
"Requests": []uint64{m.Requests, 3},
|
||||||
"TotalBytesIn": []uint64{m.TotalBytesIn, 60},
|
|
||||||
} {
|
} {
|
||||||
if values[0] != values[1] {
|
if values[0] != values[1] {
|
||||||
t.Errorf("%s: want: %d, got: %d", field, values[1], values[0])
|
t.Errorf("%s: want: %d, got: %d", field, values[1], values[0])
|
||||||
|
|||||||
@@ -18,19 +18,17 @@ func ReportText(results []Result) ([]byte, error) {
|
|||||||
out := &bytes.Buffer{}
|
out := &bytes.Buffer{}
|
||||||
|
|
||||||
w := tabwriter.NewWriter(out, 0, 8, 2, '\t', tabwriter.StripEscape)
|
w := tabwriter.NewWriter(out, 0, 8, 2, '\t', tabwriter.StripEscape)
|
||||||
fmt.Fprintf(w, "Time(avg)\tRequests\tSuccess\tBytes(rx/tx)\n")
|
fmt.Fprintf(w, "Requests\t[total]\t%d\n", m.Requests)
|
||||||
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, "Latencies\t[mean, max, 95, 99]\t%s, %s, %s, %s\n",
|
||||||
|
m.Latencies.Mean, m.Latencies.Max, m.Latencies.Mean95, m.Latencies.Mean99)
|
||||||
fmt.Fprintf(w, "\nCount:\t")
|
fmt.Fprintf(w, "Bytes In\t[total, mean]\t%d, %.2f\n", m.BytesIn.Total, m.BytesIn.Mean)
|
||||||
for _, count := range m.StatusCodes {
|
fmt.Fprintf(w, "Bytes Out\t[total, mean]\t%d, %.2f\n", m.BytesOut.Total, m.BytesOut.Mean)
|
||||||
fmt.Fprintf(w, "%d\t", count)
|
fmt.Fprintf(w, "Success\t[ratio]\t%.2f%%\n", m.Success*100)
|
||||||
|
fmt.Fprintf(w, "Status Codes\t[code:count]\t")
|
||||||
|
for code, count := range m.StatusCodes {
|
||||||
|
fmt.Fprintf(w, "%s:%d ", code, count)
|
||||||
}
|
}
|
||||||
fmt.Fprintf(w, "\nStatus:\t")
|
fmt.Fprintln(w, "\nError Set:")
|
||||||
for code := range m.StatusCodes {
|
|
||||||
fmt.Fprintf(w, "%s\t", code)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintln(w, "\n\nError Set:")
|
|
||||||
for _, err := range m.Errors {
|
for _, err := range m.Errors {
|
||||||
fmt.Fprintln(w, err)
|
fmt.Fprintln(w, err)
|
||||||
}
|
}
|
||||||
@@ -53,7 +51,7 @@ func ReportPlot(results []Result) ([]byte, error) {
|
|||||||
for _, result := range results {
|
for _, result := range results {
|
||||||
fmt.Fprintf(out, "[%f,%f],",
|
fmt.Fprintf(out, "[%f,%f],",
|
||||||
result.Timestamp.Sub(results[0].Timestamp).Seconds(),
|
result.Timestamp.Sub(results[0].Timestamp).Seconds(),
|
||||||
result.Timing.Seconds()*1000,
|
result.Latency.Seconds()*1000,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
out.Truncate(out.Len() - 1) // Remove trailing comma
|
out.Truncate(out.Len() - 1) // Remove trailing comma
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
type Result struct {
|
type Result struct {
|
||||||
Code uint16
|
Code uint16
|
||||||
Timestamp time.Time
|
Timestamp time.Time
|
||||||
Timing time.Duration
|
Latency time.Duration
|
||||||
BytesOut uint64
|
BytesOut uint64
|
||||||
BytesIn uint64
|
BytesIn uint64
|
||||||
Error string
|
Error string
|
||||||
|
|||||||
Reference in New Issue
Block a user