diff --git a/container.go b/container.go index 8144502..6cbe526 100644 --- a/container.go +++ b/container.go @@ -1,61 +1,69 @@ package main import ( - "strings" - - "github.com/bcicen/ctop/cwidgets" "github.com/bcicen/ctop/cwidgets/compact" "github.com/bcicen/ctop/metrics" ) +// Metrics and metadata representing a container type Container struct { - id string - name string - state string - metrics metrics.Metrics - widgets cwidgets.ContainerWidgets + metrics.Metrics + Id string + Name string + State string + Meta map[string]string + Updates chan [2]string + Widgets *compact.Compact + collector metrics.Collector } -func NewContainer(id, name string) *Container { - c := &Container{ - id: id, - name: name, - metrics: metrics.NewMetrics(), +func NewContainer(id, name string, collector metrics.Collector) *Container { + return &Container{ + Metrics: metrics.NewMetrics(), + Id: id, + Name: name, + Meta: make(map[string]string), + Updates: make(chan [2]string), + Widgets: compact.NewCompact(id, name), + collector: collector, } - c.widgets = compact.NewCompact(c.ShortID(), c.ShortName(), c.state) - return c } -func (c *Container) ShortID() string { - return c.id[:12] +func (c *Container) GetMeta(k string) string { + if v, ok := c.Meta[k]; ok { + return v + } + return "" } -func (c *Container) ShortName() string { - return strings.Replace(c.name, "/", "", 1) // use primary container name +func (c *Container) SetMeta(k, v string) { + c.Meta[k] = v + c.Updates <- [2]string{k, v} } func (c *Container) SetState(s string) { - c.state = s - c.widgets.SetStatus(s) -} - -// Set metrics to zero state, clear widget gauges -func (c *Container) reset() { - c.metrics = metrics.Metrics{} - c.widgets.Reset() + c.State = s + c.Widgets.Status.Set(s) + // start collector, if needed + if c.State == "running" && !c.collector.Running() { + c.collector.Start() + c.Read(c.collector.Stream()) + } + // stop collector, if needed + if c.State != "running" && c.collector.Running() { + c.collector.Stop() + } } // Read metric stream, updating widgets func (c *Container) Read(stream chan metrics.Metrics) { go func() { for metrics := range stream { - c.metrics = metrics - c.widgets.SetCPU(metrics.CPUUtil) - c.widgets.SetMem(metrics.MemUsage, metrics.MemLimit, metrics.MemPercent) - c.widgets.SetNet(metrics.NetRx, metrics.NetTx) + c.Metrics = metrics + c.Widgets.SetMetrics(metrics) } - log.Infof("reader stopped for container: %s", c.id) - c.reset() + log.Infof("reader stopped for container: %s", c.Id) + c.Widgets.Reset() }() - log.Infof("reader started for container: %s", c.id) + log.Infof("reader started for container: %s", c.Id) } diff --git a/cwidgets/compact/gauge.go b/cwidgets/compact/gauge.go new file mode 100644 index 0000000..26b3c0a --- /dev/null +++ b/cwidgets/compact/gauge.go @@ -0,0 +1,25 @@ +package compact + +import ( + ui "github.com/gizak/termui" +) + +type GaugeCol struct { + *ui.Gauge +} + +func NewGaugeCol() *GaugeCol { + g := ui.NewGauge() + g.Height = 1 + g.Border = false + g.Percent = 0 + g.PaddingBottom = 0 + g.BarColor = ui.ColorGreen + g.Label = "-" + return &GaugeCol{g} +} + +func (w *GaugeCol) Reset() { + w.Label = "-" + w.Percent = 0 +} diff --git a/cwidgets/compact/grid.go b/cwidgets/compact/grid.go index 9931861..9747028 100644 --- a/cwidgets/compact/grid.go +++ b/cwidgets/compact/grid.go @@ -1,13 +1,12 @@ package compact import ( - "github.com/bcicen/ctop/cwidgets" ui "github.com/gizak/termui" ) type CompactGrid struct { ui.GridBufferer - Rows []cwidgets.ContainerWidgets + Rows []*Compact // rows to render X, Y int Width int Height int @@ -21,31 +20,37 @@ func NewCompactGrid() *CompactGrid { } } -func (c *CompactGrid) Align() { +func (cg *CompactGrid) Align() { // Update y recursively - c.header.SetY(c.Y) - y := c.Y + 1 - for n, r := range c.Rows { + cg.header.SetY(cg.Y) + y := cg.Y + 1 + for n, r := range cg.Rows { r.SetY(y + n) } // Update width recursively - c.header.SetWidth(c.Width) - for _, r := range c.Rows { - r.SetWidth(c.Width) + cg.header.SetWidth(cg.Width) + for _, r := range cg.Rows { + r.SetWidth(cg.Width) } } -func (c *CompactGrid) Clear() { c.Rows = []cwidgets.ContainerWidgets{} } -func (c *CompactGrid) GetHeight() int { return len(c.Rows) } -func (c *CompactGrid) SetX(x int) { c.X = x } -func (c *CompactGrid) SetY(y int) { c.Y = y } -func (c *CompactGrid) SetWidth(w int) { c.Width = w } +func (cg *CompactGrid) Clear() { cg.Rows = []*Compact{} } +func (cg *CompactGrid) GetHeight() int { return len(cg.Rows) } +func (cg *CompactGrid) SetX(x int) { cg.X = x } +func (cg *CompactGrid) SetY(y int) { cg.Y = y } +func (cg *CompactGrid) SetWidth(w int) { cg.Width = w } -func (c *CompactGrid) Buffer() ui.Buffer { +func (cg *CompactGrid) Buffer() ui.Buffer { buf := ui.NewBuffer() - buf.Merge(c.header.Buffer()) - for _, r := range c.Rows { + buf.Merge(cg.header.Buffer()) + for _, r := range cg.Rows { buf.Merge(r.Buffer()) } return buf } + +func (cg *CompactGrid) AddRows(rows ...*Compact) { + for _, r := range rows { + cg.Rows = append(cg.Rows, r) + } +} diff --git a/cwidgets/compact/main.go b/cwidgets/compact/main.go index 976917b..080780e 100644 --- a/cwidgets/compact/main.go +++ b/cwidgets/compact/main.go @@ -5,50 +5,53 @@ import ( "strconv" "github.com/bcicen/ctop/cwidgets" + "github.com/bcicen/ctop/logging" + "github.com/bcicen/ctop/metrics" ui "github.com/gizak/termui" ) +var log = logging.Init() + const ( - mark = string('\u25C9') - vBar = string('\u25AE') - colSpacing = 1 - statusWidth = 3 + colSpacing = 1 ) type Compact struct { - Status *ui.Par - Name *ui.Par - Cid *ui.Par - Cpu *ui.Gauge - Memory *ui.Gauge - Net *ui.Par + Status *Status + Name *TextCol + Cid *TextCol + Cpu *GaugeCol + Memory *GaugeCol + Net *TextCol X, Y int Width int Height int } -func NewCompact(id, name, status string) *Compact { +func NewCompact(id, name string) *Compact { row := &Compact{ - Status: slimPar(mark), - Name: slimPar(name), - Cid: slimPar(id), - Cpu: slimGauge(), - Memory: slimGauge(), - Net: slimPar("-"), + Status: NewStatus(), + Name: NewTextCol(name), + Cid: NewTextCol(id), + Cpu: NewGaugeCol(), + Memory: NewGaugeCol(), + Net: NewTextCol("-"), Height: 1, } - row.Reset() - row.SetStatus(status) return row } +func (row *Compact) SetMetrics(m metrics.Metrics) { + row.SetCPU(m.CPUUtil) + row.SetNet(m.NetRx, m.NetTx) + row.SetMem(m.MemUsage, m.MemLimit, m.MemPercent) +} + // Set gauges, counters to default unread values func (row *Compact) Reset() { - row.Cpu.Percent = 0 - row.Cpu.Label = "-" - row.Memory.Percent = 0 - row.Memory.Label = "-" - row.Net.Text = "-" + row.Cpu.Reset() + row.Memory.Reset() + row.Net.Reset() } func (row *Compact) all() []ui.GridBufferer { @@ -115,24 +118,9 @@ func (row *Compact) UnHighlight() { row.Name.TextBgColor = ui.ColorDefault } -func (row *Compact) SetStatus(val string) { - switch val { - case "running": - row.Status.Text = mark - row.Status.TextFgColor = ui.ColorGreen - case "exited": - row.Status.Text = mark - row.Status.TextFgColor = ui.ColorRed - case "paused": - row.Status.Text = fmt.Sprintf("%s%s", vBar, vBar) - row.Status.TextFgColor = ui.ColorDefault - case "created": - row.Status.Text = mark - row.Status.TextFgColor = ui.ColorDefault - default: - row.Status.Text = mark - row.Status.TextFgColor = ui.ColorRed - } +func (row *Compact) SetNet(rx int64, tx int64) { + label := fmt.Sprintf("%s / %s", cwidgets.ByteFormat(rx), cwidgets.ByteFormat(tx)) + row.Net.Set(label) } func (row *Compact) SetCPU(val int) { @@ -145,10 +133,6 @@ func (row *Compact) SetCPU(val int) { row.Cpu.Percent = val } -func (row *Compact) SetNet(rx int64, tx int64) { - row.Net.Text = fmt.Sprintf("%s / %s", cwidgets.ByteFormat(rx), cwidgets.ByteFormat(tx)) -} - func (row *Compact) SetMem(val int64, limit int64, percent int) { row.Memory.Label = fmt.Sprintf("%s / %s", cwidgets.ByteFormat(val), cwidgets.ByteFormat(limit)) if percent < 5 { diff --git a/cwidgets/compact/text.go b/cwidgets/compact/text.go new file mode 100644 index 0000000..c56e4d8 --- /dev/null +++ b/cwidgets/compact/text.go @@ -0,0 +1,63 @@ +package compact + +import ( + "fmt" + + ui "github.com/gizak/termui" +) + +const ( + mark = string('\u25C9') + vBar = string('\u25AE') + statusWidth = 3 +) + +type TextCol struct { + *ui.Par +} + +func NewTextCol(s string) *TextCol { + p := ui.NewPar(s) + p.Border = false + p.Height = 1 + p.Width = 20 + return &TextCol{p} +} + +func (w *TextCol) Reset() { + w.Text = "-" +} + +func (w *TextCol) Set(s string) { + w.Text = s +} + +type Status struct { + *ui.Par +} + +func NewStatus() *Status { + p := ui.NewPar(mark) + p.Border = false + p.Height = 1 + p.Width = statusWidth + return &Status{p} +} + +func (s *Status) Set(val string) { + // defaults + text := mark + color := ui.ColorDefault + + switch val { + case "running": + color = ui.ColorGreen + case "exited": + color = ui.ColorRed + case "paused": + text = fmt.Sprintf("%s%s", vBar, vBar) + } + + s.Text = text + s.TextFgColor = color +} diff --git a/cwidgets/compact/util.go b/cwidgets/compact/util.go index 14781b1..b7f10aa 100644 --- a/cwidgets/compact/util.go +++ b/cwidgets/compact/util.go @@ -14,32 +14,14 @@ func calcWidth(width, items int) int { } func slimHeaderPar(s string) *ui.Par { - p := slimPar(s) + p := ui.NewPar(s) p.Y = 2 p.Height = 2 - return p -} - -func slimPar(s string) *ui.Par { - p := ui.NewPar(s) - p.Border = false - p.Height = 1 p.Width = 20 - p.TextFgColor = ui.ColorWhite + p.Border = false return p } -func slimGauge() *ui.Gauge { - g := ui.NewGauge() - g.Height = 1 - g.Border = false - g.Percent = 0 - g.PaddingBottom = 0 - g.BarColor = ui.ColorGreen - g.Label = "-" - return g -} - func centerParText(p *ui.Par) { var text string var padding string diff --git a/cwidgets/main.go b/cwidgets/main.go index 562c5aa..5bf9481 100644 --- a/cwidgets/main.go +++ b/cwidgets/main.go @@ -8,7 +8,6 @@ import ( var log = logging.Init() type ContainerWidgets interface { - Reset() Buffer() ui.Buffer Highlight() UnHighlight() diff --git a/dockersource.go b/dockersource.go index 9951be2..31b82b8 100644 --- a/dockersource.go +++ b/dockersource.go @@ -2,6 +2,7 @@ package main import ( "sort" + "strings" "sync" "time" @@ -20,7 +21,6 @@ type ContainerSource interface { type DockerContainerSource struct { client *docker.Client containers Containers - collectors map[string]metrics.Collector needsRefresh map[string]int // container IDs requiring refresh } @@ -32,7 +32,6 @@ func NewDockerContainerSource() *DockerContainerSource { } cm := &DockerContainerSource{ client: client, - collectors: make(map[string]metrics.Collector), needsRefresh: make(map[string]int), } cm.refreshAll() @@ -73,27 +72,16 @@ func (cm *DockerContainerSource) refresh(id string) { c, ok := cm.Get(id) // append container struct for new containers if !ok { - c = NewContainer(id, insp.Name) + // create collector + collector := metrics.NewDocker(cm.client, id) + // create container + c = NewContainer(shortID(id), shortName(insp.Name), collector) lock.Lock() cm.containers = append(cm.containers, c) lock.Unlock() - // create collector - if _, ok := cm.collectors[id]; ok == false { - cm.collectors[id] = metrics.NewDocker(cm.client, id) - } } c.SetState(insp.State.Status) - - // start collector if needed - if c.state == "running" && !cm.collectors[c.id].Running() { - cm.collectors[c.id].Start() - c.Read(cm.collectors[c.id].Stream()) - } - // stop collector if needed - if c.state != "running" && cm.collectors[c.id].Running() { - cm.collectors[c.id].Stop() - } } func (cm *DockerContainerSource) inspect(id string) *docker.Container { @@ -140,7 +128,7 @@ func (cm *DockerContainerSource) Loop() { // Get a single container, by ID func (cm *DockerContainerSource) Get(id string) (*Container, bool) { for _, c := range cm.containers { - if c.id == id { + if c.Id == id { return c, true } } @@ -150,7 +138,7 @@ func (cm *DockerContainerSource) Get(id string) (*Container, bool) { // Remove containers by ID func (cm *DockerContainerSource) delByID(id string) { for n, c := range cm.containers { - if c.id == id { + if c.Id == id { cm.del(n) return } @@ -172,3 +160,13 @@ func (cm *DockerContainerSource) All() []*Container { sort.Sort(cm.containers) return cm.containers } + +// truncate container id +func shortID(id string) string { + return id[:12] +} + +// use primary container name +func shortName(name string) string { + return strings.Replace(name, "/", "", 1) +} diff --git a/grid.go b/grid.go index 75fd2c6..ce9735b 100644 --- a/grid.go +++ b/grid.go @@ -35,15 +35,15 @@ func NewGrid() *Grid { // Set an initial cursor position, if possible func (g *Grid) cursorReset() { if len(g.containers) > 0 { - g.cursorID = g.containers[0].id - g.containers[0].widgets.Highlight() + g.cursorID = g.containers[0].Id + g.containers[0].Widgets.Highlight() } } // Return current cursor index func (g *Grid) cursorIdx() int { for n, c := range g.containers { - if c.id == g.cursorID { + if c.Id == g.cursorID { return n } } @@ -59,9 +59,9 @@ func (g *Grid) cursorUp() { active := g.containers[idx] next := g.containers[idx-1] - active.widgets.UnHighlight() - g.cursorID = next.id - next.widgets.Highlight() + active.Widgets.UnHighlight() + g.cursorID = next.Id + next.Widgets.Highlight() ui.Render(cGrid) } @@ -78,9 +78,9 @@ func (g *Grid) cursorDown() { active := g.containers[idx] next := g.containers[idx+1] - active.widgets.UnHighlight() - g.cursorID = next.id - next.widgets.Highlight() + active.Widgets.UnHighlight() + g.cursorID = next.Id + next.Widgets.Highlight() ui.Render(cGrid) } @@ -93,7 +93,6 @@ func (g *Grid) redrawRows() { if config.GetSwitchVal("enableHeader") { g.header.SetCount(len(g.containers)) g.header.SetFilter(config.GetVal("filterStr")) - g.header.Render() y += g.header.Height() } cGrid.SetY(y) @@ -104,8 +103,8 @@ func (g *Grid) redrawRows() { if n >= max { break } - cGrid.Rows = append(cGrid.Rows, c.widgets) - if c.id == g.cursorID { + cGrid.AddRows(c.Widgets) + if c.Id == g.cursorID { cursorVisible = true } } @@ -125,9 +124,9 @@ func (g *Grid) redrawRows() { // Log current container and widget state func (g *Grid) dumpContainer() { c, _ := g.cSource.Get(g.cursorID) - msg := fmt.Sprintf("logging state for container: %s\n", c.ShortID()) - msg += fmt.Sprintf("id = %s\nname = %s\nstate = %s\n", c.id, c.name, c.state) - msg += inspect(&c.metrics) + msg := fmt.Sprintf("logging state for container: %s\n", c.Id) + msg += fmt.Sprintf("Id = %s\nname = %s\nstate = %s\n", c.Id, c.Name, c.State) + msg += inspect(&c.Metrics) log.Infof(msg) } diff --git a/main.go b/main.go index fb60aec..b92efd5 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,9 @@ package main import ( + "fmt" + "os" + "github.com/bcicen/ctop/config" "github.com/bcicen/ctop/logging" ui "github.com/gizak/termui" @@ -9,6 +12,13 @@ import ( var log *logging.CTopLogger func main() { + defer func() { + if r := recover(); r != nil { + ui.Clear() + fmt.Printf("panic: %s", r) + os.Exit(1) + } + }() config.Init() log = logging.Init() if config.GetSwitchVal("loggingEnabled") { diff --git a/mocksource.go b/mocksource.go index 3628421..ed22ca5 100644 --- a/mocksource.go +++ b/mocksource.go @@ -13,13 +13,10 @@ import ( type MockContainerSource struct { containers Containers - collectors map[string]metrics.Collector } func NewMockContainerSource() *MockContainerSource { - cs := &MockContainerSource{ - collectors: make(map[string]metrics.Collector), - } + cs := &MockContainerSource{} cs.Init() go cs.Loop() return cs @@ -27,15 +24,15 @@ func NewMockContainerSource() *MockContainerSource { // Create Mock containers func (cs *MockContainerSource) Init() { - total := 10 + total := 40 rand.Seed(int64(time.Now().Nanosecond())) for i := 0; i < total; i++ { - c := NewContainer(makeID(), makeName()) + collector := metrics.NewMock() + c := NewContainer(makeID(), makeName(), collector) lock.Lock() cs.containers = append(cs.containers, c) lock.Unlock() - cs.collectors[c.id] = metrics.NewMock() c.SetState(makeState()) } @@ -45,26 +42,10 @@ func (cs *MockContainerSource) Init() { func (cs *MockContainerSource) Loop() { iter := 0 for { - for _, c := range cs.containers { - // Change state for random container - if iter%5 == 0 { - randC := cs.containers[rand.Intn(len(cs.containers))] - randC.SetState(makeState()) - } - - isCollecting := cs.collectors[c.id].Running() - //log.Infof("id=%s state=%s collector=%t", c.id, c.state, isCollecting) - - // start collector if needed - if c.state == "running" && !isCollecting { - cs.collectors[c.id].Start() - c.Read(cs.collectors[c.id].Stream()) - } - // stop collector if needed - if c.state != "running" && isCollecting { - cs.collectors[c.id].Stop() - } - + // Change state for random container + if iter%5 == 0 { + randC := cs.containers[rand.Intn(len(cs.containers))] + randC.SetState(makeState()) } iter++ time.Sleep(3 * time.Second) @@ -74,7 +55,7 @@ func (cs *MockContainerSource) Loop() { // Get a single container, by ID func (cs *MockContainerSource) Get(id string) (*Container, bool) { for _, c := range cs.containers { - if c.id == id { + if c.Id == id { return c, true } } @@ -84,7 +65,7 @@ func (cs *MockContainerSource) Get(id string) (*Container, bool) { // Remove containers by ID func (cs *MockContainerSource) delByID(id string) { for n, c := range cs.containers { - if c.id == id { + if c.Id == id { cs.del(n) return } @@ -112,7 +93,7 @@ func makeID() string { if err != nil { panic(err) } - return strings.Replace(u.String(), "-", "", -1) + return strings.Replace(u.String(), "-", "", -1)[:12] } func makeName() string { diff --git a/sort.go b/sort.go index 95dae87..2a67328 100644 --- a/sort.go +++ b/sort.go @@ -16,32 +16,32 @@ var stateMap = map[string]int{ "created": 0, } -var idSorter = func(c1, c2 *Container) bool { return c1.id < c2.id } -var nameSorter = func(c1, c2 *Container) bool { return c1.name < c2.name } +var idSorter = func(c1, c2 *Container) bool { return c1.Id < c2.Id } +var nameSorter = func(c1, c2 *Container) bool { return c1.Name < c2.Name } var Sorters = map[string]sortMethod{ "id": idSorter, "name": nameSorter, "cpu": func(c1, c2 *Container) bool { // Use secondary sort method if equal values - if c1.metrics.CPUUtil == c2.metrics.CPUUtil { + if c1.CPUUtil == c2.CPUUtil { return nameSorter(c1, c2) } - return c1.metrics.CPUUtil > c2.metrics.CPUUtil + return c1.CPUUtil > c2.CPUUtil }, "mem": func(c1, c2 *Container) bool { // Use secondary sort method if equal values - if c1.metrics.MemUsage == c2.metrics.MemUsage { + if c1.MemUsage == c2.MemUsage { return nameSorter(c1, c2) } - return c1.metrics.MemUsage > c2.metrics.MemUsage + return c1.MemUsage > c2.MemUsage }, "mem %": func(c1, c2 *Container) bool { // Use secondary sort method if equal values - if c1.metrics.MemPercent == c2.metrics.MemPercent { + if c1.MemPercent == c2.MemPercent { return nameSorter(c1, c2) } - return c1.metrics.MemPercent > c2.metrics.MemPercent + return c1.MemPercent > c2.MemPercent }, "net": func(c1, c2 *Container) bool { sum1 := sumNet(c1) @@ -54,10 +54,10 @@ var Sorters = map[string]sortMethod{ }, "state": func(c1, c2 *Container) bool { // Use secondary sort method if equal values - if c1.state == c2.state { + if c1.State == c2.State { return nameSorter(c1, c2) } - return stateMap[c1.state] > stateMap[c2.state] + return stateMap[c1.State] > stateMap[c2.State] }, } @@ -86,11 +86,11 @@ func (a Containers) Filter() (filtered []*Container) { for _, c := range a { // Apply name filter - if re.FindAllString(c.name, 1) == nil { + if re.FindAllString(c.Name, 1) == nil { continue } // Apply state filter - if !config.GetSwitchVal("allContainers") && c.state != "running" { + if !config.GetSwitchVal("allContainers") && c.State != "running" { continue } filtered = append(filtered, c) @@ -99,4 +99,4 @@ func (a Containers) Filter() (filtered []*Container) { return filtered } -func sumNet(c *Container) int64 { return c.metrics.NetRx + c.metrics.NetTx } +func sumNet(c *Container) int64 { return c.NetRx + c.NetTx }