diff --git a/background/background.go b/background/background.go index c9e3c6e..0251bf1 100644 --- a/background/background.go +++ b/background/background.go @@ -71,7 +71,7 @@ func blockConnected() { ctx := context.Background() - dcrdClient, err := dcrdRPC.Client(ctx, netParams) + dcrdClient, _, err := dcrdRPC.Client(ctx, netParams) if err != nil { log.Errorf("%s: %v", funcName, err) return @@ -314,7 +314,7 @@ func (n *NotificationHandler) Close() error { func connectNotifier(shutdownCtx context.Context, dcrdWithNotifs rpc.DcrdConnect) error { notifierClosed = make(chan struct{}) - dcrdClient, err := dcrdWithNotifs.Client(shutdownCtx, netParams) + dcrdClient, _, err := dcrdWithNotifs.Client(shutdownCtx, netParams) if err != nil { return err } @@ -411,7 +411,7 @@ func checkWalletConsistency() { ctx := context.Background() - dcrdClient, err := dcrdRPC.Client(ctx, netParams) + dcrdClient, _, err := dcrdRPC.Client(ctx, netParams) if err != nil { log.Errorf("%s: %v", funcName, err) return diff --git a/database/database.go b/database/database.go index 0c24d98..c4cda08 100644 --- a/database/database.go +++ b/database/database.go @@ -436,7 +436,7 @@ func (vdb *VspDatabase) CheckIntegrity(ctx context.Context, params *chaincfg.Par return nil } - dcrdClient, err := dcrd.Client(ctx, params) + dcrdClient, _, err := dcrd.Client(ctx, params) if err != nil { return err } diff --git a/docs/deployment.md b/docs/deployment.md index 7134a6a..9c9c033 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -118,13 +118,13 @@ The `[WRN]` level is used to indicate events which are of interest, but do not necessarily require investigation (eg. bad requests from clients, recoverable errors). -### Voting Wallet Status +### VSP Status -The current status of the voting wallets is displayed in a table on the `/admin` +The current status of the VSP is displayed in a table on the `/admin` page, and the same information can be retrieved as a JSON object from `/admin/status` for automated monitoring. This endpoint requires Basic HTTP Authentication with the username `admin` and the password set in vspd -configuration. A 200 HTTP status will be returned if the voting wallets seem +configuration. A 200 HTTP status will be returned if the VSP seems healthy, or a 500 status will be used to indicate something is wrong. ```bash @@ -133,17 +133,24 @@ $ curl --user admin:12345 --request GET http://localhost:8800/admin/status ```json { - "wss://127.0.0.1:20111/ws": - { - "connected":true, - "infoerror":false, - "daemonconnected":true, - "voteversion":8, - "unlocked":true, - "voting":true, - "bestblockerror":false, - "bestblockheight":462345 + "dcrd": { + "host": "wss://127.0.0.1:19109/ws", + "connected": true, + "bestblockerror": false, + "bestblockheight": 802572 + }, + "wallets": { + "wss://127.0.0.1:20111/ws": { + "connected": true, + "infoerror": false, + "daemonconnected": true, + "voteversion": 10, + "unlocked": true, + "voting": true, + "bestblockerror": false, + "bestblockheight": 802572 } + } } ``` diff --git a/rpc/dcrd.go b/rpc/dcrd.go index b24e57f..6a9f732 100644 --- a/rpc/dcrd.go +++ b/rpc/dcrd.go @@ -47,16 +47,16 @@ func SetupDcrd(user, pass, addr string, cert []byte, n wsrpc.Notifier) DcrdConne // Client creates a new DcrdRPC client instance. Returns an error if dialing // dcrd fails or if dcrd is misconfigured. -func (d *DcrdConnect) Client(ctx context.Context, netParams *chaincfg.Params) (*DcrdRPC, error) { +func (d *DcrdConnect) Client(ctx context.Context, netParams *chaincfg.Params) (*DcrdRPC, string, error) { c, newConnection, err := d.dial(ctx) if err != nil { - return nil, fmt.Errorf("dcrd connection error: %w", err) + return nil, d.addr, fmt.Errorf("dcrd connection error: %w", err) } // If this is a reused connection, we don't need to validate the dcrd config // again. if !newConnection { - return &DcrdRPC{c, ctx}, nil + return &DcrdRPC{c, ctx}, d.addr, nil } // Verify dcrd is at the required api version. @@ -64,19 +64,19 @@ func (d *DcrdConnect) Client(ctx context.Context, netParams *chaincfg.Params) (* err = c.Call(ctx, "version", &verMap) if err != nil { d.Close() - return nil, fmt.Errorf("dcrd version check failed: %w", err) + return nil, d.addr, fmt.Errorf("dcrd version check failed: %w", err) } ver, exists := verMap["dcrdjsonrpcapi"] if !exists { d.Close() - return nil, fmt.Errorf("dcrd version response missing 'dcrdjsonrpcapi'") + return nil, d.addr, fmt.Errorf("dcrd version response missing 'dcrdjsonrpcapi'") } sVer := semver{ver.Major, ver.Minor, ver.Patch} if !semverCompatible(requiredDcrdVersion, sVer) { d.Close() - return nil, fmt.Errorf("dcrd has incompatible JSON-RPC version: got %s, expected %s", + return nil, d.addr, fmt.Errorf("dcrd has incompatible JSON-RPC version: got %s, expected %s", sVer, requiredDcrdVersion) } @@ -85,11 +85,11 @@ func (d *DcrdConnect) Client(ctx context.Context, netParams *chaincfg.Params) (* err = c.Call(ctx, "getcurrentnet", &netID) if err != nil { d.Close() - return nil, fmt.Errorf("dcrd getcurrentnet check failed: %w", err) + return nil, d.addr, fmt.Errorf("dcrd getcurrentnet check failed: %w", err) } if netID != netParams.Net { d.Close() - return nil, fmt.Errorf("dcrd running on %s, expected %s", netID, netParams.Net) + return nil, d.addr, fmt.Errorf("dcrd running on %s, expected %s", netID, netParams.Net) } // Verify dcrd has tx index enabled (required for getrawtransaction). @@ -97,14 +97,14 @@ func (d *DcrdConnect) Client(ctx context.Context, netParams *chaincfg.Params) (* err = c.Call(ctx, "getinfo", &info) if err != nil { d.Close() - return nil, fmt.Errorf("dcrd getinfo check failed: %w", err) + return nil, d.addr, fmt.Errorf("dcrd getinfo check failed: %w", err) } if !info.TxIndex { d.Close() - return nil, errors.New("dcrd does not have transaction index enabled (--txindex)") + return nil, d.addr, errors.New("dcrd does not have transaction index enabled (--txindex)") } - return &DcrdRPC{c, ctx}, nil + return &DcrdRPC{c, ctx}, d.addr, nil } // GetRawTransaction uses getrawtransaction RPC to retrieve details about the diff --git a/webapi/admin.go b/webapi/admin.go index 53c8533..0e43989 100644 --- a/webapi/admin.go +++ b/webapi/admin.go @@ -27,6 +27,16 @@ type WalletStatus struct { BestBlockHeight int64 `json:"bestblockheight"` } +// DcrdStatus describes the current status of the local instance of dcrd used by +// vspd. This is used by the admin.html template, and also serialized to JSON +// for the /admin/status endpoint. +type DcrdStatus struct { + Host string `json:"host"` + Connected bool `json:"connected"` + BestBlockError bool `json:"bestblockerror"` + BestBlockHeight uint32 `json:"bestblockheight"` +} + type searchResult struct { Hash string Found bool @@ -36,6 +46,31 @@ type searchResult struct { MaxVoteChanges int } +func dcrdStatus(c *gin.Context) DcrdStatus { + hostname := c.MustGet("DcrdHostname").(string) + status := DcrdStatus{Host: hostname} + + dcrdClient := c.MustGet("DcrdClient").(*rpc.DcrdRPC) + dcrdErr := c.MustGet("DcrdClientErr") + if dcrdErr != nil { + log.Errorf("could not get dcrd client: %v", dcrdErr.(error)) + return status + } + + status.Connected = true + + bestBlock, err := dcrdClient.GetBestBlockHeader() + if err != nil { + log.Errorf("could not get dcrd best block header: %v", err) + status.BestBlockError = true + return status + } + + status.BestBlockHeight = bestBlock.Height + + return status +} + func walletStatus(c *gin.Context) map[string]WalletStatus { walletClients := c.MustGet("WalletClients").([]*rpc.WalletRPC) failedWalletClients := c.MustGet("FailedWalletClients").([]string) @@ -92,7 +127,17 @@ func statusJSON(c *gin.Context) { } } - c.AbortWithStatusJSON(httpStatus, wallets) + dcrd := dcrdStatus(c) + + // Respond with HTTP status 500 if dcrd has issues. + if !dcrd.Connected || dcrd.BestBlockError { + httpStatus = http.StatusInternalServerError + } + + c.AbortWithStatusJSON(httpStatus, gin.H{ + "wallets": wallets, + "dcrd": dcrd, + }) } // adminPage is the handler for "GET /admin". @@ -101,6 +146,7 @@ func adminPage(c *gin.Context) { "WebApiCache": getCache(), "WebApiCfg": cfg, "WalletStatus": walletStatus(c), + "DcrdStatus": dcrdStatus(c), }) } @@ -147,6 +193,7 @@ func ticketSearch(c *gin.Context) { "WebApiCache": getCache(), "WebApiCfg": cfg, "WalletStatus": walletStatus(c), + "DcrdStatus": dcrdStatus(c), }) } diff --git a/webapi/cache.go b/webapi/cache.go index 84edff2..d88b66b 100644 --- a/webapi/cache.go +++ b/webapi/cache.go @@ -69,7 +69,7 @@ func updateCache(ctx context.Context, db *database.VspDatabase, } // Get latest best block height. - dcrdClient, err := dcrd.Client(ctx, netParams) + dcrdClient, _, err := dcrd.Client(ctx, netParams) if err != nil { return err } diff --git a/webapi/middleware.go b/webapi/middleware.go index 6cc2649..70af66b 100644 --- a/webapi/middleware.go +++ b/webapi/middleware.go @@ -75,10 +75,11 @@ func requireAdmin() gin.HandlerFunc { // downstream handlers to make use of. func withDcrdClient(dcrd rpc.DcrdConnect) gin.HandlerFunc { return func(c *gin.Context) { - client, err := dcrd.Client(c, cfg.NetParams) + client, hostname, err := dcrd.Client(c, cfg.NetParams) // Don't handle the error here, add it to the context and let downstream // handlers decide what to do with it. c.Set("DcrdClient", client) + c.Set("DcrdHostname", hostname) c.Set("DcrdClientErr", err) } } diff --git a/webapi/public/css/vspd.css b/webapi/public/css/vspd.css index 56813f9..2b73a31 100644 --- a/webapi/public/css/vspd.css +++ b/webapi/public/css/vspd.css @@ -151,34 +151,41 @@ footer .code { font-size: 12px; } +.vsp-status { + padding: 10px; +} -#status-table th, -#status-table td { +.vsp-status table { + margin-bottom: 28px; +} + +.vsp-status table th, +.vsp-status table td { border: 1px solid #edeff1; vertical-align: middle; text-align: center; } -#status-table .center { +.vsp-status table .center { display: flex; justify-content: center; align-items: center; } -#status-table .status { +.vsp-status table .status { height: 30px; padding-left: 30px; } -#status-table .good { +.vsp-status table .good { background: url(/public/images/success-icon.svg) no-repeat center center; } -#status-table .bad { +.vsp-status table .bad { background: url(/public/images/error-icon.svg) no-repeat left center; } -#status-table .with-text { +.vsp-status table .with-text { padding-left: 40px; } diff --git a/webapi/templates/admin.html b/webapi/templates/admin.html index 27f293e..05106de 100644 --- a/webapi/templates/admin.html +++ b/webapi/templates/admin.html @@ -46,7 +46,7 @@ hidden >