|
6 | 6 | "log" |
7 | 7 | "net/http" |
8 | 8 | "strconv" |
| 9 | + "strings" |
9 | 10 | "time" |
10 | 11 |
|
11 | 12 | clerkhttp "github.com/clerk/clerk-sdk-go/v2/http" |
@@ -990,6 +991,108 @@ func GetAddrMonikerHandler(w http.ResponseWriter, r *http.Request, db *gorm.DB) |
990 | 991 | json.NewEncoder(w).Encode(map[string]string{"addr": addr, "moniker": moniker}) |
991 | 992 | } |
992 | 993 |
|
| 994 | +// =========================== Chain Health Snapshot ============================== |
| 995 | + |
| 996 | +type chainHealthValidatorJSON struct { |
| 997 | + Address string `json:"address"` |
| 998 | + VotingPower int64 `json:"voting_power"` |
| 999 | + KeepRunning bool `json:"keep_running"` |
| 1000 | + ServerType string `json:"server_type"` |
| 1001 | +} |
| 1002 | + |
| 1003 | +type chainHealthValsetChangeJSON struct { |
| 1004 | + BlockNum int64 `json:"block_num"` |
| 1005 | + Address string `json:"address"` |
| 1006 | + NewPower int64 `json:"new_power"` |
| 1007 | +} |
| 1008 | + |
| 1009 | +type chainHealthResponse struct { |
| 1010 | + RPCReachable bool `json:"rpc_reachable"` |
| 1011 | + IsStuck bool `json:"is_stuck"` |
| 1012 | + IsDisabled bool `json:"is_disabled"` |
| 1013 | + LatestBlockHeight int64 `json:"latest_block_height"` |
| 1014 | + LatestBlockTime string `json:"latest_block_time,omitempty"` |
| 1015 | + ConsensusRound int `json:"consensus_round"` |
| 1016 | + PeerCount int `json:"peer_count"` |
| 1017 | + MempoolTxCount int `json:"mempool_tx_count"` |
| 1018 | + MempoolTotalBytes int64 `json:"mempool_total_bytes"` |
| 1019 | + ValidatorSet []chainHealthValidatorJSON `json:"validator_set,omitempty"` |
| 1020 | + ValsetChanges []chainHealthValsetChangeJSON `json:"valset_changes,omitempty"` |
| 1021 | + PrecommitBitmap map[string]bool `json:"precommit_bitmap,omitempty"` |
| 1022 | +} |
| 1023 | + |
| 1024 | +func GetChainHealth(w http.ResponseWriter, r *http.Request, db *gorm.DB, chainID string) { |
| 1025 | + EnableCORS(w, r) |
| 1026 | + if r.Method == http.MethodOptions { |
| 1027 | + w.WriteHeader(http.StatusNoContent) |
| 1028 | + return |
| 1029 | + } |
| 1030 | + if r.Method != http.MethodGet { |
| 1031 | + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) |
| 1032 | + return |
| 1033 | + } |
| 1034 | + if err := internal.Config.ValidateChainID(chainID); err != nil { |
| 1035 | + http.Error(w, err.Error(), http.StatusBadRequest) |
| 1036 | + return |
| 1037 | + } |
| 1038 | + type snapResult struct { |
| 1039 | + snap gnovalidator.ChainHealthSnapshot |
| 1040 | + } |
| 1041 | + ch := make(chan snapResult, 1) |
| 1042 | + go func() { |
| 1043 | + ch <- snapResult{snap: gnovalidator.FetchChainHealthSnapshot(db, chainID)} |
| 1044 | + }() |
| 1045 | + var snap gnovalidator.ChainHealthSnapshot |
| 1046 | + select { |
| 1047 | + case res := <-ch: |
| 1048 | + snap = res.snap |
| 1049 | + case <-time.After(20 * time.Second): |
| 1050 | + http.Error(w, "health snapshot timed out", http.StatusGatewayTimeout) |
| 1051 | + return |
| 1052 | + } |
| 1053 | + |
| 1054 | + resp := chainHealthResponse{ |
| 1055 | + RPCReachable: snap.RPCReachable, |
| 1056 | + IsStuck: snap.IsStuck, |
| 1057 | + IsDisabled: snap.IsDisabled, |
| 1058 | + LatestBlockHeight: snap.LatestBlockHeight, |
| 1059 | + ConsensusRound: snap.ConsensusRound, |
| 1060 | + PeerCount: snap.PeerCount, |
| 1061 | + MempoolTxCount: snap.MempoolTxCount, |
| 1062 | + MempoolTotalBytes: snap.MempoolTotalBytes, |
| 1063 | + PrecommitBitmap: snap.PrecommitBitmap, |
| 1064 | + } |
| 1065 | + if !snap.LatestBlockTime.IsZero() { |
| 1066 | + resp.LatestBlockTime = snap.LatestBlockTime.UTC().Format(time.RFC3339) |
| 1067 | + } |
| 1068 | + if len(snap.ValidatorSet) > 0 { |
| 1069 | + vs := make([]chainHealthValidatorJSON, 0, len(snap.ValidatorSet)) |
| 1070 | + for _, vi := range snap.ValidatorSet { |
| 1071 | + vs = append(vs, chainHealthValidatorJSON{ |
| 1072 | + Address: vi.Address, |
| 1073 | + VotingPower: vi.VotingPower, |
| 1074 | + KeepRunning: vi.KeepRunning, |
| 1075 | + ServerType: vi.ServerType, |
| 1076 | + }) |
| 1077 | + } |
| 1078 | + resp.ValidatorSet = vs |
| 1079 | + } |
| 1080 | + if len(snap.ValsetChanges) > 0 { |
| 1081 | + vc := make([]chainHealthValsetChangeJSON, 0, len(snap.ValsetChanges)) |
| 1082 | + for _, c := range snap.ValsetChanges { |
| 1083 | + vc = append(vc, chainHealthValsetChangeJSON{ |
| 1084 | + BlockNum: c.BlockNum, |
| 1085 | + Address: c.Address, |
| 1086 | + NewPower: c.NewPower, |
| 1087 | + }) |
| 1088 | + } |
| 1089 | + resp.ValsetChanges = vc |
| 1090 | + } |
| 1091 | + |
| 1092 | + w.Header().Set("Content-Type", "application/json") |
| 1093 | + json.NewEncoder(w).Encode(resp) |
| 1094 | +} |
| 1095 | + |
993 | 1096 | // ======================CORS============================================= |
994 | 1097 | func EnableCORS(w http.ResponseWriter, r ...*http.Request) { |
995 | 1098 | origin := "" |
@@ -1270,6 +1373,24 @@ func StartWebhookAPI(db *gorm.DB) { |
1270 | 1373 | } |
1271 | 1374 | }) |
1272 | 1375 |
|
| 1376 | + // /api/chain/<chainID>/health |
| 1377 | + mux.HandleFunc("/api/chain/", func(w http.ResponseWriter, r *http.Request) { |
| 1378 | + // Expected path: /api/chain/<chainID>/health |
| 1379 | + // Strip the prefix "/api/chain/" to get "<chainID>/health" |
| 1380 | + rest := strings.TrimPrefix(r.URL.Path, "/api/chain/") |
| 1381 | + parts := strings.SplitN(rest, "/", 2) |
| 1382 | + if len(parts) != 2 || parts[1] != "health" { |
| 1383 | + http.NotFound(w, r) |
| 1384 | + return |
| 1385 | + } |
| 1386 | + chainID := parts[0] |
| 1387 | + if chainID == "" { |
| 1388 | + http.Error(w, "Missing chain ID", http.StatusBadRequest) |
| 1389 | + return |
| 1390 | + } |
| 1391 | + GetChainHealth(w, r, db, chainID) |
| 1392 | + }) |
| 1393 | + |
1273 | 1394 | // Starting the HTTP server - |
1274 | 1395 | addr := ":" + internal.Config.BackendPort |
1275 | 1396 |
|
|
0 commit comments