monitorcmd.go 11.4 KB
Newer Older
1 2 3 4 5 6 7 8 9 10
// Copyright 2015 The go-ethereum Authors
// This file is part of go-ethereum.
//
// go-ethereum is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// go-ethereum is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 13 14
// GNU General Public License for more details.
//
// 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 18 19
package main

import (
20
	"fmt"
21
	"math"
22
	"reflect"
23
	"runtime"
24 25 26
	"strings"
	"time"

27 28
	"sort"

29 30
	"github.com/codegangsta/cli"
	"github.com/ethereum/go-ethereum/cmd/utils"
31
	"github.com/ethereum/go-ethereum/node"
32 33 34 35
	"github.com/ethereum/go-ethereum/rpc"
	"github.com/gizak/termui"
)

36 37 38
var (
	monitorCommandAttachFlag = cli.StringFlag{
		Name:  "attach",
39
		Value: "ipc:" + node.DefaultIPCEndpoint(),
40
		Usage: "API endpoint to attach to",
41 42 43 44
	}
	monitorCommandRowsFlag = cli.IntFlag{
		Name:  "rows",
		Value: 5,
45 46 47 48 49 50
		Usage: "Maximum rows in the chart grid",
	}
	monitorCommandRefreshFlag = cli.IntFlag{
		Name:  "refresh",
		Value: 3,
		Usage: "Refresh interval in seconds",
51 52 53 54 55 56 57 58 59 60 61 62 63
	}
	monitorCommand = cli.Command{
		Action: monitor,
		Name:   "monitor",
		Usage:  `Geth Monitor: node metrics monitoring and visualization`,
		Description: `
The Geth monitor is a tool to collect and visualize various internal metrics
gathered by the node, supporting different chart types as well as the capacity
to display multiple metrics simultaneously.
`,
		Flags: []cli.Flag{
			monitorCommandAttachFlag,
			monitorCommandRowsFlag,
64
			monitorCommandRefreshFlag,
65 66 67 68
		},
	}
)

69 70 71
// monitor starts a terminal UI based monitoring tool for the requested metrics.
func monitor(ctx *cli.Context) {
	var (
72
		client rpc.Client
73 74 75
		err    error
	)
	// Attach to an Ethereum node over IPC or RPC
76
	endpoint := ctx.String(monitorCommandAttachFlag.Name)
77
	if client, err = utils.NewRemoteRPCClientFromString(endpoint); err != nil {
78
		utils.Fatalf("Unable to attach to geth node: %v", err)
79 80 81 82
	}
	defer client.Close()

	// Retrieve all the available metrics and resolve the user pattens
83
	metrics, err := retrieveMetrics(client)
84 85 86
	if err != nil {
		utils.Fatalf("Failed to retrieve system metrics: %v", err)
	}
87
	monitored := resolveMetrics(metrics, ctx.Args())
88
	if len(monitored) == 0 {
89
		list := expandMetrics(metrics, "")
90
		sort.Strings(list)
91 92 93 94

		if len(list) > 0 {
			utils.Fatalf("No metrics specified.\n\nAvailable:\n - %s", strings.Join(list, "\n - "))
		} else {
95
			utils.Fatalf("No metrics collected by geth (--%s).\n", utils.MetricsEnabledFlag.Name)
96
		}
97
	}
98
	sort.Strings(monitored)
99 100 101
	if cols := len(monitored) / ctx.Int(monitorCommandRowsFlag.Name); cols > 6 {
		utils.Fatalf("Requested metrics (%d) spans more that 6 columns:\n - %s", len(monitored), strings.Join(monitored, "\n - "))
	}
102
	// Create and configure the chart UI defaults
103 104 105 106 107
	if err := termui.Init(); err != nil {
		utils.Fatalf("Unable to initialize terminal UI: %v", err)
	}
	defer termui.Close()

108
	rows := len(monitored)
109 110
	if max := ctx.Int(monitorCommandRowsFlag.Name); rows > max {
		rows = max
111
	}
112 113 114 115 116
	cols := (len(monitored) + rows - 1) / rows
	for i := 0; i < rows; i++ {
		termui.Body.AddRows(termui.NewRow())
	}
	// Create each individual data chart
117
	footer := termui.NewPar("")
118
	footer.Block.Border = true
119 120
	footer.Height = 3

121
	charts := make([]*termui.LineChart, len(monitored))
122
	units := make([]int, len(monitored))
123
	data := make([][]float64, len(monitored))
124 125
	for i := 0; i < len(monitored); i++ {
		charts[i] = createChart((termui.TermHeight() - footer.Height) / rows)
126 127
		row := termui.Body.Rows[i%rows]
		row.Cols = append(row.Cols, termui.NewCol(12/cols, 0, charts[i]))
128
	}
129
	termui.Body.AddRows(termui.NewRow(termui.NewCol(12, 0, footer)))
130

131
	refreshCharts(client, monitored, data, units, charts, ctx, footer)
132
	termui.Body.Align()
133 134 135
	termui.Render(termui.Body)

	// Watch for various system events, and periodically refresh the charts
136 137 138 139 140 141 142 143 144 145 146 147 148 149
	termui.Handle("/sys/kbd/C-c", func(termui.Event) {
		termui.StopLoop()
	})
	termui.Handle("/sys/wnd/resize", func(termui.Event) {
		termui.Body.Width = termui.TermWidth()
		for _, chart := range charts {
			chart.Height = (termui.TermHeight() - footer.Height) / rows
		}
		termui.Body.Align()
		termui.Render(termui.Body)
	})
	go func() {
		tick := time.NewTicker(time.Duration(ctx.Int(monitorCommandRefreshFlag.Name)) * time.Second)
		for range tick.C {
150
			if refreshCharts(client, monitored, data, units, charts, ctx, footer) {
151 152
				termui.Body.Align()
			}
153 154
			termui.Render(termui.Body)
		}
155 156
	}()
	termui.Loop()
157 158
}

159 160
// retrieveMetrics contacts the attached geth node and retrieves the entire set
// of collected system metrics.
161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184
func retrieveMetrics(client rpc.Client) (map[string]interface{}, error) {
	req := map[string]interface{}{
		"id":      new(int64),
		"method":  "debug_metrics",
		"jsonrpc": "2.0",
		"params":  []interface{}{true},
	}

	if err := client.Send(req); err != nil {
		return nil, err
	}

	var res rpc.JSONSuccessResponse
	if err := client.Recv(&res); err != nil {
		return nil, err
	}

	if res.Result != nil {
		if mets, ok := res.Result.(map[string]interface{}); ok {
			return mets, nil
		}
	}

	return nil, fmt.Errorf("unable to retrieve metrics")
185 186
}

187 188 189 190 191 192 193 194 195 196 197 198 199
// resolveMetrics takes a list of input metric patterns, and resolves each to one
// or more canonical metric names.
func resolveMetrics(metrics map[string]interface{}, patterns []string) []string {
	res := []string{}
	for _, pattern := range patterns {
		res = append(res, resolveMetric(metrics, pattern, "")...)
	}
	return res
}

// resolveMetrics takes a single of input metric pattern, and resolves it to one
// or more canonical metric names.
func resolveMetric(metrics map[string]interface{}, pattern string, path string) []string {
200 201 202 203 204 205 206 207 208 209 210 211
	results := []string{}

	// If a nested metric was requested, recurse optionally branching (via comma)
	parts := strings.SplitN(pattern, "/", 2)
	if len(parts) > 1 {
		for _, variation := range strings.Split(parts[0], ",") {
			if submetrics, ok := metrics[variation].(map[string]interface{}); !ok {
				utils.Fatalf("Failed to retrieve system metrics: %s", path+variation)
				return nil
			} else {
				results = append(results, resolveMetric(submetrics, parts[1], path+variation+"/")...)
			}
212
		}
213
		return results
214 215
	}
	// Depending what the last link is, return or expand
216 217 218 219 220
	for _, variation := range strings.Split(pattern, ",") {
		switch metric := metrics[variation].(type) {
		case float64:
			// Final metric value found, return as singleton
			results = append(results, path+variation)
221

222 223
		case map[string]interface{}:
			results = append(results, expandMetrics(metric, path+variation+"/")...)
224

225 226 227 228
		default:
			utils.Fatalf("Metric pattern resolved to unexpected type: %v", reflect.TypeOf(metric))
			return nil
		}
229
	}
230
	return results
231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253
}

// expandMetrics expands the entire tree of metrics into a flat list of paths.
func expandMetrics(metrics map[string]interface{}, path string) []string {
	// Iterate over all fields and expand individually
	list := []string{}
	for name, metric := range metrics {
		switch metric := metric.(type) {
		case float64:
			// Final metric value found, append to list
			list = append(list, path+name)

		case map[string]interface{}:
			// Tree of metrics found, expand recursively
			list = append(list, expandMetrics(metric, path+name+"/")...)

		default:
			utils.Fatalf("Metric pattern %s resolved to unexpected type: %v", path+name, reflect.TypeOf(metric))
			return nil
		}
	}
	return list
}
254

255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271
// fetchMetric iterates over the metrics map and retrieves a specific one.
func fetchMetric(metrics map[string]interface{}, metric string) float64 {
	parts, found := strings.Split(metric, "/"), true
	for _, part := range parts[:len(parts)-1] {
		metrics, found = metrics[part].(map[string]interface{})
		if !found {
			return 0
		}
	}
	if v, ok := metrics[parts[len(parts)-1]].(float64); ok {
		return v
	}
	return 0
}

// refreshCharts retrieves a next batch of metrics, and inserts all the new
// values into the active datasets and charts
272 273
func refreshCharts(client rpc.Client, metrics []string, data [][]float64, units []int, charts []*termui.LineChart, ctx *cli.Context, footer *termui.Par) (realign bool) {
	values, err := retrieveMetrics(client)
274
	for i, metric := range metrics {
275 276 277 278 279
		if len(data) < 512 {
			data[i] = append([]float64{fetchMetric(values, metric)}, data[i]...)
		} else {
			data[i] = append([]float64{fetchMetric(values, metric)}, data[i][:len(data[i])-1]...)
		}
280 281 282
		if updateChart(metric, data[i], &units[i], charts[i], err) {
			realign = true
		}
283 284
	}
	updateFooter(ctx, err, footer)
285
	return
286 287
}

288 289
// updateChart inserts a dataset into a line chart, scaling appropriately as to
// not display weird labels, also updating the chart label accordingly.
290
func updateChart(metric string, data []float64, base *int, chart *termui.LineChart, err error) (realign bool) {
291 292 293
	dataUnits := []string{"", "K", "M", "G", "T", "E"}
	timeUnits := []string{"ns", "µs", "ms", "s", "ks", "ms"}
	colors := []termui.Attribute{termui.ColorBlue, termui.ColorCyan, termui.ColorGreen, termui.ColorYellow, termui.ColorRed, termui.ColorRed}
294

295
	// Extract only part of the data that's actually visible
296 297 298
	if chart.Width*2 < len(data) {
		data = data[:chart.Width*2]
	}
299
	// Find the maximum value and scale under 1K
300 301 302 303 304 305
	high := 0.0
	if len(data) > 0 {
		high = data[0]
		for _, value := range data[1:] {
			high = math.Max(high, value)
		}
306 307
	}
	unit, scale := 0, 1.0
308
	for high >= 1000 && unit+1 < len(dataUnits) {
309 310
		high, unit, scale = high/1000, unit+1, scale*1000
	}
311 312 313 314
	// If the unit changes, re-create the chart (hack to set max height...)
	if unit != *base {
		realign, *base, *chart = true, unit, *createChart(chart.Height)
	}
315
	// Update the chart's data points with the scaled values
316 317 318 319
	if cap(chart.Data) < len(data) {
		chart.Data = make([]float64, len(data))
	}
	chart.Data = chart.Data[:len(data)]
320 321 322 323
	for i, value := range data {
		chart.Data[i] = value / scale
	}
	// Update the chart's label with the scale units
324
	units := dataUnits
325
	if strings.Contains(metric, "/Percentiles/") || strings.Contains(metric, "/pauses/") || strings.Contains(metric, "/time/") {
326 327
		units = timeUnits
	}
328
	chart.BorderLabel = metric
329
	if len(units[unit]) > 0 {
330
		chart.BorderLabel += " [" + units[unit] + "]"
331
	}
332 333 334 335
	chart.LineColor = colors[unit] | termui.AttrBold
	if err != nil {
		chart.LineColor = termui.ColorRed | termui.AttrBold
	}
336 337 338 339 340 341 342 343 344 345 346 347 348 349
	return
}

// createChart creates an empty line chart with the default configs.
func createChart(height int) *termui.LineChart {
	chart := termui.NewLineChart()
	if runtime.GOOS == "windows" {
		chart.Mode = "dot"
	}
	chart.DataLabels = []string{""}
	chart.Height = height
	chart.AxesColor = termui.ColorWhite
	chart.PaddingBottom = -2

350 351
	chart.BorderLabelFg = chart.BorderFg | termui.AttrBold
	chart.BorderFg = chart.BorderBg
352 353

	return chart
354 355 356 357 358 359
}

// updateFooter updates the footer contents based on any encountered errors.
func updateFooter(ctx *cli.Context, err error, footer *termui.Par) {
	// Generate the basic footer
	refresh := time.Duration(ctx.Int(monitorCommandRefreshFlag.Name)) * time.Second
360
	footer.Text = fmt.Sprintf("Press Ctrl+C to quit. Refresh interval: %v.", refresh)
361
	footer.TextFgColor = termui.ThemeAttr("par.fg") | termui.AttrBold
362 363 364 365 366 367

	// Append any encountered errors
	if err != nil {
		footer.Text = fmt.Sprintf("Error: %v.", err)
		footer.TextFgColor = termui.ColorRed | termui.AttrBold
	}
368
}