From 3cda99a269cf57e1d2ffd7afbe7d5a970261248f Mon Sep 17 00:00:00 2001 From: smallnest Date: Sun, 19 Nov 2023 13:39:28 +0800 Subject: [PATCH] add statsview --- CHANGELOG.md | 1 + go.mod | 2 + go.sum | 4 ++ server/gateway.go | 75 ++++++++++++++++++++- server/server.go | 1 + server/statsview.go | 134 +++++++++++++++++++++++++++++++++++++ tool/xgen/parser/parser.go | 4 -- 7 files changed, 216 insertions(+), 5 deletions(-) create mode 100644 server/statsview.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 6347454b..ff98ca61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - support io_uring - add CacheDiscovery - add Oneshot method for XClient +- add statsview: http://xxx.xxx.xxx.xxx:xxxx/debug/statsview ## 1.8.0 diff --git a/go.mod b/go.mod index 2d61a405..0d01795b 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/apache/thrift v0.18.1 github.com/edwingeng/doublejump v1.0.1 github.com/fatih/color v1.14.1 + github.com/go-echarts/go-echarts/v2 v2.3.2 github.com/go-ping/ping v1.1.0 github.com/go-redis/redis/v8 v8.11.5 github.com/go-redis/redis_rate/v9 v9.1.2 @@ -30,6 +31,7 @@ require ( github.com/rs/cors v1.8.3 github.com/rubyist/circuitbreaker v2.2.1+incompatible github.com/smallnest/quick v0.1.0 + github.com/smallnest/statsview v0.0.0-20231119053547-3d59443f9ae3 github.com/soheilhy/cmux v0.1.5 github.com/stretchr/testify v1.7.2 github.com/tinylib/msgp v1.1.8 diff --git a/go.sum b/go.sum index 1a07d2bd..0dbbb337 100644 --- a/go.sum +++ b/go.sum @@ -58,6 +58,8 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/go-echarts/go-echarts/v2 v2.3.2 h1:imRxqF5sLtEPBsv5HGwz9KklNuwCo0fTITZ31mrgfzo= +github.com/go-echarts/go-echarts/v2 v2.3.2/go.mod h1:56YlvzhW/a+du15f3S2qUGNDfKnFOeJSThBIrVFHDtI= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= @@ -263,6 +265,8 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/smallnest/quick v0.1.0 h1:7/a3mvWjBNSKpcwmuiizTi5Alcn7xRkHNNuKgRda1V8= github.com/smallnest/quick v0.1.0/go.mod h1:mjmFVnOUd/Ruq8Gb7wxFjweGsrVILFsKrcwLz4QyX3g= +github.com/smallnest/statsview v0.0.0-20231119053547-3d59443f9ae3 h1:N7tssEnlhZlAEGQH+79XGYwrI1vuk5iVEdPiROKsQS4= +github.com/smallnest/statsview v0.0.0-20231119053547-3d59443f9ae3/go.mod h1:BMgxQ/U/wQ/mJY8WyPxIG+e3cx7HKbKSZL+DniC2tvc= github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/server/gateway.go b/server/gateway.go index efc52cd5..8b1f1fad 100644 --- a/server/gateway.go +++ b/server/gateway.go @@ -1,6 +1,7 @@ package server import ( + "bufio" "context" "errors" "io" @@ -38,8 +39,19 @@ func (s *Server) startGateway(network string, ln net.Listener) net.Listener { go s.startJSONRPC2(jsonrpc2Ln) } + if s.EnableProfile { + // debugLn := m.Match(http1Path("/debug/")) + debugLn := m.Match(cmux.HTTP1Fast()) + vm := NewViewManager(debugLn) + go func() { + if err := vm.Start(); err != nil { + log.Errorf("start view manager failed: %v", err) + } + }() + } + if !s.DisableHTTPGateway { - httpLn := m.Match(cmux.HTTP1Fast()) + httpLn := m.Match(cmux.HTTP1Fast()) // X-RPCX-MessageID go s.startHTTP1APIGateway(httpLn) } @@ -48,6 +60,67 @@ func (s *Server) startGateway(network string, ln net.Listener) net.Listener { return rpcxLn } +func http1Path(prefix string) cmux.Matcher { + return func(r io.Reader) bool { + return matchHTTP1Field(r, prefix, func(gotValue string) bool { + br := bufio.NewReader(&io.LimitedReader{R: r, N: 1024}) + l, part, err := br.ReadLine() + if err != nil || part { + return false + } + + _, uri, _, ok := parseRequestLine(string(l)) + if !ok { + return false + } + + if strings.HasPrefix(uri, prefix) { + return true + } + + u, err := url.Parse(uri) + if err != nil { + return false + } + + return strings.HasPrefix(u.Path, prefix) + }) + } +} + +// grabbed from net/http. +func parseRequestLine(line string) (method, uri, proto string, ok bool) { + s1 := strings.Index(line, " ") + s2 := strings.Index(line[s1+1:], " ") + if s1 < 0 || s2 < 0 { + return + } + s2 += s1 + 1 + return line[:s1], line[s1+1 : s2], line[s2+1:], true +} + +func http1HeaderExist(name string) cmux.Matcher { + return func(r io.Reader) bool { + return matchHTTP1Field(r, name, func(gotValue string) bool { + req, err := http.ReadRequest(bufio.NewReader(r)) + if err != nil { + return false + } + + return req.Header.Get(name) != "" + }) + } +} + +func matchHTTP1Field(r io.Reader, name string, matches func(string) bool) (matched bool) { + req, err := http.ReadRequest(bufio.NewReader(r)) + if err != nil { + return false + } + + return matches(req.Header.Get(name)) +} + func rpcxPrefixByteMatcher() cmux.Matcher { magic := protocol.MagicNumber() return func(r io.Reader) bool { diff --git a/server/server.go b/server/server.go index 2e9e0b18..be777573 100644 --- a/server/server.go +++ b/server/server.go @@ -84,6 +84,7 @@ type Server struct { jsonrpcHTTPServer *http.Server DisableHTTPGateway bool // disable http invoke or not. DisableJSONRPC bool // disable json rpc or not. + EnableProfile bool // enable profile and statsview or not AsyncWrite bool // set true if your server only serves few clients pool WorkerPool diff --git a/server/statsview.go b/server/statsview.go new file mode 100644 index 00000000..e4ea4f42 --- /dev/null +++ b/server/statsview.go @@ -0,0 +1,134 @@ +package server + +import ( + "context" + "fmt" + "net" + "net/http" + "net/http/pprof" + "time" + + "github.com/go-echarts/go-echarts/v2/components" + "github.com/go-echarts/go-echarts/v2/templates" + "github.com/rs/cors" + "github.com/smallnest/statsview/statics" + "github.com/smallnest/statsview/viewer" +) + +// ViewManager +type ViewManager struct { + ln net.Listener + srv *http.Server + + Smgr *viewer.StatsMgr + Ctx context.Context + Cancel context.CancelFunc + Views []viewer.Viewer +} + +// Register registers views to the ViewManager +func (vm *ViewManager) Register(views ...viewer.Viewer) { + vm.Views = append(vm.Views, views...) + +} + +// Start runs a http server and begin to collect metrics +func (vm *ViewManager) Start() error { + return vm.srv.Serve(vm.ln) +} + +// Stop shutdown the http server gracefully +func (vm *ViewManager) Stop() { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + vm.srv.Shutdown(ctx) + vm.Cancel() +} + +func init() { + templates.PageTpl = ` +{{- define "page" }} + + + {{- template "header" . }} + +

  rpcx profiler

+ +
{{- range .Charts }} {{ template "base" . }} {{- end }}
+ + +{{ end }} +` +} + +// NewViewManager creates a new ViewManager instance +func NewViewManager(ln net.Listener) *ViewManager { + viewer.SetConfiguration(viewer.WithAddr(ln.Addr().String()), viewer.WithLinkAddr(ln.Addr().String())) + + page := components.NewPage() + page.PageTitle = "Statsview" + page.AssetsHost = fmt.Sprintf("http://%s/debug/statsview/statics/", viewer.LinkAddr()) + page.Assets.JSAssets.Add("jquery.min.js") + + srv := &http.Server{ + ReadTimeout: time.Minute, + WriteTimeout: time.Minute, + MaxHeaderBytes: 1 << 20, + } + + mgr := &ViewManager{ + ln: ln, + srv: srv, + } + + mgr.Ctx, mgr.Cancel = context.WithCancel(context.Background()) + mgr.Register( + viewer.NewGoroutinesViewer(), + viewer.NewHeapViewer(), + viewer.NewStackViewer(), + viewer.NewGCNumViewer(), + viewer.NewGCSizeViewer(), + viewer.NewGCCPUFractionViewer(), + ) + smgr := viewer.NewStatsMgr(mgr.Ctx) + for _, v := range mgr.Views { + v.SetStatsMgr(smgr) + } + + mux := http.NewServeMux() + mux.HandleFunc("/debug/pprof/", pprof.Index) + mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) + mux.HandleFunc("/debug/pprof/profile", pprof.Profile) + mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) + mux.HandleFunc("/debug/pprof/trace", pprof.Trace) + + for _, v := range mgr.Views { + page.AddCharts(v.View()) + mux.HandleFunc("/debug/statsview/view/"+v.Name(), v.Serve) + } + + mux.HandleFunc("/debug/statsview", func(w http.ResponseWriter, _ *http.Request) { + page.Render(w) + }) + + staticsPrev := "/debug/statsview/statics/" + mux.HandleFunc(staticsPrev+"echarts.min.js", func(w http.ResponseWriter, _ *http.Request) { + w.Write([]byte(statics.EchartJS)) + }) + + mux.HandleFunc(staticsPrev+"jquery.min.js", func(w http.ResponseWriter, _ *http.Request) { + w.Write([]byte(statics.JqueryJS)) + }) + + mux.HandleFunc(staticsPrev+"themes/westeros.js", func(w http.ResponseWriter, _ *http.Request) { + w.Write([]byte(statics.WesterosJS)) + }) + + mux.HandleFunc(staticsPrev+"themes/macarons.js", func(w http.ResponseWriter, _ *http.Request) { + w.Write([]byte(statics.MacaronsJS)) + }) + + mgr.srv.Handler = cors.AllowAll().Handler(mux) + + return mgr +} diff --git a/tool/xgen/parser/parser.go b/tool/xgen/parser/parser.go index 0fbc80a1..63d94d89 100644 --- a/tool/xgen/parser/parser.go +++ b/tool/xgen/parser/parser.go @@ -50,10 +50,6 @@ func (v *visitor) Visit(n ast.Node) (w ast.Visitor) { } return v case *ast.StructType: - // if isExported(v.name) { - //fmt.Printf("@@@@%s: %s\n", v.name, pretty.Sprint(n.Fields)) - //v.StructNames = append(v.StructNames, v.name) - // } return nil case *ast.FuncDecl: if isExported(v.name) {