261 lines
7.2 KiB
Go

package report
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"time"
)
const rqliteBase = "http://localhost:5001"
// collectRQLite queries the local RQLite HTTP API to build a health report.
func collectRQLite() *RQLiteReport {
r := &RQLiteReport{}
// 1. GET /status — core Raft and node metadata.
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
statusBody, err := httpGet(ctx, rqliteBase+"/status")
if err != nil {
r.Responsive = false
return r
}
var status map[string]interface{}
if err := json.Unmarshal(statusBody, &status); err != nil {
r.Responsive = false
return r
}
r.Responsive = true
// Extract fields from the nested status JSON.
r.RaftState = getNestedString(status, "store", "raft", "state")
r.LeaderAddr = getNestedString(status, "store", "leader", "addr")
r.LeaderID = getNestedString(status, "store", "leader", "node_id")
r.NodeID = getNestedString(status, "store", "node_id")
r.Term = uint64(getNestedFloat(status, "store", "raft", "current_term"))
r.Applied = uint64(getNestedFloat(status, "store", "raft", "applied_index"))
r.Commit = uint64(getNestedFloat(status, "store", "raft", "commit_index"))
r.FsmPending = uint64(getNestedFloat(status, "store", "raft", "fsm_pending"))
r.LastContact = getNestedString(status, "store", "raft", "last_contact")
r.Voter = getNestedBool(status, "store", "raft", "voter")
r.DBSize = getNestedString(status, "store", "sqlite3", "db_size_friendly")
r.Uptime = getNestedString(status, "http", "uptime")
r.Version = getNestedString(status, "build", "version")
r.Goroutines = int(getNestedFloat(status, "runtime", "num_goroutine"))
// HeapMB: bytes → MB.
heapBytes := getNestedFloat(status, "runtime", "memory", "heap_alloc")
if heapBytes > 0 {
r.HeapMB = int(heapBytes / (1024 * 1024))
}
// NumPeers may be a number or a string in the JSON; handle both.
r.NumPeers = getNestedInt(status, "store", "raft", "num_peers")
// 2. GET /nodes?nonvoters — cluster node list.
{
ctx2, cancel2 := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel2()
if body, err := httpGet(ctx2, rqliteBase+"/nodes?nonvoters"); err == nil {
var rawNodes map[string]struct {
Addr string `json:"addr"`
Reachable bool `json:"reachable"`
Leader bool `json:"leader"`
Voter bool `json:"voter"`
Time float64 `json:"time"`
Error string `json:"error"`
}
if err := json.Unmarshal(body, &rawNodes); err == nil {
r.Nodes = make(map[string]RQLiteNodeInfo, len(rawNodes))
for id, n := range rawNodes {
r.Nodes[id] = RQLiteNodeInfo{
Reachable: n.Reachable,
Leader: n.Leader,
Voter: n.Voter,
TimeMS: n.Time * 1000, // seconds → milliseconds
Error: n.Error,
}
}
}
}
}
// 3. GET /readyz — readiness probe.
{
ctx3, cancel3 := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel3()
req, err := http.NewRequestWithContext(ctx3, http.MethodGet, rqliteBase+"/readyz", nil)
if err == nil {
if resp, err := http.DefaultClient.Do(req); err == nil {
resp.Body.Close()
r.Ready = resp.StatusCode == http.StatusOK
}
}
}
// 4. POST /db/query?level=strong — strong read test.
{
ctx4, cancel4 := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel4()
payload := []byte(`["SELECT 1"]`)
req, err := http.NewRequestWithContext(ctx4, http.MethodPost, rqliteBase+"/db/query?level=strong", bytes.NewReader(payload))
if err == nil {
req.Header.Set("Content-Type", "application/json")
if resp, err := http.DefaultClient.Do(req); err == nil {
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
r.StrongRead = resp.StatusCode == http.StatusOK
}
}
}
// 5. GET /debug/vars — error counters.
{
ctx5, cancel5 := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel5()
if body, err := httpGet(ctx5, rqliteBase+"/debug/vars"); err == nil {
var vars map[string]interface{}
if err := json.Unmarshal(body, &vars); err == nil {
r.DebugVars = &RQLiteDebugVarsReport{
QueryErrors: jsonUint64(vars, "api_query_errors"),
ExecuteErrors: jsonUint64(vars, "api_execute_errors"),
RemoteExecErrors: jsonUint64(vars, "api_remote_exec_errors"),
LeaderNotFound: jsonUint64(vars, "store_leader_not_found"),
SnapshotErrors: jsonUint64(vars, "snapshot_errors"),
ClientRetries: jsonUint64(vars, "client_retries"),
ClientTimeouts: jsonUint64(vars, "client_timeouts"),
}
}
}
}
return r
}
// ---------------------------------------------------------------------------
// Nested-map extraction helpers
// ---------------------------------------------------------------------------
// getNestedString traverses nested map[string]interface{} values and returns
// the final value as a string. Returns "" if any key is missing or the leaf
// is not a string.
func getNestedString(m map[string]interface{}, keys ...string) string {
v := getNestedValue(m, keys...)
if v == nil {
return ""
}
if s, ok := v.(string); ok {
return s
}
return fmt.Sprintf("%v", v)
}
// getNestedFloat traverses nested maps and returns the leaf as a float64.
// JSON numbers are decoded as float64 by encoding/json into interface{}.
func getNestedFloat(m map[string]interface{}, keys ...string) float64 {
v := getNestedValue(m, keys...)
if v == nil {
return 0
}
switch n := v.(type) {
case float64:
return n
case json.Number:
if f, err := n.Float64(); err == nil {
return f
}
case string:
if f, err := strconv.ParseFloat(n, 64); err == nil {
return f
}
}
return 0
}
// getNestedBool traverses nested maps and returns the leaf as a bool.
func getNestedBool(m map[string]interface{}, keys ...string) bool {
v := getNestedValue(m, keys...)
if v == nil {
return false
}
if b, ok := v.(bool); ok {
return b
}
return false
}
// getNestedInt traverses nested maps and returns the leaf as an int.
// Handles both numeric and string representations (RQLite sometimes
// returns num_peers as a string).
func getNestedInt(m map[string]interface{}, keys ...string) int {
v := getNestedValue(m, keys...)
if v == nil {
return 0
}
switch n := v.(type) {
case float64:
return int(n)
case json.Number:
if i, err := n.Int64(); err == nil {
return int(i)
}
case string:
if i, err := strconv.Atoi(n); err == nil {
return i
}
}
return 0
}
// getNestedValue walks through nested map[string]interface{} following the
// given key path and returns the leaf value, or nil if any step fails.
func getNestedValue(m map[string]interface{}, keys ...string) interface{} {
if len(keys) == 0 {
return nil
}
current := interface{}(m)
for _, key := range keys {
cm, ok := current.(map[string]interface{})
if !ok {
return nil
}
current, ok = cm[key]
if !ok {
return nil
}
}
return current
}
// jsonUint64 reads a top-level key from a flat map as uint64.
func jsonUint64(m map[string]interface{}, key string) uint64 {
v, ok := m[key]
if !ok {
return 0
}
switch n := v.(type) {
case float64:
return uint64(n)
case json.Number:
if i, err := n.Int64(); err == nil {
return uint64(i)
}
case string:
if i, err := strconv.ParseUint(n, 10, 64); err == nil {
return i
}
}
return 0
}