github.com/ethereumproject/go-ethereum@v5.5.2+incompatible/cmd/geth/monitorcmd.go (about) 1 // Copyright 2015 The go-ethereum Authors 2 // This file is part of go-ethereum. 3 // 4 // go-ethereum 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 // go-ethereum 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 go-ethereum. If not, see <http://www.gnu.org/licenses/>. 16 17 package main 18 19 import ( 20 "fmt" 21 "log" 22 "math" 23 "runtime" 24 "strings" 25 "time" 26 27 "sort" 28 29 "path/filepath" 30 "regexp" 31 32 "github.com/ethereumproject/go-ethereum/common" 33 "github.com/ethereumproject/go-ethereum/logger/glog" 34 "github.com/ethereumproject/go-ethereum/node" 35 "github.com/ethereumproject/go-ethereum/rpc" 36 "github.com/gizak/termui" 37 "gopkg.in/urfave/cli.v1" 38 ) 39 40 var ( 41 monitorCommandAttachFlag = cli.StringFlag{ 42 Name: "attach", 43 Value: "ipc:" + node.DefaultIPCEndpoint(filepath.Join(common.DefaultDataDir(), "mainnet")), 44 Usage: "API endpoint to attach to", 45 } 46 monitorCommandRowsFlag = cli.IntFlag{ 47 Name: "rows", 48 Value: 5, 49 Usage: "Maximum rows in the chart grid", 50 } 51 monitorCommandRefreshFlag = cli.IntFlag{ 52 Name: "refresh", 53 Value: 3, 54 Usage: "Refresh interval in seconds", 55 } 56 monitorCommand = cli.Command{ 57 Action: monitor, 58 Name: "monitor", 59 Usage: `Geth Monitor: node metrics monitoring and visualization`, 60 Description: ` 61 The Geth monitor is a tool to collect and visualize various internal metrics 62 gathered by the node, supporting different chart types as well as the capacity 63 to display multiple metrics simultaneously. 64 `, 65 Flags: []cli.Flag{ 66 monitorCommandAttachFlag, 67 monitorCommandRowsFlag, 68 monitorCommandRefreshFlag, 69 }, 70 } 71 ) 72 73 // monitor starts a terminal UI based monitoring tool for the requested metrics. 74 func monitor(ctx *cli.Context) error { 75 // Attach to an Ethereum node over IPC or RPC 76 endpoint := ctx.String(monitorCommandAttachFlag.Name) 77 // Set defaults (no arg value) to chain contextual path (via --chain or --testnet, since default val is mainnet) 78 if ctx.GlobalString(monitorCommandAttachFlag.Name) == "" { 79 endpoint = "ipc:" + node.DefaultIPCEndpoint(MustMakeChainDataDir(ctx)) 80 } 81 client, err := rpc.NewClient(endpoint) 82 if err != nil { 83 log.Fatal("attach to remote geth: ", err) 84 } 85 defer client.Close() 86 87 // Retrieve all the available metrics and resolve the user pattens 88 metrics, err := retrieveMetrics(client) 89 if err != nil { 90 log.Fatalf("Failed to retrieve system metrics: %s", err) 91 } 92 monitored := resolveMetrics(metrics, ctx.Args()) 93 if len(monitored) == 0 { 94 list := expandMetrics(metrics, "") 95 sort.Strings(list) 96 97 if len(list) > 0 { 98 if len(ctx.Args()) == 0 { 99 log.Fatalf("No metrics specified. Available: \n%s", listWithNewlines(list)) 100 } 101 log.Fatalf("No metrics found matching that pattern. Available metrics: \n%s", listWithNewlines(list)) 102 } else { 103 log.Fatal("No metrics collected by geth (--metrics).") 104 } 105 } 106 sort.Strings(monitored) 107 if cols := len(monitored) / ctx.Int(monitorCommandRowsFlag.Name); cols > 6 { 108 log.Fatalf("Requested metrics spans more that 6 columns: %q", monitored) 109 } 110 // Create and configure the chart UI defaults 111 if err := termui.Init(); err != nil { 112 log.Fatalf("Unable to initialize terminal UI: %s", err) 113 } 114 defer termui.Close() 115 116 rows := len(monitored) 117 if max := ctx.Int(monitorCommandRowsFlag.Name); rows > max { 118 rows = max 119 } 120 cols := (len(monitored) + rows - 1) / rows 121 for i := 0; i < rows; i++ { 122 termui.Body.AddRows(termui.NewRow()) 123 } 124 // Create each individual data chart 125 footer := termui.NewPar("") 126 footer.Block.Border = true 127 footer.Height = 3 128 129 charts := make([]*termui.LineChart, len(monitored)) 130 units := make([]int, len(monitored)) 131 data := make([][]float64, len(monitored)) 132 for i := 0; i < len(monitored); i++ { 133 charts[i] = createChart((termui.TermHeight() - footer.Height) / rows) 134 row := termui.Body.Rows[i%rows] 135 row.Cols = append(row.Cols, termui.NewCol(12/cols, 0, charts[i])) 136 } 137 termui.Body.AddRows(termui.NewRow(termui.NewCol(12, 0, footer))) 138 139 refreshCharts(client, monitored, data, units, charts, ctx, footer) 140 termui.Body.Align() 141 termui.Render(termui.Body) 142 143 // Watch for various system events, and periodically refresh the charts 144 termui.Handle("/sys/kbd/C-c", func(termui.Event) { 145 termui.StopLoop() 146 }) 147 termui.Handle("/sys/wnd/resize", func(termui.Event) { 148 termui.Body.Width = termui.TermWidth() 149 for _, chart := range charts { 150 chart.Height = (termui.TermHeight() - footer.Height) / rows 151 } 152 termui.Body.Align() 153 termui.Render(termui.Body) 154 }) 155 go func() { 156 tick := time.NewTicker(time.Duration(ctx.Int(monitorCommandRefreshFlag.Name)) * time.Second) 157 for range tick.C { 158 if refreshCharts(client, monitored, data, units, charts, ctx, footer) { 159 termui.Body.Align() 160 } 161 termui.Render(termui.Body) 162 } 163 }() 164 termui.Loop() 165 return nil 166 } 167 168 // listWithNewlines is a convenience function for showing available 169 // metrics in case there are none specified or no matches 170 func listWithNewlines(availableMetrics []string) string { 171 return strings.Join(availableMetrics, "\n") 172 } 173 174 // retrieveMetrics contacts the attached geth node and retrieves the entire set 175 // of collected system metrics. 176 func retrieveMetrics(client rpc.Client) (map[string]float64, error) { 177 req := map[string]interface{}{ 178 "id": new(int64), 179 "method": "debug_metrics", 180 "jsonrpc": "2.0", 181 "params": []interface{}{true}, 182 } 183 184 if err := client.Send(req); err != nil { 185 return nil, err 186 } 187 188 var res rpc.JSONResponse 189 if err := client.Recv(&res); err != nil { 190 return nil, err 191 } 192 193 if res.Result != nil { 194 r, ok := res.Result.(map[string]interface{}) 195 if !ok { 196 glog.Fatalln("Could not convert resulting JSON response to type map[string]interface{}") 197 } 198 199 if ok { 200 return flattenToFloat(r), nil 201 } 202 } 203 204 return nil, fmt.Errorf("unable to retrieve metrics") 205 } 206 207 // resolveMetrics takes a list of input metric patterns, and resolves each to one 208 // or more canonical metric names. 209 // 'patterns' are user-inputed arguments 210 // eg. 211 // $ geth monitor [--attach=api-endpoint] metric1 metric2 ... metricN 212 // 213 // Where a metric may be: a REGEX to match available metrics paths/strings/names 214 func resolveMetrics(metrics map[string]float64, patterns []string) []string { 215 res := []string{} 216 for _, pattern := range patterns { 217 res = append(res, resolveMetric(metrics, pattern, "")...) 218 } 219 return res 220 } 221 222 // resolveMetrics takes a single of input metric pattern, and resolves it to one 223 // or more canonical metric names. 224 func resolveMetric(metrics map[string]float64, pattern string, path string) []string { 225 var out []string 226 re := regexp.MustCompile(pattern) 227 for met := range metrics { 228 if re.MatchString(met) { 229 out = append(out, met) 230 } 231 } 232 return out 233 } 234 235 // expandMetrics expands the entire tree of metrics into a flat list of paths. 236 func expandMetrics(metrics map[string]float64, path string) []string { 237 var list []string 238 for k := range metrics { 239 list = append(list, k) 240 } 241 return list 242 } 243 244 // flattenToFloat takes: 245 /* 246 p2p/bytes/in: map[string]interface{} 247 where interface{} val is: 248 map{ 249 15m.rate: 0 250 5m.rate: 4 251 1m.rate: 1.3 252 mean.rate: 0.7222 253 count: 14 254 } 255 256 and returns: 257 map{ 258 p2p/bytes/in/15m.rate: 0 259 p2p/bytes/in/5m.rate: 4 260 p2p/bytes/in/1m.rate: 1.3 261 p2p/bytes/in/mean.rate: 0.7222 262 p2p/bytes/in/count: 14 263 } 264 265 */ 266 func flattenToFloat(rawMets map[string]interface{}) map[string]float64 { 267 var mets = make(map[string]float64) 268 for k, v := range rawMets { 269 if vi, ok := v.(map[string]interface{}); ok { 270 for vk, vv := range vi { 271 if f, fok := vv.(float64); fok { 272 mets[k+"/"+vk] = f 273 } 274 } 275 } 276 } 277 return mets 278 } 279 280 // refreshCharts retrieves a next batch of metrics, and inserts all the new 281 // values into the active datasets and charts 282 func refreshCharts(client rpc.Client, metrics []string, data [][]float64, units []int, charts []*termui.LineChart, ctx *cli.Context, footer *termui.Par) (realign bool) { 283 values, err := retrieveMetrics(client) 284 for i, metric := range metrics { 285 if len(data) < 512 { 286 data[i] = append([]float64{values[metric]}, data[i]...) 287 } else { 288 data[i] = append([]float64{values[metric]}, data[i][:len(data[i])-1]...) 289 } 290 if updateChart(metric, data[i], &units[i], charts[i], err) { 291 realign = true 292 } 293 } 294 updateFooter(ctx, err, footer) 295 return 296 } 297 298 // updateChart inserts a dataset into a line chart, scaling appropriately as to 299 // not display weird labels, also updating the chart label accordingly. 300 func updateChart(metric string, data []float64, base *int, chart *termui.LineChart, err error) (realign bool) { 301 dataUnits := []string{"", "K", "M", "G", "T", "E"} 302 timeUnits := []string{"ns", "µs", "ms", "s", "ks", "ms"} 303 colors := []termui.Attribute{termui.ColorBlue, termui.ColorCyan, termui.ColorGreen, termui.ColorYellow, termui.ColorRed, termui.ColorRed} 304 305 // Extract only part of the data that's actually visible 306 if chart.Width*2 < len(data) { 307 data = data[:chart.Width*2] 308 } 309 // Find the maximum value and scale under 1K 310 high := 0.0 311 if len(data) > 0 { 312 high = data[0] 313 for _, value := range data[1:] { 314 high = math.Max(high, value) 315 } 316 } 317 unit, scale := 0, 1.0 318 for high >= 1000 && unit+1 < len(dataUnits) { 319 high, unit, scale = high/1000, unit+1, scale*1000 320 } 321 // If the unit changes, re-create the chart (hack to set max height...) 322 if unit != *base { 323 realign, *base, *chart = true, unit, *createChart(chart.Height) 324 } 325 // Update the chart's data points with the scaled values 326 if cap(chart.Data) < len(data) { 327 chart.Data = make([]float64, len(data)) 328 } 329 chart.Data = chart.Data[:len(data)] 330 for i, value := range data { 331 chart.Data[i] = value / scale 332 } 333 // Update the chart's label with the scale units 334 units := dataUnits 335 if strings.Contains(metric, "/Percentiles/") || strings.Contains(metric, "/pauses/") || strings.Contains(metric, "/time/") { 336 units = timeUnits 337 } 338 chart.BorderLabel = metric 339 if len(units[unit]) > 0 { 340 chart.BorderLabel += " [" + units[unit] + "]" 341 } 342 chart.LineColor = colors[unit] | termui.AttrBold 343 if err != nil { 344 chart.LineColor = termui.ColorRed | termui.AttrBold 345 } 346 return 347 } 348 349 // createChart creates an empty line chart with the default configs. 350 func createChart(height int) *termui.LineChart { 351 chart := termui.NewLineChart() 352 if runtime.GOOS == "windows" { 353 chart.Mode = "dot" 354 } 355 chart.DataLabels = []string{""} 356 chart.Height = height 357 chart.AxesColor = termui.ColorWhite 358 chart.PaddingBottom = -2 359 360 chart.BorderLabelFg = chart.BorderFg | termui.AttrBold 361 chart.BorderFg = chart.BorderBg 362 363 return chart 364 } 365 366 // updateFooter updates the footer contents based on any encountered errors. 367 func updateFooter(ctx *cli.Context, err error, footer *termui.Par) { 368 // Generate the basic footer 369 refresh := time.Duration(ctx.Int(monitorCommandRefreshFlag.Name)) * time.Second 370 footer.Text = fmt.Sprintf("Press Ctrl+C to quit. Refresh interval: %v.", refresh) 371 footer.TextFgColor = termui.ThemeAttr("par.fg") | termui.AttrBold 372 373 // Append any encountered errors 374 if err != nil { 375 footer.Text = fmt.Sprintf("Error: %v.", err) 376 footer.TextFgColor = termui.ColorRed | termui.AttrBold 377 } 378 } 379 380 // sliceContainsStrings is a convenience helper function for resolving metrics paths 381 func sliceContainsString(slice []string, s string) bool { 382 for _, sl := range slice { 383 if sl == s { 384 return true 385 } 386 } 387 return false 388 }