mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-02 13:08:41 +00:00
Merge ff742cb8d5 into 0494cdce23
This commit is contained in:
commit
41e4215e1d
11 changed files with 1122 additions and 0 deletions
|
|
@ -269,6 +269,10 @@ func makeFullNode(ctx *cli.Context) *node.Node {
|
||||||
// Configure log filter RPC API.
|
// Configure log filter RPC API.
|
||||||
filterSystem := utils.RegisterFilterAPI(stack, backend, &cfg.Eth)
|
filterSystem := utils.RegisterFilterAPI(stack, backend, &cfg.Eth)
|
||||||
|
|
||||||
|
// Configure the healthcheck API if requested.
|
||||||
|
if ctx.IsSet(utils.HTTPHealthEnabledFlag.Name) {
|
||||||
|
utils.RegisterHealthService(stack, &cfg.Node)
|
||||||
|
}
|
||||||
// Configure GraphQL if requested.
|
// Configure GraphQL if requested.
|
||||||
if ctx.Bool(utils.GraphQLEnabledFlag.Name) {
|
if ctx.Bool(utils.GraphQLEnabledFlag.Name) {
|
||||||
utils.RegisterGraphQLService(stack, backend, filterSystem, &cfg.Node)
|
utils.RegisterGraphQLService(stack, backend, filterSystem, &cfg.Node)
|
||||||
|
|
|
||||||
|
|
@ -164,6 +164,7 @@ var (
|
||||||
utils.HTTPListenAddrFlag,
|
utils.HTTPListenAddrFlag,
|
||||||
utils.HTTPPortFlag,
|
utils.HTTPPortFlag,
|
||||||
utils.HTTPCORSDomainFlag,
|
utils.HTTPCORSDomainFlag,
|
||||||
|
utils.HTTPHealthEnabledFlag,
|
||||||
utils.AuthListenFlag,
|
utils.AuthListenFlag,
|
||||||
utils.AuthPortFlag,
|
utils.AuthPortFlag,
|
||||||
utils.AuthVirtualHostsFlag,
|
utils.AuthVirtualHostsFlag,
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ import (
|
||||||
"github.com/ethereum/go-ethereum/ethdb/remotedb"
|
"github.com/ethereum/go-ethereum/ethdb/remotedb"
|
||||||
"github.com/ethereum/go-ethereum/ethstats"
|
"github.com/ethereum/go-ethereum/ethstats"
|
||||||
"github.com/ethereum/go-ethereum/graphql"
|
"github.com/ethereum/go-ethereum/graphql"
|
||||||
|
"github.com/ethereum/go-ethereum/health"
|
||||||
"github.com/ethereum/go-ethereum/internal/ethapi"
|
"github.com/ethereum/go-ethereum/internal/ethapi"
|
||||||
"github.com/ethereum/go-ethereum/internal/flags"
|
"github.com/ethereum/go-ethereum/internal/flags"
|
||||||
"github.com/ethereum/go-ethereum/log"
|
"github.com/ethereum/go-ethereum/log"
|
||||||
|
|
@ -771,6 +772,11 @@ var (
|
||||||
Value: "",
|
Value: "",
|
||||||
Category: flags.APICategory,
|
Category: flags.APICategory,
|
||||||
}
|
}
|
||||||
|
HTTPHealthEnabledFlag = &cli.BoolFlag{
|
||||||
|
Name: "http.health",
|
||||||
|
Usage: "Enable the HTTP healthcheck API at path '/health'.",
|
||||||
|
Category: flags.APICategory,
|
||||||
|
}
|
||||||
GraphQLEnabledFlag = &cli.BoolFlag{
|
GraphQLEnabledFlag = &cli.BoolFlag{
|
||||||
Name: "graphql",
|
Name: "graphql",
|
||||||
Usage: "Enable GraphQL on the HTTP-RPC server. Note that GraphQL can only be started if an HTTP server is started as well.",
|
Usage: "Enable GraphQL on the HTTP-RPC server. Note that GraphQL can only be started if an HTTP server is started as well.",
|
||||||
|
|
@ -2222,6 +2228,14 @@ func RegisterGraphQLService(stack *node.Node, backend ethapi.Backend, filterSyst
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RegisterHealthService adds the Health API to the node.
|
||||||
|
func RegisterHealthService(stack *node.Node, cfg *node.Config) {
|
||||||
|
err := health.New(stack, cfg.HTTPCors, cfg.HTTPVirtualHosts)
|
||||||
|
if err != nil {
|
||||||
|
Fatalf("Failed to register the health service: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// RegisterFilterAPI adds the eth log filtering RPC API to the node.
|
// RegisterFilterAPI adds the eth log filtering RPC API to the node.
|
||||||
func RegisterFilterAPI(stack *node.Node, backend ethapi.Backend, ethcfg *ethconfig.Config) *filters.FilterSystem {
|
func RegisterFilterAPI(stack *node.Node, backend ethapi.Backend, ethcfg *ethconfig.Config) *filters.FilterSystem {
|
||||||
filterSystem := filters.NewFilterSystem(backend, filters.Config{
|
filterSystem := filters.NewFilterSystem(backend, filters.Config{
|
||||||
|
|
|
||||||
15
health/check_block.go
Normal file
15
health/check_block.go
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
package health
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"math/big"
|
||||||
|
)
|
||||||
|
|
||||||
|
// checkBlockNumber confirms this node is aware of a specific block.
|
||||||
|
func checkBlockNumber(ec ethClient, blockNumber *big.Int) error {
|
||||||
|
_, err := ec.BlockByNumber(context.TODO(), blockNumber)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
23
health/check_peers.go
Normal file
23
health/check_peers.go
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
package health
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errNotEnoughPeers = errors.New("not enough peers")
|
||||||
|
)
|
||||||
|
|
||||||
|
// checkMinPeers returns 'errNotEnoughPeers' if the current peer count its lower than 'minPeerCount'
|
||||||
|
func checkMinPeers(ec ethClient, minPeerCount uint64) error {
|
||||||
|
peerCount, err := ec.PeerCount(context.TODO())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if peerCount < minPeerCount {
|
||||||
|
return fmt.Errorf("%w: %d (minimum %d)", errNotEnoughPeers, peerCount, minPeerCount)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
27
health/check_synced.go
Normal file
27
health/check_synced.go
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
package health
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errNotSynced = errors.New("not synced")
|
||||||
|
)
|
||||||
|
|
||||||
|
// checkSynced returns 'errNotSynced' if the node is in the syncing state.
|
||||||
|
func checkSynced(ec ethClient, r *http.Request) error {
|
||||||
|
i, err := ec.SyncProgress(context.TODO())
|
||||||
|
if err != nil {
|
||||||
|
log.Root().Warn("Unable to check sync status for healthcheck", "err", err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if i == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return errNotSynced
|
||||||
|
}
|
||||||
30
health/check_time.go
Normal file
30
health/check_time.go
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
package health
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errTimestampTooOld = errors.New("timestamp too old")
|
||||||
|
)
|
||||||
|
|
||||||
|
// checkTime fetches the timestamp of the most recent block and returns an error if it is earlier than 'minTimestamp'.
|
||||||
|
func checkTime(
|
||||||
|
ec ethClient,
|
||||||
|
r *http.Request,
|
||||||
|
minTimestamp int,
|
||||||
|
) error {
|
||||||
|
i, err := ec.BlockByNumber(context.TODO(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
timestamp := i.Time()
|
||||||
|
if timestamp < uint64(minTimestamp) {
|
||||||
|
return fmt.Errorf("%w: got ts: %d, need: %d", errTimestampTooOld, timestamp, minTimestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
211
health/health.go
Normal file
211
health/health.go
Normal file
|
|
@ -0,0 +1,211 @@
|
||||||
|
package health
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math/big"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
healthHeader = "X-GETH-HEALTHCHECK"
|
||||||
|
query = "query"
|
||||||
|
synced = "synced"
|
||||||
|
minPeerCount = "min_peer_count"
|
||||||
|
checkBlock = "check_block"
|
||||||
|
maxSecondsBehind = "max_seconds_behind"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errCheckDisabled = errors.New("error check disabled")
|
||||||
|
errInvalidValue = errors.New("invalid value provided")
|
||||||
|
)
|
||||||
|
|
||||||
|
type requestBody struct {
|
||||||
|
Synced *bool `json:"synced"`
|
||||||
|
MinPeerCount *uint `json:"min_peer_count"`
|
||||||
|
CheckBlock *big.Int `json:"check_block"`
|
||||||
|
MaxSecondsBehind *int `json:"max_seconds_behind"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// processFromHeaders handles requests when 'X-GETH-HEALTHCHECK' header labels are present.
|
||||||
|
func processFromHeaders(ec ethClient, headers []string, w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
errCheckSynced = errCheckDisabled
|
||||||
|
errCheckPeer = errCheckDisabled
|
||||||
|
errCheckBlock = errCheckDisabled
|
||||||
|
errCheckSeconds = errCheckDisabled
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, header := range headers {
|
||||||
|
lHeader := strings.ToLower(header)
|
||||||
|
switch {
|
||||||
|
case lHeader == synced:
|
||||||
|
errCheckSynced = checkSynced(ec, r)
|
||||||
|
case strings.HasPrefix(lHeader, minPeerCount):
|
||||||
|
peers, err := strconv.Atoi(strings.TrimPrefix(lHeader, minPeerCount))
|
||||||
|
if err != nil {
|
||||||
|
errCheckPeer = err
|
||||||
|
break
|
||||||
|
}
|
||||||
|
errCheckPeer = checkMinPeers(ec, uint64(peers))
|
||||||
|
case strings.HasPrefix(lHeader, checkBlock):
|
||||||
|
block, err := strconv.Atoi(strings.TrimPrefix(lHeader, checkBlock))
|
||||||
|
if err != nil {
|
||||||
|
errCheckBlock = err
|
||||||
|
break
|
||||||
|
}
|
||||||
|
errCheckBlock = checkBlockNumber(ec, big.NewInt(int64(block)))
|
||||||
|
case strings.HasPrefix(lHeader, maxSecondsBehind):
|
||||||
|
seconds, err := strconv.Atoi(strings.TrimPrefix(lHeader, maxSecondsBehind))
|
||||||
|
if err != nil {
|
||||||
|
errCheckSeconds = err
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if seconds < 0 {
|
||||||
|
errCheckSeconds = errInvalidValue
|
||||||
|
break
|
||||||
|
}
|
||||||
|
now := time.Now().Unix()
|
||||||
|
errCheckSeconds = checkTime(ec, r, int(now)-seconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reportHealth(nil, errCheckSynced, errCheckPeer, errCheckBlock, errCheckSeconds, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// processFromBody handles requests when 'X-GETH-HEALTHCHECK' headers are not present.
|
||||||
|
func processFromBody(ec ethClient, w http.ResponseWriter, r *http.Request) {
|
||||||
|
body, errParse := parseHealthCheckBody(r.Body)
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
var (
|
||||||
|
errCheckSynced = errCheckDisabled
|
||||||
|
errCheckPeer = errCheckDisabled
|
||||||
|
errCheckBlock = errCheckDisabled
|
||||||
|
errCheckSeconds = errCheckDisabled
|
||||||
|
)
|
||||||
|
|
||||||
|
if errParse != nil {
|
||||||
|
log.Root().Warn("Unable to process healthcheck request", "err", errParse)
|
||||||
|
} else {
|
||||||
|
if body.Synced != nil {
|
||||||
|
errCheckSynced = checkSynced(ec, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
if body.MinPeerCount != nil {
|
||||||
|
errCheckPeer = checkMinPeers(ec, uint64(*body.MinPeerCount))
|
||||||
|
}
|
||||||
|
|
||||||
|
if body.CheckBlock != nil {
|
||||||
|
errCheckBlock = checkBlockNumber(ec, body.CheckBlock)
|
||||||
|
}
|
||||||
|
|
||||||
|
if body.MaxSecondsBehind != nil {
|
||||||
|
seconds := *body.MaxSecondsBehind
|
||||||
|
if seconds < 0 {
|
||||||
|
errCheckSeconds = errInvalidValue
|
||||||
|
} else {
|
||||||
|
now := time.Now().Unix()
|
||||||
|
errCheckSeconds = checkTime(ec, r, int(now)-seconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := reportHealth(errParse, errCheckSynced, errCheckPeer, errCheckBlock, errCheckSeconds, w)
|
||||||
|
if err != nil {
|
||||||
|
log.Root().Warn("Unable to process healthcheck request", "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// reportHealth builds the response body, sets the status code and calls for it to be written.
|
||||||
|
func reportHealth(errParse, errCheckSynced, errCheckPeer, errCheckBlock, errCheckSeconds error, w http.ResponseWriter) error {
|
||||||
|
statusCode := http.StatusOK
|
||||||
|
errs := make(map[string]string)
|
||||||
|
|
||||||
|
if shouldChangeStatusCode(errParse) {
|
||||||
|
statusCode = http.StatusInternalServerError
|
||||||
|
}
|
||||||
|
errs[query] = errorStringOrOK(errParse)
|
||||||
|
|
||||||
|
if shouldChangeStatusCode(errCheckSynced) {
|
||||||
|
statusCode = http.StatusInternalServerError
|
||||||
|
}
|
||||||
|
errs[synced] = errorStringOrOK(errCheckSynced)
|
||||||
|
|
||||||
|
if shouldChangeStatusCode(errCheckPeer) {
|
||||||
|
statusCode = http.StatusInternalServerError
|
||||||
|
}
|
||||||
|
errs[minPeerCount] = errorStringOrOK(errCheckPeer)
|
||||||
|
|
||||||
|
if shouldChangeStatusCode(errCheckBlock) {
|
||||||
|
statusCode = http.StatusInternalServerError
|
||||||
|
}
|
||||||
|
errs[checkBlock] = errorStringOrOK(errCheckBlock)
|
||||||
|
|
||||||
|
if shouldChangeStatusCode(errCheckSeconds) {
|
||||||
|
statusCode = http.StatusInternalServerError
|
||||||
|
}
|
||||||
|
errs[maxSecondsBehind] = errorStringOrOK(errCheckSeconds)
|
||||||
|
|
||||||
|
return writeResponse(w, errs, statusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseHealthCheckBody parses and type checks the request body when 'X-GETH-HEALTHCHECK' headers are not present.
|
||||||
|
func parseHealthCheckBody(reader io.Reader) (requestBody, error) {
|
||||||
|
var body requestBody
|
||||||
|
|
||||||
|
bodyBytes, err := io.ReadAll(reader)
|
||||||
|
if err != nil {
|
||||||
|
return body, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(bodyBytes, &body)
|
||||||
|
if err != nil {
|
||||||
|
return body, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return body, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeResponse delivers the status and body to the response writer.
|
||||||
|
func writeResponse(w http.ResponseWriter, errs map[string]string, statusCode int) error {
|
||||||
|
w.WriteHeader(statusCode)
|
||||||
|
|
||||||
|
bodyJson, err := json.Marshal(errs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = w.Write(bodyJson)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// shouldChangeStatusCode returns 'true' if an error exists and is not 'errCheckDisabled'.
|
||||||
|
func shouldChangeStatusCode(err error) bool {
|
||||||
|
return err != nil && !errors.Is(err, errCheckDisabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
// errorStringOrOK returns "OK", "DISABLED" or the error message based on the output of the check.
|
||||||
|
func errorStringOrOK(err error) string {
|
||||||
|
if err == nil {
|
||||||
|
return "OK"
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, errCheckDisabled) {
|
||||||
|
return "DISABLED"
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("ERROR: %v", err)
|
||||||
|
}
|
||||||
742
health/health_test.go
Normal file
742
health/health_test.go
Normal file
|
|
@ -0,0 +1,742 @@
|
||||||
|
package health
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"math/big"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum"
|
||||||
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ethClientStub struct {
|
||||||
|
peersResult uint64
|
||||||
|
peersError error
|
||||||
|
blockResult *types.Block
|
||||||
|
blockError error
|
||||||
|
syncingResult *ethereum.SyncProgress
|
||||||
|
syncingError error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ethClientStub) PeerCount(_ context.Context) (uint64, error) {
|
||||||
|
return e.peersResult, e.peersError
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ethClientStub) BlockByNumber(_ context.Context, _ *big.Int) (*types.Block, error) {
|
||||||
|
return e.blockResult, e.blockError
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ethClientStub) SyncProgress(_ context.Context) (*ethereum.SyncProgress, error) {
|
||||||
|
return e.syncingResult, e.syncingError
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessFromHeaders(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
headers []string
|
||||||
|
clientPeerResult uint64
|
||||||
|
clientPeerError error
|
||||||
|
clientBlockResult *types.Block
|
||||||
|
clientBlockError error
|
||||||
|
clientSyncingResult *ethereum.SyncProgress
|
||||||
|
clientSyncingError error
|
||||||
|
expectedStatusCode int
|
||||||
|
expectedBody map[string]string
|
||||||
|
}{
|
||||||
|
// 0 - sync check enabled - syncing
|
||||||
|
{
|
||||||
|
headers: []string{"synced"},
|
||||||
|
clientPeerResult: uint64(1),
|
||||||
|
clientPeerError: nil,
|
||||||
|
clientBlockResult: &types.Block{},
|
||||||
|
clientBlockError: nil,
|
||||||
|
clientSyncingResult: nil,
|
||||||
|
clientSyncingError: nil,
|
||||||
|
expectedStatusCode: http.StatusOK,
|
||||||
|
expectedBody: map[string]string{
|
||||||
|
synced: "OK",
|
||||||
|
minPeerCount: "DISABLED",
|
||||||
|
checkBlock: "DISABLED",
|
||||||
|
maxSecondsBehind: "DISABLED",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 1 - sync check enabled - not syncing
|
||||||
|
{
|
||||||
|
headers: []string{"synced"},
|
||||||
|
clientPeerResult: uint64(1),
|
||||||
|
clientPeerError: nil,
|
||||||
|
clientBlockResult: nil,
|
||||||
|
clientBlockError: nil,
|
||||||
|
clientSyncingResult: ðereum.SyncProgress{},
|
||||||
|
clientSyncingError: nil,
|
||||||
|
expectedStatusCode: http.StatusInternalServerError,
|
||||||
|
expectedBody: map[string]string{
|
||||||
|
synced: "ERROR: not synced",
|
||||||
|
minPeerCount: "DISABLED",
|
||||||
|
checkBlock: "DISABLED",
|
||||||
|
maxSecondsBehind: "DISABLED",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 2 - sync check enabled - error checking sync
|
||||||
|
{
|
||||||
|
headers: []string{"synced"},
|
||||||
|
clientPeerResult: uint64(1),
|
||||||
|
clientPeerError: nil,
|
||||||
|
clientBlockResult: nil,
|
||||||
|
clientBlockError: nil,
|
||||||
|
clientSyncingResult: ðereum.SyncProgress{},
|
||||||
|
clientSyncingError: errors.New("problem checking sync"),
|
||||||
|
expectedStatusCode: http.StatusInternalServerError,
|
||||||
|
expectedBody: map[string]string{
|
||||||
|
synced: "ERROR: problem checking sync",
|
||||||
|
minPeerCount: "DISABLED",
|
||||||
|
checkBlock: "DISABLED",
|
||||||
|
maxSecondsBehind: "DISABLED",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 3 - peer count enabled - good request
|
||||||
|
{
|
||||||
|
headers: []string{"min_peer_count1"},
|
||||||
|
clientPeerResult: uint64(1),
|
||||||
|
clientPeerError: nil,
|
||||||
|
clientBlockResult: nil,
|
||||||
|
clientBlockError: nil,
|
||||||
|
clientSyncingResult: nil,
|
||||||
|
clientSyncingError: nil,
|
||||||
|
expectedStatusCode: http.StatusOK,
|
||||||
|
expectedBody: map[string]string{
|
||||||
|
synced: "DISABLED",
|
||||||
|
minPeerCount: "OK",
|
||||||
|
checkBlock: "DISABLED",
|
||||||
|
maxSecondsBehind: "DISABLED",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 4 - peer count enabled - not enough peers
|
||||||
|
{
|
||||||
|
headers: []string{"min_peer_count10"},
|
||||||
|
clientPeerResult: uint64(1),
|
||||||
|
clientPeerError: nil,
|
||||||
|
clientBlockResult: nil,
|
||||||
|
clientBlockError: nil,
|
||||||
|
clientSyncingResult: nil,
|
||||||
|
clientSyncingError: nil,
|
||||||
|
expectedStatusCode: http.StatusInternalServerError,
|
||||||
|
expectedBody: map[string]string{
|
||||||
|
synced: "DISABLED",
|
||||||
|
minPeerCount: "ERROR: not enough peers: 1 (minimum 10)",
|
||||||
|
checkBlock: "DISABLED",
|
||||||
|
maxSecondsBehind: "DISABLED",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 5 - peer count enabled - error checking peers
|
||||||
|
{
|
||||||
|
headers: []string{"min_peer_count10"},
|
||||||
|
clientPeerResult: uint64(1),
|
||||||
|
clientPeerError: errors.New("problem checking peers"),
|
||||||
|
clientBlockResult: nil,
|
||||||
|
clientBlockError: nil,
|
||||||
|
clientSyncingResult: nil,
|
||||||
|
clientSyncingError: nil,
|
||||||
|
expectedStatusCode: http.StatusInternalServerError,
|
||||||
|
expectedBody: map[string]string{
|
||||||
|
synced: "DISABLED",
|
||||||
|
minPeerCount: "ERROR: problem checking peers",
|
||||||
|
checkBlock: "DISABLED",
|
||||||
|
maxSecondsBehind: "DISABLED",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 6 - peer count enabled - badly formed request
|
||||||
|
{
|
||||||
|
headers: []string{"min_peer_countABC"},
|
||||||
|
clientPeerResult: uint64(1),
|
||||||
|
clientPeerError: nil,
|
||||||
|
clientBlockResult: nil,
|
||||||
|
clientBlockError: nil,
|
||||||
|
clientSyncingResult: nil,
|
||||||
|
clientSyncingError: nil,
|
||||||
|
expectedStatusCode: http.StatusInternalServerError,
|
||||||
|
expectedBody: map[string]string{
|
||||||
|
synced: "DISABLED",
|
||||||
|
minPeerCount: "ERROR: strconv.Atoi: parsing \"abc\": invalid syntax",
|
||||||
|
checkBlock: "DISABLED",
|
||||||
|
maxSecondsBehind: "DISABLED",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 7 - block check - all ok
|
||||||
|
{
|
||||||
|
headers: []string{"check_block10"},
|
||||||
|
clientPeerResult: uint64(1),
|
||||||
|
clientPeerError: nil,
|
||||||
|
clientBlockResult: &types.Block{},
|
||||||
|
clientBlockError: nil,
|
||||||
|
clientSyncingResult: nil,
|
||||||
|
clientSyncingError: nil,
|
||||||
|
expectedStatusCode: http.StatusOK,
|
||||||
|
expectedBody: map[string]string{
|
||||||
|
synced: "DISABLED",
|
||||||
|
minPeerCount: "DISABLED",
|
||||||
|
checkBlock: "OK",
|
||||||
|
maxSecondsBehind: "DISABLED",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 8 - block check - no block found
|
||||||
|
{
|
||||||
|
headers: []string{"check_block10"},
|
||||||
|
clientPeerResult: uint64(1),
|
||||||
|
clientPeerError: nil,
|
||||||
|
clientBlockResult: nil,
|
||||||
|
clientBlockError: errors.New("not found"),
|
||||||
|
clientSyncingResult: nil,
|
||||||
|
clientSyncingError: nil,
|
||||||
|
expectedStatusCode: http.StatusInternalServerError,
|
||||||
|
expectedBody: map[string]string{
|
||||||
|
synced: "DISABLED",
|
||||||
|
minPeerCount: "DISABLED",
|
||||||
|
checkBlock: "ERROR: not found",
|
||||||
|
maxSecondsBehind: "DISABLED",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 9 - block check - error checking block
|
||||||
|
{
|
||||||
|
headers: []string{"check_block10"},
|
||||||
|
clientPeerResult: uint64(1),
|
||||||
|
clientPeerError: nil,
|
||||||
|
clientBlockResult: &types.Block{},
|
||||||
|
clientBlockError: errors.New("problem checking block"),
|
||||||
|
clientSyncingResult: nil,
|
||||||
|
clientSyncingError: nil,
|
||||||
|
expectedStatusCode: http.StatusInternalServerError,
|
||||||
|
expectedBody: map[string]string{
|
||||||
|
synced: "DISABLED",
|
||||||
|
minPeerCount: "DISABLED",
|
||||||
|
checkBlock: "ERROR: problem checking block",
|
||||||
|
maxSecondsBehind: "DISABLED",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 10 - block check - badly formed request
|
||||||
|
{
|
||||||
|
headers: []string{"check_blockABC"},
|
||||||
|
clientPeerResult: uint64(1),
|
||||||
|
clientPeerError: nil,
|
||||||
|
clientBlockResult: nil,
|
||||||
|
clientBlockError: nil,
|
||||||
|
clientSyncingResult: nil,
|
||||||
|
clientSyncingError: nil,
|
||||||
|
expectedStatusCode: http.StatusInternalServerError,
|
||||||
|
expectedBody: map[string]string{
|
||||||
|
synced: "DISABLED",
|
||||||
|
minPeerCount: "DISABLED",
|
||||||
|
checkBlock: "ERROR: strconv.Atoi: parsing \"abc\": invalid syntax",
|
||||||
|
maxSecondsBehind: "DISABLED",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 11 - seconds check - all ok
|
||||||
|
{
|
||||||
|
headers: []string{"max_seconds_behind60"},
|
||||||
|
clientPeerResult: uint64(1),
|
||||||
|
clientPeerError: nil,
|
||||||
|
clientBlockResult: types.NewBlockWithHeader(&types.Header{
|
||||||
|
Time: uint64(time.Now().Add(-10 * time.Second).Unix()),
|
||||||
|
}),
|
||||||
|
clientBlockError: nil,
|
||||||
|
clientSyncingResult: nil,
|
||||||
|
clientSyncingError: nil,
|
||||||
|
expectedStatusCode: http.StatusOK,
|
||||||
|
expectedBody: map[string]string{
|
||||||
|
synced: "DISABLED",
|
||||||
|
minPeerCount: "DISABLED",
|
||||||
|
checkBlock: "DISABLED",
|
||||||
|
maxSecondsBehind: "OK",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 12 - seconds check - too old
|
||||||
|
{
|
||||||
|
headers: []string{"max_seconds_behind60"},
|
||||||
|
clientPeerResult: uint64(1),
|
||||||
|
clientPeerError: nil,
|
||||||
|
clientBlockResult: types.NewBlockWithHeader(&types.Header{
|
||||||
|
Time: uint64(time.Now().Add(-1 * time.Hour).Unix()),
|
||||||
|
}),
|
||||||
|
clientBlockError: nil,
|
||||||
|
clientSyncingResult: nil,
|
||||||
|
clientSyncingError: nil,
|
||||||
|
expectedStatusCode: http.StatusInternalServerError,
|
||||||
|
expectedBody: map[string]string{
|
||||||
|
synced: "DISABLED",
|
||||||
|
minPeerCount: "DISABLED",
|
||||||
|
checkBlock: "DISABLED",
|
||||||
|
maxSecondsBehind: "ERROR: timestamp too old: got ts:",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 13 - seconds check - less than 0 seconds
|
||||||
|
{
|
||||||
|
headers: []string{"max_seconds_behind-1"},
|
||||||
|
clientPeerResult: uint64(1),
|
||||||
|
clientPeerError: nil,
|
||||||
|
clientBlockResult: types.NewBlockWithHeader(&types.Header{
|
||||||
|
Time: uint64(time.Now().Add(1 * time.Hour).Unix()),
|
||||||
|
}),
|
||||||
|
clientBlockError: nil,
|
||||||
|
clientSyncingResult: nil,
|
||||||
|
clientSyncingError: nil,
|
||||||
|
expectedStatusCode: http.StatusInternalServerError,
|
||||||
|
expectedBody: map[string]string{
|
||||||
|
synced: "DISABLED",
|
||||||
|
minPeerCount: "DISABLED",
|
||||||
|
checkBlock: "DISABLED",
|
||||||
|
maxSecondsBehind: "ERROR: invalid value provided",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 14 - seconds check - badly formed request
|
||||||
|
{
|
||||||
|
headers: []string{"max_seconds_behindABC"},
|
||||||
|
clientPeerResult: uint64(1),
|
||||||
|
clientPeerError: nil,
|
||||||
|
clientBlockResult: &types.Block{},
|
||||||
|
clientBlockError: nil,
|
||||||
|
clientSyncingResult: nil,
|
||||||
|
clientSyncingError: nil,
|
||||||
|
expectedStatusCode: http.StatusInternalServerError,
|
||||||
|
expectedBody: map[string]string{
|
||||||
|
synced: "DISABLED",
|
||||||
|
minPeerCount: "DISABLED",
|
||||||
|
checkBlock: "DISABLED",
|
||||||
|
maxSecondsBehind: "ERROR: strconv.Atoi: parsing \"abc\": invalid syntax",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 15 - all checks - report ok
|
||||||
|
{
|
||||||
|
headers: []string{"synced", "check_block10", "min_peer_count1", "max_seconds_behind60"},
|
||||||
|
clientPeerResult: uint64(10),
|
||||||
|
clientPeerError: nil,
|
||||||
|
clientBlockResult: types.NewBlockWithHeader(&types.Header{
|
||||||
|
Time: uint64(time.Now().Add(1 * time.Second).Unix()),
|
||||||
|
}),
|
||||||
|
clientBlockError: nil,
|
||||||
|
clientSyncingResult: nil,
|
||||||
|
clientSyncingError: nil,
|
||||||
|
expectedStatusCode: http.StatusOK,
|
||||||
|
expectedBody: map[string]string{
|
||||||
|
synced: "OK",
|
||||||
|
minPeerCount: "OK",
|
||||||
|
checkBlock: "OK",
|
||||||
|
maxSecondsBehind: "OK",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx, c := range cases {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r, err := http.NewRequest(http.MethodGet, "http://localhost:9090/health", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%v: creating request: %v", idx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, header := range c.headers {
|
||||||
|
r.Header.Add("X-GETH-HEALTHCHECK", header)
|
||||||
|
}
|
||||||
|
|
||||||
|
ethClient := ðClientStub{
|
||||||
|
peersResult: c.clientPeerResult,
|
||||||
|
peersError: c.clientPeerError,
|
||||||
|
blockResult: c.clientBlockResult,
|
||||||
|
blockError: c.clientBlockError,
|
||||||
|
syncingResult: c.clientSyncingResult,
|
||||||
|
syncingError: c.clientSyncingError,
|
||||||
|
}
|
||||||
|
|
||||||
|
processFromHeaders(ethClient, r.Header.Values(healthHeader), w, r)
|
||||||
|
|
||||||
|
result := w.Result()
|
||||||
|
if result.StatusCode != c.expectedStatusCode {
|
||||||
|
t.Errorf("%v: expected status code: %v, but got: %v", idx, c.expectedStatusCode, result.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyBytes, err := io.ReadAll(result.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%v: reading response body: %s", idx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body map[string]string
|
||||||
|
err = json.Unmarshal(bodyBytes, &body)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%v: unmarshalling the response body: %s", idx, err)
|
||||||
|
}
|
||||||
|
result.Body.Close()
|
||||||
|
|
||||||
|
for k, v := range c.expectedBody {
|
||||||
|
val, found := body[k]
|
||||||
|
if !found {
|
||||||
|
t.Errorf("%v: expected the key: %s to be in the response body but it wasn't there", idx, k)
|
||||||
|
}
|
||||||
|
if !strings.Contains(val, v) {
|
||||||
|
t.Errorf("%v: expected the response body key: %s to contain: %s, but it contained: %s", idx, k, v, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessFromBody(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
body string
|
||||||
|
clientPeerResult uint64
|
||||||
|
clientPeerError error
|
||||||
|
clientBlockResult *types.Block
|
||||||
|
clientBlockError error
|
||||||
|
clientSyncingResult *ethereum.SyncProgress
|
||||||
|
clientSyncingError error
|
||||||
|
expectedStatusCode int
|
||||||
|
expectedBody map[string]string
|
||||||
|
}{
|
||||||
|
// 0 - sync check enabled - syncing
|
||||||
|
{
|
||||||
|
body: "{\"synced\": true}",
|
||||||
|
clientPeerResult: uint64(1),
|
||||||
|
clientPeerError: nil,
|
||||||
|
clientBlockResult: nil,
|
||||||
|
clientBlockError: nil,
|
||||||
|
clientSyncingResult: nil,
|
||||||
|
clientSyncingError: nil,
|
||||||
|
expectedStatusCode: http.StatusOK,
|
||||||
|
expectedBody: map[string]string{
|
||||||
|
query: "OK",
|
||||||
|
synced: "OK",
|
||||||
|
minPeerCount: "DISABLED",
|
||||||
|
checkBlock: "DISABLED",
|
||||||
|
maxSecondsBehind: "DISABLED",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 1 - sync check enabled - not syncing
|
||||||
|
{
|
||||||
|
body: "{\"synced\": true}",
|
||||||
|
clientPeerResult: uint64(1),
|
||||||
|
clientPeerError: nil,
|
||||||
|
clientBlockResult: nil,
|
||||||
|
clientBlockError: nil,
|
||||||
|
clientSyncingResult: ðereum.SyncProgress{},
|
||||||
|
clientSyncingError: nil,
|
||||||
|
expectedStatusCode: http.StatusInternalServerError,
|
||||||
|
expectedBody: map[string]string{
|
||||||
|
query: "OK",
|
||||||
|
synced: "ERROR: not synced",
|
||||||
|
minPeerCount: "DISABLED",
|
||||||
|
checkBlock: "DISABLED",
|
||||||
|
maxSecondsBehind: "DISABLED",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 2 - sync check enabled - error checking sync
|
||||||
|
{
|
||||||
|
body: "{\"synced\": true}",
|
||||||
|
clientPeerResult: uint64(1),
|
||||||
|
clientPeerError: nil,
|
||||||
|
clientBlockResult: nil,
|
||||||
|
clientBlockError: nil,
|
||||||
|
clientSyncingResult: ðereum.SyncProgress{},
|
||||||
|
clientSyncingError: errors.New("problem checking sync"),
|
||||||
|
expectedStatusCode: http.StatusInternalServerError,
|
||||||
|
expectedBody: map[string]string{
|
||||||
|
query: "OK",
|
||||||
|
synced: "ERROR: problem checking sync",
|
||||||
|
minPeerCount: "DISABLED",
|
||||||
|
checkBlock: "DISABLED",
|
||||||
|
maxSecondsBehind: "DISABLED",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 3 - peer count enabled - good request
|
||||||
|
{
|
||||||
|
body: "{\"min_peer_count\": 1}",
|
||||||
|
clientPeerResult: uint64(1),
|
||||||
|
clientPeerError: nil,
|
||||||
|
clientBlockResult: nil,
|
||||||
|
clientBlockError: nil,
|
||||||
|
clientSyncingResult: nil,
|
||||||
|
clientSyncingError: nil,
|
||||||
|
expectedStatusCode: http.StatusOK,
|
||||||
|
expectedBody: map[string]string{
|
||||||
|
query: "OK",
|
||||||
|
synced: "DISABLED",
|
||||||
|
minPeerCount: "OK",
|
||||||
|
checkBlock: "DISABLED",
|
||||||
|
maxSecondsBehind: "DISABLED",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 4 - peer count enabled - not enough peers
|
||||||
|
{
|
||||||
|
body: "{\"min_peer_count\": 10}",
|
||||||
|
clientPeerResult: uint64(1),
|
||||||
|
clientPeerError: nil,
|
||||||
|
clientBlockResult: nil,
|
||||||
|
clientBlockError: nil,
|
||||||
|
clientSyncingResult: nil,
|
||||||
|
clientSyncingError: nil,
|
||||||
|
expectedStatusCode: http.StatusInternalServerError,
|
||||||
|
expectedBody: map[string]string{
|
||||||
|
query: "OK",
|
||||||
|
synced: "DISABLED",
|
||||||
|
minPeerCount: "ERROR: not enough peers: 1 (minimum 10)",
|
||||||
|
checkBlock: "DISABLED",
|
||||||
|
maxSecondsBehind: "DISABLED",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 5 - peer count enabled - error checking peers
|
||||||
|
{
|
||||||
|
body: "{\"min_peer_count\": 10}",
|
||||||
|
clientPeerResult: uint64(1),
|
||||||
|
clientPeerError: errors.New("problem checking peers"),
|
||||||
|
clientBlockResult: nil,
|
||||||
|
clientBlockError: nil,
|
||||||
|
clientSyncingResult: nil,
|
||||||
|
clientSyncingError: nil,
|
||||||
|
expectedStatusCode: http.StatusInternalServerError,
|
||||||
|
expectedBody: map[string]string{
|
||||||
|
query: "OK",
|
||||||
|
synced: "DISABLED",
|
||||||
|
minPeerCount: "ERROR: problem checking peers",
|
||||||
|
checkBlock: "DISABLED",
|
||||||
|
maxSecondsBehind: "DISABLED",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 6 - peer count enabled - badly formed request
|
||||||
|
{
|
||||||
|
body: "{\"min_peer_count\": \"ABC\"}",
|
||||||
|
clientPeerResult: uint64(1),
|
||||||
|
clientPeerError: nil,
|
||||||
|
clientBlockResult: nil,
|
||||||
|
clientBlockError: nil,
|
||||||
|
clientSyncingResult: nil,
|
||||||
|
clientSyncingError: nil,
|
||||||
|
expectedStatusCode: http.StatusInternalServerError,
|
||||||
|
expectedBody: map[string]string{
|
||||||
|
query: "ERROR: json: cannot unmarshal string into Go struct field requestBody.min_peer_count of type uint",
|
||||||
|
synced: "DISABLED",
|
||||||
|
minPeerCount: "DISABLED",
|
||||||
|
checkBlock: "DISABLED",
|
||||||
|
maxSecondsBehind: "DISABLED",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 7 - block check - all ok
|
||||||
|
{
|
||||||
|
body: "{\"check_block\": 10}",
|
||||||
|
clientPeerResult: uint64(1),
|
||||||
|
clientPeerError: nil,
|
||||||
|
clientBlockResult: &types.Block{},
|
||||||
|
clientBlockError: nil,
|
||||||
|
clientSyncingResult: nil,
|
||||||
|
clientSyncingError: nil,
|
||||||
|
expectedStatusCode: http.StatusOK,
|
||||||
|
expectedBody: map[string]string{
|
||||||
|
query: "OK",
|
||||||
|
synced: "DISABLED",
|
||||||
|
minPeerCount: "DISABLED",
|
||||||
|
checkBlock: "OK",
|
||||||
|
maxSecondsBehind: "DISABLED",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 8 - block check - no block found
|
||||||
|
{
|
||||||
|
body: "{\"check_block\": 10}",
|
||||||
|
clientPeerResult: uint64(1),
|
||||||
|
clientPeerError: nil,
|
||||||
|
clientBlockResult: nil,
|
||||||
|
clientBlockError: errors.New("not found"),
|
||||||
|
clientSyncingResult: nil,
|
||||||
|
clientSyncingError: nil,
|
||||||
|
expectedStatusCode: http.StatusInternalServerError,
|
||||||
|
expectedBody: map[string]string{
|
||||||
|
query: "OK",
|
||||||
|
synced: "DISABLED",
|
||||||
|
minPeerCount: "DISABLED",
|
||||||
|
checkBlock: "ERROR: not found",
|
||||||
|
maxSecondsBehind: "DISABLED",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 9 - block check - error checking block
|
||||||
|
{
|
||||||
|
body: "{\"check_block\": 10}",
|
||||||
|
clientPeerResult: uint64(1),
|
||||||
|
clientPeerError: nil,
|
||||||
|
clientBlockResult: &types.Block{},
|
||||||
|
clientBlockError: errors.New("problem checking block"),
|
||||||
|
clientSyncingResult: nil,
|
||||||
|
clientSyncingError: nil,
|
||||||
|
expectedStatusCode: http.StatusInternalServerError,
|
||||||
|
expectedBody: map[string]string{
|
||||||
|
query: "OK",
|
||||||
|
synced: "DISABLED",
|
||||||
|
minPeerCount: "DISABLED",
|
||||||
|
checkBlock: "ERROR: problem checking block",
|
||||||
|
maxSecondsBehind: "DISABLED",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 10 - block check - badly formed request
|
||||||
|
{
|
||||||
|
body: "{\"check_block\": \"ABC\"}",
|
||||||
|
clientPeerResult: uint64(1),
|
||||||
|
clientPeerError: nil,
|
||||||
|
clientBlockResult: nil,
|
||||||
|
clientBlockError: nil,
|
||||||
|
clientSyncingResult: nil,
|
||||||
|
clientSyncingError: nil,
|
||||||
|
expectedStatusCode: http.StatusInternalServerError,
|
||||||
|
expectedBody: map[string]string{
|
||||||
|
query: "ERROR: math/big: cannot unmarshal \"\\\"ABC\\\"\" into a *big.Int",
|
||||||
|
synced: "DISABLED",
|
||||||
|
minPeerCount: "DISABLED",
|
||||||
|
checkBlock: "DISABLED",
|
||||||
|
maxSecondsBehind: "DISABLED",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 11 - seconds check - all ok
|
||||||
|
{
|
||||||
|
body: "{\"max_seconds_behind\": 60}",
|
||||||
|
clientPeerResult: uint64(1),
|
||||||
|
clientPeerError: nil,
|
||||||
|
clientBlockResult: types.NewBlockWithHeader(&types.Header{
|
||||||
|
Time: uint64(time.Now().Add(-10 * time.Second).Unix()),
|
||||||
|
}),
|
||||||
|
clientBlockError: nil,
|
||||||
|
clientSyncingResult: nil,
|
||||||
|
clientSyncingError: nil,
|
||||||
|
expectedStatusCode: http.StatusOK,
|
||||||
|
expectedBody: map[string]string{
|
||||||
|
query: "OK",
|
||||||
|
synced: "DISABLED",
|
||||||
|
minPeerCount: "DISABLED",
|
||||||
|
checkBlock: "DISABLED",
|
||||||
|
maxSecondsBehind: "OK",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 12 - seconds check - too old
|
||||||
|
{
|
||||||
|
body: "{\"max_seconds_behind\": 60}",
|
||||||
|
clientPeerResult: uint64(1),
|
||||||
|
clientPeerError: nil,
|
||||||
|
clientBlockResult: types.NewBlockWithHeader(&types.Header{
|
||||||
|
Time: uint64(time.Now().Add(-1 * time.Hour).Unix()),
|
||||||
|
}),
|
||||||
|
clientBlockError: nil,
|
||||||
|
clientSyncingResult: nil,
|
||||||
|
clientSyncingError: nil,
|
||||||
|
expectedStatusCode: http.StatusInternalServerError,
|
||||||
|
expectedBody: map[string]string{
|
||||||
|
query: "OK",
|
||||||
|
synced: "DISABLED",
|
||||||
|
minPeerCount: "DISABLED",
|
||||||
|
checkBlock: "DISABLED",
|
||||||
|
maxSecondsBehind: "ERROR: timestamp too old: got ts:",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 13 - seconds check - less than 0 seconds
|
||||||
|
{
|
||||||
|
body: "{\"max_seconds_behind\": -1}",
|
||||||
|
clientPeerResult: uint64(1),
|
||||||
|
clientPeerError: nil,
|
||||||
|
clientBlockResult: types.NewBlockWithHeader(&types.Header{
|
||||||
|
Time: uint64(time.Now().Add(1 * time.Hour).Unix()),
|
||||||
|
}),
|
||||||
|
clientBlockError: nil,
|
||||||
|
clientSyncingResult: nil,
|
||||||
|
clientSyncingError: nil,
|
||||||
|
expectedStatusCode: http.StatusInternalServerError,
|
||||||
|
expectedBody: map[string]string{
|
||||||
|
query: "OK",
|
||||||
|
synced: "DISABLED",
|
||||||
|
minPeerCount: "DISABLED",
|
||||||
|
checkBlock: "DISABLED",
|
||||||
|
maxSecondsBehind: "ERROR: invalid value provided",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 14 - seconds check - badly formed request
|
||||||
|
{
|
||||||
|
body: "{\"max_seconds_behind\": \"ABC\"}",
|
||||||
|
clientPeerResult: uint64(1),
|
||||||
|
clientPeerError: nil,
|
||||||
|
clientBlockResult: &types.Block{},
|
||||||
|
clientBlockError: nil,
|
||||||
|
clientSyncingResult: nil,
|
||||||
|
clientSyncingError: nil,
|
||||||
|
expectedStatusCode: http.StatusInternalServerError,
|
||||||
|
expectedBody: map[string]string{
|
||||||
|
query: "ERROR: json: cannot unmarshal string into Go struct field requestBody.max_seconds_behind of type int",
|
||||||
|
synced: "DISABLED",
|
||||||
|
minPeerCount: "DISABLED",
|
||||||
|
checkBlock: "DISABLED",
|
||||||
|
maxSecondsBehind: "DISABLED",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 15 - all checks - report ok
|
||||||
|
{
|
||||||
|
body: "{\"synced\": true, \"min_peer_count\": 1, \"check_block\": 10, \"max_seconds_behind\": 60}",
|
||||||
|
clientPeerResult: uint64(10),
|
||||||
|
clientPeerError: nil,
|
||||||
|
clientBlockResult: types.NewBlockWithHeader(&types.Header{
|
||||||
|
Time: uint64(time.Now().Add(1 * time.Second).Unix()),
|
||||||
|
}),
|
||||||
|
clientBlockError: nil,
|
||||||
|
clientSyncingResult: nil,
|
||||||
|
clientSyncingError: nil,
|
||||||
|
expectedStatusCode: http.StatusOK,
|
||||||
|
expectedBody: map[string]string{
|
||||||
|
query: "OK",
|
||||||
|
synced: "OK",
|
||||||
|
minPeerCount: "OK",
|
||||||
|
checkBlock: "OK",
|
||||||
|
maxSecondsBehind: "OK",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx, c := range cases {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r, err := http.NewRequest(http.MethodGet, "http://localhost:9090/health", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%v: creating request: %v", idx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Body = io.NopCloser(strings.NewReader(c.body))
|
||||||
|
|
||||||
|
ethClient := ðClientStub{
|
||||||
|
peersResult: c.clientPeerResult,
|
||||||
|
peersError: c.clientPeerError,
|
||||||
|
blockResult: c.clientBlockResult,
|
||||||
|
blockError: c.clientBlockError,
|
||||||
|
syncingResult: c.clientSyncingResult,
|
||||||
|
syncingError: c.clientSyncingError,
|
||||||
|
}
|
||||||
|
|
||||||
|
processFromBody(ethClient, w, r)
|
||||||
|
|
||||||
|
result := w.Result()
|
||||||
|
if result.StatusCode != c.expectedStatusCode {
|
||||||
|
t.Errorf("%v: expected status code: %v, but got: %v", idx, c.expectedStatusCode, result.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyBytes, err := io.ReadAll(result.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%v: reading response body: %s", idx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body map[string]string
|
||||||
|
err = json.Unmarshal(bodyBytes, &body)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%v: unmarshalling the response body: %s", idx, err)
|
||||||
|
}
|
||||||
|
result.Body.Close()
|
||||||
|
|
||||||
|
for k, v := range c.expectedBody {
|
||||||
|
val, found := body[k]
|
||||||
|
if !found {
|
||||||
|
t.Errorf("%v: expected the key: %s to be in the response body but it wasn't there", idx, k)
|
||||||
|
}
|
||||||
|
if !strings.Contains(val, v) {
|
||||||
|
t.Errorf("%v: expected the response body key: %s to contain: %s, but it contained: %s", idx, k, v, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
health/interface.go
Normal file
15
health/interface.go
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
package health
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"math/big"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum"
|
||||||
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ethClient interface {
|
||||||
|
PeerCount(context.Context) (uint64, error)
|
||||||
|
BlockByNumber(context.Context, *big.Int) (*types.Block, error)
|
||||||
|
SyncProgress(context.Context) (*ethereum.SyncProgress, error)
|
||||||
|
}
|
||||||
40
health/service.go
Normal file
40
health/service.go
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
package health
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/ethclient"
|
||||||
|
"github.com/ethereum/go-ethereum/node"
|
||||||
|
)
|
||||||
|
|
||||||
|
type handler struct {
|
||||||
|
ec *ethclient.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeHTTP implements the http.Handler interface.
|
||||||
|
func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
headers := r.Header.Values(healthHeader)
|
||||||
|
if len(headers) != 0 {
|
||||||
|
processFromHeaders(h.ec, headers, w, r)
|
||||||
|
} else {
|
||||||
|
processFromBody(h.ec, w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New constructs a new health service instance.
|
||||||
|
func New(stack *node.Node, cors, vhosts []string) error {
|
||||||
|
_, err := newHandler(stack, cors, vhosts)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// newHandler returns a new `http.Handler` that will answer node health queries.
|
||||||
|
func newHandler(stack *node.Node, cors, vhosts []string) (*handler, error) {
|
||||||
|
ec := ethclient.NewClient(stack.Attach())
|
||||||
|
h := handler{ec}
|
||||||
|
handler := node.NewHTTPHandlerStack(h, cors, vhosts, nil)
|
||||||
|
|
||||||
|
stack.RegisterHandler("Health API", "/health", handler)
|
||||||
|
stack.RegisterHandler("Health API", "/health/", handler)
|
||||||
|
|
||||||
|
return &h, nil
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue