-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement a simple healthcheck (#2740)
- Loading branch information
Showing
7 changed files
with
260 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package health | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
|
||
"github.com/ledgerwatch/erigon/rpc" | ||
) | ||
|
||
func checkBlockNumber(blockNumber rpc.BlockNumber, api EthAPI) error { | ||
if api == nil { | ||
return fmt.Errorf("no connection to the Erigon server or `eth` namespace isn't enabled") | ||
} | ||
data, err := api.GetBlockByNumber(context.TODO(), blockNumber, false) | ||
if err != nil { | ||
return err | ||
} | ||
if len(data) == 0 { // block not found | ||
return fmt.Errorf("no known block with number %v (%x hex)", blockNumber, blockNumber) | ||
} | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package health | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
) | ||
|
||
func checkMinPeers(minPeerCount uint, api NetAPI) error { | ||
if api == nil { | ||
return fmt.Errorf("no connection to the Erigon server or `net` namespace isn't enabled") | ||
} | ||
|
||
peerCount, err := api.PeerCount(context.TODO()) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if uint64(peerCount) < uint64(minPeerCount) { | ||
return fmt.Errorf("not enough peers: %d (minimum %d))", peerCount, minPeerCount) | ||
} | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
package health | ||
|
||
import ( | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"io/ioutil" | ||
"net/http" | ||
"strings" | ||
|
||
"github.com/ledgerwatch/erigon/rpc" | ||
"github.com/ledgerwatch/log/v3" | ||
) | ||
|
||
type requestBody struct { | ||
MinPeerCount *uint `json:"min_peer_count"` | ||
BlockNumber *rpc.BlockNumber `json:"known_block"` | ||
} | ||
|
||
const ( | ||
urlPath = "/health" | ||
) | ||
|
||
var ( | ||
errCheckDisabled = errors.New("error check disabled") | ||
) | ||
|
||
func ProcessHealthcheckIfNeeded( | ||
w http.ResponseWriter, | ||
r *http.Request, | ||
rpcAPI []rpc.API, | ||
) bool { | ||
if !strings.EqualFold(r.URL.Path, urlPath) { | ||
return false | ||
} | ||
|
||
netAPI, ethAPI := parseAPI(rpcAPI) | ||
|
||
var errMinPeerCount = errCheckDisabled | ||
var errCheckBlock = errCheckDisabled | ||
|
||
body, errParse := parseHealthCheckBody(r.Body) | ||
defer r.Body.Close() | ||
|
||
if errParse != nil { | ||
log.Root().Warn("unable to process healthcheck request", "error", errParse) | ||
} else { | ||
// 1. net_peerCount | ||
if body.MinPeerCount != nil { | ||
errMinPeerCount = checkMinPeers(*body.MinPeerCount, netAPI) | ||
} | ||
// 2. custom query (shouldn't fail) | ||
if body.BlockNumber != nil { | ||
errCheckBlock = checkBlockNumber(*body.BlockNumber, ethAPI) | ||
} | ||
// TODO add time from the last sync cycle | ||
} | ||
|
||
err := reportHealth(errParse, errMinPeerCount, errCheckBlock, w) | ||
if err != nil { | ||
log.Root().Warn("unable to process healthcheck request", "error", err) | ||
} | ||
|
||
return true | ||
} | ||
|
||
func parseHealthCheckBody(reader io.Reader) (requestBody, error) { | ||
var body requestBody | ||
|
||
bodyBytes, err := ioutil.ReadAll(reader) | ||
if err != nil { | ||
return body, err | ||
} | ||
|
||
err = json.Unmarshal(bodyBytes, &body) | ||
if err != nil { | ||
return body, err | ||
} | ||
|
||
return body, nil | ||
} | ||
|
||
func reportHealth(errParse, errMinPeerCount, errCheckBlock error, w http.ResponseWriter) error { | ||
statusCode := http.StatusOK | ||
errors := make(map[string]string) | ||
|
||
if shouldChangeStatusCode(errParse) { | ||
statusCode = http.StatusInternalServerError | ||
} | ||
errors["healthcheck_query"] = errorStringOrOK(errParse) | ||
|
||
if shouldChangeStatusCode(errMinPeerCount) { | ||
statusCode = http.StatusInternalServerError | ||
} | ||
errors["min_peer_count"] = errorStringOrOK(errMinPeerCount) | ||
|
||
if shouldChangeStatusCode(errCheckBlock) { | ||
statusCode = http.StatusInternalServerError | ||
} | ||
errors["check_block"] = errorStringOrOK(errCheckBlock) | ||
|
||
w.WriteHeader(statusCode) | ||
|
||
bodyJson, err := json.Marshal(errors) | ||
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package health | ||
|
||
import ( | ||
"context" | ||
|
||
"github.com/ledgerwatch/erigon/common/hexutil" | ||
"github.com/ledgerwatch/erigon/rpc" | ||
) | ||
|
||
type NetAPI interface { | ||
PeerCount(_ context.Context) (hexutil.Uint, error) | ||
} | ||
|
||
type EthAPI interface { | ||
GetBlockByNumber(_ context.Context, number rpc.BlockNumber, fullTx bool) (map[string]interface{}, error) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package health | ||
|
||
import ( | ||
"github.com/ledgerwatch/erigon/rpc" | ||
) | ||
|
||
func parseAPI(api []rpc.API) (netAPI NetAPI, ethAPI EthAPI) { | ||
for _, rpc := range api { | ||
if rpc.Service == nil { | ||
continue | ||
} | ||
|
||
if netCandidate, ok := rpc.Service.(NetAPI); ok { | ||
netAPI = netCandidate | ||
} | ||
|
||
if ethCandidate, ok := rpc.Service.(EthAPI); ok { | ||
ethAPI = ethCandidate | ||
} | ||
} | ||
return netAPI, ethAPI | ||
} |