Compare commits

...

43 Commits

Author SHA1 Message Date
Bradley Cicenas
5db90f31dc v0.5.1 2017-03-21 10:35:24 +10:00
Bradley Cicenas
82677d52ef add build section to docs 2017-03-20 08:39:57 +10:00
Bradley Cicenas
2b2338805b update circleci to build image from source 2017-03-19 16:14:56 +10:00
Bradley Cicenas
60213f1551 add debug section to docs 2017-03-19 15:10:03 +10:00
Bradley Cicenas
8aa932b29f Toggle debug mode via env var
remove logging param from global config, allowing logging server and
level to be configured inside logging subpackage from CTOP_DEBUG env var
2017-03-19 15:10:03 +10:00
Bradley Cicenas
35cc8d095d include Makefile instructions for building image from source 2017-03-19 15:10:03 +10:00
bradley
30530bc2a1 Merge pull request #52 from InTheCloudDan/patch-1
change glide to github repo, url is expired.
2017-03-19 10:50:33 +10:00
Dan O'Brien
2c282923c0 change glide to github repo, url is expired. 2017-03-18 20:39:53 -04:00
bradley
d0d39749de Merge pull request #47 from thomasleveil/patch-1
README: optimize install instructions
2017-03-18 10:47:19 +10:00
Bradley Cicenas
26b88a9790 add Makefile 2017-03-18 10:38:03 +10:00
bradley
a135a67c06 Merge pull request #49 from firecat53/patch-2
Add minimal Docker image build instructions
2017-03-17 16:57:29 +10:00
Scott Hansen
19b212f45d Add minimal Docker image build instructions
Update README to include instructions for building from source a minimal Docker image with only ctop.
2017-03-16 11:53:45 -07:00
Thomas LÉVEIL
34987df010 README: optimize install instructions 2017-03-15 20:22:08 +01:00
bradley
e2bc4d0a08 fix newline 2017-03-15 22:54:23 +10:00
bradley
4ac1348fbb Merge pull request #43 from mieciu/patch-1
Update README.md
2017-03-15 20:21:38 +10:00
mieciu
66d78a7d74 Update README.md 2017-03-15 11:14:00 +01:00
Bradley Cicenas
e62a8881a2 add brew install steps 2017-03-15 19:45:41 +10:00
Bradley Cicenas
a5b2e7b074 update aur link 2017-03-15 12:49:42 +10:00
Bradley Cicenas
a87bdce0fe update keybindings in readme 2017-03-15 12:00:40 +10:00
Bradley Cicenas
2228188ebf v0.5 2017-03-15 10:08:26 +10:00
Bradley Cicenas
e94a9c0cc2 remove redundant bool comparisons 2017-03-15 10:06:52 +10:00
Bradley Cicenas
e82d77ecb0 add option for color inversion 2017-03-15 10:02:46 +10:00
Bradley Cicenas
50b4181866 update circle config 2017-03-15 09:16:04 +10:00
Bradley Cicenas
1285288b9e add validation to sort field option 2017-03-15 08:49:11 +10:00
Bradley Cicenas
2a709577bd add options to readme 2017-03-15 08:41:45 +10:00
Bradley Cicenas
38599bbd19 add keymap, handle wrapper for common keybindings 2017-03-15 08:34:58 +10:00
Bradley Cicenas
b3cdb33efc add explicit version to Dockerfile, circleci 2017-03-15 08:34:58 +10:00
bradley
0ac70c96eb Merge pull request #37 from drAlberT/patch-1
Improve suggested "docker run" cmd
2017-03-13 20:36:42 +11:00
Emiliano 'AlberT' Gabrielli
36a5bbdfe1 Improve suggested "docker run" cmd
- make it use a given name "ctop"
- make it feasible for an `alias`
2017-03-13 09:31:15 +01:00
Bradley Cicenas
3553b0af9d cap cpu gauge % to 100 in compact view 2017-03-13 09:00:17 +11:00
Bradley Cicenas
ca61ec712e prepopulate filter input with current filter, add esc handler 2017-03-13 08:32:33 +11:00
Bradley Cicenas
06c4b24212 add 0..9 to valid input chars 2017-03-13 08:31:51 +11:00
Bradley Cicenas
12fa716825 add y pos scrolling to expanded view 2017-03-13 07:53:17 +11:00
Bradley Cicenas
8327406069 add sort by pid count 2017-03-12 21:11:19 +11:00
Bradley Cicenas
2134110224 add static width for specific columns 2017-03-12 20:58:56 +11:00
Bradley Cicenas
77c3d00e67 update io labels 2017-03-12 16:31:12 +11:00
Bradley Cicenas
85eb5228ae append build steps 2017-03-12 15:00:52 +11:00
bradley
3a3950e395 Merge pull request #36 from kevinschoon/vendoring
vendor dependencies with glide
2017-03-12 14:49:13 +11:00
Kevin Schoon
eaac079b15 vendor dependencies with glide 2017-03-12 10:15:21 +07:00
bradley
ab1ccb3cd8 Merge pull request #35 from f1yegor/feature/more-stats
add pids, IO stat
2017-03-12 14:07:34 +11:00
f1yegor
dbaebe0192 add pids, IO stat 2017-03-12 02:35:40 +01:00
bradley
d5ef818c8d Merge pull request #31 from scriptnull/master
adds more commandline arguments
2017-03-11 15:01:33 +11:00
Vishnu Bharathi
8203d0b883 adds more commandline arguments 2017-03-11 00:09:06 +05:30
32 changed files with 565 additions and 121 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
ctop
.idea

View File

@@ -1,7 +1,3 @@
FROM quay.io/vektorcloud/glibc:latest FROM scratch
COPY ./ctop /ctop
RUN ctop_url=$(wget -q -O - https://api.github.com/repos/bcicen/ctop/releases/latest | grep 'browser_' | cut -d\" -f4 |grep 'linux-amd64') && \
wget -q $ctop_url -O /ctop && \
chmod +x /ctop
ENTRYPOINT ["/ctop"] ENTRYPOINT ["/ctop"]

12
Dockerfile_build Normal file
View File

@@ -0,0 +1,12 @@
FROM quay.io/vektorcloud/go:1.8
RUN apk add --no-cache make
COPY glide.* /go/src/github.com/bcicen/ctop/
WORKDIR /go/src/github.com/bcicen/ctop/
RUN glide install
COPY . /go/src/github.com/bcicen/ctop
RUN make build && \
mkdir -p /go/bin && \
mv -v ctop /go/bin/

34
Makefile Normal file
View File

@@ -0,0 +1,34 @@
NAME=ctop
VERSION=$(shell cat VERSION)
BUILD=$(shell git rev-parse --short HEAD)
LD_FLAGS="-w -X main.version=$(VERSION) -X main.build=$(BUILD)"
clean:
rm -rf build/ release/
build:
glide install
CGO_ENABLED=0 go build -tags release -ldflags $(LD_FLAGS) -o ctop
build-dev:
go build -ldflags "-w -X main.version=$(VERSION)-dev -X main.build=$(BUILD)"
build-all:
mkdir -p build
GOOS=darwin GOARCH=amd64 go build -tags release -ldflags $(LD_FLAGS) -o build/ctop-$(VERSION)-darwin-amd64
GOOS=linux GOARCH=amd64 go build -tags release -ldflags $(LD_FLAGS) -o build/ctop-$(VERSION)-linux-amd64
GOOS=linux GOARCH=arm go build -tags release -ldflags $(LD_FLAGS) -o build/ctop-$(VERSION)-linux-arm
image:
docker build -t ctop_build -f Dockerfile_build .
docker run -ti --rm -v $(shell pwd):/target ctop_build cp -v /go/bin/ctop /target/
docker build -t ctop -f Dockerfile .
release:
mkdir release
go get github.com/progrium/gh-release/...
cp build/* release
gh-release create bcicen/$(NAME) $(VERSION) \
$(shell git rev-parse --abbrev-ref HEAD) $(VERSION)
.PHONY: build

View File

@@ -1,4 +1,5 @@
<p align="center"><img width="200px" src="/_docs/img/logo.png" alt="ctop"/></p> <p align="center"><img width="200px" src="/_docs/img/logo.png" alt="ctop"/></p>
# #
Top-like interface for container metrics Top-like interface for container metrics
@@ -17,25 +18,31 @@ Fetch the [latest release](https://github.com/bcicen/ctop/releases) for your pla
#### Linux #### Linux
```bash ```bash
wget https://github.com/bcicen/ctop/releases/download/v0.4.1/ctop-0.4.1-linux-amd64 -O ctop sudo wget https://github.com/bcicen/ctop/releases/download/v0.5/ctop-0.5-linux-amd64 -O /usr/local/bin/ctop
sudo mv ctop /usr/local/bin/
sudo chmod +x /usr/local/bin/ctop sudo chmod +x /usr/local/bin/ctop
``` ```
#### OS X #### OS X
```bash ```bash
curl -Lo ctop https://github.com/bcicen/ctop/releases/download/v0.4.1/ctop-0.4.1-darwin-amd64 brew install ctop
sudo mv ctop /usr/local/bin/ ```
or
```bash
sudo curl -Lo /usr/local/bin/ctop https://github.com/bcicen/ctop/releases/download/v0.5/ctop-0.5-darwin-amd64
sudo chmod +x /usr/local/bin/ctop sudo chmod +x /usr/local/bin/ctop
``` ```
or run via Docker: or run via Docker:
```bash ```bash
docker run -ti -v /var/run/docker.sock:/var/run/docker.sock quay.io/vektorlab/ctop:latest docker run -ti --name ctop --rm -v /var/run/docker.sock:/var/run/docker.sock quay.io/vektorlab/ctop:latest
``` ```
`ctop` is also available for Arch in the [AUR](https://aur.archlinux.org/packages/ctop/) `ctop` is also available for Arch in the [AUR](https://aur.archlinux.org/packages/ctop-bin/)
## Building
Build steps can be found [here][build].
## Usage ## Usage
@@ -45,16 +52,29 @@ export DOCKER_HOST=tcp://127.0.0.1:4243
ctop ctop
``` ```
### Options
Option | Description
--- | ---
-a | show active containers only
-f <string> | set an initial filter string
-h | display help dialog
-i | invert default colors
-r | reverse container sort order
-s | select initial container sort field
-v | output version information and exit
### Keybindings ### Keybindings
Key | Action Key | Action
--- | --- --- | ---
a | Toggle display of all (running and non-running) containers a | Toggle display of all (running and non-running) containers
f | Filter displayed containers f | Filter displayed containers (`esc` to clear when open)
H | Toggle ctop header H | Toggle ctop header
h | Open help dialog h | Open help dialog
s | Select container sort field s | Select container sort field
r | Reverse container sort order r | Reverse container sort order
q | Quit ctop q | Quit ctop
[build]: _docs/build.md
[expanded_view]: _docs/expanded.md [expanded_view]: _docs/expanded.md

View File

@@ -1 +1 @@
0.4.1 0.5.1

20
_docs/build.md Normal file
View File

@@ -0,0 +1,20 @@
# Build
To build `ctop` from source, ensure you have a recent version of [glide](https://github.com/Masterminds/glide) installed and run:
```bash
go get github.com/bcicen/ctop && \
cd $GOPATH/src/github.com/bcicen/ctop && \
make build
```
To build a minimal Docker image containing only `ctop`:
```bash
make image
```
Now you can run your local image:
```bash
docker run -ti --name ctop --rm -v /var/run/docker.sock:/var/run/docker.sock ctop
```

24
_docs/debug.md Normal file
View File

@@ -0,0 +1,24 @@
# Debug Mode
`ctop` comes with a built-in logging facility and local socket server to simplify debugging at run time. Debug mode can be enabled via the `CTOP_DEBUG` environment variable:
```bash
CTOP_DEBUG=1 ./ctop
```
While `ctop` is running, you can connect to the logging socket via socat or similar tools:
```bash
socat unix-connect:/tmp/ctop.sock stdio
```
example output:
```
15:06:43.881 ▶ NOTI 002 logger initialized
15:06:43.881 ▶ INFO 003 loaded config param: "filterStr": ""
15:06:43.881 ▶ INFO 004 loaded config param: "sortField": "state"
15:06:43.881 ▶ INFO 005 loaded config switch: "sortReversed": false
15:06:43.881 ▶ INFO 006 loaded config switch: "allContainers": true
15:06:43.881 ▶ INFO 007 loaded config switch: "enableHeader": true
15:06:43.883 ▶ INFO 008 collector started for container: 7120f83ca...
...
```

View File

@@ -1,24 +1,23 @@
machine: machine:
services: services:
- docker - docker
environment:
IMAGE_NAME: quay.io/vektorlab/ctop
dependencies: dependencies:
override: override:
- docker info - docker info
- | - make image
if [[ "$CIRCLE_BRANCH" == "master" ]]; then
docker build -t quay.io/vektorlab/ctop:latest .
else
docker build -t quay.io/vektorlab/ctop:${CIRCLE_BRANCH} .
fi
test: test:
override: override:
- docker run -t --entrypoint /bin/sh quay.io/vektorlab/ctop:latest -v - docker run -ti ctop -v
deployment: deployment:
hub: hub:
branch: master branch: master
commands: commands:
- docker tag ctop ${IMAGE_NAME}:latest
- docker tag ctop ${IMAGE_NAME}:$(cat VERSION)
- docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS quay.io - docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS quay.io
- docker push quay.io/vektorlab/ctop:latest - docker push ${IMAGE_NAME}

View File

@@ -1,6 +1,10 @@
package main package main
import ui "github.com/gizak/termui" import (
"regexp"
ui "github.com/gizak/termui"
)
/* /*
Valid colors: Valid colors:
@@ -38,6 +42,17 @@ var ColorMap = map[string]ui.Attribute{
"mbarchart.text.fg": ui.ColorWhite, "mbarchart.text.fg": ui.ColorWhite,
"par.text.fg": ui.ColorWhite, "par.text.fg": ui.ColorWhite,
"par.text.bg": ui.ColorDefault, "par.text.bg": ui.ColorDefault,
"par.text.hi": ui.ColorBlack,
"sparkline.line.fg": ui.ColorGreen, "sparkline.line.fg": ui.ColorGreen,
"sparkline.title.fg": ui.ColorWhite, "sparkline.title.fg": ui.ColorWhite,
} }
func InvertColorMap() {
re := regexp.MustCompile(".*.fg")
for k, _ := range ColorMap {
if re.FindAllString(k, 1) != nil {
ColorMap[k] = ui.ColorBlack
}
}
ColorMap["par.text.hi"] = ui.ColorWhite
}

View File

@@ -18,7 +18,6 @@ func Init() {
GlobalParams = append(GlobalParams, p) GlobalParams = append(GlobalParams, p)
log.Infof("loaded config param: %s: %s", quote(p.Key), quote(p.Val)) log.Infof("loaded config param: %s: %s", quote(p.Key), quote(p.Val))
} }
for _, s := range switches { for _, s := range switches {
GlobalSwitches = append(GlobalSwitches, s) GlobalSwitches = append(GlobalSwitches, s)
log.Infof("loaded config switch: %s: %t", quote(s.Key), s.Val) log.Infof("loaded config switch: %s: %t", quote(s.Key), s.Val)

View File

@@ -17,11 +17,6 @@ var switches = []*Switch{
Val: true, Val: true,
Label: "Enable Status Header", Label: "Enable Status Header",
}, },
&Switch{
Key: "loggingEnabled",
Val: false,
Label: "Enable Logging Server",
},
} }
type Switch struct { type Switch struct {

View File

@@ -4,7 +4,7 @@ import (
ui "github.com/gizak/termui" ui "github.com/gizak/termui"
) )
var header = NewCompactHeader() var header *CompactHeader
type CompactGrid struct { type CompactGrid struct {
ui.GridBufferer ui.GridBufferer
@@ -16,6 +16,7 @@ type CompactGrid struct {
} }
func NewCompactGrid() *CompactGrid { func NewCompactGrid() *CompactGrid {
header = NewCompactHeader() // init column header
return &CompactGrid{} return &CompactGrid{}
} }

View File

@@ -12,7 +12,7 @@ type CompactHeader struct {
} }
func NewCompactHeader() *CompactHeader { func NewCompactHeader() *CompactHeader {
fields := []string{"", "NAME", "CID", "CPU", "MEM", "NET RX/TX"} fields := []string{"", "NAME", "CID", "CPU", "MEM", "NET RX/TX", "IO R/W", "PIDS"}
ch := &CompactHeader{} ch := &CompactHeader{}
ch.Height = 2 ch.Height = 2
for _, f := range fields { for _, f := range fields {
@@ -27,13 +27,13 @@ func (ch *CompactHeader) GetHeight() int {
func (ch *CompactHeader) SetWidth(w int) { func (ch *CompactHeader) SetWidth(w int) {
x := ch.X x := ch.X
autoWidth := calcWidth(w, 5) autoWidth := calcWidth(w)
for n, col := range ch.pars { for n, col := range ch.pars {
// set status column to static width // set column to static width
if n == 0 { if colWidths[n] != 0 {
col.SetX(x) col.SetX(x)
col.SetWidth(statusWidth) col.SetWidth(colWidths[n])
x += statusWidth x += colWidths[n]
continue continue
} }
col.SetX(x) col.SetX(x)

View File

@@ -15,6 +15,8 @@ type Compact struct {
Cpu *GaugeCol Cpu *GaugeCol
Memory *GaugeCol Memory *GaugeCol
Net *TextCol Net *TextCol
IO *TextCol
Pids *TextCol
X, Y int X, Y int
Width int Width int
Height int Height int
@@ -32,6 +34,8 @@ func NewCompact(id string) *Compact {
Cpu: NewGaugeCol(), Cpu: NewGaugeCol(),
Memory: NewGaugeCol(), Memory: NewGaugeCol(),
Net: NewTextCol("-"), Net: NewTextCol("-"),
IO: NewTextCol("-"),
Pids: NewTextCol("-"),
X: 1, X: 1,
Height: 1, Height: 1,
} }
@@ -59,6 +63,8 @@ func (row *Compact) SetMetrics(m metrics.Metrics) {
row.SetCPU(m.CPUUtil) row.SetCPU(m.CPUUtil)
row.SetNet(m.NetRx, m.NetTx) row.SetNet(m.NetRx, m.NetTx)
row.SetMem(m.MemUsage, m.MemLimit, m.MemPercent) row.SetMem(m.MemUsage, m.MemLimit, m.MemPercent)
row.SetIO(m.IOBytesRead, m.IOBytesWrite)
row.SetPids(m.Pids)
} }
// Set gauges, counters to default unread values // Set gauges, counters to default unread values
@@ -66,6 +72,8 @@ func (row *Compact) Reset() {
row.Cpu.Reset() row.Cpu.Reset()
row.Memory.Reset() row.Memory.Reset()
row.Net.Reset() row.Net.Reset()
row.IO.Reset()
row.Pids.Reset()
} }
func (row *Compact) GetHeight() int { func (row *Compact) GetHeight() int {
@@ -91,13 +99,12 @@ func (row *Compact) SetWidth(width int) {
return return
} }
x := row.X x := row.X
autoWidth := calcWidth(width, 5) autoWidth := calcWidth(width)
for n, col := range row.all() { for n, col := range row.all() {
// set status column to static width if colWidths[n] != 0 {
if n == 0 {
col.SetX(x) col.SetX(x)
col.SetWidth(statusWidth) col.SetWidth(colWidths[n])
x += statusWidth x += colWidths[n]
continue continue
} }
col.SetX(x) col.SetX(x)
@@ -116,7 +123,8 @@ func (row *Compact) Buffer() ui.Buffer {
buf.Merge(row.Cpu.Buffer()) buf.Merge(row.Cpu.Buffer())
buf.Merge(row.Memory.Buffer()) buf.Merge(row.Memory.Buffer())
buf.Merge(row.Net.Buffer()) buf.Merge(row.Net.Buffer())
buf.Merge(row.IO.Buffer())
buf.Merge(row.Pids.Buffer())
return buf return buf
} }
@@ -128,5 +136,7 @@ func (row *Compact) all() []ui.GridBufferer {
row.Cpu, row.Cpu,
row.Memory, row.Memory,
row.Net, row.Net,
row.IO,
row.Pids,
} }
} }

View File

@@ -13,6 +13,16 @@ func (row *Compact) SetNet(rx int64, tx int64) {
row.Net.Set(label) row.Net.Set(label)
} }
func (row *Compact) SetIO(read int64, write int64) {
label := fmt.Sprintf("%s / %s", cwidgets.ByteFormat(read), cwidgets.ByteFormat(write))
row.IO.Set(label)
}
func (row *Compact) SetPids(val int) {
label := fmt.Sprintf("%s", strconv.Itoa(val))
row.Pids.Set(label)
}
func (row *Compact) SetCPU(val int) { func (row *Compact) SetCPU(val int) {
row.Cpu.BarColor = colorScale(val) row.Cpu.BarColor = colorScale(val)
row.Cpu.Label = fmt.Sprintf("%s%%", strconv.Itoa(val)) row.Cpu.Label = fmt.Sprintf("%s%%", strconv.Itoa(val))
@@ -20,6 +30,9 @@ func (row *Compact) SetCPU(val int) {
val = 5 val = 5
row.Cpu.BarColor = ui.ThemeAttr("gauge.bar.bg") row.Cpu.BarColor = ui.ThemeAttr("gauge.bar.bg")
} }
if val > 100 {
val = 100
}
row.Cpu.Percent = val row.Cpu.Percent = val
} }

View File

@@ -17,7 +17,7 @@ func NewTextCol(s string) *TextCol {
} }
func (w *TextCol) Highlight() { func (w *TextCol) Highlight() {
w.TextFgColor = ui.ColorBlack w.TextFgColor = ui.ThemeAttr("par.text.hi")
w.TextBgColor = ui.ThemeAttr("par.text.fg") w.TextBgColor = ui.ThemeAttr("par.text.fg")
} }

View File

@@ -9,10 +9,29 @@ import (
const colSpacing = 1 const colSpacing = 1
// Calculate per-column width, given total width and number of items // per-column width. 0 == auto width
func calcWidth(width, items int) int { var colWidths = []int{
spacing := colSpacing * items 3, // status
return (width - statusWidth - spacing) / items 0, // name
0, // cid
0, // cpu
0, // memory
0, // net
0, // io
4, // pids
}
// Calculate per-column width, given total width
func calcWidth(width int) int {
spacing := colSpacing * len(colWidths)
var staticCols int
for _, w := range colWidths {
width -= w
if w == 0 {
staticCols += 1
}
}
return (width - spacing) / staticCols
} }
func centerParText(p *ui.Par) { func centerParText(p *ui.Par) {

51
cwidgets/expanded/io.go Normal file
View File

@@ -0,0 +1,51 @@
package expanded
import (
"fmt"
"strings"
"github.com/bcicen/ctop/cwidgets"
ui "github.com/gizak/termui"
)
type IO struct {
*ui.Sparklines
readHist *DiffHist
writeHist *DiffHist
}
func NewIO() *IO {
io := &IO{ui.NewSparklines(), NewDiffHist(60), NewDiffHist(60)}
io.BorderLabel = "IO"
io.Height = 6
io.Width = colWidth[0]
io.X = 0
io.Y = 24
read := ui.NewSparkline()
read.Title = "READ"
read.Height = 1
read.Data = io.readHist.Data
read.LineColor = ui.ColorGreen
write := ui.NewSparkline()
write.Title = "WRITE"
write.Height = 1
write.Data = io.writeHist.Data
write.LineColor = ui.ColorYellow
io.Lines = []ui.Sparkline{read, write}
return io
}
func (w *IO) Update(read int64, write int64) {
var rate string
w.readHist.Append(int(read))
rate = strings.ToLower(cwidgets.ByteFormatInt(w.readHist.Val))
w.Lines[0].Title = fmt.Sprintf("read [%s/s]", rate)
w.writeHist.Append(int(write))
rate = strings.ToLower(cwidgets.ByteFormatInt(w.writeHist.Val))
w.Lines[1].Title = fmt.Sprintf("write [%s/s]", rate)
}

View File

@@ -17,6 +17,8 @@ type Expanded struct {
Net *Net Net *Net
Cpu *Cpu Cpu *Cpu
Mem *Mem Mem *Mem
IO *IO
X, Y int
Width int Width int
} }
@@ -29,30 +31,59 @@ func NewExpanded(id string) *Expanded {
Net: NewNet(), Net: NewNet(),
Cpu: NewCpu(), Cpu: NewCpu(),
Mem: NewMem(), Mem: NewMem(),
IO: NewIO(),
Width: ui.TermWidth(), Width: ui.TermWidth(),
} }
} }
func (e *Expanded) SetWidth(w int) { func (e *Expanded) Up() {
e.Width = w if e.Y < 0 {
e.Y++
e.Align()
ui.Render(e)
}
} }
func (e *Expanded) SetMeta(k, v string) { func (e *Expanded) Down() {
e.Info.Set(k, v) if e.Y > (ui.TermHeight() - e.GetHeight()) {
e.Y--
e.Align()
ui.Render(e)
}
} }
func (e *Expanded) SetWidth(w int) { e.Width = w }
func (e *Expanded) SetMeta(k, v string) { e.Info.Set(k, v) }
func (e *Expanded) SetMetrics(m metrics.Metrics) { func (e *Expanded) SetMetrics(m metrics.Metrics) {
e.Cpu.Update(m.CPUUtil) e.Cpu.Update(m.CPUUtil)
e.Net.Update(m.NetRx, m.NetTx) e.Net.Update(m.NetRx, m.NetTx)
e.Mem.Update(int(m.MemUsage), int(m.MemLimit)) e.Mem.Update(int(m.MemUsage), int(m.MemLimit))
e.IO.Update(m.IOBytesRead, m.IOBytesWrite)
}
// Return total column height
func (e *Expanded) GetHeight() (h int) {
h += e.Info.Height
h += e.Net.Height
h += e.Cpu.Height
h += e.Mem.Height
h += e.IO.Height
return h
} }
func (e *Expanded) Align() { func (e *Expanded) Align() {
y := 0 // reset offset if needed
if e.GetHeight() <= ui.TermHeight() {
e.Y = 0
}
y := e.Y
for _, i := range e.all() { for _, i := range e.all() {
i.SetY(y) i.SetY(y)
y += i.GetHeight() y += i.GetHeight()
} }
if e.Width > colWidth[0] { if e.Width > colWidth[0] {
colWidth[1] = e.Width - (colWidth[0] + 1) colWidth[1] = e.Width - (colWidth[0] + 1)
} }
@@ -74,6 +105,7 @@ func (e *Expanded) Buffer() ui.Buffer {
buf.Merge(e.Cpu.Buffer()) buf.Merge(e.Cpu.Buffer())
buf.Merge(e.Mem.Buffer()) buf.Merge(e.Mem.Buffer())
buf.Merge(e.Net.Buffer()) buf.Merge(e.Net.Buffer())
buf.Merge(e.IO.Buffer())
return buf return buf
} }
@@ -83,6 +115,7 @@ func (e *Expanded) all() []ui.GridBufferer {
e.Cpu, e.Cpu,
e.Mem, e.Mem,
e.Net, e.Net,
e.IO,
} }
} }

85
glide.lock generated Normal file
View File

@@ -0,0 +1,85 @@
hash: c13011881e895378f374b68596a59a0ea6def372b4f5239b2d8aa342eaa46a4b
updated: 2017-03-12T09:53:35.212073637+07:00
imports:
- name: github.com/Azure/go-ansiterm
version: fa152c58bc15761d0200cb75fe958b89a9d4888e
subpackages:
- winterm
- name: github.com/docker/docker
version: ce07fb6b0f1b8765b92022e45f96bd4349812e06
subpackages:
- api/types
- api/types/blkiodev
- api/types/container
- api/types/filters
- api/types/mount
- api/types/network
- api/types/registry
- api/types/strslice
- api/types/swarm
- api/types/versions
- opts
- pkg/archive
- pkg/fileutils
- pkg/homedir
- pkg/idtools
- pkg/ioutils
- pkg/jsonlog
- pkg/jsonmessage
- pkg/longpath
- pkg/pools
- pkg/promise
- pkg/stdcopy
- pkg/system
- pkg/term
- pkg/term/windows
- name: github.com/docker/go-connections
version: a2afab9802043837035592f1c24827fb70766de9
subpackages:
- nat
- name: github.com/docker/go-units
version: 0dadbb0345b35ec7ef35e228dabb8de89a65bf52
- name: github.com/fsouza/go-dockerclient
version: 318513eb1ab27495afbc67f671ba1080513d8aa0
- name: github.com/gizak/termui
version: ea10e6ccee219e572ffad0ac1909f1a17f6db7d6
repo: https://github.com/bcicen/termui
vcs: git
- name: github.com/hashicorp/go-cleanhttp
version: 3573b8b52aa7b37b9358d966a898feb387f62437
- name: github.com/jgautheron/codename-generator
version: 16d037c7cc3c9b552fe4af9828b7338d752dbaf9
- name: github.com/maruel/panicparse
version: 25bcac0d793cf4109483505a0d66e066a3a90a80
subpackages:
- stack
- name: github.com/mattn/go-runewidth
version: 14207d285c6c197daabb5c9793d63e7af9ab2d50
- name: github.com/Microsoft/go-winio
version: fff283ad5116362ca252298cfc9b95828956d85d
- name: github.com/mitchellh/go-wordwrap
version: ad45545899c7b13c020ea92b2072220eefad42b8
- name: github.com/nsf/termbox-go
version: 91bae1bb5fa9ee504905ecbe7043fa30e92feaa3
- name: github.com/nu7hatch/gouuid
version: 179d4d0c4d8d407a32af483c2354df1d2c91e6c3
- name: github.com/op/go-logging
version: b2cb9fa56473e98db8caba80237377e83fe44db5
- name: github.com/opencontainers/runc
version: 31980a53ae7887b2c8f8715d13c3eb486c27b6cf
subpackages:
- libcontainer/system
- libcontainer/user
- name: github.com/Sirupsen/logrus
version: 1deb2db2a6fff8a35532079061b903c3a25eed52
- name: golang.org/x/net
version: a6577fac2d73be281a500b310739095313165611
subpackages:
- context
- context/ctxhttp
- name: golang.org/x/sys
version: 99f16d856c9836c42d24e7ab64ea72916925fa97
subpackages:
- unix
- windows
testImports: []

11
glide.yaml Normal file
View File

@@ -0,0 +1,11 @@
package: github.com/bcicen/ctop
import:
- package: github.com/fsouza/go-dockerclient
- package: github.com/gizak/termui
version: barchart-numfmt
repo: https://github.com/bcicen/termui
vcs: git
- package: github.com/jgautheron/codename-generator
- package: github.com/nu7hatch/gouuid
- package: github.com/op/go-logging
version: ^1.0.0

32
grid.go
View File

@@ -44,19 +44,19 @@ func ExpandView(c *Container) {
ex.Align() ex.Align()
ui.Render(ex) ui.Render(ex)
ui.Handle("/timer/1s", func(ui.Event) {
ui.Render(ex) HandleKeys("up", ex.Up)
}) HandleKeys("down", ex.Down)
ui.Handle("/sys/kbd/", func(ui.Event) { ui.StopLoop() })
ui.Handle("/timer/1s", func(ui.Event) { ui.Render(ex) })
ui.Handle("/sys/wnd/resize", func(e ui.Event) { ui.Handle("/sys/wnd/resize", func(e ui.Event) {
ex.SetWidth(ui.TermWidth()) ex.SetWidth(ui.TermWidth())
ex.Align() ex.Align()
log.Infof("resize: width=%v max-rows=%v", ex.Width, cGrid.MaxRows()) log.Infof("resize: width=%v max-rows=%v", ex.Width, cGrid.MaxRows())
}) })
ui.Handle("/sys/kbd/", func(ui.Event) {
ui.StopLoop()
})
ui.Loop()
ui.Loop()
c.SetUpdater(c.Widgets) c.SetUpdater(c.Widgets)
} }
@@ -77,16 +77,18 @@ func Display() bool {
cursor.RefreshContainers() cursor.RefreshContainers()
RedrawRows(true) RedrawRows(true)
ui.Handle("/sys/kbd/<up>", func(ui.Event) { cursor.Up() }) HandleKeys("up", cursor.Up)
ui.Handle("/sys/kbd/<down>", func(ui.Event) { cursor.Down() }) HandleKeys("down", cursor.Down)
HandleKeys("exit", ui.StopLoop)
HandleKeys("help", func() {
menu = HelpMenu
ui.StopLoop()
})
ui.Handle("/sys/kbd/<enter>", func(ui.Event) { ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
expand = true expand = true
ui.StopLoop() ui.StopLoop()
}) })
ui.Handle("/sys/kbd/q", func(ui.Event) { ui.StopLoop() })
ui.Handle("/sys/kbd/C-c", func(ui.Event) { ui.StopLoop() })
ui.Handle("/sys/kbd/a", func(ui.Event) { ui.Handle("/sys/kbd/a", func(ui.Event) {
config.Toggle("allContainers") config.Toggle("allContainers")
RefreshDisplay() RefreshDisplay()
@@ -98,10 +100,6 @@ func Display() bool {
menu = FilterMenu menu = FilterMenu
ui.StopLoop() ui.StopLoop()
}) })
ui.Handle("/sys/kbd/h", func(ui.Event) {
menu = HelpMenu
ui.StopLoop()
})
ui.Handle("/sys/kbd/H", func(ui.Event) { ui.Handle("/sys/kbd/H", func(ui.Event) {
config.Toggle("enableHeader") config.Toggle("enableHeader")
RedrawRows(true) RedrawRows(true)

32
keys.go Normal file
View File

@@ -0,0 +1,32 @@
package main
import (
ui "github.com/gizak/termui"
)
// Common action keybindings
var keyMap = map[string][]string{
"up": []string{
"/sys/kbd/<up>",
"/sys/kbd/k",
},
"down": []string{
"/sys/kbd/<down>",
"/sys/kbd/j",
},
"exit": []string{
"/sys/kbd/q",
"/sys/kbd/C-c",
},
"help": []string{
"/sys/kbd/h",
"/sys/kbd/?",
},
}
// Apply a common handler function to all given keys
func HandleKeys(i string, f func()) {
for _, k := range keyMap[i] {
ui.Handle(k, func(ui.Event) { f() })
}
}

View File

@@ -1,6 +1,7 @@
package logging package logging
import ( import (
"os"
"time" "time"
"github.com/op/go-logging" "github.com/op/go-logging"
@@ -13,7 +14,7 @@ const (
var ( var (
Log *CTopLogger Log *CTopLogger
exited bool exited bool
level = logging.INFO level = logging.INFO // default level
format = logging.MustStringFormatter( format = logging.MustStringFormatter(
`%{color}%{time:15:04:05.000} ▶ %{level:.4s} %{id:03x}%{color:reset} %{message}`, `%{color}%{time:15:04:05.000} ▶ %{level:.4s} %{id:03x}%{color:reset} %{message}`,
) )
@@ -33,6 +34,11 @@ func Init() *CTopLogger {
logging.NewMemoryBackend(size), logging.NewMemoryBackend(size),
} }
if debugMode() {
level = logging.DEBUG
StartServer()
}
backendLvl := logging.AddModuleLevel(Log.backend) backendLvl := logging.AddModuleLevel(Log.backend)
backendLvl.SetLevel(level, "") backendLvl.SetLevel(level, "")
@@ -71,3 +77,5 @@ func (log *CTopLogger) Exit() {
exited = true exited = true
StopServer() StopServer()
} }
func debugMode() bool { return os.Getenv("CTOP_DEBUG") == "1" }

80
main.go
View File

@@ -1,6 +1,7 @@
package main package main
import ( import (
"flag"
"fmt" "fmt"
"os" "os"
@@ -22,25 +23,62 @@ var (
) )
func main() { func main() {
readArgs()
defer panicExit() defer panicExit()
// parse command line arguments
var versionFlag = flag.Bool("v", false, "output version information and exit")
var helpFlag = flag.Bool("h", false, "display this help dialog")
var filterFlag = flag.String("f", "", "filter containers")
var activeOnlyFlag = flag.Bool("a", false, "show active containers only")
var sortFieldFlag = flag.String("s", "", "select container sort field")
var reverseSortFlag = flag.Bool("r", false, "reverse container sort order")
var invertFlag = flag.Bool("i", false, "invert default colors")
flag.Parse()
if *versionFlag {
printVersion()
os.Exit(0)
}
if *helpFlag {
printHelp()
os.Exit(0)
}
// init logger
log = logging.Init()
// init global config
config.Init()
// override default config values with command line flags
if *filterFlag != "" {
config.Update("filterStr", *filterFlag)
}
if *activeOnlyFlag {
config.Toggle("allContainers")
}
if *sortFieldFlag != "" {
validSort(*sortFieldFlag)
config.Update("sortField", *sortFieldFlag)
}
if *reverseSortFlag {
config.Toggle("sortReversed")
}
// init ui // init ui
if *invertFlag {
InvertColorMap()
}
ui.ColorMap = ColorMap // override default colormap ui.ColorMap = ColorMap // override default colormap
if err := ui.Init(); err != nil { if err := ui.Init(); err != nil {
panic(err) panic(err)
} }
defer ui.Close() defer ui.Close()
// init global config
config.Init()
// init logger
log = logging.Init()
if config.GetSwitchVal("loggingEnabled") {
logging.StartServer()
}
// init grid, cursor, header // init grid, cursor, header
cursor = NewGridCursor() cursor = NewGridCursor()
cGrid = compact.NewCompactGrid() cGrid = compact.NewCompactGrid()
@@ -56,23 +94,12 @@ func main() {
} }
} }
func readArgs() { // ensure a given sort field is valid
if len(os.Args) < 2 { func validSort(s string) {
return if _, ok := Sorters[s]; !ok {
} fmt.Printf("invalid sort field: %s\n", s)
for _, arg := range os.Args[1:] {
switch arg {
case "-v", "version":
printVersion()
os.Exit(0)
case "-h", "help":
printHelp()
os.Exit(0)
default:
fmt.Printf("invalid option or argument: \"%s\"\n", arg)
os.Exit(1) os.Exit(1)
} }
}
} }
func panicExit() { func panicExit() {
@@ -88,12 +115,11 @@ var helpMsg = `ctop - container metric viewer
usage: ctop [options] usage: ctop [options]
options: options:
-h display this help dialog
-v output version information and exit
` `
func printHelp() { func printHelp() {
fmt.Println(helpMsg) fmt.Println(helpMsg)
flag.PrintDefaults()
} }
func printVersion() { func printVersion() {

View File

@@ -39,6 +39,7 @@ func FilterMenu() {
i := widgets.NewInput() i := widgets.NewInput()
i.BorderLabel = "Filter" i.BorderLabel = "Filter"
i.SetY(ui.TermHeight() - i.Height) i.SetY(ui.TermHeight() - i.Height)
i.Data = config.GetVal("filterStr")
ui.Render(i) ui.Render(i)
// refresh container rows on input // refresh container rows on input
@@ -52,6 +53,10 @@ func FilterMenu() {
}() }()
i.InputHandlers() i.InputHandlers()
ui.Handle("/sys/kbd/<escape>", func(ui.Event) {
config.Update("filterStr", "")
ui.StopLoop()
})
ui.Handle("/sys/kbd/<enter>", func(ui.Event) { ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
config.Update("filterStr", i.Data) config.Update("filterStr", i.Data)
ui.StopLoop() ui.StopLoop()
@@ -76,11 +81,15 @@ func SortMenu() {
// set cursor position to current sort field // set cursor position to current sort field
m.SetCursor(config.GetVal("sortField")) m.SetCursor(config.GetVal("sortField"))
ui.Render(m) HandleKeys("up", m.Up)
m.NavigationHandlers() HandleKeys("down", m.Down)
HandleKeys("exit", ui.StopLoop)
ui.Handle("/sys/kbd/<enter>", func(ui.Event) { ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
config.Update("sortField", m.SelectedItem().Val) config.Update("sortField", m.SelectedItem().Val)
ui.StopLoop() ui.StopLoop()
}) })
ui.Render(m)
ui.Loop() ui.Loop()
} }

View File

@@ -46,6 +46,7 @@ func (c *Docker) Start() {
c.ReadCPU(s) c.ReadCPU(s)
c.ReadMem(s) c.ReadMem(s)
c.ReadNet(s) c.ReadNet(s)
c.ReadIO(s)
c.stream <- c.Metrics c.stream <- c.Metrics
} }
log.Infof("collector stopped for container: %s", c.id) log.Infof("collector stopped for container: %s", c.id)
@@ -79,6 +80,7 @@ func (c *Docker) ReadCPU(stats *api.Stats) {
c.CPUUtil = round((cpudiff / syscpudiff * 100) * ncpus) c.CPUUtil = round((cpudiff / syscpudiff * 100) * ncpus)
c.lastCpu = total c.lastCpu = total
c.lastSysCpu = system c.lastSysCpu = system
c.Pids = int(stats.PidsStats.Current)
} }
func (c *Docker) ReadMem(stats *api.Stats) { func (c *Docker) ReadMem(stats *api.Stats) {
@@ -95,3 +97,16 @@ func (c *Docker) ReadNet(stats *api.Stats) {
} }
c.NetRx, c.NetTx = rx, tx c.NetRx, c.NetTx = rx, tx
} }
func (c *Docker) ReadIO(stats *api.Stats) {
var read, write int64
for _, blk := range stats.BlkioStats.IOServiceBytesRecursive {
if blk.Op == "Read" {
read = int64(blk.Value)
}
if blk.Op == "Write" {
write = int64(blk.Value)
}
}
c.IOBytesRead, c.IOBytesWrite = read, write
}

View File

@@ -15,6 +15,9 @@ type Metrics struct {
MemLimit int64 MemLimit int64
MemPercent int MemPercent int
MemUsage int64 MemUsage int64
IOBytesRead int64
IOBytesWrite int64
Pids int
} }
func NewMetrics() Metrics { func NewMetrics() Metrics {
@@ -24,6 +27,9 @@ func NewMetrics() Metrics {
NetRx: -1, NetRx: -1,
MemUsage: -1, MemUsage: -1,
MemPercent: -1, MemPercent: -1,
IOBytesRead: -1,
IOBytesWrite: -1,
Pids: -1,
} }
} }

18
sort.go
View File

@@ -53,6 +53,22 @@ var Sorters = map[string]sortMethod{
} }
return sum1 > sum2 return sum1 > sum2
}, },
"pids": func(c1, c2 *Container) bool {
// Use secondary sort method if equal values
if c1.Pids == c2.Pids {
return nameSorter(c1, c2)
}
return c1.Pids > c2.Pids
},
"io": func(c1, c2 *Container) bool {
sum1 := sumIO(c1)
sum2 := sumIO(c2)
// Use secondary sort method if equal values
if sum1 == sum2 {
return nameSorter(c1, c2)
}
return sum1 > sum2
},
"state": func(c1, c2 *Container) bool { "state": func(c1, c2 *Container) bool {
// Use secondary sort method if equal values // Use secondary sort method if equal values
c1state := c1.GetMeta("state") c1state := c1.GetMeta("state")
@@ -101,3 +117,5 @@ func (a Containers) Filter() {
} }
func sumNet(c *Container) int64 { return c.NetRx + c.NetTx } func sumNet(c *Container) int64 { return c.NetRx + c.NetTx }
func sumIO(c *Container) int64 { return c.IOBytesRead + c.IOBytesWrite }

View File

@@ -7,7 +7,7 @@ import (
) )
var ( var (
input_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_." input_chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_."
) )
type Padding [2]int // x,y padding type Padding [2]int // x,y padding

View File

@@ -100,27 +100,20 @@ func (m *Menu) Buffer() ui.Buffer {
return buf return buf
} }
func (m *Menu) Up(ui.Event) { func (m *Menu) Up() {
if m.cursorPos > 0 { if m.cursorPos > 0 {
m.cursorPos-- m.cursorPos--
ui.Render(m) ui.Render(m)
} }
} }
func (m *Menu) Down(ui.Event) { func (m *Menu) Down() {
if m.cursorPos < (len(m.items) - 1) { if m.cursorPos < (len(m.items) - 1) {
m.cursorPos++ m.cursorPos++
ui.Render(m) ui.Render(m)
} }
} }
// Setup some default handlers for menu navigation
func (m *Menu) NavigationHandlers() {
ui.Handle("/sys/kbd/<up>", m.Up)
ui.Handle("/sys/kbd/<down>", m.Down)
ui.Handle("/sys/kbd/q", func(ui.Event) { ui.StopLoop() })
}
// Set width and height based on menu items // Set width and height based on menu items
func (m *Menu) calcSize() { func (m *Menu) calcSize() {
m.Width = 7 // minimum width m.Width = 7 // minimum width