diff --git a/README.md b/README.md index e671f7ff..9610aac7 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,11 @@ report command: -output="stdout": Output file -reporter="text": Reporter [text, json, plot, hist[buckets]] +dump command: + -dumper="": Dumper [json, csv] + -inputs="stdin": Input files (comma separated) + -output="stdout": Output file + global flags: -cpus=8 Number of CPUs to use @@ -171,12 +176,12 @@ means every single hit runs in its own worker. ``` $ vegeta report -h Usage of vegeta report: - -input="stdin": Input files (comma separated) + -inputs="stdin": Input files (comma separated) -output="stdout": Output file -reporter="text": Reporter [text, json, plot, hist[buckets]] ``` -#### -input +#### -inputs Specifies the input files to generate the report of, defaulting to stdin. These are the output of vegeta attack. You can specify more than one (comma separated) and they will be merged and sorted before being used by the @@ -259,6 +264,32 @@ Bucket # % Histogram [6ms, +Inf] 4771 25.93% ################### ``` +### dump +``` +$ vegeta dump -h +Usage of vegeta dump: + -dumper="": Dumper [json, csv] + -inputs="stdin": Input files (comma separated) + -output="stdout": Output file +``` + +#### -inputs +Specifies the input files containing attack results to be dumped. You can specify more than one (comma separated). + +#### -output +Specifies the output file to which the dump will be written to. + +#### -dumper +Specifies the dump format. + +##### json +Dumps attack results as JSON objects. + +##### csv +Dumps attack results as CSV records with six columns. +The columns are: unix timestamp in ns since epoch, http status code, +request latency in ns, bytes out, bytes in, and lastly the error. + ## Usage (Library) ```go package main diff --git a/dump.go b/dump.go new file mode 100644 index 00000000..e22d35f2 --- /dev/null +++ b/dump.go @@ -0,0 +1,78 @@ +package main + +import ( + "flag" + "fmt" + "io" + "os" + "os/signal" + "strings" + + vegeta "github.com/tsenart/vegeta/lib" +) + +func dumpCmd() command { + fs := flag.NewFlagSet("vegeta dump", flag.ExitOnError) + dumper := fs.String("dumper", "", "Dumper [json, csv]") + inputs := fs.String("inputs", "stdin", "Input files (comma separated)") + output := fs.String("output", "stdout", "Output file") + return command{fs, func(args []string) error { + fs.Parse(args) + return dump(*dumper, *inputs, *output) + }} +} + +func dump(dumper, inputs, output string) error { + dump, ok := dumpers[dumper] + if !ok { + return fmt.Errorf("unsupported dumper: %s", dumper) + } + + files := strings.Split(inputs, ",") + srcs := make([]io.Reader, len(files)) + for i, f := range files { + in, err := file(f, false) + if err != nil { + return err + } + defer in.Close() + srcs[i] = in + } + + out, err := file(output, true) + if err != nil { + return err + } + defer out.Close() + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt) + res, errs := vegeta.Collect(srcs...) + + for { + select { + case _ = <-sig: + return nil + case r, ok := <-res: + if !ok { + return nil + } + dmp, err := dump.Dump(r) + if err != nil { + return err + } else if _, err = out.Write(dmp); err != nil { + return err + } + case err, ok := <-errs: + if !ok { + return nil + } + return err + } + } +} + +var dumpers = map[string]vegeta.Dumper{ + "csv": vegeta.DumpCSV, + "json": vegeta.DumpJSON, +} diff --git a/lib/dumpers.go b/lib/dumpers.go new file mode 100644 index 00000000..3e5e199e --- /dev/null +++ b/lib/dumpers.go @@ -0,0 +1,42 @@ +package vegeta + +import ( + "bytes" + "encoding/json" + "fmt" +) + +// Dumper is an interface defining Results dumping. +type Dumper interface { + Dump(*Result) ([]byte, error) +} + +// DumperFunc is an adapter to allow the use of ordinary functions as +// Dumpers. If f is a function with the appropriate signature, DumperFunc(f) +// is a Dumper object that calls f. +type DumperFunc func(*Result) ([]byte, error) + +func (f DumperFunc) Dump(r *Result) ([]byte, error) { return f(r) } + +// DumpCSV dumps a Result as a CSV record with six columns. +// The columns are: unix timestamp in ns since epoch, http status code, +// request latency in ns, bytes out, bytes in, and lastly the error. +var DumpCSV DumperFunc = func(r *Result) ([]byte, error) { + var buf bytes.Buffer + _, err := fmt.Fprintf(&buf, "%d,%d,%d,%d,%d,'%s'\n", + r.Timestamp.UnixNano(), + r.Code, + r.Latency.Nanoseconds(), + r.BytesOut, + r.BytesIn, + r.Error, + ) + return buf.Bytes(), err +} + +// DumpJSON dumps a Result as a JSON object. +var DumpJSON DumperFunc = func(r *Result) ([]byte, error) { + var buf bytes.Buffer + err := json.NewEncoder(&buf).Encode(r) + return buf.Bytes(), err +} diff --git a/lib/results.go b/lib/results.go index 8fab99b6..93c758f1 100644 --- a/lib/results.go +++ b/lib/results.go @@ -14,12 +14,12 @@ func init() { // Result represents the metrics defined out of an http.Response // generated by each target hit type Result struct { - Code uint16 - Timestamp time.Time - Latency time.Duration - BytesOut uint64 - BytesIn uint64 - Error string + Code uint16 `json:"code"` + Timestamp time.Time `json:"timestamp"` + Latency time.Duration `json:"latency"` + BytesOut uint64 `json:"bytes_out"` + BytesIn uint64 `json:"bytes_in"` + Error string `json:"error"` } // Collect concurrently reads Results from multiple io.Readers until all of diff --git a/main.go b/main.go index a8be6396..e48fb289 100644 --- a/main.go +++ b/main.go @@ -12,6 +12,7 @@ func main() { commands := map[string]command{ "attack": attackCmd(), "report": reportCmd(), + "dump": dumpCmd(), } flag.Usage = func() {