Compare commits

...

55 Commits

Author SHA1 Message Date
Bradley Cicenas
617b1b2863 omit runc connector from darwin build 2017-06-14 10:11:40 -03:00
bradley
e59a360b60 Create connectors.md 2017-06-14 09:32:48 -03:00
bradley
91cd53a878 change option header sizing 2017-06-14 09:32:26 -03:00
Bradley Cicenas
c6f2c7b617 add badges back 2017-06-14 09:30:03 -03:00
Bradley Cicenas
568bfb2513 v0.6 doc updates 2017-06-14 09:17:44 -03:00
Bradley Cicenas
66ff8ad7ec update potentially conflicting variable name 2017-06-13 17:35:00 -03:00
Bradley Cicenas
400d9471b6 add pidcount,io to mock collector 2017-06-13 17:25:58 -03:00
Bradley Cicenas
288380ca8d remove arbitrary branch deploy commands 2017-06-12 11:25:02 -03:00
Bradley Cicenas
429d5b9101 v0.6.0 2017-06-12 11:16:03 -03:00
Bradley Cicenas
1be452d7c0 refactor collectors into subpackage 2017-06-12 11:12:03 -03:00
Bradley Cicenas
671c944272 disable timed display refresh while scrolling 2017-06-12 10:52:45 -03:00
Bradley Cicenas
a48a9031cc move container sort to struct method 2017-06-12 10:40:52 -03:00
Bradley Cicenas
aff6943d07 add runc connector doc 2017-06-10 12:25:18 -03:00
Bradley Cicenas
f56ff96b88 Revert "remove build-time container in make image"
This reverts commit b49e174483.
2017-06-10 12:15:42 -03:00
Bradley Cicenas
b5361c2a28 panic on missing runc root 2017-06-10 11:44:12 -03:00
Bradley Cicenas
e71b6cacce prevent unlock until container sort complete 2017-06-10 11:09:21 -03:00
Bradley Cicenas
8a1297d3c5 add created meta field to expanded view 2017-06-10 10:46:54 -03:00
Bradley Cicenas
bdea7d5853 remove containers from connector map on destroyed state 2017-06-10 10:46:16 -03:00
Bradley Cicenas
389dee0f3c add percent helper method to metrics 2017-06-10 10:00:54 -03:00
Bradley Cicenas
b49e174483 remove build-time container in make image 2017-06-10 09:37:36 -03:00
Bradley Cicenas
53b612ab07 add additional logging messages 2017-06-10 09:36:34 -03:00
Bradley Cicenas
446708e456 add default runc root path 2017-06-09 18:35:28 -03:00
Bradley Cicenas
1233ff0ead add arbitrary branch image pushes to circle ci 2017-06-09 18:18:48 -03:00
Bradley Cicenas
af3f1e2a85 update glide deps 2017-06-09 18:11:59 -03:00
Bradley Cicenas
4dbc5653ff update build ldflags to permit multiple runc dep versions 2017-06-09 15:18:11 -03:00
Bradley Cicenas
e8d9f3327c runc connector optimizations 2017-06-09 14:56:39 -03:00
Bradley Cicenas
d372043a17 add --connector switch, validation 2017-06-09 14:35:29 -03:00
Bradley Cicenas
eeac65da8c add sys proc methods to metrics 2017-06-09 14:15:12 -03:00
Bradley Cicenas
c1780ae30a add byte formatting for tb 2017-06-09 14:14:27 -03:00
Bradley Cicenas
fb39d69fa7 add runc metric collector 2017-06-09 13:07:25 -03:00
Bradley Cicenas
6392d63ff8 prevent panic messages from being hidden due to ui.Init() race condition 2017-06-08 17:19:34 -03:00
Bradley Cicenas
b009a260a4 initial runc connector implementation 2017-06-08 15:33:34 -03:00
Bradley Cicenas
44379cd9fd rename connectors 2017-06-08 12:01:08 -03:00
Bradley Cicenas
b85ca680f0 restructure container,connectors in subpackage 2017-06-08 11:51:02 -03:00
Bradley Cicenas
8fb7a8988f include <escape> in exit keygroup 2017-06-08 09:47:30 -03:00
Bradley Cicenas
6d097c2085 add quickstart section to debug doc 2017-06-08 09:33:07 -03:00
Bradley Cicenas
f9d68d688d add tcp logging section to debug doc 2017-05-31 10:55:31 -04:00
Bradley Cicenas
bc08b85191 add option to log debug messages to unix or tcp socket 2017-05-31 10:45:48 -04:00
Bradley Cicenas
f3d26e038d use current directory as default logging socket path 2017-05-31 10:21:32 -04:00
Bradley Cicenas
b4e1fbf290 add release, homebrew badges to README 2017-05-28 17:03:19 -04:00
Bradley Cicenas
58d5fba945 update port formatting for multi-line display 2017-05-15 11:54:35 +01:00
Bradley Cicenas
c76036a6f2 allow multi-line metadata in expanded view 2017-05-15 11:37:27 +01:00
bradley
6a8848d1e2 Merge pull request #65 from kenan-rhoton/ports
Add Ports information to the expanded view
2017-05-15 11:35:20 +01:00
Kenan Rhoton
02d1050130 Revert "Added Ports information to the expanded view"
This reverts commit b2165b6a29.
2017-05-15 06:53:15 +02:00
Kenan Rhoton
ccb44c964c Moved port Info to be fetched from the standard inspect call to avoid superfluous calls. Also moved the information to the info section instead of a whole new section in the expanded view 2017-05-15 06:52:38 +02:00
Kenan Rhoton
b2165b6a29 Added Ports information to the expanded view 2017-05-11 07:38:12 +02:00
bradley
d81d10ec27 Merge pull request #69 from vielmetti/patch-1
WORK IN PROGRESS Add arm64 build
2017-04-10 10:15:51 +08:00
Edward Vielmetti
9529c04680 Update Makefile
Foolish typo; several lines changed, to make future additions more obvious.
2017-04-07 17:43:18 -04:00
Edward Vielmetti
6a89c9af38 WORK IN PROGRESS Add arm64 build
This change would close #68 and would add an arm64 (ARMv8) build.
2017-04-07 02:10:37 -04:00
Bradley Cicenas
06a29fc912 fix circleci build
circleci apparently does not allow containers/volumes to be removed
2017-04-06 20:58:54 +08:00
Bradley Cicenas
2cba7253fc use create container in makefile 2017-04-03 20:51:05 +08:00
Bradley Cicenas
47d60fe51b gofmt main package 2017-03-28 13:57:30 +10:00
bradley
28f16c9a17 Merge pull request #62 from yashpatel5400/yash
Added page up/down features
2017-03-27 10:09:44 +10:00
yashpatel5400
6560768e08 Added page up/down features 2017-03-23 22:31:08 -04:00
Bradley Cicenas
084c0c4ec8 add global Shutdown() method for exit cleanup 2017-03-23 16:48:25 +10:00
31 changed files with 876 additions and 151 deletions

View File

@@ -1,7 +1,8 @@
NAME=ctop NAME=ctop
VERSION=$(shell cat VERSION) VERSION=$(shell cat VERSION)
BUILD=$(shell git rev-parse --short HEAD) BUILD=$(shell git rev-parse --short HEAD)
LD_FLAGS="-w -X main.version=$(VERSION) -X main.build=$(BUILD)" EXT_LD_FLAGS="-Wl,--allow-multiple-definition"
LD_FLAGS="-w -X main.version=$(VERSION) -X main.build=$(BUILD) -extldflags=$(EXT_LD_FLAGS)"
clean: clean:
rm -rf build/ release/ rm -rf build/ release/
@@ -11,17 +12,19 @@ build:
CGO_ENABLED=0 go build -tags release -ldflags $(LD_FLAGS) -o ctop CGO_ENABLED=0 go build -tags release -ldflags $(LD_FLAGS) -o ctop
build-dev: build-dev:
go build -ldflags "-w -X main.version=$(VERSION)-dev -X main.build=$(BUILD)" go build -ldflags "-w -X main.version=$(VERSION)-dev -X main.build=$(BUILD) -extldflags=$(EXT_LD_FLAGS)"
build-all: build-all:
mkdir -p build mkdir -p build
GOOS=darwin GOARCH=amd64 go build -tags release -ldflags $(LD_FLAGS) -o build/ctop-$(VERSION)-darwin-amd64 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=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 GOOS=linux GOARCH=arm go build -tags release -ldflags $(LD_FLAGS) -o build/ctop-$(VERSION)-linux-arm
GOOS=linux GOARCH=arm64 go build -tags release -ldflags $(LD_FLAGS) -o build/ctop-$(VERSION)-linux-arm64
image: image:
docker build -t ctop_build -f Dockerfile_build . 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 create --name=ctop_built ctop_build ctop -v
docker cp ctop_built:/go/bin/ctop .
docker build -t ctop -f Dockerfile . docker build -t ctop -f Dockerfile .
release: release:

View File

@@ -2,6 +2,8 @@
# #
![release][release] ![homebrew][homebrew]
Top-like interface for container metrics Top-like interface for container metrics
`ctop` provides a concise and condensed overview of real-time metrics for multiple containers: `ctop` provides a concise and condensed overview of real-time metrics for multiple containers:
@@ -9,7 +11,7 @@ Top-like interface for container metrics
as well as an [expanded view][expanded_view] for inspecting a specific container. as well as an [expanded view][expanded_view] for inspecting a specific container.
`ctop` currently comes with built-in support for Docker; connectors for other container and cluster systems are planned for future releases. `ctop` comes with built-in support for Docker and runC; connectors for other container and cluster systems are planned for future releases.
## Install ## Install
@@ -18,7 +20,7 @@ Fetch the [latest release](https://github.com/bcicen/ctop/releases) for your pla
#### Linux #### Linux
```bash ```bash
sudo wget https://github.com/bcicen/ctop/releases/download/v0.5/ctop-0.5-linux-amd64 -O /usr/local/bin/ctop sudo wget https://github.com/bcicen/ctop/releases/download/v0.6/ctop-0.6-linux-amd64 -O /usr/local/bin/ctop
sudo chmod +x /usr/local/bin/ctop sudo chmod +x /usr/local/bin/ctop
``` ```
@@ -29,13 +31,17 @@ brew install ctop
``` ```
or or
```bash ```bash
sudo curl -Lo /usr/local/bin/ctop https://github.com/bcicen/ctop/releases/download/v0.5/ctop-0.5-darwin-amd64 sudo curl -Lo /usr/local/bin/ctop https://github.com/bcicen/ctop/releases/download/v0.6/ctop-0.6-darwin-amd64
sudo chmod +x /usr/local/bin/ctop sudo chmod +x /usr/local/bin/ctop
``` ```
or run via Docker: #### Docker
```bash ```bash
docker run -ti --name ctop --rm -v /var/run/docker.sock:/var/run/docker.sock quay.io/vektorlab/ctop:latest docker run --rm -ti \
--name=ctop \
-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-bin/) `ctop` is also available for Arch in the [AUR](https://aur.archlinux.org/packages/ctop-bin/)
@@ -46,11 +52,7 @@ Build steps can be found [here][build].
## Usage ## Usage
`ctop` requires no arguments and will configure itself using the `DOCKER_HOST` environment variable `ctop` requires no arguments and uses Docker host variables by default. See [connectors][connectors] for further configuration options.
```bash
export DOCKER_HOST=tcp://127.0.0.1:4243
ctop
```
### Options ### Options
@@ -77,4 +79,7 @@ r | Reverse container sort order
q | Quit ctop q | Quit ctop
[build]: _docs/build.md [build]: _docs/build.md
[connectors]: _docs/connectors.md
[expanded_view]: _docs/expanded.md [expanded_view]: _docs/expanded.md
[release]: https://img.shields.io/github/release/bcicen/ctop.svg "ctop"
[homebrew]: https://img.shields.io/homebrew/v/ctop.svg "ctop"

View File

@@ -1 +1 @@
0.5.1 0.6.0

26
_docs/connectors.md Normal file
View File

@@ -0,0 +1,26 @@
# connectors
`ctop` comes with the below native connectors, enabled via the `--connector` option.
Default connector behavior can be changed by setting the relevant environment variables.
## Docker
Default connector, configurable via standard [Docker commandline varaibles](https://docs.docker.com/engine/reference/commandline/cli/#environment-variables)
#### Options
Var | Description
--- | ---
DOCKER_HOST | Daemon socket to connect to (default: `unix://var/run/docker.sock`)
## RunC
Using this connector requires full privileges to the local runC root dir (default: `/run/runc`)
#### Options
Var | Description
--- | ---
RUNC_ROOT | path to runc root (default: `/run/runc`)
RUNC_SYSTEMD_CGROUP | if set, enable systemd cgroups

View File

@@ -1,14 +1,23 @@
# Debug Mode # 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: `ctop` comes with a built-in logging facility and local socket server to simplify debugging at run time.
## Quick Start
If running `ctop` via Docker, debug logging can be most easily enabled as below:
```bash ```bash
CTOP_DEBUG=1 ./ctop docker run -ti --rm \
--name=ctop \
-e CTOP_DEBUG=1 \
-e CTOP_DEBUG_TCP=1 \
-p 9000:9000 \
-v /var/run/docker.sock:/var/run/docker.sock \
quay.io/vektorlab/ctop:latest
``` ```
While `ctop` is running, you can connect to the logging socket via socat or similar tools: Log messages can be followed by connecting to the default listen address:
```bash ```bash
socat unix-connect:/tmp/ctop.sock stdio curl -s localhost:9000
``` ```
example output: example output:
@@ -22,3 +31,26 @@ example output:
15:06:43.883 ▶ INFO 008 collector started for container: 7120f83ca... 15:06:43.883 ▶ INFO 008 collector started for container: 7120f83ca...
... ...
``` ```
## Unix Socket
Debug mode is 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:./ctop.sock stdio
```
## TCP Logging Socket
In lieu of using a local unix socket, TCP logging can be enabled via the `CTOP_DEBUG_TCP` environment variable:
```bash
CTOP_DEBUG=1 CTOP_DEBUG_TCP=1 ./ctop
```
A TCP listener for streaming log messages will be started on the default listen address(`0.0.0.0:9000`)

View File

@@ -1,16 +1,17 @@
package metrics package collector
import ( import (
"github.com/bcicen/ctop/metrics"
api "github.com/fsouza/go-dockerclient" api "github.com/fsouza/go-dockerclient"
) )
// Docker collector // Docker collector
type Docker struct { type Docker struct {
Metrics metrics.Metrics
id string id string
client *api.Client client *api.Client
running bool running bool
stream chan Metrics stream chan metrics.Metrics
done chan bool done chan bool
lastCpu float64 lastCpu float64
lastSysCpu float64 lastSysCpu float64
@@ -18,7 +19,7 @@ type Docker struct {
func NewDocker(client *api.Client, id string) *Docker { func NewDocker(client *api.Client, id string) *Docker {
return &Docker{ return &Docker{
Metrics: Metrics{}, Metrics: metrics.Metrics{},
id: id, id: id,
client: client, client: client,
} }
@@ -26,7 +27,7 @@ func NewDocker(client *api.Client, id string) *Docker {
func (c *Docker) Start() { func (c *Docker) Start() {
c.done = make(chan bool) c.done = make(chan bool)
c.stream = make(chan Metrics) c.stream = make(chan metrics.Metrics)
stats := make(chan *api.Stats) stats := make(chan *api.Stats)
go func() { go func() {
@@ -60,7 +61,7 @@ func (c *Docker) Running() bool {
return c.running return c.running
} }
func (c *Docker) Stream() chan Metrics { func (c *Docker) Stream() chan metrics.Metrics {
return c.stream return c.stream
} }
@@ -86,7 +87,7 @@ func (c *Docker) ReadCPU(stats *api.Stats) {
func (c *Docker) ReadMem(stats *api.Stats) { func (c *Docker) ReadMem(stats *api.Stats) {
c.MemUsage = int64(stats.MemoryStats.Usage) c.MemUsage = int64(stats.MemoryStats.Usage)
c.MemLimit = int64(stats.MemoryStats.Limit) c.MemLimit = int64(stats.MemoryStats.Limit)
c.MemPercent = round((float64(c.MemUsage) / float64(c.MemLimit)) * 100) c.MemPercent = percent(float64(c.MemUsage), float64(c.MemLimit))
} }
func (c *Docker) ReadNet(stats *api.Stats) { func (c *Docker) ReadNet(stats *api.Stats) {

View File

@@ -0,0 +1,21 @@
package collector
import (
"math"
"github.com/bcicen/ctop/logging"
)
var log = logging.Init()
func round(num float64) int {
return int(num + math.Copysign(0.5, num))
}
// return rounded percentage
func percent(val float64, total float64) int {
if total <= 0 {
return 0
}
return round((val / total) * 100)
}

View File

@@ -1,16 +1,18 @@
// +build !release // +build !release
package metrics package collector
import ( import (
"math/rand" "math/rand"
"time" "time"
"github.com/bcicen/ctop/metrics"
) )
// Mock collector // Mock collector
type Mock struct { type Mock struct {
Metrics metrics.Metrics
stream chan Metrics stream chan metrics.Metrics
done bool done bool
running bool running bool
aggression int64 aggression int64
@@ -18,7 +20,7 @@ type Mock struct {
func NewMock(a int64) *Mock { func NewMock(a int64) *Mock {
c := &Mock{ c := &Mock{
Metrics: Metrics{}, Metrics: metrics.Metrics{},
aggression: a, aggression: a,
} }
c.MemLimit = 2147483648 c.MemLimit = 2147483648
@@ -31,7 +33,7 @@ func (c *Mock) Running() bool {
func (c *Mock) Start() { func (c *Mock) Start() {
c.done = false c.done = false
c.stream = make(chan Metrics) c.stream = make(chan metrics.Metrics)
go c.run() go c.run()
} }
@@ -39,7 +41,7 @@ func (c *Mock) Stop() {
c.done = true c.done = true
} }
func (c *Mock) Stream() chan Metrics { func (c *Mock) Stream() chan metrics.Metrics {
return c.stream return c.stream
} }
@@ -48,6 +50,11 @@ func (c *Mock) run() {
rand.Seed(int64(time.Now().Nanosecond())) rand.Seed(int64(time.Now().Nanosecond()))
defer close(c.stream) defer close(c.stream)
// set to random static value, once
c.Pids = rand.Intn(12)
c.IOBytesRead = rand.Int63n(8098) * c.aggression
c.IOBytesWrite = rand.Int63n(8098) * c.aggression
for { for {
c.CPUUtil += rand.Intn(2) * int(c.aggression) c.CPUUtil += rand.Intn(2) * int(c.aggression)
if c.CPUUtil >= 100 { if c.CPUUtil >= 100 {
@@ -60,7 +67,7 @@ func (c *Mock) run() {
if c.MemUsage > c.MemLimit { if c.MemUsage > c.MemLimit {
c.MemUsage = 0 c.MemUsage = 0
} }
c.MemPercent = round((float64(c.MemUsage) / float64(c.MemLimit)) * 100) c.MemPercent = percent(float64(c.MemUsage), float64(c.MemLimit))
c.stream <- c.Metrics c.stream <- c.Metrics
if c.done { if c.done {
break break

View File

@@ -0,0 +1,44 @@
// +build !darwin
package collector
import (
linuxproc "github.com/c9s/goprocinfo/linux"
"github.com/opencontainers/runc/libcontainer/system"
)
var sysMemTotal = getSysMemTotal()
var clockTicksPerSecond = uint64(system.GetClockTicks())
const nanoSecondsPerSecond = 1e9
func getSysMemTotal() int64 {
stat, err := linuxproc.ReadMemInfo("/proc/meminfo")
if err != nil {
log.Errorf("error reading system stats: %s", err)
return 0
}
return int64(stat.MemTotal * 1024)
}
// return cumulative system cpu usage in nanoseconds
func getSysCPUUsage() uint64 {
stat, err := linuxproc.ReadStat("/proc/stat")
if err != nil {
log.Errorf("error reading system stats: %s", err)
return 0
}
sum := stat.CPUStatAll.User +
stat.CPUStatAll.Nice +
stat.CPUStatAll.System +
stat.CPUStatAll.Idle +
stat.CPUStatAll.IOWait +
stat.CPUStatAll.IRQ +
stat.CPUStatAll.SoftIRQ +
stat.CPUStatAll.Steal +
stat.CPUStatAll.Guest +
stat.CPUStatAll.GuestNice
return (sum * nanoSecondsPerSecond) / clockTicksPerSecond
}

124
connector/collector/runc.go Normal file
View File

@@ -0,0 +1,124 @@
// +build !darwin
package collector
import (
"time"
"github.com/bcicen/ctop/metrics"
"github.com/opencontainers/runc/libcontainer"
"github.com/opencontainers/runc/libcontainer/cgroups"
)
// Runc collector
type Runc struct {
metrics.Metrics
id string
libc libcontainer.Container
stream chan metrics.Metrics
done bool
running bool
interval int // collection interval, in seconds
lastCpu float64
lastSysCpu float64
}
func NewRunc(libc libcontainer.Container) *Runc {
c := &Runc{
Metrics: metrics.Metrics{},
id: libc.ID(),
libc: libc,
interval: 1,
}
return c
}
func (c *Runc) Running() bool {
return c.running
}
func (c *Runc) Start() {
c.done = false
c.stream = make(chan metrics.Metrics)
go c.run()
}
func (c *Runc) Stop() {
c.done = true
}
func (c *Runc) Stream() chan metrics.Metrics {
return c.stream
}
func (c *Runc) run() {
c.running = true
defer close(c.stream)
log.Debugf("collector started for container: %s", c.id)
for {
stats, err := c.libc.Stats()
if err != nil {
log.Errorf("failed to collect stats for container %s:\n%s", c.id, err)
break
}
c.ReadCPU(stats.CgroupStats)
c.ReadMem(stats.CgroupStats)
c.ReadNet(stats.Interfaces)
c.stream <- c.Metrics
if c.done {
break
}
time.Sleep(1 * time.Second)
}
c.running = false
}
func (c *Runc) ReadCPU(stats *cgroups.Stats) {
u := stats.CpuStats.CpuUsage
ncpus := float64(len(u.PercpuUsage))
total := float64(u.TotalUsage)
system := float64(getSysCPUUsage())
cpudiff := total - c.lastCpu
syscpudiff := system - c.lastSysCpu
c.CPUUtil = round((cpudiff / syscpudiff * 100) * ncpus)
c.lastCpu = total
c.lastSysCpu = system
c.Pids = int(stats.PidsStats.Current)
}
func (c *Runc) ReadMem(stats *cgroups.Stats) {
c.MemUsage = int64(stats.MemoryStats.Usage.Usage)
c.MemLimit = int64(stats.MemoryStats.Usage.Limit)
if c.MemLimit > sysMemTotal && sysMemTotal > 0 {
c.MemLimit = sysMemTotal
}
c.MemPercent = percent(float64(c.MemUsage), float64(c.MemLimit))
}
func (c *Runc) ReadNet(interfaces []*libcontainer.NetworkInterface) {
var rx, tx int64
for _, network := range interfaces {
rx += int64(network.RxBytes)
tx += int64(network.TxBytes)
}
c.NetRx, c.NetTx = rx, tx
}
func (c *Runc) ReadIO(stats *cgroups.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

@@ -1,35 +1,31 @@
package main package connector
import ( import (
"sort" "fmt"
"strings" "strings"
"sync" "sync"
"github.com/bcicen/ctop/metrics" "github.com/bcicen/ctop/connector/collector"
"github.com/fsouza/go-dockerclient" "github.com/bcicen/ctop/container"
api "github.com/fsouza/go-dockerclient"
) )
type ContainerSource interface { type Docker struct {
All() Containers client *api.Client
Get(string) (*Container, bool) containers map[string]*container.Container
}
type DockerContainerSource struct {
client *docker.Client
containers map[string]*Container
needsRefresh chan string // container IDs requiring refresh needsRefresh chan string // container IDs requiring refresh
lock sync.RWMutex lock sync.RWMutex
} }
func NewDockerContainerSource() *DockerContainerSource { func NewDocker() Connector {
// init docker client // init docker client
client, err := docker.NewClientFromEnv() client, err := api.NewClientFromEnv()
if err != nil { if err != nil {
panic(err) panic(err)
} }
cm := &DockerContainerSource{ cm := &Docker{
client: client, client: client,
containers: make(map[string]*Container), containers: make(map[string]*container.Container),
needsRefresh: make(chan string, 60), needsRefresh: make(chan string, 60),
lock: sync.RWMutex{}, lock: sync.RWMutex{},
} }
@@ -40,9 +36,9 @@ func NewDockerContainerSource() *DockerContainerSource {
} }
// Docker events watcher // Docker events watcher
func (cm *DockerContainerSource) watchEvents() { func (cm *Docker) watchEvents() {
log.Info("docker event listener starting") log.Info("docker event listener starting")
events := make(chan *docker.APIEvents) events := make(chan *api.APIEvents)
cm.client.AddEventListener(events) cm.client.AddEventListener(events)
for e := range events { for e := range events {
@@ -60,7 +56,25 @@ func (cm *DockerContainerSource) watchEvents() {
} }
} }
func (cm *DockerContainerSource) refresh(c *Container) { func portsFormat(ports map[api.Port][]api.PortBinding) string {
var exposed []string
var published []string
for k, v := range ports {
if len(v) == 0 {
exposed = append(exposed, string(k))
continue
}
for _, binding := range v {
s := fmt.Sprintf("%s -> %s:%s", k, binding.HostIP, binding.HostPort)
published = append(published, s)
}
}
return strings.Join(append(exposed, published...), "\n")
}
func (cm *Docker) refresh(c *container.Container) {
insp := cm.inspect(c.Id) insp := cm.inspect(c.Id)
// remove container if no longer exists // remove container if no longer exists
if insp == nil { if insp == nil {
@@ -69,14 +83,15 @@ func (cm *DockerContainerSource) refresh(c *Container) {
} }
c.SetMeta("name", shortName(insp.Name)) c.SetMeta("name", shortName(insp.Name))
c.SetMeta("image", insp.Config.Image) c.SetMeta("image", insp.Config.Image)
c.SetMeta("ports", portsFormat(insp.NetworkSettings.Ports))
c.SetMeta("created", insp.Created.Format("Mon Jan 2 15:04:05 2006")) c.SetMeta("created", insp.Created.Format("Mon Jan 2 15:04:05 2006"))
c.SetState(insp.State.Status) c.SetState(insp.State.Status)
} }
func (cm *DockerContainerSource) inspect(id string) *docker.Container { func (cm *Docker) inspect(id string) *api.Container {
c, err := cm.client.InspectContainer(id) c, err := cm.client.InspectContainer(id)
if err != nil { if err != nil {
if _, ok := err.(*docker.NoSuchContainer); ok == false { if _, ok := err.(*api.NoSuchContainer); ok == false {
log.Errorf(err.Error()) log.Errorf(err.Error())
} }
} }
@@ -84,8 +99,8 @@ func (cm *DockerContainerSource) inspect(id string) *docker.Container {
} }
// Mark all container IDs for refresh // Mark all container IDs for refresh
func (cm *DockerContainerSource) refreshAll() { func (cm *Docker) refreshAll() {
opts := docker.ListContainersOptions{All: true} opts := api.ListContainersOptions{All: true}
allContainers, err := cm.client.ListContainers(opts) allContainers, err := cm.client.ListContainers(opts)
if err != nil { if err != nil {
panic(err) panic(err)
@@ -99,7 +114,7 @@ func (cm *DockerContainerSource) refreshAll() {
} }
} }
func (cm *DockerContainerSource) Loop() { func (cm *Docker) Loop() {
for id := range cm.needsRefresh { for id := range cm.needsRefresh {
c := cm.MustGet(id) c := cm.MustGet(id)
cm.refresh(c) cm.refresh(c)
@@ -107,14 +122,14 @@ func (cm *DockerContainerSource) Loop() {
} }
// Get a single container, creating one anew if not existing // Get a single container, creating one anew if not existing
func (cm *DockerContainerSource) MustGet(id string) *Container { func (cm *Docker) MustGet(id string) *container.Container {
c, ok := cm.Get(id) c, ok := cm.Get(id)
// append container struct for new containers // append container struct for new containers
if !ok { if !ok {
// create collector // create collector
collector := metrics.NewDocker(cm.client, id) collector := collector.NewDocker(cm.client, id)
// create container // create container
c = NewContainer(id, collector) c = container.New(id, collector)
cm.lock.Lock() cm.lock.Lock()
cm.containers[id] = c cm.containers[id] = c
cm.lock.Unlock() cm.lock.Unlock()
@@ -123,7 +138,7 @@ func (cm *DockerContainerSource) MustGet(id string) *Container {
} }
// Get a single container, by ID // Get a single container, by ID
func (cm *DockerContainerSource) Get(id string) (*Container, bool) { func (cm *Docker) Get(id string) (*container.Container, bool) {
cm.lock.Lock() cm.lock.Lock()
c, ok := cm.containers[id] c, ok := cm.containers[id]
cm.lock.Unlock() cm.lock.Unlock()
@@ -131,7 +146,7 @@ func (cm *DockerContainerSource) Get(id string) (*Container, bool) {
} }
// Remove containers by ID // Remove containers by ID
func (cm *DockerContainerSource) delByID(id string) { func (cm *Docker) delByID(id string) {
cm.lock.Lock() cm.lock.Lock()
delete(cm.containers, id) delete(cm.containers, id)
cm.lock.Unlock() cm.lock.Unlock()
@@ -139,14 +154,14 @@ func (cm *DockerContainerSource) delByID(id string) {
} }
// Return array of all containers, sorted by field // Return array of all containers, sorted by field
func (cm *DockerContainerSource) All() (containers Containers) { func (cm *Docker) All() (containers container.Containers) {
cm.lock.Lock() cm.lock.Lock()
for _, c := range cm.containers { for _, c := range cm.containers {
containers = append(containers, c) containers = append(containers, c)
} }
cm.lock.Unlock() containers.Sort()
sort.Sort(containers)
containers.Filter() containers.Filter()
cm.lock.Unlock()
return containers return containers
} }

View File

@@ -0,0 +1,7 @@
// +build !linux
package connector
var enabled = map[string]func() Connector{
"docker": NewDocker,
}

View File

@@ -0,0 +1,8 @@
// +build !darwin
package connector
var enabled = map[string]func() Connector{
"docker": NewDocker,
"runc": NewRunc,
}

26
connector/main.go Normal file
View File

@@ -0,0 +1,26 @@
package connector
import (
"fmt"
"github.com/bcicen/ctop/container"
"github.com/bcicen/ctop/logging"
)
var log = logging.Init()
func ByName(s string) (Connector, error) {
if _, ok := enabled[s]; !ok {
msg := fmt.Sprintf("invalid connector type \"%s\"\nconnector must be one of:", s)
for k, _ := range enabled {
msg += fmt.Sprintf("\n %s", k)
}
return nil, fmt.Errorf(msg)
}
return enabled[s](), nil
}
type Connector interface {
All() container.Containers
Get(string) (*container.Container, bool)
}

View File

@@ -1,31 +1,31 @@
// +build !release // +build !release
package main package connector
import ( import (
"math/rand" "math/rand"
"sort"
"strings" "strings"
"time" "time"
"github.com/bcicen/ctop/metrics" "github.com/bcicen/ctop/connector/collector"
"github.com/bcicen/ctop/container"
"github.com/jgautheron/codename-generator" "github.com/jgautheron/codename-generator"
"github.com/nu7hatch/gouuid" "github.com/nu7hatch/gouuid"
) )
type MockContainerSource struct { type Mock struct {
containers Containers containers container.Containers
} }
func NewMockContainerSource() *MockContainerSource { func NewMock() *Mock {
cs := &MockContainerSource{} cs := &Mock{}
go cs.Init() go cs.Init()
go cs.Loop() go cs.Loop()
return cs return cs
} }
// Create Mock containers // Create Mock containers
func (cs *MockContainerSource) Init() { func (cs *Mock) Init() {
rand.Seed(int64(time.Now().Nanosecond())) rand.Seed(int64(time.Now().Nanosecond()))
for i := 0; i < 4; i++ { for i := 0; i < 4; i++ {
@@ -38,15 +38,15 @@ func (cs *MockContainerSource) Init() {
} }
func (cs *MockContainerSource) makeContainer(aggression int64) { func (cs *Mock) makeContainer(aggression int64) {
collector := metrics.NewMock(aggression) collector := collector.NewMock(aggression)
c := NewContainer(makeID(), collector) c := container.New(makeID(), collector)
c.SetMeta("name", makeName()) c.SetMeta("name", makeName())
c.SetState(makeState()) c.SetState(makeState())
cs.containers = append(cs.containers, c) cs.containers = append(cs.containers, c)
} }
func (cs *MockContainerSource) Loop() { func (cs *Mock) Loop() {
iter := 0 iter := 0
for { for {
// Change state for random container // Change state for random container
@@ -60,7 +60,7 @@ func (cs *MockContainerSource) Loop() {
} }
// Get a single container, by ID // Get a single container, by ID
func (cs *MockContainerSource) Get(id string) (*Container, bool) { func (cs *Mock) Get(id string) (*container.Container, bool) {
for _, c := range cs.containers { for _, c := range cs.containers {
if c.Id == id { if c.Id == id {
return c, true return c, true
@@ -70,14 +70,14 @@ func (cs *MockContainerSource) Get(id string) (*Container, bool) {
} }
// Return array of all containers, sorted by field // Return array of all containers, sorted by field
func (cs *MockContainerSource) All() Containers { func (cs *Mock) All() container.Containers {
sort.Sort(cs.containers) cs.containers.Sort()
cs.containers.Filter() cs.containers.Filter()
return cs.containers return cs.containers
} }
// Remove containers by ID // Remove containers by ID
func (cs *MockContainerSource) delByID(id string) { func (cs *Mock) delByID(id string) {
for n, c := range cs.containers { for n, c := range cs.containers {
if c.Id == id { if c.Id == id {
cs.del(n) cs.del(n)
@@ -87,7 +87,7 @@ func (cs *MockContainerSource) delByID(id string) {
} }
// Remove one or more containers by index // Remove one or more containers by index
func (cs *MockContainerSource) del(idx ...int) { func (cs *Mock) del(idx ...int) {
for _, i := range idx { for _, i := range idx {
cs.containers = append(cs.containers[:i], cs.containers[i+1:]...) cs.containers = append(cs.containers[:i], cs.containers[i+1:]...)
} }

243
connector/runc.go Normal file
View File

@@ -0,0 +1,243 @@
// +build !darwin
package connector
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sync"
"time"
"github.com/bcicen/ctop/connector/collector"
"github.com/bcicen/ctop/container"
"github.com/opencontainers/runc/libcontainer"
"github.com/opencontainers/runc/libcontainer/cgroups/systemd"
)
type RuncOpts struct {
root string // runc root path
systemdCgroups bool // use systemd cgroups
}
func NewRuncOpts() (RuncOpts, error) {
var opts RuncOpts
// read runc root path
root := os.Getenv("RUNC_ROOT")
if root == "" {
root = "/run/runc"
}
abs, err := filepath.Abs(root)
if err != nil {
return opts, err
}
opts.root = abs
// ensure runc root path is readable
_, err = ioutil.ReadDir(opts.root)
if err != nil {
return opts, err
}
if os.Getenv("RUNC_SYSTEMD_CGROUP") == "1" {
opts.systemdCgroups = true
}
return opts, nil
}
type Runc struct {
opts RuncOpts
factory libcontainer.Factory
containers map[string]*container.Container
libContainers map[string]libcontainer.Container
needsRefresh chan string // container IDs requiring refresh
lock sync.RWMutex
}
func NewRunc() Connector {
opts, err := NewRuncOpts()
runcFailOnErr(err)
factory, err := getFactory(opts)
runcFailOnErr(err)
cm := &Runc{
opts: opts,
factory: factory,
containers: make(map[string]*container.Container),
libContainers: make(map[string]libcontainer.Container),
needsRefresh: make(chan string, 60),
lock: sync.RWMutex{},
}
go func() {
for {
cm.refreshAll()
time.Sleep(5 * time.Second)
}
}()
go cm.Loop()
return cm
}
func (cm *Runc) GetLibc(id string) libcontainer.Container {
// return previously loaded container
libc, ok := cm.libContainers[id]
if ok {
return libc
}
// load container
libc, err := cm.factory.Load(id)
if err != nil {
// remove container if no longer exists
if lerr, ok := err.(libcontainer.Error); ok && lerr.Code() == libcontainer.ContainerNotExists {
cm.delByID(id)
} else {
log.Warningf("failed to read container: %s\n", err)
}
return nil
}
return libc
}
// update a ctop container from libcontainer
func (cm *Runc) refresh(id string) {
libc := cm.GetLibc(id)
if libc == nil {
return
}
c := cm.MustGet(id)
// remove container if entered destroyed state on last refresh
// this gives adequate time for the collector to be shut down
if c.GetMeta("state") == "destroyed" {
cm.delByID(id)
return
}
status, err := libc.Status()
if err != nil {
log.Warningf("failed to read status for container: %s\n", err)
} else {
c.SetState(status.String())
}
state, err := libc.State()
if err != nil {
log.Warningf("failed to read state for container: %s\n", err)
} else {
c.SetMeta("created", state.BaseState.Created.Format("Mon Jan 2 15:04:05 2006"))
}
conf := libc.Config()
c.SetMeta("rootfs", conf.Rootfs)
}
// Read runc root, creating any new containers
func (cm *Runc) refreshAll() {
list, err := ioutil.ReadDir(cm.opts.root)
runcFailOnErr(err)
for _, i := range list {
if i.IsDir() {
name := i.Name()
// attempt to load
libc := cm.GetLibc(name)
if libc == nil {
continue
}
_ = cm.MustGet(i.Name()) // ensure container exists
}
}
// queue all existing containers for refresh
for id, _ := range cm.containers {
cm.needsRefresh <- id
}
log.Debugf("queued %d containers for refresh", len(cm.containers))
}
func (cm *Runc) Loop() {
for id := range cm.needsRefresh {
cm.refresh(id)
}
}
// Get a single ctop container in the map matching libc container, creating one anew if not existing
func (cm *Runc) MustGet(id string) *container.Container {
c, ok := cm.Get(id)
if !ok {
libc := cm.GetLibc(id)
// create collector
collector := collector.NewRunc(libc)
// create container
c = container.New(id, collector)
name := libc.ID()
// set initial metadata
if len(name) > 12 {
name = name[0:12]
}
c.SetMeta("name", name)
// add to map
cm.lock.Lock()
cm.containers[id] = c
cm.libContainers[id] = libc
cm.lock.Unlock()
log.Debugf("saw new container: %s", id)
}
return c
}
// Get a single container, by ID
func (cm *Runc) Get(id string) (*container.Container, bool) {
cm.lock.Lock()
defer cm.lock.Unlock()
c, ok := cm.containers[id]
return c, ok
}
// Remove containers by ID
func (cm *Runc) delByID(id string) {
cm.lock.Lock()
delete(cm.containers, id)
delete(cm.libContainers, id)
cm.lock.Unlock()
log.Infof("removed dead container: %s", id)
}
// Return array of all containers, sorted by field
func (cm *Runc) All() (containers container.Containers) {
cm.lock.Lock()
for _, c := range cm.containers {
containers = append(containers, c)
}
containers.Sort()
containers.Filter()
cm.lock.Unlock()
return containers
}
func getFactory(opts RuncOpts) (libcontainer.Factory, error) {
cgroupManager := libcontainer.Cgroupfs
if opts.systemdCgroups {
if systemd.UseSystemd() {
cgroupManager = libcontainer.SystemdCgroups
} else {
return nil, fmt.Errorf("systemd cgroup enabled, but systemd support for managing cgroups is not available")
}
}
return libcontainer.New(opts.root, cgroupManager)
}
func runcFailOnErr(err error) {
if err != nil {
panic(fmt.Errorf("fatal runc error: %s", err))
}
}

View File

@@ -1,23 +1,28 @@
package main package container
import ( import (
"github.com/bcicen/ctop/cwidgets" "github.com/bcicen/ctop/cwidgets"
"github.com/bcicen/ctop/cwidgets/compact" "github.com/bcicen/ctop/cwidgets/compact"
"github.com/bcicen/ctop/logging"
"github.com/bcicen/ctop/metrics" "github.com/bcicen/ctop/metrics"
) )
var (
log = logging.Init()
)
// Metrics and metadata representing a container // Metrics and metadata representing a container
type Container struct { type Container struct {
metrics.Metrics metrics.Metrics
Id string Id string
Meta map[string]string Meta map[string]string
Widgets *compact.Compact Widgets *compact.Compact
Display bool // display this container in compact view
updater cwidgets.WidgetUpdater updater cwidgets.WidgetUpdater
collector metrics.Collector collector metrics.Collector
display bool // display this container in compact view
} }
func NewContainer(id string, collector metrics.Collector) *Container { func New(id string, collector metrics.Collector) *Container {
widgets := compact.NewCompact(id) widgets := compact.NewCompact(id)
return &Container{ return &Container{
Metrics: metrics.NewMetrics(), Metrics: metrics.NewMetrics(),

View File

@@ -1,8 +1,9 @@
package main package container
import ( import (
"fmt" "fmt"
"regexp" "regexp"
"sort"
"github.com/bcicen/ctop/config" "github.com/bcicen/ctop/config"
) )
@@ -89,6 +90,7 @@ func SortFields() (fields []string) {
type Containers []*Container type Containers []*Container
func (a Containers) Sort() { sort.Sort(a) }
func (a Containers) Len() int { return len(a) } func (a Containers) Len() int { return len(a) }
func (a Containers) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a Containers) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a Containers) Less(i, j int) bool { func (a Containers) Less(i, j int) bool {
@@ -104,14 +106,14 @@ func (a Containers) Filter() {
re := regexp.MustCompile(fmt.Sprintf(".*%s", filter)) re := regexp.MustCompile(fmt.Sprintf(".*%s", filter))
for _, c := range a { for _, c := range a {
c.display = true c.Display = true
// Apply name filter // Apply name filter
if re.FindAllString(c.GetMeta("name"), 1) == nil { if re.FindAllString(c.GetMeta("name"), 1) == nil {
c.display = false c.Display = false
} }
// Apply state filter // Apply state filter
if !config.GetSwitchVal("allContainers") && c.GetMeta("state") != "running" { if !config.GetSwitchVal("allContainers") && c.GetMeta("state") != "running" {
c.display = false c.Display = false
} }
} }
} }

View File

@@ -1,24 +1,23 @@
package main package main
import ( import (
"math"
"github.com/bcicen/ctop/connector"
"github.com/bcicen/ctop/container"
ui "github.com/gizak/termui" ui "github.com/gizak/termui"
) )
type GridCursor struct { type GridCursor struct {
selectedID string // id of currently selected container selectedID string // id of currently selected container
filtered Containers filtered container.Containers
cSource ContainerSource cSource connector.Connector
} isScrolling bool // toggled when actively scrolling
func NewGridCursor() *GridCursor {
return &GridCursor{
cSource: NewDockerContainerSource(),
}
} }
func (gc *GridCursor) Len() int { return len(gc.filtered) } func (gc *GridCursor) Len() int { return len(gc.filtered) }
func (gc *GridCursor) Selected() *Container { func (gc *GridCursor) Selected() *container.Container {
idx := gc.Idx() idx := gc.Idx()
if idx < gc.Len() { if idx < gc.Len() {
return gc.filtered[idx] return gc.filtered[idx]
@@ -31,10 +30,10 @@ func (gc *GridCursor) RefreshContainers() (lenChanged bool) {
oldLen := gc.Len() oldLen := gc.Len()
// Containers filtered by display bool // Containers filtered by display bool
gc.filtered = Containers{} gc.filtered = container.Containers{}
var cursorVisible bool var cursorVisible bool
for _, c := range gc.cSource.All() { for _, c := range gc.cSource.All() {
if c.display { if c.Display {
if c.Id == gc.selectedID { if c.Id == gc.selectedID {
cursorVisible = true cursorVisible = true
} }
@@ -100,6 +99,9 @@ func (gc *GridCursor) ScrollPage() {
} }
func (gc *GridCursor) Up() { func (gc *GridCursor) Up() {
gc.isScrolling = true
defer func() { gc.isScrolling = false }()
idx := gc.Idx() idx := gc.Idx()
if idx <= 0 { // already at top if idx <= 0 { // already at top
return return
@@ -116,6 +118,9 @@ func (gc *GridCursor) Up() {
} }
func (gc *GridCursor) Down() { func (gc *GridCursor) Down() {
gc.isScrolling = true
defer func() { gc.isScrolling = false }()
idx := gc.Idx() idx := gc.Idx()
if idx >= gc.Len()-1 { // already at bottom if idx >= gc.Len()-1 { // already at bottom
return return
@@ -130,3 +135,48 @@ func (gc *GridCursor) Down() {
gc.ScrollPage() gc.ScrollPage()
ui.Render(cGrid) ui.Render(cGrid)
} }
func (gc *GridCursor) PgUp() {
idx := gc.Idx()
if idx <= 0 { // already at top
return
}
var nextidx int
nextidx = int(math.Max(0.0, float64(idx-cGrid.MaxRows())))
cGrid.Offset = int(math.Max(float64(cGrid.Offset-cGrid.MaxRows()),
float64(0)))
active := gc.filtered[idx]
next := gc.filtered[nextidx]
active.Widgets.Name.UnHighlight()
gc.selectedID = next.Id
next.Widgets.Name.Highlight()
cGrid.Align()
ui.Render(cGrid)
}
func (gc *GridCursor) PgDown() {
idx := gc.Idx()
if idx >= gc.Len()-1 { // already at bottom
return
}
var nextidx int
nextidx = int(math.Min(float64(gc.Len()-1),
float64(idx+cGrid.MaxRows())))
cGrid.Offset = int(math.Min(float64(cGrid.Offset+cGrid.MaxRows()),
float64(gc.Len()-cGrid.MaxRows())))
active := gc.filtered[idx]
next := gc.filtered[nextidx]
active.Widgets.Name.UnHighlight()
gc.selectedID = next.Id
next.Widgets.Name.Highlight()
cGrid.Align()
ui.Render(cGrid)
}

View File

@@ -1,10 +1,12 @@
package expanded package expanded
import ( import (
"strings"
ui "github.com/gizak/termui" ui "github.com/gizak/termui"
) )
var displayInfo = []string{"id", "name", "image", "state"} var displayInfo = []string{"id", "name", "image", "ports", "state", "created"}
type Info struct { type Info struct {
*ui.Table *ui.Table
@@ -24,12 +26,33 @@ func NewInfo(id string) *Info {
func (w *Info) Set(k, v string) { func (w *Info) Set(k, v string) {
w.data[k] = v w.data[k] = v
// rebuild rows // rebuild rows
w.Rows = [][]string{} w.Rows = [][]string{}
for _, k := range displayInfo { for _, k := range displayInfo {
if v, ok := w.data[k]; ok { if v, ok := w.data[k]; ok {
w.Rows = append(w.Rows, []string{k, v}) w.Rows = append(w.Rows, mkInfoRows(k, v)...)
} }
} }
w.Height = len(w.Rows) + 2 w.Height = len(w.Rows) + 2
} }
// Build row(s) from a key and value string
func mkInfoRows(k, v string) (rows [][]string) {
lines := strings.Split(v, "\n")
// initial row with field name
rows = append(rows, []string{k, lines[0]})
// append any additional lines in seperate row
if len(lines) > 1 {
for _, line := range lines[1:] {
if line != "" {
rows = append(rows, []string{"", line})
}
}
}
return rows
}

View File

@@ -9,6 +9,7 @@ const (
kb = 1024 kb = 1024
mb = kb * 1024 mb = kb * 1024
gb = mb * 1024 gb = mb * 1024
tb = gb * 1024
) )
// convenience method // convenience method
@@ -28,9 +29,13 @@ func ByteFormat(n int64) string {
n = n / mb n = n / mb
return fmt.Sprintf("%sM", strconv.FormatInt(n, 10)) return fmt.Sprintf("%sM", strconv.FormatInt(n, 10))
} }
if n < tb {
nf := float64(n) / gb nf := float64(n) / gb
return fmt.Sprintf("%sG", unpadFloat(nf)) return fmt.Sprintf("%sG", unpadFloat(nf))
} }
nf := float64(n) / tb
return fmt.Sprintf("%sT", unpadFloat(nf))
}
func unpadFloat(f float64) string { func unpadFloat(f float64) string {
return strconv.FormatFloat(f, 'f', getPrecision(f), 64) return strconv.FormatFloat(f, 'f', getPrecision(f), 64)

View File

@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"reflect" "reflect"
"github.com/bcicen/ctop/container"
ui "github.com/gizak/termui" ui "github.com/gizak/termui"
) )
@@ -19,7 +20,7 @@ func logEvent(e ui.Event) {
} }
// log container, metrics, and widget state // log container, metrics, and widget state
func dumpContainer(c *Container) { func dumpContainer(c *container.Container) {
msg := fmt.Sprintf("logging state for container: %s\n", c.Id) msg := fmt.Sprintf("logging state for container: %s\n", c.Id)
for k, v := range c.Meta { for k, v := range c.Meta {
msg += fmt.Sprintf("Meta.%s = %s\n", k, v) msg += fmt.Sprintf("Meta.%s = %s\n", k, v)

54
glide.lock generated
View File

@@ -1,12 +1,22 @@
hash: c13011881e895378f374b68596a59a0ea6def372b4f5239b2d8aa342eaa46a4b hash: 0d550b01b3a1c4751a8f5c3fba0c43f62252055e231712729628e514bb494da8
updated: 2017-03-12T09:53:35.212073637+07:00 updated: 2017-06-09T18:11:10.930196504-03:00
imports: imports:
- name: github.com/Azure/go-ansiterm - name: github.com/Azure/go-ansiterm
version: fa152c58bc15761d0200cb75fe958b89a9d4888e version: fa152c58bc15761d0200cb75fe958b89a9d4888e
subpackages: subpackages:
- winterm - winterm
- name: github.com/c9s/goprocinfo
version: b34328d6e0cd139894ea7347d2624ccf31fa3c58
subpackages:
- linux
- name: github.com/coreos/go-systemd
version: b4a58d95188dd092ae20072bac14cece0e67c388
subpackages:
- activation
- dbus
- util
- name: github.com/docker/docker - name: github.com/docker/docker
version: ce07fb6b0f1b8765b92022e45f96bd4349812e06 version: 90d35abf7b3535c1c319c872900fbd76374e521c
subpackages: subpackages:
- api/types - api/types
- api/types/blkiodev - api/types/blkiodev
@@ -27,9 +37,11 @@ imports:
- pkg/jsonlog - pkg/jsonlog
- pkg/jsonmessage - pkg/jsonmessage
- pkg/longpath - pkg/longpath
- pkg/mount
- pkg/pools - pkg/pools
- pkg/promise - pkg/promise
- pkg/stdcopy - pkg/stdcopy
- pkg/symlink
- pkg/system - pkg/system
- pkg/term - pkg/term
- pkg/term/windows - pkg/term/windows
@@ -45,6 +57,12 @@ imports:
version: ea10e6ccee219e572ffad0ac1909f1a17f6db7d6 version: ea10e6ccee219e572ffad0ac1909f1a17f6db7d6
repo: https://github.com/bcicen/termui repo: https://github.com/bcicen/termui
vcs: git vcs: git
- name: github.com/godbus/dbus
version: c7fdd8b5cd55e87b4e1f4e372cdb1db61dd6c66f
- name: github.com/golang/protobuf
version: f7137ae6b19afbfd61a94b746fda3b3fe0491874
subpackages:
- proto
- name: github.com/hashicorp/go-cleanhttp - name: github.com/hashicorp/go-cleanhttp
version: 3573b8b52aa7b37b9358d966a898feb387f62437 version: 3573b8b52aa7b37b9358d966a898feb387f62437
- name: github.com/jgautheron/codename-generator - name: github.com/jgautheron/codename-generator
@@ -63,15 +81,41 @@ imports:
version: 91bae1bb5fa9ee504905ecbe7043fa30e92feaa3 version: 91bae1bb5fa9ee504905ecbe7043fa30e92feaa3
- name: github.com/nu7hatch/gouuid - name: github.com/nu7hatch/gouuid
version: 179d4d0c4d8d407a32af483c2354df1d2c91e6c3 version: 179d4d0c4d8d407a32af483c2354df1d2c91e6c3
- name: github.com/Nvveen/Gotty
version: cd527374f1e5bff4938207604a14f2e38a9cf512
- name: github.com/op/go-logging - name: github.com/op/go-logging
version: b2cb9fa56473e98db8caba80237377e83fe44db5 version: b2cb9fa56473e98db8caba80237377e83fe44db5
- name: github.com/opencontainers/runc - name: github.com/opencontainers/runc
version: 31980a53ae7887b2c8f8715d13c3eb486c27b6cf version: baf6536d6259209c3edfa2b22237af82942d3dfa
subpackages: subpackages:
- libcontainer
- libcontainer/apparmor
- libcontainer/cgroups
- libcontainer/cgroups/fs
- libcontainer/cgroups/systemd
- libcontainer/configs
- libcontainer/configs/validate
- libcontainer/criurpc
- libcontainer/keys
- libcontainer/label
- libcontainer/seccomp
- libcontainer/selinux
- libcontainer/stacktrace
- libcontainer/system - libcontainer/system
- libcontainer/user - libcontainer/user
- libcontainer/utils
- name: github.com/seccomp/libseccomp-golang
version: 1b506fc7c24eec5a3693cdcbed40d9c226cfc6a1
- name: github.com/Sirupsen/logrus - name: github.com/Sirupsen/logrus
version: 1deb2db2a6fff8a35532079061b903c3a25eed52 version: 26709e2714106fb8ad40b773b711ebce25b78914
- name: github.com/syndtr/gocapability
version: 2c00daeb6c3b45114c80ac44119e7b8801fdd852
subpackages:
- capability
- name: github.com/vishvananda/netlink
version: 1e2e08e8a2dcdacaae3f14ac44c5cfa31361f270
subpackages:
- nl
- name: golang.org/x/net - name: golang.org/x/net
version: a6577fac2d73be281a500b310739095313165611 version: a6577fac2d73be281a500b310739095313165611
subpackages: subpackages:

View File

@@ -1,11 +1,18 @@
package: github.com/bcicen/ctop package: github.com/bcicen/ctop
import: import:
- package: github.com/c9s/goprocinfo/linux
- package: github.com/docker/docker
version: ^17.5.0-ce-rc3
- package: github.com/opencontainers/runc
version: 0.1.1
- package: github.com/fsouza/go-dockerclient - package: github.com/fsouza/go-dockerclient
version: 318513eb1ab27495afbc67f671ba1080513d8aa0
- package: github.com/gizak/termui - package: github.com/gizak/termui
version: barchart-numfmt version: barchart-numfmt
repo: https://github.com/bcicen/termui repo: https://github.com/bcicen/termui
vcs: git vcs: git
- package: github.com/jgautheron/codename-generator - package: github.com/jgautheron/codename-generator
- package: github.com/nu7hatch/gouuid - package: github.com/nu7hatch/gouuid
- package: github.com/c9s/goprocinfo/linux
- package: github.com/op/go-logging - package: github.com/op/go-logging
version: ^1.0.0 version: ^1.0.0

10
grid.go
View File

@@ -2,6 +2,7 @@ package main
import ( import (
"github.com/bcicen/ctop/config" "github.com/bcicen/ctop/config"
"github.com/bcicen/ctop/container"
"github.com/bcicen/ctop/cwidgets/expanded" "github.com/bcicen/ctop/cwidgets/expanded"
ui "github.com/gizak/termui" ui "github.com/gizak/termui"
) )
@@ -34,7 +35,7 @@ func RedrawRows(clr bool) {
ui.Render(cGrid) ui.Render(cGrid)
} }
func ExpandView(c *Container) { func ExpandView(c *container.Container) {
ui.Clear() ui.Clear()
ui.DefaultEvtStream.ResetHandlers() ui.DefaultEvtStream.ResetHandlers()
defer ui.DefaultEvtStream.ResetHandlers() defer ui.DefaultEvtStream.ResetHandlers()
@@ -61,9 +62,12 @@ func ExpandView(c *Container) {
} }
func RefreshDisplay() { func RefreshDisplay() {
// skip display refresh during scroll
if !cursor.isScrolling {
needsClear := cursor.RefreshContainers() needsClear := cursor.RefreshContainers()
RedrawRows(needsClear) RedrawRows(needsClear)
} }
}
func Display() bool { func Display() bool {
var menu func() var menu func()
@@ -79,6 +83,10 @@ func Display() bool {
HandleKeys("up", cursor.Up) HandleKeys("up", cursor.Up)
HandleKeys("down", cursor.Down) HandleKeys("down", cursor.Down)
HandleKeys("pgup", cursor.PgUp)
HandleKeys("pgdown", cursor.PgDown)
HandleKeys("exit", ui.StopLoop) HandleKeys("exit", ui.StopLoop)
HandleKeys("help", func() { HandleKeys("help", func() {
menu = HelpMenu menu = HelpMenu

View File

@@ -14,9 +14,18 @@ var keyMap = map[string][]string{
"/sys/kbd/<down>", "/sys/kbd/<down>",
"/sys/kbd/j", "/sys/kbd/j",
}, },
"pgup": []string{
"/sys/kbd/<previous>",
"/sys/kbd/C-<up>",
},
"pgdown": []string{
"/sys/kbd/<next>",
"/sys/kbd/C-<down>",
},
"exit": []string{ "exit": []string{
"/sys/kbd/q", "/sys/kbd/q",
"/sys/kbd/C-c", "/sys/kbd/C-c",
"/sys/kbd/<escape>",
}, },
"help": []string{ "help": []string{
"/sys/kbd/h", "/sys/kbd/h",

View File

@@ -79,3 +79,4 @@ func (log *CTopLogger) Exit() {
} }
func debugMode() bool { return os.Getenv("CTOP_DEBUG") == "1" } func debugMode() bool { return os.Getenv("CTOP_DEBUG") == "1" }
func debugModeTCP() bool { return os.Getenv("CTOP_DEBUG_TCP") == "1" }

View File

@@ -7,7 +7,8 @@ import (
) )
const ( const (
path = "/tmp/ctop.sock" socketPath = "./ctop.sock"
socketAddr = "0.0.0.0:9000"
) )
var server struct { var server struct {
@@ -16,7 +17,13 @@ var server struct {
} }
func getListener() net.Listener { func getListener() net.Listener {
ln, err := net.Listen("unix", path) var ln net.Listener
var err error
if debugModeTCP() {
ln, err = net.Listen("tcp", socketAddr)
} else {
ln, err = net.Listen("unix", socketPath)
}
if err != nil { if err != nil {
panic(err) panic(err)
} }

36
main.go
View File

@@ -6,10 +6,13 @@ import (
"os" "os"
"github.com/bcicen/ctop/config" "github.com/bcicen/ctop/config"
"github.com/bcicen/ctop/connector"
"github.com/bcicen/ctop/container"
"github.com/bcicen/ctop/cwidgets/compact" "github.com/bcicen/ctop/cwidgets/compact"
"github.com/bcicen/ctop/logging" "github.com/bcicen/ctop/logging"
"github.com/bcicen/ctop/widgets" "github.com/bcicen/ctop/widgets"
ui "github.com/gizak/termui" ui "github.com/gizak/termui"
tm "github.com/nsf/termbox-go"
) )
var ( var (
@@ -20,6 +23,8 @@ var (
cursor *GridCursor cursor *GridCursor
cGrid *compact.CompactGrid cGrid *compact.CompactGrid
header *widgets.CTopHeader header *widgets.CTopHeader
versionStr = fmt.Sprintf("ctop version %v, build %v", version, build)
) )
func main() { func main() {
@@ -33,10 +38,11 @@ func main() {
var sortFieldFlag = flag.String("s", "", "select container sort field") var sortFieldFlag = flag.String("s", "", "select container sort field")
var reverseSortFlag = flag.Bool("r", false, "reverse container sort order") var reverseSortFlag = flag.Bool("r", false, "reverse container sort order")
var invertFlag = flag.Bool("i", false, "invert default colors") var invertFlag = flag.Bool("i", false, "invert default colors")
var connectorFlag = flag.String("connector", "docker", "container connector to use")
flag.Parse() flag.Parse()
if *versionFlag { if *versionFlag {
printVersion() fmt.Println(versionStr)
os.Exit(0) os.Exit(0)
} }
@@ -77,26 +83,36 @@ func main() {
if err := ui.Init(); err != nil { if err := ui.Init(); err != nil {
panic(err) panic(err)
} }
defer ui.Close()
defer Shutdown()
// init grid, cursor, header // init grid, cursor, header
cursor = NewGridCursor() conn, err := connector.ByName(*connectorFlag)
if err != nil {
panic(err)
}
cursor = &GridCursor{cSource: conn}
cGrid = compact.NewCompactGrid() cGrid = compact.NewCompactGrid()
header = widgets.NewCTopHeader() header = widgets.NewCTopHeader()
for { for {
exit := Display() exit := Display()
if exit { if exit {
log.Notice("shutting down")
log.Exit()
return return
} }
} }
} }
func Shutdown() {
log.Notice("shutting down")
log.Exit()
if tm.IsInit {
ui.Close()
}
}
// ensure a given sort field is valid // ensure a given sort field is valid
func validSort(s string) { func validSort(s string) {
if _, ok := Sorters[s]; !ok { if _, ok := container.Sorters[s]; !ok {
fmt.Printf("invalid sort field: %s\n", s) fmt.Printf("invalid sort field: %s\n", s)
os.Exit(1) os.Exit(1)
} }
@@ -104,8 +120,8 @@ func validSort(s string) {
func panicExit() { func panicExit() {
if r := recover(); r != nil { if r := recover(); r != nil {
ui.Clear() Shutdown()
fmt.Printf("panic: %s\n", r) fmt.Printf("error: %s\n", r)
os.Exit(1) os.Exit(1)
} }
} }
@@ -121,7 +137,3 @@ func printHelp() {
fmt.Println(helpMsg) fmt.Println(helpMsg)
flag.PrintDefaults() flag.PrintDefaults()
} }
func printVersion() {
fmt.Printf("ctop version %v, build %v\n", version, build)
}

View File

@@ -2,6 +2,7 @@ package main
import ( import (
"github.com/bcicen/ctop/config" "github.com/bcicen/ctop/config"
"github.com/bcicen/ctop/container"
"github.com/bcicen/ctop/widgets" "github.com/bcicen/ctop/widgets"
"github.com/bcicen/ctop/widgets/menu" "github.com/bcicen/ctop/widgets/menu"
ui "github.com/gizak/termui" ui "github.com/gizak/termui"
@@ -74,7 +75,7 @@ func SortMenu() {
m.SortItems = true m.SortItems = true
m.BorderLabel = "Sort Field" m.BorderLabel = "Sort Field"
for _, field := range SortFields() { for _, field := range container.SortFields() {
m.AddItems(menu.Item{field, ""}) m.AddItems(menu.Item{field, ""})
} }

View File

@@ -1,13 +1,5 @@
package metrics package metrics
import (
"math"
"github.com/bcicen/ctop/logging"
)
var log = logging.Init()
type Metrics struct { type Metrics struct {
CPUUtil int CPUUtil int
NetTx int64 NetTx int64
@@ -39,7 +31,3 @@ type Collector interface {
Start() Start()
Stop() Stop()
} }
func round(num float64) int {
return int(num + math.Copysign(0.5, num))
}