|
|
@ -1,6 +1,7 @@ |
|
|
|
package main |
|
|
|
package main |
|
|
|
|
|
|
|
|
|
|
|
import ( |
|
|
|
import ( |
|
|
|
|
|
|
|
"fmt" |
|
|
|
"math" |
|
|
|
"math" |
|
|
|
"reflect" |
|
|
|
"reflect" |
|
|
|
"sort" |
|
|
|
"sort" |
|
|
@ -20,12 +21,17 @@ var ( |
|
|
|
monitorCommandAttachFlag = cli.StringFlag{ |
|
|
|
monitorCommandAttachFlag = cli.StringFlag{ |
|
|
|
Name: "attach", |
|
|
|
Name: "attach", |
|
|
|
Value: "ipc:" + common.DefaultIpcPath(), |
|
|
|
Value: "ipc:" + common.DefaultIpcPath(), |
|
|
|
Usage: "IPC or RPC API endpoint to attach to", |
|
|
|
Usage: "API endpoint to attach to", |
|
|
|
} |
|
|
|
} |
|
|
|
monitorCommandRowsFlag = cli.IntFlag{ |
|
|
|
monitorCommandRowsFlag = cli.IntFlag{ |
|
|
|
Name: "rows", |
|
|
|
Name: "rows", |
|
|
|
Value: 5, |
|
|
|
Value: 5, |
|
|
|
Usage: "Rows (maximum) to display the charts in", |
|
|
|
Usage: "Maximum rows in the chart grid", |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
monitorCommandRefreshFlag = cli.IntFlag{ |
|
|
|
|
|
|
|
Name: "refresh", |
|
|
|
|
|
|
|
Value: 3, |
|
|
|
|
|
|
|
Usage: "Refresh interval in seconds", |
|
|
|
} |
|
|
|
} |
|
|
|
monitorCommand = cli.Command{ |
|
|
|
monitorCommand = cli.Command{ |
|
|
|
Action: monitor, |
|
|
|
Action: monitor, |
|
|
@ -39,6 +45,7 @@ to display multiple metrics simultaneously. |
|
|
|
Flags: []cli.Flag{ |
|
|
|
Flags: []cli.Flag{ |
|
|
|
monitorCommandAttachFlag, |
|
|
|
monitorCommandAttachFlag, |
|
|
|
monitorCommandRowsFlag, |
|
|
|
monitorCommandRowsFlag, |
|
|
|
|
|
|
|
monitorCommandRefreshFlag, |
|
|
|
}, |
|
|
|
}, |
|
|
|
} |
|
|
|
} |
|
|
|
) |
|
|
|
) |
|
|
@ -66,21 +73,6 @@ func monitor(ctx *cli.Context) { |
|
|
|
monitored := resolveMetrics(metrics, ctx.Args()) |
|
|
|
monitored := resolveMetrics(metrics, ctx.Args()) |
|
|
|
sort.Strings(monitored) |
|
|
|
sort.Strings(monitored) |
|
|
|
|
|
|
|
|
|
|
|
// Create the access function and check that the metric exists
|
|
|
|
|
|
|
|
value := func(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 { |
|
|
|
|
|
|
|
utils.Fatalf("Metric not found: %s", metric) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if v, ok := metrics[parts[len(parts)-1]].(float64); ok { |
|
|
|
|
|
|
|
return v |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
utils.Fatalf("Metric not float64: %s", metric) |
|
|
|
|
|
|
|
return 0 |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
// Create and configure the chart UI defaults
|
|
|
|
// Create and configure the chart UI defaults
|
|
|
|
if err := termui.Init(); err != nil { |
|
|
|
if err := termui.Init(); err != nil { |
|
|
|
utils.Fatalf("Unable to initialize terminal UI: %v", err) |
|
|
|
utils.Fatalf("Unable to initialize terminal UI: %v", err) |
|
|
@ -98,6 +90,10 @@ func monitor(ctx *cli.Context) { |
|
|
|
termui.Body.AddRows(termui.NewRow()) |
|
|
|
termui.Body.AddRows(termui.NewRow()) |
|
|
|
} |
|
|
|
} |
|
|
|
// Create each individual data chart
|
|
|
|
// Create each individual data chart
|
|
|
|
|
|
|
|
footer := termui.NewPar("") |
|
|
|
|
|
|
|
footer.HasBorder = true |
|
|
|
|
|
|
|
footer.Height = 3 |
|
|
|
|
|
|
|
|
|
|
|
charts := make([]*termui.LineChart, len(monitored)) |
|
|
|
charts := make([]*termui.LineChart, len(monitored)) |
|
|
|
data := make([][]float64, len(monitored)) |
|
|
|
data := make([][]float64, len(monitored)) |
|
|
|
for i := 0; i < len(data); i++ { |
|
|
|
for i := 0; i < len(data); i++ { |
|
|
@ -105,25 +101,28 @@ func monitor(ctx *cli.Context) { |
|
|
|
} |
|
|
|
} |
|
|
|
for i, metric := range monitored { |
|
|
|
for i, metric := range monitored { |
|
|
|
charts[i] = termui.NewLineChart() |
|
|
|
charts[i] = termui.NewLineChart() |
|
|
|
|
|
|
|
|
|
|
|
charts[i].Data = make([]float64, 512) |
|
|
|
charts[i].Data = make([]float64, 512) |
|
|
|
charts[i].DataLabels = []string{""} |
|
|
|
charts[i].DataLabels = []string{""} |
|
|
|
charts[i].Height = termui.TermHeight() / rows |
|
|
|
charts[i].Height = (termui.TermHeight() - footer.Height) / rows |
|
|
|
charts[i].AxesColor = termui.ColorWhite |
|
|
|
charts[i].AxesColor = termui.ColorWhite |
|
|
|
charts[i].LineColor = termui.ColorGreen |
|
|
|
|
|
|
|
charts[i].PaddingBottom = -1 |
|
|
|
charts[i].PaddingBottom = -1 |
|
|
|
|
|
|
|
|
|
|
|
charts[i].Border.Label = metric |
|
|
|
charts[i].Border.Label = metric |
|
|
|
charts[i].Border.LabelFgColor = charts[i].Border.FgColor |
|
|
|
charts[i].Border.LabelFgColor = charts[i].Border.FgColor | termui.AttrBold |
|
|
|
charts[i].Border.FgColor = charts[i].Border.BgColor |
|
|
|
charts[i].Border.FgColor = charts[i].Border.BgColor |
|
|
|
|
|
|
|
|
|
|
|
row := termui.Body.Rows[i%rows] |
|
|
|
row := termui.Body.Rows[i%rows] |
|
|
|
row.Cols = append(row.Cols, termui.NewCol(12/cols, 0, charts[i])) |
|
|
|
row.Cols = append(row.Cols, termui.NewCol(12/cols, 0, charts[i])) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
termui.Body.AddRows(termui.NewRow(termui.NewCol(12, 0, footer))) |
|
|
|
termui.Body.Align() |
|
|
|
termui.Body.Align() |
|
|
|
termui.Render(termui.Body) |
|
|
|
termui.Render(termui.Body) |
|
|
|
|
|
|
|
|
|
|
|
refresh := time.Tick(time.Second) |
|
|
|
refreshCharts(xeth, monitored, data, charts, ctx, footer) |
|
|
|
|
|
|
|
termui.Render(termui.Body) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Watch for various system events, and periodically refresh the charts
|
|
|
|
|
|
|
|
refresh := time.Tick(time.Duration(ctx.Int(monitorCommandRefreshFlag.Name)) * time.Second) |
|
|
|
for { |
|
|
|
for { |
|
|
|
select { |
|
|
|
select { |
|
|
|
case event := <-termui.EventCh(): |
|
|
|
case event := <-termui.EventCh(): |
|
|
@ -133,20 +132,13 @@ func monitor(ctx *cli.Context) { |
|
|
|
if event.Type == termui.EventResize { |
|
|
|
if event.Type == termui.EventResize { |
|
|
|
termui.Body.Width = termui.TermWidth() |
|
|
|
termui.Body.Width = termui.TermWidth() |
|
|
|
for _, chart := range charts { |
|
|
|
for _, chart := range charts { |
|
|
|
chart.Height = termui.TermHeight() / rows |
|
|
|
chart.Height = (termui.TermHeight() - footer.Height) / rows |
|
|
|
} |
|
|
|
} |
|
|
|
termui.Body.Align() |
|
|
|
termui.Body.Align() |
|
|
|
termui.Render(termui.Body) |
|
|
|
termui.Render(termui.Body) |
|
|
|
} |
|
|
|
} |
|
|
|
case <-refresh: |
|
|
|
case <-refresh: |
|
|
|
metrics, err := retrieveMetrics(xeth) |
|
|
|
refreshCharts(xeth, monitored, data, charts, ctx, footer) |
|
|
|
if err != nil { |
|
|
|
|
|
|
|
utils.Fatalf("Failed to retrieve system metrics: %v", err) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
for i, metric := range monitored { |
|
|
|
|
|
|
|
data[i] = append([]float64{value(metrics, metric)}, data[i][:len(data[i])-1]...) |
|
|
|
|
|
|
|
updateChart(metric, data[i], charts[i]) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
termui.Render(termui.Body) |
|
|
|
termui.Render(termui.Body) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
@ -226,13 +218,42 @@ func expandMetrics(metrics map[string]interface{}, path string) []string { |
|
|
|
return list |
|
|
|
return list |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
|
|
|
func refreshCharts(xeth *rpc.Xeth, metrics []string, data [][]float64, charts []*termui.LineChart, ctx *cli.Context, footer *termui.Par) { |
|
|
|
|
|
|
|
values, err := retrieveMetrics(xeth) |
|
|
|
|
|
|
|
for i, metric := range metrics { |
|
|
|
|
|
|
|
data[i] = append([]float64{fetchMetric(values, metric)}, data[i][:len(data[i])-1]...) |
|
|
|
|
|
|
|
updateChart(metric, data[i], charts[i], err) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
updateFooter(ctx, err, footer) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// updateChart inserts a dataset into a line chart, scaling appropriately as to
|
|
|
|
// updateChart inserts a dataset into a line chart, scaling appropriately as to
|
|
|
|
// not display weird labels, also updating the chart label accordingly.
|
|
|
|
// not display weird labels, also updating the chart label accordingly.
|
|
|
|
func updateChart(metric string, data []float64, chart *termui.LineChart) { |
|
|
|
func updateChart(metric string, data []float64, chart *termui.LineChart, err error) { |
|
|
|
dataUnits := []string{"", "K", "M", "G", "T", "E"} |
|
|
|
dataUnits := []string{"", "K", "M", "G", "T", "E"} |
|
|
|
timeUnits := []string{"ns", "µs", "ms", "s", "ks", "ms"} |
|
|
|
timeUnits := []string{"ns", "µs", "ms", "s", "ks", "ms"} |
|
|
|
colors := []termui.Attribute{termui.ColorBlue, termui.ColorCyan, termui.ColorGreen, termui.ColorYellow, termui.ColorRed, termui.ColorRed} |
|
|
|
colors := []termui.Attribute{termui.ColorBlue, termui.ColorCyan, termui.ColorGreen, termui.ColorYellow, termui.ColorRed, termui.ColorRed} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Extract only part of the data that's actually visible
|
|
|
|
|
|
|
|
data = data[:chart.Width*2] |
|
|
|
|
|
|
|
|
|
|
|
// Find the maximum value and scale under 1K
|
|
|
|
// Find the maximum value and scale under 1K
|
|
|
|
high := data[0] |
|
|
|
high := data[0] |
|
|
|
for _, value := range data[1:] { |
|
|
|
for _, value := range data[1:] { |
|
|
@ -256,5 +277,22 @@ func updateChart(metric string, data []float64, chart *termui.LineChart) { |
|
|
|
if len(units[unit]) > 0 { |
|
|
|
if len(units[unit]) > 0 { |
|
|
|
chart.Border.Label += " [" + units[unit] + "]" |
|
|
|
chart.Border.Label += " [" + units[unit] + "]" |
|
|
|
} |
|
|
|
} |
|
|
|
chart.LineColor = colors[unit] |
|
|
|
chart.LineColor = colors[unit] | termui.AttrBold |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
chart.LineColor = termui.ColorRed | termui.AttrBold |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 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 |
|
|
|
|
|
|
|
footer.Text = fmt.Sprintf("Press q to quit. Refresh interval: %v.", refresh) |
|
|
|
|
|
|
|
footer.TextFgColor = termui.Theme().ParTextFg | termui.AttrBold |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Append any encountered errors
|
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
footer.Text = fmt.Sprintf("Error: %v.", err) |
|
|
|
|
|
|
|
footer.TextFgColor = termui.ColorRed | termui.AttrBold |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|