From 398d0e21f5472845a8ed37ae40e8fd0298505047 Mon Sep 17 00:00:00 2001 From: liuchen Date: Mon, 26 Feb 2024 23:09:34 +0800 Subject: [PATCH] feat: add http status chart --- bench_server/main.go | 6 ++-- charts.go | 83 ++++++++++++++++++++++++++++++++++++++++++-- print.go | 2 +- report.go | 23 ++++++++---- requester.go | 19 ++-------- 5 files changed, 105 insertions(+), 28 deletions(-) diff --git a/bench_server/main.go b/bench_server/main.go index dd49d17..6c7cd82 100644 --- a/bench_server/main.go +++ b/bench_server/main.go @@ -4,6 +4,7 @@ import ( "flag" "log" "math/rand" + "net/http" "strconv" "time" @@ -19,9 +20,10 @@ func main() { log.Println("Starting HTTP server on:", addr) log.Fatalln(fasthttp.ListenAndServe(addr, func(c *fasthttp.RequestCtx) { //time.Sleep(time.Duration(rand.Int63n(int64(5 * time.Second)))) - if rand.Intn(5) == 0 { - c.SetStatusCode(400) + statusCodes := []int{ + http.StatusOK, http.StatusOK, http.StatusBadRequest, http.StatusTooManyRequests, http.StatusBadGateway, } + c.SetStatusCode(statusCodes[rand.Intn(len(statusCodes))]) _, werr := c.Write(c.Request.Body()) if werr != nil { log.Println(werr) diff --git a/charts.go b/charts.go index e19b6d6..bf383e3 100644 --- a/charts.go +++ b/charts.go @@ -32,8 +32,15 @@ var ( apiPath = "/data/" latencyView = "latency" rpsView = "rps" + codeView = "code" timeFormat = "15:04:05" refreshInterval = time.Second + + templateRegistry = map[string]string{ + rpsView: ViewTpl, + latencyView: ViewTpl, + codeView: CodeViewTpl, + } ) const ( @@ -71,10 +78,65 @@ function {{ .ViewID }}_sync() { {{ end }} ` + CodeViewTpl = ` +$(function () { setInterval({{ .ViewID }}_sync, {{ .Interval }}); }); +function {{ .ViewID }}_sync() { + $.ajax({ + type: "GET", + url: "{{ .APIPath }}{{ .Route }}", + dataType: "json", + success: function (result) { + let opt = goecharts_{{ .ViewID }}.getOption(); + let x = opt.xAxis[0].data; + x.push(result.time); + opt.xAxis[0].data = x; + + let nameAndSeriesMapping = {}; + for (let i = 0; i < opt.series.length; i++) { + nameAndSeriesMapping[opt.series[i].name] = opt.series[i]; + } + + let code200Count = nameAndSeriesMapping['200'].data.length; + + let codes = result.values[0]; + if (codes === null){ + for (let key in nameAndSeriesMapping) { + let series = nameAndSeriesMapping[key]; + series.data.push({value:null}); + } + }else{ + if (!('200' in codes)) { + codes['200'] = null; + } + + for (let code in codes) { + let count = codes[code]; + if (code in nameAndSeriesMapping){ + let series = nameAndSeriesMapping[code]; + series.data.push({value:count}); + }else{ + let data = []; + for (let i = 0; i < code200Count; i++) { + data.push[null]; + } + var newSeries = { + name: code, + type: 'line', + data: data + }; + opt.series.push(newSeries); + } + } + } + + goecharts_{{ .ViewID }}.setOption(opt); + } + }); +}` ) func (c *Charts) genViewTemplate(vid, route string) string { - tpl, err := template.New("view").Parse(ViewTpl) + tpl, err := template.New("view").Parse(templateRegistry[route]) if err != nil { panic("failed to parse template " + err.Error()) } @@ -141,6 +203,17 @@ func (c *Charts) newRPSView() components.Charter { return graph } +func (c *Charts) newCodeView() components.Charter { + graph := c.newBasicView(codeView) + graph.SetGlobalOptions( + charts.WithTitleOpts(opts.Title{Title: "Response Status"}), + charts.WithYAxisOpts(opts.YAxis{Scale: true}), + charts.WithLegendOpts(opts.Legend{Show: true}), + ) + graph.AddSeries("200", []opts.LineData{}) + return graph +} + type Metrics struct { Values []interface{} `json:"values"` Time string `json:"time"` @@ -160,7 +233,7 @@ func NewCharts(ln net.Listener, dataFunc func() *ChartsReport, desc string) (*Ch c.page.PageTitle = "plow" c.page.AssetsHost = assetsPath c.page.Assets.JSAssets.Add("jquery.min.js") - c.page.AddCharts(c.newLatencyView(), c.newRPSView()) + c.page.AddCharts(c.newLatencyView(), c.newRPSView(), c.newCodeView()) return c, nil } @@ -186,6 +259,12 @@ func (c *Charts) Handler(ctx *fasthttp.RequestCtx) { } else { values = append(values, nil) } + case codeView: + if reportData != nil { + values = append(values, reportData.CodeMap) + } else { + values = append(values, nil) + } } metrics := &Metrics{ Time: time.Now().Format(timeFormat), diff --git a/print.go b/print.go index c427e03..f53747c 100644 --- a/print.go +++ b/print.go @@ -123,7 +123,7 @@ func (p *Printer) PrintLoop(snapshot func() *SnapshotReport, interval time.Durat echo(true) } -//nolint +// nolint const ( FgBlackColor int = iota + 30 FgRedColor diff --git a/report.go b/report.go index 2e25295..9ee5d4b 100644 --- a/report.go +++ b/report.go @@ -20,6 +20,14 @@ var quantilesTarget = map[float64]float64{ 0.9999: 0.00001, } +var httpStatusSectionLabelMap = map[int]string{ + 1: "1xx", + 2: "2xx", + 3: "3xx", + 4: "4xx", + 5: "5xx", +} + type Stats struct { count int64 sum float64 @@ -71,7 +79,7 @@ type StreamReport struct { rpsStats *Stats latencyQuantile *quantile.Stream latencyHistogram *histogram.Histogram - codes map[string]int64 + codes map[int]int64 errors map[string]int64 latencyWithinSec *Stats @@ -88,7 +96,7 @@ func NewStreamReport() *StreamReport { return &StreamReport{ latencyQuantile: quantile.NewTargeted(quantilesTarget), latencyHistogram: histogram.New(8), - codes: make(map[string]int64, 1), + codes: make(map[int]int64, 1), errors: make(map[string]int64, 1), doneChan: make(chan struct{}, 1), latencyStats: &Stats{}, @@ -144,11 +152,11 @@ func (s *StreamReport) Collect(records <-chan *ReportRecord) { s.lock.Lock() latencyWithinSecTemp.Update(float64(r.cost)) s.insert(float64(r.cost)) - if r.code != "" { - s.codes[r.code] ++ + if r.code != 0 { + s.codes[r.code]++ } if r.error != "" { - s.errors[r.error] ++ + s.errors[r.error]++ } s.readBytes = r.readBytes s.writeBytes = r.writeBytes @@ -222,7 +230,8 @@ func (s *StreamReport) Snapshot() *SnapshotReport { rs.Codes = make(map[string]int64, len(s.codes)) for k, v := range s.codes { - rs.Codes[k] = v + section := k / 100 + rs.Codes[httpStatusSectionLabelMap[section]] = v } rs.Errors = make(map[string]int64, len(s.errors)) for k, v := range s.errors { @@ -263,6 +272,7 @@ func (s *StreamReport) Done() <-chan struct{} { type ChartsReport struct { RPS float64 Latency Stats + CodeMap map[int]int64 } func (s *StreamReport) Charts() *ChartsReport { @@ -274,6 +284,7 @@ func (s *StreamReport) Charts() *ChartsReport { cr = &ChartsReport{ RPS: s.rpsWithinSec, Latency: *s.latencyWithinSec, + CodeMap: s.codes, } } s.lock.Unlock() diff --git a/requester.go b/requester.go index 32bc47d..c423afe 100644 --- a/requester.go +++ b/requester.go @@ -28,7 +28,7 @@ var ( type ReportRecord struct { cost time.Duration - code string + code int error string readBytes int64 writeBytes int64 @@ -253,36 +253,21 @@ func (r *Requester) DoRequest(req *fasthttp.Request, resp *fasthttp.Response, rr } else { err = r.httpClient.Do(req, resp) } - var code string if err != nil { rr.cost = time.Since(startTime) - t1 - rr.code = "" rr.error = err.Error() return } - switch resp.StatusCode() / 100 { - case 1: - code = "1xx" - case 2: - code = "2xx" - case 3: - code = "3xx" - case 4: - code = "4xx" - case 5: - code = "5xx" - } err = resp.BodyWriteTo(ioutil.Discard) if err != nil { rr.cost = time.Since(startTime) - t1 - rr.code = "" rr.error = err.Error() return } rr.cost = time.Since(startTime) - t1 - rr.code = code + rr.code = resp.StatusCode() rr.error = "" }