1package vegeta 2 3import ( 4 "encoding/json" 5 "fmt" 6 "io" 7 "sort" 8 "strings" 9 "text/tabwriter" 10 "time" 11) 12 13// A Report represents the state a Reporter uses to write out its reports. 14type Report interface { 15 // Add adds a given *Result to a Report. 16 Add(*Result) 17} 18 19// Closer wraps the optional Report Close method. 20type Closer interface { 21 // Close permantently closes a Report, running any necessary book keeping. 22 Close() 23} 24 25// A Reporter function writes out reports to the given io.Writer or returns an 26// error in case of failure. 27type Reporter func(io.Writer) error 28 29// Report is a convenience method wrapping the Reporter function type. 30func (rep Reporter) Report(w io.Writer) error { return rep(w) } 31 32// NewHistogramReporter returns a Reporter that writes out a Histogram as 33// aligned, formatted text. 34func NewHistogramReporter(h *Histogram) Reporter { 35 return func(w io.Writer) (err error) { 36 tw := tabwriter.NewWriter(w, 0, 8, 2, ' ', tabwriter.StripEscape) 37 if _, err = fmt.Fprintf(tw, "Bucket\t\t#\t%%\tHistogram\n"); err != nil { 38 return err 39 } 40 41 for i, count := range h.Counts { 42 ratio := float64(count) / float64(h.Total) 43 lo, hi := h.Buckets.Nth(i) 44 pad := strings.Repeat("#", int(ratio*75)) 45 _, err = fmt.Fprintf(tw, "[%s,\t%s]\t%d\t%.2f%%\t%s\n", lo, hi, count, ratio*100, pad) 46 if err != nil { 47 return nil 48 } 49 } 50 51 return tw.Flush() 52 } 53} 54 55// NewTextReporter returns a Reporter that writes out Metrics as aligned, 56// formatted text. 57func NewTextReporter(m *Metrics) Reporter { 58 const fmtstr = "Requests\t[total, rate, throughput]\t%d, %.2f, %.2f\n" + 59 "Duration\t[total, attack, wait]\t%s, %s, %s\n" + 60 "Latencies\t[min, mean, 50, 90, 95, 99, max]\t%s, %s, %s, %s, %s, %s, %s\n" + 61 "Bytes In\t[total, mean]\t%d, %.2f\n" + 62 "Bytes Out\t[total, mean]\t%d, %.2f\n" + 63 "Success\t[ratio]\t%.2f%%\n" + 64 "Status Codes\t[code:count]\t" 65 66 return func(w io.Writer) (err error) { 67 tw := tabwriter.NewWriter(w, 0, 8, 2, ' ', tabwriter.StripEscape) 68 if _, err = fmt.Fprintf(tw, fmtstr, 69 m.Requests, m.Rate, m.Throughput, 70 round(m.Duration+m.Wait), 71 round(m.Duration), 72 round(m.Wait), 73 round(m.Latencies.Min), 74 round(m.Latencies.Mean), 75 round(m.Latencies.P50), 76 round(m.Latencies.P90), 77 round(m.Latencies.P95), 78 round(m.Latencies.P99), 79 round(m.Latencies.Max), 80 m.BytesIn.Total, m.BytesIn.Mean, 81 m.BytesOut.Total, m.BytesOut.Mean, 82 m.Success*100, 83 ); err != nil { 84 return err 85 } 86 87 codes := make([]string, 0, len(m.StatusCodes)) 88 for code := range m.StatusCodes { 89 codes = append(codes, code) 90 } 91 92 sort.Strings(codes) 93 94 for _, code := range codes { 95 count := m.StatusCodes[code] 96 if _, err = fmt.Fprintf(tw, "%s:%d ", code, count); err != nil { 97 return err 98 } 99 } 100 101 if _, err = fmt.Fprintln(tw, "\nError Set:"); err != nil { 102 return err 103 } 104 105 for _, e := range m.Errors { 106 if _, err = fmt.Fprintln(tw, e); err != nil { 107 return err 108 } 109 } 110 111 return tw.Flush() 112 } 113} 114 115var durations = [...]time.Duration{ 116 time.Hour, 117 time.Minute, 118 time.Second, 119 time.Millisecond, 120 time.Microsecond, 121 time.Nanosecond, 122} 123 124// round to the next most precise unit 125func round(d time.Duration) time.Duration { 126 for i, unit := range durations { 127 if d >= unit && i < len(durations)-1 { 128 return d.Round(durations[i+1]) 129 } 130 } 131 return d 132} 133 134// NewJSONReporter returns a Reporter that writes out Metrics as JSON. 135func NewJSONReporter(m *Metrics) Reporter { 136 return func(w io.Writer) error { 137 return json.NewEncoder(w).Encode(m) 138 } 139} 140 141var logarithmic = []float64{ 142 0.00, 143 0.100, 144 0.200, 145 0.300, 146 0.400, 147 0.500, 148 0.550, 149 0.600, 150 0.650, 151 0.700, 152 0.750, 153 0.775, 154 0.800, 155 0.825, 156 0.850, 157 0.875, 158 0.8875, 159 0.900, 160 0.9125, 161 0.925, 162 0.9375, 163 0.94375, 164 0.950, 165 0.95625, 166 0.9625, 167 0.96875, 168 0.971875, 169 0.975, 170 0.978125, 171 0.98125, 172 0.984375, 173 0.985938, 174 0.9875, 175 0.989062, 176 0.990625, 177 0.992188, 178 0.992969, 179 0.99375, 180 0.994531, 181 0.995313, 182 0.996094, 183 0.996484, 184 0.996875, 185 0.997266, 186 0.997656, 187 0.998047, 188 0.998242, 189 0.998437, 190 0.998633, 191 0.998828, 192 0.999023, 193 0.999121, 194 0.999219, 195 0.999316, 196 0.999414, 197 0.999512, 198 0.999561, 199 0.999609, 200 0.999658, 201 0.999707, 202 0.999756, 203 0.99978, 204 0.999805, 205 0.999829, 206 0.999854, 207 0.999878, 208 0.99989, 209 0.999902, 210 0.999915, 211 0.999927, 212 0.999939, 213 0.999945, 214 0.999951, 215 0.999957, 216 0.999963, 217 0.999969, 218 0.999973, 219 0.999976, 220 0.999979, 221 0.999982, 222 0.999985, 223 0.999986, 224 0.999988, 225 0.999989, 226 0.999991, 227 0.999992, 228 0.999993, 229 0.999994, 230 0.999995, 231 0.999996, 232 0.999997, 233 0.999998, 234 0.999999, 235 1.0, 236} 237 238// NewHDRHistogramPlotReporter returns a Reporter that writes out latency metrics 239// in a format plottable by http://hdrhistogram.github.io/HdrHistogram/plotFiles.html. 240func NewHDRHistogramPlotReporter(m *Metrics) Reporter { 241 return func(w io.Writer) error { 242 tw := tabwriter.NewWriter(w, 0, 8, 2, ' ', tabwriter.StripEscape) 243 _, err := fmt.Fprintf(tw, "Value(ms)\tPercentile\tTotalCount\t1/(1-Percentile)\n") 244 if err != nil { 245 return err 246 } 247 248 total := float64(m.Requests) 249 for _, q := range logarithmic { 250 value := milliseconds(m.Latencies.Quantile(q)) 251 oneBy := oneByQuantile(q) 252 count := int64((q * total) + 0.5) // Count at quantile 253 _, err = fmt.Fprintf(tw, "%f\t%f\t%d\t%f\n", value, q, count, oneBy) 254 if err != nil { 255 return err 256 } 257 } 258 259 return tw.Flush() 260 } 261} 262 263// milliseconds converts the given duration to a number of 264// fractional milliseconds. Splitting the integer and fraction 265// ourselves guarantees that converting the returned float64 to an 266// integer rounds the same way that a pure integer conversion would have, 267// even in cases where, say, float64(d.Nanoseconds())/1e9 would have rounded 268// differently. 269func milliseconds(d time.Duration) float64 { 270 msec, nsec := d/time.Millisecond, d%time.Millisecond 271 return float64(msec) + float64(nsec)/1e6 272} 273 274func oneByQuantile(q float64) float64 { 275 if q < 1.0 { 276 return 1 / (1 - q) 277 } 278 return float64(10000000) 279} 280