refactor widgets, add wrapper structs

This commit is contained in:
Bradley Cicenas 2017-03-03 07:57:26 +00:00
parent 9f5cd42b73
commit 56be64367b
12 changed files with 249 additions and 195 deletions

View File

@ -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)
}

25
cwidgets/compact/gauge.go Normal file
View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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 {

63
cwidgets/compact/text.go Normal file
View File

@ -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
}

View File

@ -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

View File

@ -8,7 +8,6 @@ import (
var log = logging.Init()
type ContainerWidgets interface {
Reset()
Buffer() ui.Buffer
Highlight()
UnHighlight()

View File

@ -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)
}

29
grid.go
View File

@ -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)
}

10
main.go
View File

@ -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") {

View File

@ -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 {

26
sort.go
View File

@ -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 }