2015-05-02 21:17:51 +03:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
2015-05-03 15:59:37 +03:00
|
|
|
"time"
|
2015-05-02 21:17:51 +03:00
|
|
|
|
2015-10-28 11:48:02 -04:00
|
|
|
"gopkg.in/gizak/termui.v1"
|
2015-05-02 21:17:51 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
// TermUISingle is a termUI implementation of UI interface.
|
|
|
|
type TermUISingle struct {
|
|
|
|
Title *termui.Par
|
|
|
|
Status *termui.Par
|
|
|
|
Sparklines map[VarName]*termui.Sparkline
|
|
|
|
Sparkline *termui.Sparklines
|
2015-05-03 18:22:26 +03:00
|
|
|
Pars []*termui.Par
|
2016-11-13 17:05:00 +01:00
|
|
|
|
|
|
|
// barcharts for GC pauses and intervals
|
|
|
|
GCChart *termui.BarChart
|
|
|
|
GCStats *termui.Par
|
|
|
|
GCIChart *termui.BarChart
|
|
|
|
GCIStats *termui.Par
|
2016-11-13 13:57:58 +01:00
|
|
|
|
|
|
|
bins int // histograms' bins count
|
2015-05-02 21:17:51 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// Init creates widgets, sets sizes and labels.
|
|
|
|
func (t *TermUISingle) Init(data UIData) error {
|
|
|
|
err := termui.Init()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
t.Sparklines = make(map[VarName]*termui.Sparkline)
|
|
|
|
|
|
|
|
termui.UseTheme("helloworld")
|
|
|
|
|
|
|
|
t.Title = func() *termui.Par {
|
|
|
|
p := termui.NewPar("")
|
|
|
|
p.Height = 3
|
|
|
|
p.TextFgColor = termui.ColorWhite
|
|
|
|
p.Border.Label = "Services Monitor"
|
|
|
|
p.Border.FgColor = termui.ColorCyan
|
|
|
|
return p
|
|
|
|
}()
|
|
|
|
t.Status = func() *termui.Par {
|
|
|
|
p := termui.NewPar("")
|
|
|
|
p.Height = 3
|
|
|
|
p.TextFgColor = termui.ColorWhite
|
|
|
|
p.Border.Label = "Status"
|
|
|
|
p.Border.FgColor = termui.ColorCyan
|
|
|
|
return p
|
|
|
|
}()
|
|
|
|
|
2015-05-03 18:22:26 +03:00
|
|
|
t.Pars = make([]*termui.Par, len(data.Vars))
|
|
|
|
for i, name := range data.Vars {
|
|
|
|
par := termui.NewPar("")
|
|
|
|
par.TextFgColor = colorByKind(name.Kind())
|
|
|
|
par.Border.Label = name.Short()
|
|
|
|
par.Border.LabelFgColor = termui.ColorGreen
|
|
|
|
par.Height = 3
|
|
|
|
t.Pars[i] = par
|
|
|
|
}
|
|
|
|
|
2015-05-02 22:29:02 +03:00
|
|
|
var sparklines []termui.Sparkline
|
2015-05-02 21:17:51 +03:00
|
|
|
for _, name := range data.Vars {
|
2015-05-02 22:29:02 +03:00
|
|
|
spl := termui.NewSparkline()
|
|
|
|
spl.Height = 1
|
|
|
|
spl.TitleColor = colorByKind(name.Kind())
|
|
|
|
spl.LineColor = colorByKind(name.Kind())
|
|
|
|
spl.Title = name.Long()
|
|
|
|
sparklines = append(sparklines, spl)
|
2015-05-02 21:17:51 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
t.Sparkline = func() *termui.Sparklines {
|
2015-05-02 22:29:02 +03:00
|
|
|
s := termui.NewSparklines(sparklines...)
|
|
|
|
s.Height = 2*len(sparklines) + 2
|
2015-05-02 21:17:51 +03:00
|
|
|
s.HasBorder = true
|
|
|
|
s.Border.Label = fmt.Sprintf("Monitoring")
|
|
|
|
return s
|
|
|
|
}()
|
|
|
|
|
2016-11-13 15:23:30 +01:00
|
|
|
if data.HasGCPauses {
|
2016-11-13 17:05:00 +01:00
|
|
|
t.GCChart = func() *termui.BarChart {
|
2016-11-13 15:23:30 +01:00
|
|
|
bc := termui.NewBarChart()
|
|
|
|
bc.Border.Label = "Bar Chart"
|
|
|
|
bc.TextColor = termui.ColorGreen
|
|
|
|
bc.BarColor = termui.ColorGreen
|
|
|
|
bc.NumColor = termui.ColorBlack
|
|
|
|
return bc
|
|
|
|
}()
|
2016-11-13 16:26:23 +01:00
|
|
|
t.GCStats = func() *termui.Par {
|
|
|
|
p := termui.NewPar("")
|
|
|
|
p.Height = 4
|
|
|
|
p.Width = len("Max: 123ms") // example
|
|
|
|
p.HasBorder = false
|
|
|
|
p.TextFgColor = termui.ColorGreen
|
|
|
|
return p
|
|
|
|
}()
|
2016-11-13 15:23:30 +01:00
|
|
|
}
|
2016-11-10 18:44:46 +01:00
|
|
|
|
2016-11-13 15:23:30 +01:00
|
|
|
if data.HasGCIntervals {
|
2016-11-13 17:05:00 +01:00
|
|
|
t.GCIChart = func() *termui.BarChart {
|
2016-11-13 15:23:30 +01:00
|
|
|
bc := termui.NewBarChart()
|
|
|
|
bc.Border.Label = "Bar Chart"
|
|
|
|
bc.TextColor = termui.ColorGreen
|
|
|
|
bc.BarColor = termui.ColorGreen
|
|
|
|
bc.NumColor = termui.ColorBlack
|
|
|
|
return bc
|
|
|
|
}()
|
2016-11-13 17:05:00 +01:00
|
|
|
t.GCIStats = func() *termui.Par {
|
|
|
|
p := termui.NewPar("")
|
|
|
|
p.Height = 4
|
|
|
|
p.Width = len("Max: 123ms (0.21/s, 1234/min)") // example
|
|
|
|
p.HasBorder = false
|
|
|
|
p.TextFgColor = termui.ColorGreen
|
|
|
|
return p
|
|
|
|
}()
|
2016-11-13 15:23:30 +01:00
|
|
|
}
|
2016-11-13 13:57:58 +01:00
|
|
|
|
2015-05-03 18:03:49 +03:00
|
|
|
t.Relayout()
|
2015-05-02 21:17:51 +03:00
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update updates UI widgets from UIData.
|
|
|
|
func (t *TermUISingle) Update(data UIData) {
|
|
|
|
// single mode assumes we have one service only to monitor
|
|
|
|
service := data.Services[0]
|
|
|
|
|
2015-05-02 22:54:00 +03:00
|
|
|
t.Title.Text = fmt.Sprintf("monitoring %s every %v, press q to quit", service.Name, *interval)
|
2015-05-03 15:59:37 +03:00
|
|
|
t.Status.Text = fmt.Sprintf("Last update: %v", data.LastTimestamp.Format(time.Stamp))
|
2015-05-02 21:17:51 +03:00
|
|
|
|
2015-05-03 18:22:26 +03:00
|
|
|
// Pars
|
|
|
|
for i, name := range data.Vars {
|
|
|
|
t.Pars[i].Text = service.Value(name)
|
|
|
|
}
|
|
|
|
|
2015-05-02 21:17:51 +03:00
|
|
|
// Sparklines
|
|
|
|
for i, name := range data.Vars {
|
2016-11-10 22:34:31 +01:00
|
|
|
v, ok := service.Vars[name].(IntVar)
|
|
|
|
if !ok {
|
|
|
|
continue
|
|
|
|
}
|
2016-11-11 00:29:06 +01:00
|
|
|
data.SparklineData[0].Stacks[name].Push(v)
|
|
|
|
data.SparklineData[0].Stats[name].Update(v)
|
2016-11-10 22:34:31 +01:00
|
|
|
|
2015-05-02 21:17:51 +03:00
|
|
|
spl := &t.Sparkline.Lines[i]
|
2015-05-02 22:29:02 +03:00
|
|
|
|
2016-11-13 21:29:14 +01:00
|
|
|
max := data.SparklineData[0].Stats[name].Max()
|
2016-11-10 22:34:31 +01:00
|
|
|
spl.Title = fmt.Sprintf("%s: %v (max: %v)", name.Long(), service.Value(name), max)
|
2015-05-02 21:17:51 +03:00
|
|
|
spl.TitleColor = colorByKind(name.Kind())
|
|
|
|
spl.LineColor = colorByKind(name.Kind())
|
2015-05-02 22:48:27 +03:00
|
|
|
|
2016-11-11 00:29:06 +01:00
|
|
|
spl.Data = data.SparklineData[0].Stacks[name].Values()
|
2015-05-02 21:17:51 +03:00
|
|
|
}
|
|
|
|
|
2016-11-10 18:44:46 +01:00
|
|
|
// BarChart
|
2016-11-13 13:57:58 +01:00
|
|
|
if data.HasGCPauses {
|
2016-11-13 12:05:26 +01:00
|
|
|
var gcpauses *GCPauses
|
|
|
|
for _, v := range service.Vars {
|
|
|
|
if v.Kind() == KindGCPauses {
|
|
|
|
gcpauses = v.(*GCPauses)
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
2016-11-13 13:57:58 +01:00
|
|
|
hist := gcpauses.Histogram(t.bins)
|
2016-11-13 12:05:26 +01:00
|
|
|
values, counts := hist.BarchartData()
|
|
|
|
vals := make([]int, 0, len(counts))
|
|
|
|
labels := make([]string, 0, len(counts))
|
|
|
|
for i := 0; i < len(counts); i++ {
|
|
|
|
vals = append(vals, int(counts[i]))
|
2016-11-13 16:26:23 +01:00
|
|
|
d := round(time.Duration(values[i]))
|
2016-11-13 12:05:26 +01:00
|
|
|
labels = append(labels, d.String())
|
|
|
|
}
|
2016-11-13 17:05:00 +01:00
|
|
|
t.GCChart.Data = vals
|
|
|
|
t.GCChart.DataLabels = labels
|
|
|
|
t.GCChart.Border.Label = "GC Pauses (last 256)"
|
2016-11-13 13:57:58 +01:00
|
|
|
|
2016-11-13 16:26:23 +01:00
|
|
|
t.GCStats.Text = fmt.Sprintf("Min: %v\nAvg: %v\n95p: %v\nMax: %v",
|
|
|
|
round(time.Duration(hist.Min())),
|
|
|
|
round(time.Duration(hist.Mean())),
|
|
|
|
round(time.Duration(hist.Quantile(0.95))),
|
|
|
|
round(time.Duration(hist.Max())),
|
|
|
|
)
|
2016-11-13 15:23:30 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if data.HasGCIntervals {
|
|
|
|
var gcintervals *GCIntervals
|
|
|
|
for _, v := range service.Vars {
|
|
|
|
if v.Kind() == KindGCIntervals {
|
|
|
|
gcintervals = v.(*GCIntervals)
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
hist := gcintervals.Histogram(t.bins)
|
|
|
|
values, counts := hist.BarchartData()
|
|
|
|
vals := make([]int, 0, len(counts))
|
|
|
|
labels := make([]string, 0, len(counts))
|
|
|
|
for i := 0; i < len(counts); i++ {
|
|
|
|
vals = append(vals, int(counts[i]))
|
2016-11-13 16:26:23 +01:00
|
|
|
d := round(time.Duration(values[i]))
|
2016-11-13 15:23:30 +01:00
|
|
|
labels = append(labels, d.String())
|
|
|
|
}
|
2016-11-13 17:05:00 +01:00
|
|
|
t.GCIChart.Data = vals
|
|
|
|
t.GCIChart.DataLabels = labels
|
|
|
|
t.GCIChart.Border.Label = "Intervals between GC (last 256)"
|
|
|
|
|
|
|
|
mean := time.Duration(hist.Mean())
|
|
|
|
p95 := time.Duration(hist.Quantile(0.95))
|
|
|
|
t.GCIStats.Text = fmt.Sprintf("Min: %v\nAvg: %v (%.2f/s, %.0f/min)\n95p: %v (%.2f/s, %.0f/min)\nMax: %v",
|
|
|
|
round(time.Duration(hist.Min())),
|
|
|
|
round(mean), rate(mean, time.Second), rate(mean, time.Minute),
|
|
|
|
round(p95), rate(p95, time.Second), rate(p95, time.Minute),
|
|
|
|
round(time.Duration(hist.Max())),
|
|
|
|
)
|
2016-11-10 18:44:46 +01:00
|
|
|
}
|
|
|
|
|
2015-05-03 18:03:49 +03:00
|
|
|
t.Relayout()
|
|
|
|
|
|
|
|
var widgets []termui.Bufferer
|
2016-11-13 12:05:26 +01:00
|
|
|
widgets = append(widgets, t.Title, t.Status, t.Sparkline)
|
2016-11-13 13:57:58 +01:00
|
|
|
if data.HasGCPauses {
|
2016-11-13 17:05:00 +01:00
|
|
|
widgets = append(widgets, t.GCChart, t.GCStats)
|
2016-11-13 12:05:26 +01:00
|
|
|
}
|
2016-11-13 15:23:30 +01:00
|
|
|
if data.HasGCIntervals {
|
2016-11-13 17:05:00 +01:00
|
|
|
widgets = append(widgets, t.GCIChart, t.GCIStats)
|
2016-11-13 13:57:58 +01:00
|
|
|
}
|
2015-05-03 18:22:26 +03:00
|
|
|
for _, par := range t.Pars {
|
|
|
|
widgets = append(widgets, par)
|
|
|
|
}
|
2015-05-03 18:03:49 +03:00
|
|
|
termui.Render(widgets...)
|
2015-05-02 21:17:51 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// Close shuts down UI module.
|
|
|
|
func (t *TermUISingle) Close() {
|
|
|
|
termui.Close()
|
|
|
|
}
|
2015-05-02 22:39:39 +03:00
|
|
|
|
2015-05-03 18:03:49 +03:00
|
|
|
// Relayout recalculates widgets sizes and coords.
|
|
|
|
func (t *TermUISingle) Relayout() {
|
|
|
|
tw, th := termui.TermWidth(), termui.TermHeight()
|
|
|
|
h := th
|
|
|
|
|
|
|
|
// First row: Title and Status pars
|
|
|
|
firstRowH := 3
|
|
|
|
t.Title.Height = firstRowH
|
|
|
|
t.Title.Width = tw / 2
|
|
|
|
if tw%2 == 1 {
|
2015-05-03 21:59:45 +03:00
|
|
|
t.Title.Width++
|
2015-05-03 18:03:49 +03:00
|
|
|
}
|
|
|
|
t.Status.Height = firstRowH
|
|
|
|
t.Status.Width = tw / 2
|
|
|
|
t.Status.X = t.Title.X + t.Title.Width
|
|
|
|
h -= firstRowH
|
|
|
|
|
2015-05-03 18:22:26 +03:00
|
|
|
// Second row: lists
|
|
|
|
secondRowH := 3
|
|
|
|
num := len(t.Pars)
|
|
|
|
parW := tw / num
|
|
|
|
for i, par := range t.Pars {
|
|
|
|
par.Y = th - h
|
|
|
|
par.Width = parW
|
|
|
|
par.Height = secondRowH
|
|
|
|
par.X = i * parW
|
|
|
|
}
|
|
|
|
if num*parW < tw {
|
|
|
|
t.Pars[num-1].Width = tw - ((num - 1) * parW)
|
|
|
|
}
|
|
|
|
h -= secondRowH
|
|
|
|
|
|
|
|
// Third row: Sparklines
|
2016-11-13 12:05:26 +01:00
|
|
|
calcHeight := len(t.Sparkline.Lines) * 2
|
|
|
|
if calcHeight > (h / 2) {
|
|
|
|
calcHeight = h / 2
|
|
|
|
}
|
|
|
|
|
2015-05-03 18:03:49 +03:00
|
|
|
t.Sparkline.Width = tw
|
2016-11-13 12:05:26 +01:00
|
|
|
t.Sparkline.Height = calcHeight
|
2015-05-03 18:03:49 +03:00
|
|
|
t.Sparkline.Y = th - h
|
2016-11-10 18:44:46 +01:00
|
|
|
|
2016-11-13 15:23:30 +01:00
|
|
|
// Fourth row: Barcharts
|
|
|
|
var barchartWidth, charts int
|
2016-11-13 17:05:00 +01:00
|
|
|
if t.GCChart != nil {
|
2016-11-13 15:23:30 +01:00
|
|
|
charts++
|
|
|
|
}
|
2016-11-13 17:05:00 +01:00
|
|
|
if t.GCIChart != nil {
|
2016-11-13 15:23:30 +01:00
|
|
|
charts++
|
|
|
|
}
|
|
|
|
|
|
|
|
if charts > 0 {
|
|
|
|
barchartWidth = tw / charts
|
|
|
|
bins, binWidth := recalcBins(barchartWidth)
|
|
|
|
t.bins = bins
|
2016-11-13 13:57:58 +01:00
|
|
|
|
2016-11-13 17:05:00 +01:00
|
|
|
if t.GCChart != nil {
|
|
|
|
t.GCChart.Width = barchartWidth
|
|
|
|
t.GCChart.Height = h - calcHeight
|
|
|
|
t.GCChart.Y = th - t.GCChart.Height
|
|
|
|
t.GCChart.BarWidth = binWidth
|
2016-11-13 16:26:23 +01:00
|
|
|
|
|
|
|
t.GCStats.X = barchartWidth - t.GCStats.Width - 1
|
2016-11-13 17:05:00 +01:00
|
|
|
t.GCStats.Y = t.GCChart.Y + 1
|
2016-11-13 15:23:30 +01:00
|
|
|
}
|
2016-11-13 13:57:58 +01:00
|
|
|
|
2016-11-13 17:05:00 +01:00
|
|
|
if t.GCIChart != nil {
|
|
|
|
t.GCIChart.Width = barchartWidth
|
|
|
|
t.GCIChart.X = 0 + barchartWidth
|
|
|
|
t.GCIChart.Height = h - calcHeight
|
|
|
|
t.GCIChart.Y = th - t.GCChart.Height
|
|
|
|
t.GCIChart.BarWidth = binWidth
|
|
|
|
|
|
|
|
t.GCIStats.X = t.GCIChart.X + 1
|
|
|
|
t.GCIStats.Y = t.GCIChart.Y + 1
|
2016-11-13 15:23:30 +01:00
|
|
|
}
|
|
|
|
}
|
2015-05-03 18:03:49 +03:00
|
|
|
}
|
|
|
|
|
2016-11-13 13:57:58 +01:00
|
|
|
// recalcBins attempts to select optimal value for the number
|
|
|
|
// of bins for histograms.
|
|
|
|
//
|
|
|
|
// Optimal range is 10-30, but we must try to keep bins' width
|
|
|
|
// no less then 5 to fit the labels ("123ms"). Hence some heuristics.
|
|
|
|
//
|
|
|
|
// Should be called on resize or creation.
|
|
|
|
func recalcBins(tw int) (int, int) {
|
|
|
|
var (
|
|
|
|
bins, w int
|
|
|
|
minWidth = 5
|
|
|
|
minBins = 10
|
|
|
|
maxBins = 30
|
|
|
|
)
|
|
|
|
w = minWidth
|
|
|
|
|
|
|
|
tryWidth := func(w int) int {
|
|
|
|
return tw / w
|
2015-05-02 22:39:39 +03:00
|
|
|
}
|
2016-11-13 13:57:58 +01:00
|
|
|
|
|
|
|
bins = tryWidth(w)
|
|
|
|
for bins > maxBins {
|
|
|
|
w++
|
|
|
|
bins = tryWidth(w)
|
|
|
|
}
|
|
|
|
|
|
|
|
for bins < minBins && w > minWidth {
|
|
|
|
w--
|
|
|
|
bins = tryWidth(w)
|
|
|
|
}
|
|
|
|
|
|
|
|
return bins, w
|
2015-05-02 22:39:39 +03:00
|
|
|
}
|