Skip to content
File

Blob: chord/local_stats_handler.go

go339 lines
1package chord
2 
3import (
4 "context"
5 "fmt"
6 "html"
7 "net/http"
8 "runtime"
9 "slices"
10 "sort"
11 "strconv"
12 "strings"
13 "time"
14 
15 "go.miragespace.co/specter/spec/chord"
16 "go.miragespace.co/specter/spec/rtt"
17 
18 "github.com/go-chi/chi/v5/middleware"
19 "github.com/jedib0t/go-pretty/v6/table"
20 "github.com/jedib0t/go-pretty/v6/text"
21)
22 
23func minmax(nums []int) (min, max int) {
24 min = nums[0]
25 max = nums[0]
26 for _, num := range nums {
27 if num > max {
28 max = num
29 }
30 if num < min {
31 min = num
32 }
33 }
34 return
35}
36 
37func (n *LocalNode) fingerTrace() map[string]string {
38 ftMap := map[uint64][]int{}
39 n.fingerRangeView(func(i int, f chord.VNode) bool {
40 id := f.ID()
41 if _, found := ftMap[id]; !found {
42 ftMap[id] = make([]int, 0)
43 }
44 ftMap[id] = append(ftMap[id], i)
45 return true
46 })
47 
48 keys := make([]uint64, 0, len(ftMap))
49 for k := range ftMap {
50 keys = append(keys, k)
51 }
52 slices.Sort(keys)
53 
54 f := make(map[string]string)
55 for _, k := range keys {
56 min, max := minmax(ftMap[k])
57 f[fmt.Sprintf("%d/%d", min, max)] = strconv.FormatUint(k, 10)
58 }
59 
60 return f
61}
62 
63func printSummary(ctx context.Context, w http.ResponseWriter, virtualNodes []*LocalNode) {
64 rootNode := virtualNodes[0]
65 numKeys := 0
66 
67 phy := &strings.Builder{}
68 vir := &strings.Builder{}
69 lis := &strings.Builder{}
70 
71 nodesTable := table.NewWriter()
72 nodesTable.SetOutputMirror(vir)
73 nodesTable.AppendHeader(table.Row{"Kind", "ID", "State", "History", "Stablized", "Chord RPC QPS", "RPC Error", "KV RPC QPS", "Stale KV"})
74 nodesTable.SetColumnConfigs([]table.ColumnConfig{
75 {Number: 1, AutoMerge: true, AlignHeader: text.AlignCenter},
76 {Number: 2, AlignHeader: text.AlignCenter},
77 {Number: 3, AutoMerge: true, AlignHeader: text.AlignCenter},
78 {Number: 4, AlignHeader: text.AlignCenter, WidthMax: 40, WidthMaxEnforcer: text.WrapSoft},
79 {Number: 5, AutoMerge: true, AlignHeader: text.AlignCenter},
80 {Number: 6, AutoMerge: true, AlignHeader: text.AlignCenter, Align: text.AlignRight},
81 {Number: 7, AutoMerge: true, AlignHeader: text.AlignCenter, Align: text.AlignRight},
82 {Number: 8, AutoMerge: true, AlignHeader: text.AlignCenter, Align: text.AlignRight},
83 {Number: 9, AutoMerge: true, AlignHeader: text.AlignCenter, Align: text.AlignRight},
84 })
85 for i, node := range virtualNodes {
86 kind := "Root"
87 if i != 0 {
88 kind = "Virtual"
89 }
90 nodesTable.AppendRow(table.Row{
91 kind,
92 fmt.Sprintf("%d", node.ID()),
93 node.state.Get().String(),
94 fmt.Sprintf("%v", node.state.History()),
95 node.lastStabilized.Load().Round(time.Second).Format(time.RFC3339),
96 fmt.Sprintf("%.2f", node.chordRate.RatePerInterval()),
97 fmt.Sprintf("%d", node.rpcErrorCount.Load()),
98 fmt.Sprintf("%.2f", node.kvRate.RatePerInterval()),
99 fmt.Sprintf("%d", node.kvStaleCount.Load()),
100 })
101 }
102 nodesTable.SetStyle(table.StyleDefault)
103 nodesTable.Style().Options.SeparateRows = true
104 nodesTable.Render()
105 
106 for _, node := range virtualNodes {
107 pre, err := node.GetPredecessor()
108 if err != nil {
109 w.WriteHeader(http.StatusInternalServerError)
110 fmt.Fprintf(w, "error getting predecessor: %v", err)
111 return
112 }
113 succList, err := node.GetSuccessors()
114 if err != nil {
115 w.WriteHeader(http.StatusInternalServerError)
116 fmt.Fprintf(w, "error getting successor list: %v", err)
117 return
118 }
119 
120 sbs := table.NewWriter()
121 sbs.SetOutputMirror(lis)
122 
123 nodesString := &strings.Builder{}
124 nodesTable := table.NewWriter()
125 nodesTable.SetOutputMirror(nodesString)
126 nodesTable.AppendHeader(table.Row{"Position", "ID", "Address", "RTT (-10s)"})
127 nodesTable.SetColumnConfigs([]table.ColumnConfig{
128 {Number: 1, AutoMerge: true, AlignHeader: text.AlignCenter},
129 {Number: 2, AlignHeader: text.AlignCenter},
130 {Number: 3, AutoMerge: true, AlignHeader: text.AlignCenter},
131 {Number: 4, AutoMerge: true, AlignHeader: text.AlignCenter},
132 })
133 nodesTable.AppendRows([]table.Row{
134 {
135 "Predecessor",
136 fmt.Sprintf("%v", pre.ID()),
137 pre.Identity().GetAddress(),
138 node.NodesRTT.Snapshot(rtt.MakeMeasurementKey(pre.Identity()), time.Second*10).String(),
139 },
140 {
141 "Local",
142 fmt.Sprintf("%v", node.ID()),
143 node.Identity().GetAddress(),
144 node.NodesRTT.Snapshot(rtt.MakeMeasurementKey(node.Identity()), time.Second*10).String(),
145 },
146 })
147 
148 for _, succ := range succList {
149 nodesTable.AppendRow(table.Row{
150 fmt.Sprintf("Successor (L = %d)", chord.ExtendedSuccessorEntries),
151 fmt.Sprintf("%v", succ.ID()),
152 succ.Identity().GetAddress(),
153 node.NodesRTT.Snapshot(rtt.MakeMeasurementKey(succ.Identity()), time.Second*10).String(),
154 })
155 }
156 nodesTable.SetStyle(table.StyleDefault)
157 nodesTable.Style().Options.SeparateRows = true
158 nodesTable.Render()
159 
160 fingerString := &strings.Builder{}
161 finger := node.fingerTrace()
162 fingerTable := table.NewWriter()
163 fingerTable.SetOutputMirror(fingerString)
164 fingerTable.AppendHeader(table.Row{"Range", "ID"})
165 rows := make([]table.Row, 0)
166 for r, id := range finger {
167 rows = append(rows, table.Row{r, id})
168 }
169 sort.SliceStable(rows, func(i, j int) bool {
170 a, b := rows[i][0], rows[j][0]
171 aParts, bParts := strings.Split(a.(string), "/"), strings.Split(b.(string), "/")
172 aMin, _ := strconv.ParseInt(aParts[0], 10, 64)
173 bMin, _ := strconv.ParseInt(bParts[0], 10, 64)
174 return aMin < bMin
175 })
176 fingerTable.AppendRows(rows, table.RowConfig{AutoMerge: true})
177 fingerTable.SetCaption("(range: %v)", chord.MaxIdentitifer)
178 fingerTable.SetStyle(table.StyleDefault)
179 fingerTable.Style().Options.SeparateRows = true
180 fingerTable.SetColumnConfigs([]table.ColumnConfig{
181 {Number: 1, AlignHeader: text.AlignCenter},
182 {Number: 2, AlignHeader: text.AlignCenter},
183 })
184 fingerTable.Render()
185 
186 sbs.AppendHeader(table.Row{"Finger", "Ring"})
187 sbs.SetColumnConfigs([]table.ColumnConfig{
188 {Number: 1, AlignHeader: text.AlignCenter},
189 {Number: 2, AlignHeader: text.AlignCenter},
190 })
191 sbs.AppendRow(table.Row{fingerString.String(), nodesString.String()})
192 sbs.SetStyle(table.StyleDefault)
193 sbs.Style().Options.DrawBorder = false
194 sbs.Style().Options.SeparateHeader = false
195 sbs.Style().Options.SeparateColumns = false
196 sbs.Style().Options.SeparateRows = false
197 sbs.Render()
198 
199 keysTable := table.NewWriter()
200 keysTable.SetOutputMirror(lis)
201 keysTable.AppendHeader(table.Row{"owner", "hash(key)", "key", "simple", "prefix", "lease"})
202 keysTable.SetColumnConfigs([]table.ColumnConfig{
203 {Number: 1, AutoMerge: true, AlignHeader: text.AlignCenter},
204 {Number: 2, AlignHeader: text.AlignCenter},
205 {Number: 3, AlignHeader: text.AlignCenter, WidthMin: 20},
206 {Number: 4, AutoMerge: true, AlignHeader: text.AlignCenter},
207 {Number: 5, AutoMerge: true, AlignHeader: text.AlignCenter},
208 {Number: 6, AutoMerge: true, AlignHeader: text.AlignCenter},
209 })
210 
211 keys, err := node.kv.RangeKeys(ctx, 0, 0)
212 if err != nil {
213 http.Error(w, err.Error(), 500)
214 return
215 }
216 exp, err := node.kv.Export(ctx, keys)
217 if err != nil {
218 http.Error(w, err.Error(), 500)
219 return
220 }
221 for i, key := range keys {
222 plain := exp[i].GetSimpleValue()
223 children := exp[i].GetPrefixChildren()
224 lease := exp[i].GetLeaseToken()
225 
226 id := chord.Hash(key)
227 ownership := ""
228 if !chord.Between(pre.ID(), id, node.ID(), true) {
229 ownership = "X"
230 }
231 keysTable.AppendRow(table.Row{
232 ownership, id, string(key), len(plain), len(children), lease,
233 })
234 }
235 numKeys += len(keys)
236 keysTable.SetCaption("(With %d keys; X in owner column indicates incorrect owner)", len(keys))
237 keysTable.SetStyle(table.StyleDefault)
238 keysTable.Style().Options.SeparateRows = true
239 keysTable.SuppressEmptyColumns()
240 keysTable.Render()
241 
242 fmt.Fprintf(lis, "---\n\n")
243 }
244 
245 // read mem stats
246 var rtm runtime.MemStats
247 runtime.ReadMemStats(&rtm)
248 phyTable := table.NewWriter()
249 phyTable.SetOutputMirror(phy)
250 phyTable.AppendRows([]table.Row{
251 {
252 "Advertise Address",
253 rootNode.Identity().GetAddress(),
254 },
255 {
256 "Outbound RPC qps",
257 fmt.Sprintf("%.2f", rootNode.ChordClient.RatePer(time.Second)),
258 },
259 {
260 "Number of KV Keys",
261 fmt.Sprintf("%d", numKeys),
262 },
263 {"Mallocs", fmt.Sprintf("%d", rtm.Alloc)},
264 {"Frees", fmt.Sprintf("%d", rtm.Frees)},
265 {"LiveObjects", fmt.Sprintf("%d", rtm.Mallocs-rtm.Frees)},
266 {"HeapObjects", fmt.Sprintf("%d", rtm.HeapObjects)},
267 {"HeapAlloc", fmt.Sprintf("%d", rtm.HeapAlloc)},
268 {"NumGC", fmt.Sprintf("%d", rtm.NumGC)},
269 {"PauseTotalNs", fmt.Sprintf("%d", rtm.PauseTotalNs)},
270 {"LastGC", time.UnixMilli(int64(rtm.LastGC / 1_000_000)).Format(time.RFC3339)},
271 })
272 phyTable.SetStyle(table.StyleDefault)
273 phyTable.Style().Options.SeparateRows = true
274 phyTable.SetColumnConfigs([]table.ColumnConfig{
275 {Number: 2, Align: text.AlignRight},
276 })
277 phyTable.Render()
278 
279 w.Write([]byte(phy.String()))
280 fmt.Fprintf(w, "---\n\n")
281 w.Write([]byte(vir.String()))
282 fmt.Fprintf(w, "---\n\n")
283 w.Write([]byte(lis.String()))
284}
285 
286func printKey(virtualNodes []*LocalNode, w http.ResponseWriter, r *http.Request, key string) {
287 for _, n := range virtualNodes {
288 val, err := n.kv.Get(r.Context(), []byte(key))
289 if err != nil {
290 w.WriteHeader(http.StatusInternalServerError)
291 fmt.Fprintf(w, "error getting kv: %v", err)
292 return
293 }
294 if len(val) != 0 {
295 w.Write(val)
296 return
297 }
298 }
299 w.WriteHeader(http.StatusNotFound)
300 fmt.Fprint(w, "key not found on this node")
301}
302 
303func statsHandler(virtualNodes []*LocalNode) func(w http.ResponseWriter, r *http.Request) {
304 return func(w http.ResponseWriter, r *http.Request) {
305 query := r.URL.Query()
306 if query.Has("key") {
307 w.Header().Set("content-type", "text/plain; charset=utf-8")
308 printKey(virtualNodes, w, r, query.Get("key"))
309 return
310 }
311 
312 urlFormat, _ := r.Context().Value(middleware.URLFormatCtxKey).(string)
313 switch urlFormat {
314 case "html":
315 w.Header().Set("content-type", "text/html; charset=utf-8")
316 defer fmt.Fprint(w, `</pre></body></html>`)
317 fmt.Fprint(w, `<html><head><meta name="viewport" content="width=device-width, initial-scale=1.0"></head><body><pre>`)
318 w = escapedStatsWriter{ResponseWriter: w}
319 default:
320 w.Header().Set("content-type", "text/plain; charset=utf-8")
321 }
322 
323 printSummary(r.Context(), w, virtualNodes)
324 }
325}
326 
327// Preserve the text diagnostic's streaming behavior while treating stored keys,
328// peer addresses and errors as text inside the HTML representation.
329type escapedStatsWriter struct {
330 http.ResponseWriter
331}
332 
333func (w escapedStatsWriter) Write(p []byte) (int, error) {
334 if _, err := w.ResponseWriter.Write([]byte(html.EscapeString(string(p)))); err != nil {
335 return 0, err
336 }
337 return len(p), nil
338}