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