mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-01 12:38:40 +00:00
Initial cut
This commit is contained in:
parent
f1c27c286e
commit
96968b119e
8 changed files with 349 additions and 0 deletions
|
|
@ -196,6 +196,9 @@ func makeFullNode(ctx *cli.Context) (*node.Node, ethapi.Backend) {
|
||||||
// Configure log filter RPC API.
|
// Configure log filter RPC API.
|
||||||
filterSystem := utils.RegisterFilterAPI(stack, backend, &cfg.Eth)
|
filterSystem := utils.RegisterFilterAPI(stack, backend, &cfg.Eth)
|
||||||
|
|
||||||
|
// TODO: ENG191 add flag to request endpoint
|
||||||
|
utils.RegisterHealthService(stack, backend, &cfg.Node)
|
||||||
|
|
||||||
// Configure GraphQL if requested.
|
// Configure GraphQL if requested.
|
||||||
if ctx.IsSet(utils.GraphQLEnabledFlag.Name) {
|
if ctx.IsSet(utils.GraphQLEnabledFlag.Name) {
|
||||||
utils.RegisterGraphQLService(stack, backend, filterSystem, &cfg.Node)
|
utils.RegisterGraphQLService(stack, backend, filterSystem, &cfg.Node)
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,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"
|
||||||
|
|
@ -1883,6 +1884,14 @@ func RegisterGraphQLService(stack *node.Node, backend ethapi.Backend, filterSyst
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RegisterHealthService adds the Health API to the node.
|
||||||
|
func RegisterHealthService(stack *node.Node, backend ethapi.Backend, cfg *node.Config) {
|
||||||
|
err := health.New(stack, backend, cfg.GraphQLCors, 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{
|
||||||
|
|
|
||||||
17
health/check_block.go
Normal file
17
health/check_block.go
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
package health
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/ethclient"
|
||||||
|
)
|
||||||
|
|
||||||
|
func checkBlockNumber(ec *ethclient.Client, blockNumber *big.Int) error {
|
||||||
|
_, err := ec.BlockByNumber(context.TODO(), blockNumber)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("no known block with number %v (%x hex)", blockNumber.Int64(), blockNumber.Int64())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
24
health/check_peers.go
Normal file
24
health/check_peers.go
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
package health
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/ethclient"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errNotEnoughPeers = errors.New("not enough peers")
|
||||||
|
)
|
||||||
|
|
||||||
|
func checkMinPeers(ec *ethclient.Client, minPeerCount uint) error {
|
||||||
|
peerCount, err := ec.PeerCount(context.TODO())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if uint64(peerCount) < uint64(minPeerCount) {
|
||||||
|
return fmt.Errorf("%w: %d (minimum %d)", errNotEnoughPeers, peerCount, minPeerCount)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
26
health/check_synced.go
Normal file
26
health/check_synced.go
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
package health
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/ethclient"
|
||||||
|
"github.com/ethereum/go-ethereum/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errNotSynced = errors.New("not synced")
|
||||||
|
)
|
||||||
|
|
||||||
|
func checkSynced(ec *ethclient.Client, r *http.Request) error {
|
||||||
|
i, err := ec.SyncProgress(r.Context())
|
||||||
|
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
|
||||||
|
}
|
||||||
31
health/check_time.go
Normal file
31
health/check_time.go
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
package health
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/ethclient"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errTimestampTooOld = errors.New("timestamp too old")
|
||||||
|
)
|
||||||
|
|
||||||
|
func checkTime(
|
||||||
|
ec *ethclient.Client,
|
||||||
|
r *http.Request,
|
||||||
|
seconds int,
|
||||||
|
) error {
|
||||||
|
i, err := ec.BlockByNumber(context.TODO(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
timestamp := i.Time()
|
||||||
|
if timestamp < uint64(seconds) {
|
||||||
|
return fmt.Errorf("%w: got ts: %d, need: %d", errTimestampTooOld, timestamp, seconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
199
health/health.go
Normal file
199
health/health.go
Normal file
|
|
@ -0,0 +1,199 @@
|
||||||
|
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"
|
||||||
|
synced = "synced"
|
||||||
|
minPeerCount = "min_peer_count"
|
||||||
|
checkBlock = "check_block"
|
||||||
|
maxSecondsBehind = "max_seconds_behind"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errCheckDisabled = errors.New("error check disabled")
|
||||||
|
errBadHeaderValue = errors.New("bad header value")
|
||||||
|
)
|
||||||
|
|
||||||
|
type requestBody struct {
|
||||||
|
Synced *bool `json:"synced"`
|
||||||
|
MinPeerCount *uint `json:"min_peer_count"`
|
||||||
|
CheckBlock *uint64 `json:"check_block"`
|
||||||
|
MaxSecondsBehind *int `json:"max_seconds_behind"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *handler) processFromHeaders(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)
|
||||||
|
if lHeader == synced {
|
||||||
|
errCheckSynced = checkSynced(h.ec, r)
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(lHeader, minPeerCount) {
|
||||||
|
peers, err := strconv.Atoi(strings.TrimPrefix(lHeader, minPeerCount))
|
||||||
|
if err != nil {
|
||||||
|
errCheckPeer = err
|
||||||
|
break
|
||||||
|
}
|
||||||
|
errCheckPeer = checkMinPeers(h.ec, uint(peers))
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(lHeader, checkBlock) {
|
||||||
|
block, err := strconv.Atoi(strings.TrimPrefix(lHeader, checkBlock))
|
||||||
|
if err != nil {
|
||||||
|
errCheckBlock = err
|
||||||
|
break
|
||||||
|
}
|
||||||
|
errCheckBlock = checkBlockNumber(h.ec, big.NewInt(int64(block)))
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(lHeader, maxSecondsBehind) {
|
||||||
|
seconds, err := strconv.Atoi(strings.TrimPrefix(lHeader, maxSecondsBehind))
|
||||||
|
if err != nil {
|
||||||
|
errCheckSeconds = err
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if seconds < 0 {
|
||||||
|
errCheckSeconds = errBadHeaderValue
|
||||||
|
break
|
||||||
|
}
|
||||||
|
now := time.Now().Unix()
|
||||||
|
errCheckSeconds = checkTime(h.ec, r, int(now)-seconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reportHealth(errCheckSynced, errCheckPeer, errCheckBlock, errCheckSeconds, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *handler) processFromBody(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(h.ec, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
if body.MinPeerCount != nil {
|
||||||
|
errCheckPeer = checkMinPeers(h.ec, *body.MinPeerCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
if body.CheckBlock != nil {
|
||||||
|
errCheckBlock = checkBlockNumber(h.ec, big.NewInt(int64(*body.CheckBlock)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if body.MaxSecondsBehind != nil {
|
||||||
|
seconds := *body.MaxSecondsBehind
|
||||||
|
if seconds < 0 {
|
||||||
|
errCheckSeconds = errBadHeaderValue
|
||||||
|
}
|
||||||
|
now := time.Now().Unix()
|
||||||
|
errCheckSeconds = checkTime(h.ec, r, int(now)-seconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := reportHealth(errCheckSynced, errCheckPeer, errCheckBlock, errCheckSeconds, w)
|
||||||
|
if err != nil {
|
||||||
|
log.Root().Warn("Unable to process healthcheck request", "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func reportHealth(errCheckSynced, errCheckPeer, errCheckBlock, errCheckSeconds error, w http.ResponseWriter) error {
|
||||||
|
statusCode := http.StatusOK
|
||||||
|
errs := make(map[string]string)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldChangeStatusCode(err error) bool {
|
||||||
|
return err != nil && !errors.Is(err, errCheckDisabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
func errorStringOrOK(err error) string {
|
||||||
|
if err == nil {
|
||||||
|
return "HEALTHY"
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, errCheckDisabled) {
|
||||||
|
return "DISABLED"
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("ERROR: %v", err)
|
||||||
|
}
|
||||||
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/internal/ethapi"
|
||||||
|
"github.com/ethereum/go-ethereum/node"
|
||||||
|
)
|
||||||
|
|
||||||
|
type handler struct {
|
||||||
|
ec *ethclient.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
headers := r.Header.Values(healthHeader)
|
||||||
|
if len(headers) != 0 {
|
||||||
|
h.processFromHeaders(headers, w, r)
|
||||||
|
} else {
|
||||||
|
h.processFromBody(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New constructs a new health service instance.
|
||||||
|
func New(stack *node.Node, backend ethapi.Backend, cors, vhosts []string) error {
|
||||||
|
_, err := newHandler(stack, backend, cors, vhosts)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// newHandler returns a new `http.Handler` that will answer node health queries.
|
||||||
|
func newHandler(stack *node.Node, backend ethapi.Backend, 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