From 4c56887758ec7d5c618d554f7035a2e6a3769da2 Mon Sep 17 00:00:00 2001 From: Raul Sevilla Date: Fri, 3 Nov 2023 14:12:54 +0100 Subject: [PATCH] CSV output Signed-off-by: Raul Sevilla --- README.md | 23 ++++++++++++++++++++++- cmd/hloader.go | 4 +++- pkg/loader/loader.go | 5 +++-- pkg/loader/results.go | 29 +++++++++++++++++++++++++++-- pkg/loader/types.go | 1 + 5 files changed, 56 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 7b67c6a..733acbd 100644 --- a/README.md +++ b/README.md @@ -25,12 +25,33 @@ Flags: -k, --keepalive Enable HTTP keepalive (default true) --http2 Use HTTP2 protocol, if possible (default true) --pprof Enable pprof endpoint in localhost:6060 + -o, --output string Dump request outputs in the given CSV file -h, --help help for ./bin/hloader + ``` ## Compilation -``` +```shell $ make build GOARCH=amd64 CGO_ENABLED=0 go build -v -ldflags "-X github.com/cloud-bulldozer/go-commons/version.GitCommit=a5c03b3c983255096635b872d4153c98419f8bd1 -X github.com/cloud-bulldozer/go-commons/version.Version=main -X github.com/cloud-bulldozer/go-commons/version.BuildDate=2023-10-24-12:13:18" -o bin/hloader cmd/hloader.go ``` + +## CSV output + +The csv output has the following format: + +```csv +1699016948650,200,276028,537,false,false +,,,,, +``` + +Like for example: + +```csv +1699016948640,200,283085,537,false,false +1699016948650,200,276028,537,false,false +1699016948670,200,255849,537,false,false +1699016948700,200,225850,537,false,false +1699016948709,200,216579,537,false,false +``` diff --git a/cmd/hloader.go b/cmd/hloader.go index 9d8400e..49de0c6 100644 --- a/cmd/hloader.go +++ b/cmd/hloader.go @@ -30,6 +30,7 @@ func main() { var requestRate, connections int var url string var pprof, http2, insecure, keepalive bool + var csv string rootCmd := &cobra.Command{ Use: fmt.Sprintf(os.Args[0]), Short: "HTTP loader", @@ -43,7 +44,7 @@ func main() { log.Println(http.ListenAndServe("localhost:6060", nil)) }() } - l := loader.NewLoader(duration, requestTimeout, requestRate, connections, url, insecure, keepalive, http2) + l := loader.NewLoader(duration, requestTimeout, requestRate, connections, url, insecure, keepalive, http2, csv) return l.Run() }, } @@ -56,6 +57,7 @@ func main() { rootCmd.Flags().BoolVarP(&keepalive, "keepalive", "k", true, "Enable HTTP keepalive") rootCmd.Flags().BoolVar(&http2, "http2", true, "Use HTTP2 protocol, if possible") rootCmd.Flags().BoolVar(&pprof, "pprof", false, "Enable pprof endpoint in localhost:6060") + rootCmd.Flags().StringVarP(&csv, "output", "o", "", "Dump request outputs in the given CSV file") rootCmd.Flags().SortFlags = false rootCmd.MarkFlagRequired("url") rootCmd.AddCommand(versionCmd) diff --git a/pkg/loader/loader.go b/pkg/loader/loader.go index 6534158..315784d 100644 --- a/pkg/loader/loader.go +++ b/pkg/loader/loader.go @@ -14,7 +14,7 @@ import ( "golang.org/x/time/rate" ) -func NewLoader(duration, requestTimeout time.Duration, requestRate, connections int, url string, insecure, keepalive, http2 bool) Loader { +func NewLoader(duration, requestTimeout time.Duration, requestRate, connections int, url string, insecure, keepalive, http2 bool, csv string) Loader { var limit rate.Limit if requestRate > 0 { limit = rate.Limit(requestRate + 1) // We add 1 to count the main goroutine @@ -29,6 +29,7 @@ func NewLoader(duration, requestTimeout time.Duration, requestRate, connections keepalive: keepalive, limiter: rate.NewLimiter(limit, 1), http2: http2, + csv: csv, } } @@ -55,7 +56,7 @@ out: close(stopCh) wg.Wait() l.duration = time.Since(now) - err := normaliceResults(l.results, l.duration) + err := normaliceResults(l.results, l.duration, l.csv) if err != nil { fmt.Println(err) os.Exit(1) diff --git a/pkg/loader/results.go b/pkg/loader/results.go index 6dc9e8d..3c01625 100644 --- a/pkg/loader/results.go +++ b/pkg/loader/results.go @@ -1,22 +1,47 @@ package loader import ( + "encoding/csv" "encoding/json" "fmt" "math" "net/http" + "os" + "strconv" "time" "github.com/montanaflynn/stats" ) -func normaliceResults(results []requestResult, duration time.Duration) error { +func normaliceResults(results []requestResult, duration time.Duration, csvFile string) error { var latencies []float64 var totalRead int64 + var csvWriter *csv.Writer + var err error + var f *os.File + if csvFile != "" { + f, err = os.Create(csvFile) + if err != nil { + return err + } + csvWriter = csv.NewWriter(f) + } result := testResult{ StatusCodes: make(map[int]int64), } for _, r := range results { + if csvFile != "" { + line := []string{ + strconv.FormatInt(r.timestamp.UnixMilli(), 10), + strconv.Itoa(r.code), + strconv.FormatInt(r.latency, 10), + strconv.FormatInt(r.bytesRead, 10), + strconv.FormatBool(r.timeout), + strconv.FormatBool(r.readError), + } + csvWriter.Write(line) + csvWriter.Flush() + } result.StatusCodes[r.code]++ if r.timeout { result.Timeouts++ @@ -37,7 +62,7 @@ func normaliceResults(results []requestResult, duration time.Duration) error { result.P99Latency, _ = stats.Percentile(latencies, 99) result.LatencyStdev, _ = stats.StandardDeviation(latencies) result.LatencyStdev, _ = stats.Round(result.LatencyStdev, 2) - jsonResult, err := json.Marshal(&result) + jsonResult, err := json.MarshalIndent(&result, "", " ") if err != nil { return err } diff --git a/pkg/loader/types.go b/pkg/loader/types.go index 95bb90d..25908d6 100644 --- a/pkg/loader/types.go +++ b/pkg/loader/types.go @@ -20,6 +20,7 @@ type Loader struct { results []requestResult limiter *rate.Limiter http2 bool + csv string sync.Mutex }