Skip to content

Commit b7260fe

Browse files
authored
Merge pull request #94 from samouraiworld/feat/daily-report-rpc-enrichment
Feat/daily report rpc enrichment
2 parents 3d13c7a + 5882ff5 commit b7260fe

11 files changed

Lines changed: 1371 additions & 1826 deletions

backend/internal/api/api.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"log"
77
"net/http"
88
"strconv"
9+
"strings"
910
"time"
1011

1112
clerkhttp "github.com/clerk/clerk-sdk-go/v2/http"
@@ -990,6 +991,108 @@ func GetAddrMonikerHandler(w http.ResponseWriter, r *http.Request, db *gorm.DB)
990991
json.NewEncoder(w).Encode(map[string]string{"addr": addr, "moniker": moniker})
991992
}
992993

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+
9931096
// ======================CORS=============================================
9941097
func EnableCORS(w http.ResponseWriter, r ...*http.Request) {
9951098
origin := ""
@@ -1270,6 +1373,24 @@ func StartWebhookAPI(db *gorm.DB) {
12701373
}
12711374
})
12721375

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+
12731394
// Starting the HTTP server -
12741395
addr := ":" + internal.Config.BackendPort
12751396

backend/internal/gnovalidator/Prometheus.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,39 @@ var (
142142
[]string{"chain", "level"},
143143
)
144144

145+
// Phase 4: RPC-enriched metrics
146+
ValidatorVotingPower = prometheus.NewGaugeVec(
147+
prometheus.GaugeOpts{
148+
Name: "gnoland_validator_voting_power",
149+
Help: "Current voting power of each validator",
150+
},
151+
[]string{"chain", "validator_address", "moniker"},
152+
)
153+
154+
ChainPeerCount = prometheus.NewGaugeVec(
155+
prometheus.GaugeOpts{
156+
Name: "gnoland_chain_peer_count",
157+
Help: "Number of connected peers",
158+
},
159+
[]string{"chain"},
160+
)
161+
162+
ChainMempoolTxCount = prometheus.NewGaugeVec(
163+
prometheus.GaugeOpts{
164+
Name: "gnoland_chain_mempool_tx_count",
165+
Help: "Pending transactions in mempool",
166+
},
167+
[]string{"chain"},
168+
)
169+
170+
ChainValsetSize = prometheus.NewGaugeVec(
171+
prometheus.GaugeOpts{
172+
Name: "gnoland_chain_valset_size",
173+
Help: "Number of active validators in current set",
174+
},
175+
[]string{"chain"},
176+
)
177+
145178
initOnce sync.Once
146179
)
147180

@@ -163,6 +196,11 @@ func Init() {
163196
// Phase 3: Alert metrics
164197
prometheus.MustRegister(ActiveAlerts)
165198
prometheus.MustRegister(AlertsTotal)
199+
// Phase 4: RPC-enriched metrics
200+
prometheus.MustRegister(ValidatorVotingPower)
201+
prometheus.MustRegister(ChainPeerCount)
202+
prometheus.MustRegister(ChainMempoolTxCount)
203+
prometheus.MustRegister(ChainValsetSize)
166204
})
167205
}
168206

@@ -374,6 +412,24 @@ func UpdatePrometheusMetricsFromDB(db *gorm.DB, chainID string, ctxOpts ...conte
374412
AlertsTotal.WithLabelValues(chainID, level).Set(float64(totalCount))
375413
}
376414

415+
// Phase 4: RPC-enriched metrics (voting power, peer count, mempool, valset size)
416+
snap := FetchChainHealthSnapshot(db, chainID)
417+
if snap.RPCReachable {
418+
monikerMap := GetMonikerMap(chainID)
419+
420+
ValidatorVotingPower.DeletePartialMatch(chainLabel)
421+
for _, v := range snap.ValidatorSet {
422+
moniker := monikerMap[v.Address]
423+
ValidatorVotingPower.WithLabelValues(chainID, v.Address, moniker).Set(float64(v.VotingPower))
424+
}
425+
426+
ChainPeerCount.WithLabelValues(chainID).Set(float64(snap.PeerCount))
427+
ChainMempoolTxCount.WithLabelValues(chainID).Set(float64(snap.MempoolTxCount))
428+
ChainValsetSize.WithLabelValues(chainID).Set(float64(len(snap.ValidatorSet)))
429+
} else {
430+
log.Printf("[metrics][%s] RPC unreachable, skipping Phase 4 metrics", chainID)
431+
}
432+
377433
log.Printf("[metrics][%s] update complete", chainID)
378434
return nil
379435
}

0 commit comments

Comments
 (0)