mirror of https://github.com/ethereum/go-ethereum
parent
f1c27c286e
commit
96968b119e
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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) |
||||
} |
@ -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 new issue