github.com/sberex/go-sberex@v1.8.2-0.20181113200658-ed96ac38f7d7/cmd/geth/monitorcmd.go (about) 1 // This file is part of the go-sberex library. The go-sberex library is 2 // free software: you can redistribute it and/or modify it under the terms 3 // of the GNU Lesser General Public License as published by the Free 4 // Software Foundation, either version 3 of the License, or (at your option) 5 // any later version. 6 // 7 // The go-sberex library is distributed in the hope that it will be useful, 8 // but WITHOUT ANY WARRANTY; without even the implied warranty of 9 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser 10 // General Public License <http://www.gnu.org/licenses/> for more details. 11 12 package main 13 14 import ( 15 "fmt" 16 "math" 17 "reflect" 18 "runtime" 19 "sort" 20 "strings" 21 "time" 22 23 "github.com/Sberex/go-sberex/cmd/utils" 24 "github.com/Sberex/go-sberex/node" 25 "github.com/Sberex/go-sberex/rpc" 26 "github.com/gizak/termui" 27 "gopkg.in/urfave/cli.v1" 28 ) 29 30 var ( 31 monitorCommandAttachFlag = cli.StringFlag{ 32 Name: "attach", 33 Value: node.DefaultIPCEndpoint(clientIdentifier), 34 Usage: "API endpoint to attach to", 35 } 36 monitorCommandRowsFlag = cli.IntFlag{ 37 Name: "rows", 38 Value: 5, 39 Usage: "Maximum rows in the chart grid", 40 } 41 monitorCommandRefreshFlag = cli.IntFlag{ 42 Name: "refresh", 43 Value: 3, 44 Usage: "Refresh interval in seconds", 45 } 46 monitorCommand = cli.Command{ 47 Action: utils.MigrateFlags(monitor), // keep track of migration progress 48 Name: "monitor", 49 Usage: "Monitor and visualize node metrics", 50 ArgsUsage: " ", 51 Category: "MONITOR COMMANDS", 52 Description: ` 53 The Geth monitor is a tool to collect and visualize various internal metrics 54 gathered by the node, supporting different chart types as well as the capacity 55 to display multiple metrics simultaneously. 56 `, 57 Flags: []cli.Flag{ 58 monitorCommandAttachFlag, 59 monitorCommandRowsFlag, 60 monitorCommandRefreshFlag, 61 }, 62 } 63 ) 64 65 // monitor starts a terminal UI based monitoring tool for the requested metrics. 66 func monitor(ctx *cli.Context) error { 67 var ( 68 client *rpc.Client 69 err error 70 ) 71 // Attach to an Sberex node over IPC or RPC 72 endpoint := ctx.String(monitorCommandAttachFlag.Name) 73 if client, err = dialRPC(endpoint); err != nil { 74 utils.Fatalf("Unable to attach to geth node: %v", err) 75 } 76 defer client.Close() 77 78 // Retrieve all the available metrics and resolve the user pattens 79 metrics, err := retrieveMetrics(client) 80 if err != nil { 81 utils.Fatalf("Failed to retrieve system metrics: %v", err) 82 } 83 monitored := resolveMetrics(metrics, ctx.Args()) 84 if len(monitored) == 0 { 85 list := expandMetrics(metrics, "") 86 sort.Strings(list) 87 88 if len(list) > 0 { 89 utils.Fatalf("No metrics specified.\n\nAvailable:\n - %s", strings.Join(list, "\n - ")) 90 } else { 91 utils.Fatalf("No metrics collected by geth (--%s).\n", utils.MetricsEnabledFlag.Name) 92 } 93 } 94 sort.Strings(monitored) 95 if cols := len(monitored) / ctx.Int(monitorCommandRowsFlag.Name); cols > 6 { 96 utils.Fatalf("Requested metrics (%d) spans more that 6 columns:\n - %s", len(monitored), strings.Join(monitored, "\n - ")) 97 } 98 // Create and configure the chart UI defaults 99 if err := termui.Init(); err != nil { 100 utils.Fatalf("Unable to initialize terminal UI: %v", err) 101 } 102 defer termui.Close() 103 104 rows := len(monitored) 105 if max := ctx.Int(monitorCommandRowsFlag.Name); rows > max { 106 rows = max 107 } 108 cols := (len(monitored) + rows - 1) / rows 109 for i := 0; i < rows; i++ { 110 termui.Body.AddRows(termui.NewRow()) 111 } 112 // Create each individual data chart 113 footer := termui.NewPar("") 114 footer.Block.Border = true 115 footer.Height = 3 116 117 charts := make([]*termui.LineChart, len(monitored)) 118 units := make([]int, len(monitored)) 119 data := make([][]float64, len(monitored)) 120 for i := 0; i < len(monitored); i++ { 121 charts[i] = createChart((termui.TermHeight() - footer.Height) / rows) 122 row := termui.Body.Rows[i%rows] 123 row.Cols = append(row.Cols, termui.NewCol(12/cols, 0, charts[i])) 124 } 125 termui.Body.AddRows(termui.NewRow(termui.NewCol(12, 0, footer))) 126 127 refreshCharts(client, monitored, data, units, charts, ctx, footer) 128 termui.Body.Align() 129 termui.Render(termui.Body) 130 131 // Watch for various system events, and periodically refresh the charts 132 termui.Handle("/sys/kbd/C-c", func(termui.Event) { 133 termui.StopLoop() 134 }) 135 termui.Handle("/sys/wnd/resize", func(termui.Event) { 136 termui.Body.Width = termui.TermWidth() 137 for _, chart := range charts { 138 chart.Height = (termui.TermHeight() - footer.Height) / rows 139 } 140 termui.Body.Align() 141 termui.Render(termui.Body) 142 }) 143 go func() { 144 tick := time.NewTicker(time.Duration(ctx.Int(monitorCommandRefreshFlag.Name)) * time.Second) 145 for range tick.C { 146 if refreshCharts(client, monitored, data, units, charts, ctx, footer) { 147 termui.Body.Align() 148 } 149 termui.Render(termui.Body) 150 } 151 }() 152 termui.Loop() 153 return nil 154 } 155 156 // retrieveMetrics contacts the attached geth node and retrieves the entire set 157 // of collected system metrics. 158 func retrieveMetrics(client *rpc.Client) (map[string]interface{}, error) { 159 var metrics map[string]interface{} 160 err := client.Call(&metrics, "debug_metrics", true) 161 return metrics, err 162 } 163 164 // resolveMetrics takes a list of input metric patterns, and resolves each to one 165 // or more canonical metric names. 166 func resolveMetrics(metrics map[string]interface{}, patterns []string) []string { 167 res := []string{} 168 for _, pattern := range patterns { 169 res = append(res, resolveMetric(metrics, pattern, "")...) 170 } 171 return res 172 } 173 174 // resolveMetrics takes a single of input metric pattern, and resolves it to one 175 // or more canonical metric names. 176 func resolveMetric(metrics map[string]interface{}, pattern string, path string) []string { 177 results := []string{} 178 179 // If a nested metric was requested, recurse optionally branching (via comma) 180 parts := strings.SplitN(pattern, "/", 2) 181 if len(parts) > 1 { 182 for _, variation := range strings.Split(parts[0], ",") { 183 if submetrics, ok := metrics[variation].(map[string]interface{}); !ok { 184 utils.Fatalf("Failed to retrieve system metrics: %s", path+variation) 185 return nil 186 } else { 187 results = append(results, resolveMetric(submetrics, parts[1], path+variation+"/")...) 188 } 189 } 190 return results 191 } 192 // Depending what the last link is, return or expand 193 for _, variation := range strings.Split(pattern, ",") { 194 switch metric := metrics[variation].(type) { 195 case float64: 196 // Final metric value found, return as singleton 197 results = append(results, path+variation) 198 199 case map[string]interface{}: 200 results = append(results, expandMetrics(metric, path+variation+"/")...) 201 202 default: 203 utils.Fatalf("Metric pattern resolved to unexpected type: %v", reflect.TypeOf(metric)) 204 return nil 205 } 206 } 207 return results 208 } 209 210 // expandMetrics expands the entire tree of metrics into a flat list of paths. 211 func expandMetrics(metrics map[string]interface{}, path string) []string { 212 // Iterate over all fields and expand individually 213 list := []string{} 214 for name, metric := range metrics { 215 switch metric := metric.(type) { 216 case float64: 217 // Final metric value found, append to list 218 list = append(list, path+name) 219 220 case map[string]interface{}: 221 // Tree of metrics found, expand recursively 222 list = append(list, expandMetrics(metric, path+name+"/")...) 223 224 default: 225 utils.Fatalf("Metric pattern %s resolved to unexpected type: %v", path+name, reflect.TypeOf(metric)) 226 return nil 227 } 228 } 229 return list 230 } 231 232 // fetchMetric iterates over the metrics map and retrieves a specific one. 233 func fetchMetric(metrics map[string]interface{}, metric string) float64 { 234 parts := strings.Split(metric, "/") 235 for _, part := range parts[:len(parts)-1] { 236 var found bool 237 metrics, found = metrics[part].(map[string]interface{}) 238 if !found { 239 return 0 240 } 241 } 242 if v, ok := metrics[parts[len(parts)-1]].(float64); ok { 243 return v 244 } 245 return 0 246 } 247 248 // refreshCharts retrieves a next batch of metrics, and inserts all the new 249 // values into the active datasets and charts 250 func refreshCharts(client *rpc.Client, metrics []string, data [][]float64, units []int, charts []*termui.LineChart, ctx *cli.Context, footer *termui.Par) (realign bool) { 251 values, err := retrieveMetrics(client) 252 for i, metric := range metrics { 253 if len(data) < 512 { 254 data[i] = append([]float64{fetchMetric(values, metric)}, data[i]...) 255 } else { 256 data[i] = append([]float64{fetchMetric(values, metric)}, data[i][:len(data[i])-1]...) 257 } 258 if updateChart(metric, data[i], &units[i], charts[i], err) { 259 realign = true 260 } 261 } 262 updateFooter(ctx, err, footer) 263 return 264 } 265 266 // updateChart inserts a dataset into a line chart, scaling appropriately as to 267 // not display weird labels, also updating the chart label accordingly. 268 func updateChart(metric string, data []float64, base *int, chart *termui.LineChart, err error) (realign bool) { 269 dataUnits := []string{"", "K", "M", "G", "T", "E"} 270 timeUnits := []string{"ns", "µs", "ms", "s", "ks", "ms"} 271 colors := []termui.Attribute{termui.ColorBlue, termui.ColorCyan, termui.ColorGreen, termui.ColorYellow, termui.ColorRed, termui.ColorRed} 272 273 // Extract only part of the data that's actually visible 274 if chart.Width*2 < len(data) { 275 data = data[:chart.Width*2] 276 } 277 // Find the maximum value and scale under 1K 278 high := 0.0 279 if len(data) > 0 { 280 high = data[0] 281 for _, value := range data[1:] { 282 high = math.Max(high, value) 283 } 284 } 285 unit, scale := 0, 1.0 286 for high >= 1000 && unit+1 < len(dataUnits) { 287 high, unit, scale = high/1000, unit+1, scale*1000 288 } 289 // If the unit changes, re-create the chart (hack to set max height...) 290 if unit != *base { 291 realign, *base, *chart = true, unit, *createChart(chart.Height) 292 } 293 // Update the chart's data points with the scaled values 294 if cap(chart.Data) < len(data) { 295 chart.Data = make([]float64, len(data)) 296 } 297 chart.Data = chart.Data[:len(data)] 298 for i, value := range data { 299 chart.Data[i] = value / scale 300 } 301 // Update the chart's label with the scale units 302 units := dataUnits 303 if strings.Contains(metric, "/Percentiles/") || strings.Contains(metric, "/pauses/") || strings.Contains(metric, "/time/") { 304 units = timeUnits 305 } 306 chart.BorderLabel = metric 307 if len(units[unit]) > 0 { 308 chart.BorderLabel += " [" + units[unit] + "]" 309 } 310 chart.LineColor = colors[unit] | termui.AttrBold 311 if err != nil { 312 chart.LineColor = termui.ColorRed | termui.AttrBold 313 } 314 return 315 } 316 317 // createChart creates an empty line chart with the default configs. 318 func createChart(height int) *termui.LineChart { 319 chart := termui.NewLineChart() 320 if runtime.GOOS == "windows" { 321 chart.Mode = "dot" 322 } 323 chart.DataLabels = []string{""} 324 chart.Height = height 325 chart.AxesColor = termui.ColorWhite 326 chart.PaddingBottom = -2 327 328 chart.BorderLabelFg = chart.BorderFg | termui.AttrBold 329 chart.BorderFg = chart.BorderBg 330 331 return chart 332 } 333 334 // updateFooter updates the footer contents based on any encountered errors. 335 func updateFooter(ctx *cli.Context, err error, footer *termui.Par) { 336 // Generate the basic footer 337 refresh := time.Duration(ctx.Int(monitorCommandRefreshFlag.Name)) * time.Second 338 footer.Text = fmt.Sprintf("Press Ctrl+C to quit. Refresh interval: %v.", refresh) 339 footer.TextFgColor = termui.ThemeAttr("par.fg") | termui.AttrBold 340 341 // Append any encountered errors 342 if err != nil { 343 footer.Text = fmt.Sprintf("Error: %v.", err) 344 footer.TextFgColor = termui.ColorRed | termui.AttrBold 345 } 346 }