diff --git a/cmd/lotus-provider/rpc/rpc.go b/cmd/lotus-provider/rpc/rpc.go index 1f075f79a..4b4a77cf9 100644 --- a/cmd/lotus-provider/rpc/rpc.go +++ b/cmd/lotus-provider/rpc/rpc.go @@ -13,6 +13,7 @@ import ( "github.com/gorilla/mux" logging "github.com/ipfs/go-log/v2" "go.opencensus.io/tag" + "golang.org/x/sync/errgroup" "golang.org/x/xerrors" "github.com/filecoin-project/go-jsonrpc" @@ -20,6 +21,7 @@ import ( "github.com/filecoin-project/lotus/api" "github.com/filecoin-project/lotus/cmd/lotus-provider/deps" + "github.com/filecoin-project/lotus/cmd/lotus-provider/web" "github.com/filecoin-project/lotus/lib/rpcenc" "github.com/filecoin-project/lotus/metrics" "github.com/filecoin-project/lotus/metrics/proxy" @@ -126,15 +128,27 @@ func ListenAndServe(ctx context.Context, dependencies *deps.Deps, shutdownChan c Addr: dependencies.ListenAddr, } + log.Infof("Setting up RPC server at %s", dependencies.ListenAddr) + + web, err := web.GetSrv(ctx, dependencies) + if err != nil { + return err + } + go func() { <-ctx.Done() log.Warn("Shutting down...") if err := srv.Shutdown(context.TODO()); err != nil { log.Errorf("shutting down RPC server failed: %s", err) } + if err := web.Shutdown(context.Background()); err != nil { + log.Errorf("shutting down web server failed: %s", err) + } log.Warn("Graceful shutdown successful") }() - log.Infof("Setting up RPC server at %s", dependencies.ListenAddr) - return srv.ListenAndServe() + eg := errgroup.Group{} + eg.Go(srv.ListenAndServe) + eg.Go(web.ListenAndServe) + return eg.Wait() } diff --git a/cmd/lotus-provider/web/api/debug/debug.go b/cmd/lotus-provider/web/api/debug/debug.go new file mode 100644 index 000000000..1dcd7c5a3 --- /dev/null +++ b/cmd/lotus-provider/web/api/debug/debug.go @@ -0,0 +1,95 @@ +// Package debug provides the API for various debug endpoints in lotus-provider. +package debug + +import ( + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/gorilla/mux" + logging "github.com/ipfs/go-log/v2" + + "github.com/filecoin-project/lotus/build" + cliutil "github.com/filecoin-project/lotus/cli/util" + "github.com/filecoin-project/lotus/cmd/lotus-provider/deps" +) + +var log = logging.Logger("lp/web/debug") + +type debug struct { + *deps.Deps +} + +func Routes(r *mux.Router, deps *deps.Deps) { + d := debug{deps} + r.Methods("GET").Path("chain-state-sse").HandlerFunc(d.chainStateSSE) +} + +type rpcInfo struct { + Address string + CLayers []string + Reachable bool + SyncState string + Version string +} + +func (d *debug) chainStateSSE(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type") + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + + v1api := d.Deps.Full + ctx := r.Context() + + ai := cliutil.ParseApiInfo(d.Deps.Cfg.Apis.ChainApiInfo[0]) + ver, err := v1api.Version(ctx) + if err != nil { + log.Warnw("Version", "error", err) + return + } + +sse: + for { + head, err := v1api.ChainHead(ctx) + if err != nil { + log.Warnw("ChainHead", "error", err) + return + } + + var syncState string + switch { + case time.Now().Unix()-int64(head.MinTimestamp()) < int64(build.BlockDelaySecs*3/2): // within 1.5 epochs + syncState = "ok" + case time.Now().Unix()-int64(head.MinTimestamp()) < int64(build.BlockDelaySecs*5): // within 5 epochs + syncState = fmt.Sprintf("slow (%s behind)", time.Since(time.Unix(int64(head.MinTimestamp()), 0)).Truncate(time.Second)) + default: + syncState = fmt.Sprintf("behind (%s behind)", time.Since(time.Unix(int64(head.MinTimestamp()), 0)).Truncate(time.Second)) + } + + select { + case <-ctx.Done(): + break sse + default: + } + + fmt.Fprintf(w, "data: ") + err = json.NewEncoder(w).Encode(rpcInfo{ + Address: ai.Addr, + CLayers: []string{}, + Reachable: true, + Version: ver.Version, + SyncState: syncState, + }) + if err != nil { + log.Warnw("json encode", "error", err) + return + } + fmt.Fprintf(w, "\n\n") + if f, ok := w.(http.Flusher); ok { + f.Flush() + } + } +} diff --git a/cmd/lotus-provider/web/api/routes.go b/cmd/lotus-provider/web/api/routes.go new file mode 100644 index 000000000..91e317722 --- /dev/null +++ b/cmd/lotus-provider/web/api/routes.go @@ -0,0 +1,12 @@ +// Package api provides the HTTP API for the lotus provider web gui. +package api + +import ( + "github.com/filecoin-project/lotus/cmd/lotus-provider/deps" + "github.com/filecoin-project/lotus/cmd/lotus-provider/web/api/debug" + "github.com/gorilla/mux" +) + +func Routes(r *mux.Router, deps *deps.Deps) { + debug.Routes(r.PathPrefix("/debug").Subrouter(), deps) +} diff --git a/cmd/lotus-provider/web/srv.go b/cmd/lotus-provider/web/srv.go new file mode 100644 index 000000000..f59a8d837 --- /dev/null +++ b/cmd/lotus-provider/web/srv.go @@ -0,0 +1,40 @@ +// Package web defines the HTTP web server for static files and endpoints. +package web + +import ( + "context" + "embed" + "net" + "net/http" + "strings" + + "github.com/filecoin-project/lotus/cmd/lotus-provider/deps" + "github.com/filecoin-project/lotus/cmd/lotus-provider/web/api" + "github.com/filecoin-project/lotus/metrics" + "github.com/gorilla/mux" + "go.opencensus.io/tag" +) + +// go:embed static +var static embed.FS + +func GetSrv(ctx context.Context, deps *deps.Deps) (*http.Server, error) { + mux := mux.NewRouter() + api.Routes(mux.PathPrefix("/api").Subrouter(), deps) + mux.NotFoundHandler = http.FileServer(http.FS(static)) + + return &http.Server{ + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.HasSuffix(r.URL.Path, "/") { + r.URL.Path = r.URL.Path + "index.html" + return + } + mux.ServeHTTP(w, r) + }), + BaseContext: func(listener net.Listener) context.Context { + ctx, _ := tag.New(context.Background(), tag.Upsert(metrics.APIInterface, "lotus-provider")) + return ctx + }, + Addr: deps.Cfg.Subsystems.GuiAddress, + }, nil +} diff --git a/cmd/lotus-provider/web/static/index.html b/cmd/lotus-provider/web/static/index.html new file mode 100644 index 000000000..731ee799b --- /dev/null +++ b/cmd/lotus-provider/web/static/index.html @@ -0,0 +1,181 @@ + + + + Lotus Provider Cluster Overview + + + + + +
+
+

Lotus Provider Cluster

+
+
+ version [todo] +
+
+
+
+
+

Chain Connectivity

+ +
+
+
+
+

Actor Summary

+ + + + + + + + + + + + + + + + + +
AddressConfig LayersQaPDeadlines
f01234mig023TiB +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + \ No newline at end of file diff --git a/cmd/lotus-provider/web/static/modules/chain-connectivity.js b/cmd/lotus-provider/web/static/modules/chain-connectivity.js new file mode 100644 index 000000000..871f40094 --- /dev/null +++ b/cmd/lotus-provider/web/static/modules/chain-connectivity.js @@ -0,0 +1,73 @@ +import { LitElement, html, css } from 'https://cdn.jsdelivr.net/npm/lit-html@3.1.0/lit-html.min.js'; +window.customElements.define('chain-connectivity', class MyElement extends LitElement { + constructor() { + super(); + this.data = []; + this.loadData(); + } + loadData() { + const eventSource = new EventSource('/api/debug/chain-state-sse'); + eventSource.onmessage = (event) => { + this.data.push(JSON.parse(event.data)); + }; + eventSource.onerror = (error) => { + console.error('Error:', error); + loadData(); + }; + }; + + static get styles() { + return [css` + :host { + box-sizing: border-box; /* Don't forgert this to include padding/border inside width calculation */ + } + table { + border-collapse: collapse; + } + + table td, table th { + border-left: 1px solid #f0f0f0; + padding: 1px 5px; + } + + table tr td:first-child, table tr th:first-child { + border-left: none; + } + + .success { + color: green; + } + .warning { + color: yellow; + } + .error { + color: red; + } + `]; + } + render = () => html` + + + + + + + + + + + ${this.data.map(item => html` + + + + + + + + `)} + + + + +
RPC AddressReachabilitySync StatusVersion
{{.Address}}${item.Address}${item.Reachable ? html`ok` : html`FAIL`}${item.SyncState === "ok" ? html`ok` : html`${item.SyncState}`}${item.Version}
Data incoming...
` +}); diff --git a/node/config/def.go b/node/config/def.go index dc358b140..e8f315add 100644 --- a/node/config/def.go +++ b/node/config/def.go @@ -351,7 +351,9 @@ func DefaultUserRaftConfig() *UserRaftConfig { func DefaultLotusProvider() *LotusProviderConfig { return &LotusProviderConfig{ - Subsystems: ProviderSubsystemsConfig{}, + Subsystems: ProviderSubsystemsConfig{ + GuiAddress: ":4701", + }, Fees: LotusProviderFees{ DefaultMaxFee: DefaultDefaultMaxFee, MaxPreCommitGasFee: types.MustParseFIL("0.025"), diff --git a/node/config/types.go b/node/config/types.go index 2152e0795..6233191f4 100644 --- a/node/config/types.go +++ b/node/config/types.go @@ -96,6 +96,9 @@ type ProviderSubsystemsConfig struct { WindowPostMaxTasks int EnableWinningPost bool WinningPostMaxTasks int + + // The address that should listen for Web GUI requests. + GuiAddress string } type DAGStoreConfig struct {