package main import ( "bytes" "embed" "encoding/json" "fmt" "net" "os" "os/exec" "runtime" "strings" "text/template" "time" _ "embed" cors "github.com/AdhityaRamadhanus/fasthttpcors" "github.com/go-echarts/go-echarts/v2/charts" "github.com/go-echarts/go-echarts/v2/components" "github.com/go-echarts/go-echarts/v2/opts" "github.com/go-echarts/go-echarts/v2/templates" "github.com/valyala/fasthttp" ) //go:embed echarts.min.js //go:embed jquery.min.js var assetsFS embed.FS var ( assetsPath = "/echarts/statics/" apiPath = "/data/" latencyView = "latency" rpsView = "rps" timeFormat = "15:04:05" refreshInterval = time.Second ) const ( ViewTpl = ` $(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; for (let i = 0; i < result.values.length; i++) { let y = opt.series[i].data; y.push({ value: result.values[i] }); opt.series[i].data = y; goecharts_{{ .ViewID }}.setOption(opt); } } }); }` PageTpl = ` {{- define "page" }} {{- template "header" . }}

🚀 Plow %s

{{- range .Charts }} {{ template "base" . }} {{- end }}
{{ end }} ` ) func (c *Charts) genViewTemplate(vid, route string) string { tpl, err := template.New("view").Parse(ViewTpl) if err != nil { panic("failed to parse template " + err.Error()) } var d = struct { Interval int APIPath string Route string ViewID string }{ Interval: int(refreshInterval.Milliseconds()), APIPath: apiPath, Route: route, ViewID: vid, } buf := bytes.Buffer{} if err := tpl.Execute(&buf, d); err != nil { panic("failed to execute template " + err.Error()) } return buf.String() } func (c *Charts) newBasicView(route string) *charts.Line { graph := charts.NewLine() graph.SetGlobalOptions( charts.WithTooltipOpts(opts.Tooltip{Show: true, Trigger: "axis"}), charts.WithXAxisOpts(opts.XAxis{Name: "Time"}), charts.WithInitializationOpts(opts.Initialization{ Width: "700px", Height: "400px", }), charts.WithDataZoomOpts(opts.DataZoom{ Type: "slider", XAxisIndex: []int{0}, }), ) graph.SetXAxis([]string{}).SetSeriesOptions(charts.WithLineChartOpts(opts.LineChart{Smooth: true})) graph.AddJSFuncs(c.genViewTemplate(graph.ChartID, route)) return graph } func (c *Charts) newLatencyView() components.Charter { graph := c.newBasicView(latencyView) graph.SetGlobalOptions( charts.WithTitleOpts(opts.Title{Title: "Latency"}), charts.WithYAxisOpts(opts.YAxis{Scale: true, AxisLabel: &opts.AxisLabel{Formatter: "{value} ms"}}), charts.WithLegendOpts(opts.Legend{Show: true, Selected: map[string]bool{"Min": false, "Max": false}}), ) graph.AddSeries("Min", []opts.LineData{}). AddSeries("Mean", []opts.LineData{}). AddSeries("Max", []opts.LineData{}) return graph } func (c *Charts) newRPSView() components.Charter { graph := c.newBasicView(rpsView) graph.SetGlobalOptions( charts.WithTitleOpts(opts.Title{Title: "Reqs/sec"}), charts.WithYAxisOpts(opts.YAxis{Scale: true}), ) graph.AddSeries("RPS", []opts.LineData{}) return graph } type Metrics struct { Values []interface{} `json:"values"` Time string `json:"time"` } type Charts struct { page *components.Page ln net.Listener dataFunc func() *ChartsReport } func NewCharts(ln net.Listener, dataFunc func() *ChartsReport, desc string) (*Charts, error) { templates.PageTpl = fmt.Sprintf(PageTpl, desc) c := &Charts{ln: ln, dataFunc: dataFunc} c.page = components.NewPage() c.page.PageTitle = "plow" c.page.AssetsHost = assetsPath c.page.Assets.JSAssets.Add("jquery.min.js") c.page.AddCharts(c.newLatencyView(), c.newRPSView()) return c, nil } func (c *Charts) Handler(ctx *fasthttp.RequestCtx) { path := string(ctx.Path()) if strings.HasPrefix(path, apiPath) { view := path[len(apiPath):] var values []interface{} reportData := c.dataFunc() switch view { case latencyView: if reportData != nil { values = append(values, reportData.Latency.min/1e6) values = append(values, reportData.Latency.Mean()/1e6) values = append(values, reportData.Latency.max/1e6) } else { values = append(values, nil, nil, nil) } case rpsView: if reportData != nil { values = append(values, reportData.RPS) } else { values = append(values, nil) } } metrics := &Metrics{ Time: time.Now().Format(timeFormat), Values: values, } _ = json.NewEncoder(ctx).Encode(metrics) } else if path == "/" { ctx.SetContentType("text/html") _ = c.page.Render(ctx) } else if strings.HasPrefix(path, assetsPath) { ap := path[len(assetsPath):] f, err := assetsFS.Open(ap) if err != nil { ctx.Error(err.Error(), 404) } else { ctx.SetBodyStream(f, -1) } } else { ctx.Error("NotFound", fasthttp.StatusNotFound) } } func (c *Charts) Serve(open bool) { server := fasthttp.Server{ Handler: cors.DefaultHandler().CorsMiddleware(c.Handler), } if open { go openBrowser("http://" + c.ln.Addr().String()) } _ = server.Serve(c.ln) } // openBrowser go/src/cmd/internal/browser/browser.go func openBrowser(url string) bool { var cmds [][]string if exe := os.Getenv("BROWSER"); exe != "" { cmds = append(cmds, []string{exe}) } switch runtime.GOOS { case "darwin": cmds = append(cmds, []string{"/usr/bin/open"}) case "windows": cmds = append(cmds, []string{"cmd", "/c", "start"}) default: if os.Getenv("DISPLAY") != "" { // xdg-open is only for use in a desktop environment. cmds = append(cmds, []string{"xdg-open"}) } } cmds = append(cmds, []string{"chrome"}, []string{"google-chrome"}, []string{"chromium"}, []string{"firefox"}, ) for _, args := range cmds { cmd := exec.Command(args[0], append(args[1:], url)...) if cmd.Start() == nil && appearsSuccessful(cmd, 3*time.Second) { return true } } return false } // appearsSuccessful reports whether the command appears to have run successfully. // If the command runs longer than the timeout, it's deemed successful. // If the command runs within the timeout, it's deemed successful if it exited cleanly. func appearsSuccessful(cmd *exec.Cmd, timeout time.Duration) bool { errc := make(chan error, 1) go func() { errc <- cmd.Wait() }() select { case <-time.After(timeout): return true case err := <-errc: return err == nil } }