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