Compare commits

..

1 Commits

Author SHA1 Message Date
Kevin Schoon
47b27a7786 vendor dependencies with glide 2017-03-13 08:05:40 +11:00
53 changed files with 281 additions and 1599 deletions

2
.gitignore vendored
View File

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

View File

@@ -1,16 +1,7 @@
FROM quay.io/vektorcloud/go:1.8 FROM quay.io/vektorcloud/glibc:latest
RUN apk add --no-cache make 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
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/
FROM scratch
COPY --from=0 /go/bin/ctop /ctop
ENTRYPOINT ["/ctop"] ENTRYPOINT ["/ctop"]

View File

@@ -1,34 +0,0 @@
NAME=ctop
VERSION=$(shell cat VERSION)
BUILD=$(shell git rev-parse --short HEAD)
EXT_LD_FLAGS="-Wl,--allow-multiple-definition"
LD_FLAGS="-w -X main.version=$(VERSION) -X main.build=$(BUILD) -extldflags=$(EXT_LD_FLAGS)"
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) -extldflags=$(EXT_LD_FLAGS)"
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
GOOS=linux GOARCH=arm64 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-linux-arm64
image:
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,9 +1,6 @@
<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>
# #
![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:
@@ -11,7 +8,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` comes with built-in support for Docker and runC; connectors for other container and cluster systems are planned for future releases. `ctop` currently comes with built-in support for Docker; connectors for other container and cluster systems are planned for future releases.
## Install ## Install
@@ -20,66 +17,53 @@ 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.6.0/ctop-0.6.0-linux-amd64 -O /usr/local/bin/ctop wget https://github.com/bcicen/ctop/releases/download/v0.4.1/ctop-0.4.1-linux-amd64 -O 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
brew install ctop curl -Lo ctop https://github.com/bcicen/ctop/releases/download/v0.4.1/ctop-0.4.1-darwin-amd64
``` sudo mv ctop /usr/local/bin/
or
```bash
sudo curl -Lo /usr/local/bin/ctop https://github.com/bcicen/ctop/releases/download/v0.6.0/ctop-0.6.0-darwin-amd64
sudo chmod +x /usr/local/bin/ctop sudo chmod +x /usr/local/bin/ctop
``` ```
#### Docker or run via Docker:
```bash ```bash
docker run --rm -ti \ docker run -ti -v /var/run/docker.sock:/var/run/docker.sock quay.io/vektorlab/ctop:latest
--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/)
## Building ## Building
Build steps can be found [here][build]. To build `ctop` from source ensure you have a recent version of [glide](http://glide.sh/) installed.
```bash
cd $GOPATH/src/github.com/bcicen/ctop
glide install
```
## Usage ## Usage
`ctop` requires no arguments and uses Docker host variables by default. See [connectors][connectors] for further configuration options. `ctop` requires no arguments and will configure itself using the `DOCKER_HOST` environment variable
```bash
### Options export DOCKER_HOST=tcp://127.0.0.1:4243
ctop
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 (`esc` to clear when open) f | Filter displayed containers
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
[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.6.1 0.4.1

View File

@@ -1,20 +0,0 @@
# 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
```

View File

@@ -1,26 +0,0 @@
# 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,56 +0,0 @@
# Debug Mode
`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
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
```
Log messages can be followed by connecting to the default listen address:
```bash
curl -s localhost:9000
```
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...
...
```
## 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,23 +1,24 @@
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 -ti ctop -v - docker run -t --entrypoint /bin/sh quay.io/vektorlab/ctop:latest -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 ${IMAGE_NAME} - docker push quay.io/vektorlab/ctop:latest

View File

@@ -1,10 +1,6 @@
package main package main
import ( import ui "github.com/gizak/termui"
"regexp"
ui "github.com/gizak/termui"
)
/* /*
Valid colors: Valid colors:
@@ -42,17 +38,6 @@ 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,6 +18,7 @@ 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,6 +17,11 @@ 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

@@ -1,74 +0,0 @@
package collector
import (
"bufio"
"context"
"io"
"strings"
"time"
"github.com/bcicen/ctop/models"
api "github.com/fsouza/go-dockerclient"
)
type DockerLogs struct {
id string
client *api.Client
done chan bool
}
func (l *DockerLogs) Stream() chan models.Log {
r, w := io.Pipe()
logCh := make(chan models.Log)
ctx, cancel := context.WithCancel(context.Background())
opts := api.LogsOptions{
Context: ctx,
Container: l.id,
OutputStream: w,
ErrorStream: w,
Stdout: true,
Stderr: true,
Tail: "10",
Follow: true,
Timestamps: true,
}
// read io pipe into channel
go func() {
scanner := bufio.NewScanner(r)
for scanner.Scan() {
parts := strings.Split(scanner.Text(), " ")
ts := l.parseTime(parts[0])
logCh <- models.Log{ts, strings.Join(parts[1:], " ")}
}
}()
// connect to container log stream
go func() {
err := l.client.Logs(opts)
if err != nil {
log.Errorf("error reading container logs: %s", err)
}
}()
go func() {
select {
case <-l.done:
cancel()
}
}()
return logCh
}
func (l *DockerLogs) Stop() { l.done <- true }
func (l *DockerLogs) parseTime(s string) time.Time {
ts, err := time.Parse("2006-01-02T15:04:05.000000000Z", s)
if err != nil {
log.Errorf("failed to parse container log: %s", err)
ts = time.Now()
}
return ts
}

View File

@@ -1,35 +0,0 @@
package collector
import (
"math"
"github.com/bcicen/ctop/logging"
"github.com/bcicen/ctop/models"
)
var log = logging.Init()
type LogCollector interface {
Stream() chan models.Log
Stop()
}
type Collector interface {
Stream() chan models.Metrics
Logs() LogCollector
Running() bool
Start()
Stop()
}
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,31 +0,0 @@
package collector
import (
"time"
"github.com/bcicen/ctop/models"
)
const mockLog = "Cura ob pro qui tibi inveni dum qua fit donec amare illic mea, regem falli contexo pro peregrinorum heremo absconditi araneae meminerim deliciosas actionibus facere modico dura sonuerunt psalmi contra rerum, tempus mala anima volebant dura quae o modis."
type MockLogs struct {
done chan bool
}
func (l *MockLogs) Stream() chan models.Log {
logCh := make(chan models.Log)
go func() {
for {
select {
case <-l.done:
break
default:
logCh <- models.Log{time.Now(), mockLog}
time.Sleep(250 * time.Millisecond)
}
}
}()
return logCh
}
func (l *MockLogs) Stop() { l.done <- true }

View File

@@ -1,44 +0,0 @@
// +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
}

View File

@@ -1,128 +0,0 @@
// +build !darwin
package collector
import (
"time"
"github.com/bcicen/ctop/models"
"github.com/opencontainers/runc/libcontainer"
"github.com/opencontainers/runc/libcontainer/cgroups"
)
// Runc collector
type Runc struct {
models.Metrics
id string
libc libcontainer.Container
stream chan models.Metrics
done bool
running bool
interval int // collection interval, in seconds
lastCpu float64
lastSysCpu float64
}
func NewRunc(libc libcontainer.Container) *Runc {
c := &Runc{
Metrics: models.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 models.Metrics)
go c.run()
}
func (c *Runc) Stop() {
c.done = true
}
func (c *Runc) Stream() chan models.Metrics {
return c.stream
}
func (c *Runc) Logs() LogCollector {
return nil
}
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,7 +0,0 @@
// +build !linux
package connector
var enabled = map[string]func() Connector{
"docker": NewDocker,
}

View File

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

View File

@@ -1,26 +0,0 @@
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,243 +0,0 @@
// +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,32 +1,26 @@
package container package main
import ( import (
"github.com/bcicen/ctop/connector/collector"
"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/models"
)
var (
log = logging.Init()
) )
// Metrics and metadata representing a container // Metrics and metadata representing a container
type Container struct { type Container struct {
models.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 collector.Collector collector metrics.Collector
display bool // display this container in compact view
} }
func New(id string, collector collector.Collector) *Container { func NewContainer(id string, collector metrics.Collector) *Container {
widgets := compact.NewCompact(id) widgets := compact.NewCompact(id)
return &Container{ return &Container{
Metrics: models.NewMetrics(), Metrics: metrics.NewMetrics(),
Id: id, Id: id,
Meta: make(map[string]string), Meta: make(map[string]string),
Widgets: widgets, Widgets: widgets,
@@ -67,20 +61,15 @@ func (c *Container) SetState(s string) {
} }
} }
// Return container log collector
func (c *Container) Logs() collector.LogCollector {
return c.collector.Logs()
}
// Read metric stream, updating widgets // Read metric stream, updating widgets
func (c *Container) Read(stream chan models.Metrics) { func (c *Container) Read(stream chan metrics.Metrics) {
go func() { go func() {
for metrics := range stream { for metrics := range stream {
c.Metrics = metrics c.Metrics = metrics
c.updater.SetMetrics(metrics) c.updater.SetMetrics(metrics)
} }
log.Infof("reader stopped for container: %s", c.Id) log.Infof("reader stopped for container: %s", c.Id)
c.Metrics = models.NewMetrics() c.Metrics = metrics.NewMetrics()
c.Widgets.Reset() c.Widgets.Reset()
}() }()
log.Infof("reader started for container: %s", c.Id) log.Infof("reader started for container: %s", c.Id)

View File

@@ -1,23 +1,24 @@
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 container.Containers filtered Containers
cSource connector.Connector cSource ContainerSource
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.Container { func (gc *GridCursor) Selected() *Container {
idx := gc.Idx() idx := gc.Idx()
if idx < gc.Len() { if idx < gc.Len() {
return gc.filtered[idx] return gc.filtered[idx]
@@ -30,10 +31,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 = container.Containers{} gc.filtered = 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
} }
@@ -99,9 +100,6 @@ 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
@@ -118,9 +116,6 @@ 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
@@ -135,58 +130,3 @@ 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
}
nextidx := int(math.Max(0.0, float64(idx-cGrid.MaxRows())))
if gc.pgCount() > 0 {
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
}
nextidx := int(math.Min(float64(gc.Len()-1), float64(idx+cGrid.MaxRows())))
if gc.pgCount() > 0 {
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)
}
// number of pages at current row count and term height
func (gc *GridCursor) pgCount() int {
pages := gc.Len() / cGrid.MaxRows()
if gc.Len()%cGrid.MaxRows() > 0 {
pages++
}
return pages
}

View File

@@ -4,7 +4,7 @@ import (
ui "github.com/gizak/termui" ui "github.com/gizak/termui"
) )
var header *CompactHeader var header = NewCompactHeader()
type CompactGrid struct { type CompactGrid struct {
ui.GridBufferer ui.GridBufferer
@@ -16,20 +16,14 @@ type CompactGrid struct {
} }
func NewCompactGrid() *CompactGrid { func NewCompactGrid() *CompactGrid {
header = NewCompactHeader() // init column header
return &CompactGrid{} return &CompactGrid{}
} }
func (cg *CompactGrid) Align() { func (cg *CompactGrid) Align() {
y := cg.Y y := cg.Y
if cg.Offset >= len(cg.Rows) { if cg.Offset >= len(cg.Rows) {
cg.Offset = 0 cg.Offset = 0
} }
if cg.Offset < 0 {
cg.Offset = 0
}
// update row ypos, width recursively // update row ypos, width recursively
for _, r := range cg.pageRows() { for _, r := range cg.pageRows() {
r.SetY(y) r.SetY(y)

View File

@@ -12,7 +12,7 @@ type CompactHeader struct {
} }
func NewCompactHeader() *CompactHeader { func NewCompactHeader() *CompactHeader {
fields := []string{"", "NAME", "CID", "CPU", "MEM", "NET RX/TX", "IO R/W", "PIDS"} fields := []string{"", "NAME", "CID", "CPU", "MEM", "NET RX/TX"}
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) autoWidth := calcWidth(w, 5)
for n, col := range ch.pars { for n, col := range ch.pars {
// set column to static width // set status column to static width
if colWidths[n] != 0 { if n == 0 {
col.SetX(x) col.SetX(x)
col.SetWidth(colWidths[n]) col.SetWidth(statusWidth)
x += colWidths[n] x += statusWidth
continue continue
} }
col.SetX(x) col.SetX(x)

View File

@@ -2,7 +2,7 @@ package compact
import ( import (
"github.com/bcicen/ctop/logging" "github.com/bcicen/ctop/logging"
"github.com/bcicen/ctop/models" "github.com/bcicen/ctop/metrics"
ui "github.com/gizak/termui" ui "github.com/gizak/termui"
) )
@@ -15,8 +15,6 @@ 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
@@ -34,8 +32,6 @@ 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,12 +55,10 @@ func (row *Compact) SetMeta(k, v string) {
} }
} }
func (row *Compact) SetMetrics(m models.Metrics) { 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
@@ -72,8 +66,6 @@ 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 {
@@ -99,12 +91,13 @@ func (row *Compact) SetWidth(width int) {
return return
} }
x := row.X x := row.X
autoWidth := calcWidth(width) autoWidth := calcWidth(width, 5)
for n, col := range row.all() { for n, col := range row.all() {
if colWidths[n] != 0 { // set status column to static width
if n == 0 {
col.SetX(x) col.SetX(x)
col.SetWidth(colWidths[n]) col.SetWidth(statusWidth)
x += colWidths[n] x += statusWidth
continue continue
} }
col.SetX(x) col.SetX(x)
@@ -123,8 +116,7 @@ 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
} }
@@ -136,7 +128,5 @@ 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,16 +13,6 @@ 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))
@@ -30,9 +20,6 @@ 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.ThemeAttr("par.text.hi") w.TextFgColor = ui.ColorBlack
w.TextBgColor = ui.ThemeAttr("par.text.fg") w.TextBgColor = ui.ThemeAttr("par.text.fg")
} }

View File

@@ -9,29 +9,10 @@ import (
const colSpacing = 1 const colSpacing = 1
// per-column width. 0 == auto width // Calculate per-column width, given total width and number of items
var colWidths = []int{ func calcWidth(width, items int) int {
3, // status spacing := colSpacing * items
0, // name return (width - statusWidth - spacing) / items
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) {

View File

@@ -1,12 +1,10 @@
package expanded package expanded
import ( import (
"strings"
ui "github.com/gizak/termui" ui "github.com/gizak/termui"
) )
var displayInfo = []string{"id", "name", "image", "ports", "state", "created"} var displayInfo = []string{"id", "name", "image", "state"}
type Info struct { type Info struct {
*ui.Table *ui.Table
@@ -26,33 +24,12 @@ 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, mkInfoRows(k, v)...) w.Rows = append(w.Rows, []string{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

@@ -1,51 +0,0 @@
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

@@ -1,83 +0,0 @@
package expanded
import (
"time"
"github.com/bcicen/ctop/models"
ui "github.com/gizak/termui"
)
type LogLines struct {
ts []time.Time
data []string
}
func NewLogLines(max int) *LogLines {
ll := &LogLines{
ts: make([]time.Time, max),
data: make([]string, max),
}
return ll
}
func (ll *LogLines) tail(n int) []string {
lines := make([]string, n)
for i := 0; i < n; i++ {
lines = append(lines, ll.data[len(ll.data)-i])
}
return lines
}
func (ll *LogLines) getLines(start, end int) []string {
if end < 0 {
return ll.data[start:]
}
return ll.data[start:end]
}
func (ll *LogLines) add(l models.Log) {
if len(ll.data) == cap(ll.data) {
ll.data = append(ll.data[:0], ll.data[1:]...)
ll.ts = append(ll.ts[:0], ll.ts[1:]...)
}
ll.ts = append(ll.ts, l.Timestamp)
ll.data = append(ll.data, l.Message)
log.Debugf("recorded log line: %v", l)
}
type Logs struct {
*ui.List
lines *LogLines
}
func NewLogs(stream chan models.Log) *Logs {
p := ui.NewList()
p.Y = ui.TermHeight() / 2
p.X = 0
p.Height = ui.TermHeight() - p.Y
p.Width = ui.TermWidth()
//p.Overflow = "wrap"
p.ItemFgColor = ui.ThemeAttr("par.text.fg")
i := &Logs{p, NewLogLines(4098)}
go func() {
for line := range stream {
i.lines.add(line)
ui.Render(i)
}
}()
return i
}
func (w *Logs) Align() {
w.X = colWidth[0]
w.List.Align()
}
func (w *Logs) Buffer() ui.Buffer {
maxLines := w.Height - 2
offset := len(w.lines.data) - maxLines
w.Items = w.lines.getLines(offset, -1)
return w.List.Buffer()
}
// number of rows a line will occupy at current panel width
func (w *Logs) lineHeight(s string) int { return (len(s) / w.InnerWidth()) + 1 }

View File

@@ -2,7 +2,7 @@ package expanded
import ( import (
"github.com/bcicen/ctop/logging" "github.com/bcicen/ctop/logging"
"github.com/bcicen/ctop/models" "github.com/bcicen/ctop/metrics"
ui "github.com/gizak/termui" ui "github.com/gizak/termui"
) )
@@ -17,8 +17,6 @@ type Expanded struct {
Net *Net Net *Net
Cpu *Cpu Cpu *Cpu
Mem *Mem Mem *Mem
IO *IO
X, Y int
Width int Width int
} }
@@ -31,59 +29,30 @@ 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) Up() { func (e *Expanded) SetWidth(w int) {
if e.Y < 0 { e.Width = w
e.Y++
e.Align()
ui.Render(e)
}
} }
func (e *Expanded) Down() { func (e *Expanded) SetMeta(k, v string) {
if e.Y > (ui.TermHeight() - e.GetHeight()) { e.Info.Set(k, v)
e.Y--
e.Align()
ui.Render(e)
}
} }
func (e *Expanded) SetWidth(w int) { e.Width = w } func (e *Expanded) SetMetrics(m metrics.Metrics) {
func (e *Expanded) SetMeta(k, v string) { e.Info.Set(k, v) }
func (e *Expanded) SetMetrics(m models.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() {
// reset offset if needed y := 0
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)
} }
@@ -105,7 +74,6 @@ 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
} }
@@ -115,7 +83,6 @@ func (e *Expanded) all() []ui.GridBufferer {
e.Cpu, e.Cpu,
e.Mem, e.Mem,
e.Net, e.Net,
e.IO,
} }
} }

View File

@@ -2,12 +2,12 @@ package cwidgets
import ( import (
"github.com/bcicen/ctop/logging" "github.com/bcicen/ctop/logging"
"github.com/bcicen/ctop/models" "github.com/bcicen/ctop/metrics"
) )
var log = logging.Init() var log = logging.Init()
type WidgetUpdater interface { type WidgetUpdater interface {
SetMeta(string, string) SetMeta(string, string)
SetMetrics(models.Metrics) SetMetrics(metrics.Metrics)
} }

View File

@@ -9,7 +9,6 @@ const (
kb = 1024 kb = 1024
mb = kb * 1024 mb = kb * 1024
gb = mb * 1024 gb = mb * 1024
tb = gb * 1024
) )
// convenience method // convenience method
@@ -29,12 +28,8 @@ 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 {

View File

@@ -3,14 +3,10 @@ package main
import ( import (
"fmt" "fmt"
"reflect" "reflect"
"runtime"
"github.com/bcicen/ctop/container"
ui "github.com/gizak/termui" ui "github.com/gizak/termui"
) )
var mstats = &runtime.MemStats{}
func logEvent(e ui.Event) { func logEvent(e ui.Event) {
var s string var s string
s += fmt.Sprintf("Type=%s", quote(e.Type)) s += fmt.Sprintf("Type=%s", quote(e.Type))
@@ -22,24 +18,8 @@ func logEvent(e ui.Event) {
log.Debugf("new event: %s", s) log.Debugf("new event: %s", s)
} }
func runtimeStats() {
var msg string
msg += fmt.Sprintf("cgo calls=%v", runtime.NumCgoCall())
msg += fmt.Sprintf(" routines=%v", runtime.NumGoroutine())
runtime.ReadMemStats(mstats)
msg += fmt.Sprintf(" numgc=%v", mstats.NumGC)
msg += fmt.Sprintf(" alloc=%v", mstats.Alloc)
log.Debugf("runtime: %v", msg)
}
func runtimeStack() {
buf := make([]byte, 32768)
buf = buf[:runtime.Stack(buf, true)]
log.Infof(fmt.Sprintf("stack:\n%v", string(buf)))
}
// log container, metrics, and widget state // log container, metrics, and widget state
func dumpContainer(c *container.Container) { func dumpContainer(c *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)

View File

@@ -1,31 +1,35 @@
package connector package main
import ( import (
"fmt" "sort"
"strings" "strings"
"sync" "sync"
"github.com/bcicen/ctop/connector/collector" "github.com/bcicen/ctop/metrics"
"github.com/bcicen/ctop/container" "github.com/fsouza/go-dockerclient"
api "github.com/fsouza/go-dockerclient"
) )
type Docker struct { type ContainerSource interface {
client *api.Client All() Containers
containers map[string]*container.Container Get(string) (*Container, bool)
}
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 NewDocker() Connector { func NewDockerContainerSource() *DockerContainerSource {
// init docker client // init docker client
client, err := api.NewClientFromEnv() client, err := docker.NewClientFromEnv()
if err != nil { if err != nil {
panic(err) panic(err)
} }
cm := &Docker{ cm := &DockerContainerSource{
client: client, client: client,
containers: make(map[string]*container.Container), containers: make(map[string]*Container),
needsRefresh: make(chan string, 60), needsRefresh: make(chan string, 60),
lock: sync.RWMutex{}, lock: sync.RWMutex{},
} }
@@ -36,9 +40,9 @@ func NewDocker() Connector {
} }
// Docker events watcher // Docker events watcher
func (cm *Docker) watchEvents() { func (cm *DockerContainerSource) watchEvents() {
log.Info("docker event listener starting") log.Info("docker event listener starting")
events := make(chan *api.APIEvents) events := make(chan *docker.APIEvents)
cm.client.AddEventListener(events) cm.client.AddEventListener(events)
for e := range events { for e := range events {
@@ -56,25 +60,7 @@ func (cm *Docker) watchEvents() {
} }
} }
func portsFormat(ports map[api.Port][]api.PortBinding) string { func (cm *DockerContainerSource) refresh(c *Container) {
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", binding.HostIP, binding.HostPort, k)
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 {
@@ -83,15 +69,14 @@ func (cm *Docker) refresh(c *container.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 *Docker) inspect(id string) *api.Container { func (cm *DockerContainerSource) inspect(id string) *docker.Container {
c, err := cm.client.InspectContainer(id) c, err := cm.client.InspectContainer(id)
if err != nil { if err != nil {
if _, ok := err.(*api.NoSuchContainer); ok == false { if _, ok := err.(*docker.NoSuchContainer); ok == false {
log.Errorf(err.Error()) log.Errorf(err.Error())
} }
} }
@@ -99,8 +84,8 @@ func (cm *Docker) inspect(id string) *api.Container {
} }
// Mark all container IDs for refresh // Mark all container IDs for refresh
func (cm *Docker) refreshAll() { func (cm *DockerContainerSource) refreshAll() {
opts := api.ListContainersOptions{All: true} opts := docker.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)
@@ -114,7 +99,7 @@ func (cm *Docker) refreshAll() {
} }
} }
func (cm *Docker) Loop() { func (cm *DockerContainerSource) 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)
@@ -122,14 +107,14 @@ func (cm *Docker) Loop() {
} }
// Get a single container, creating one anew if not existing // Get a single container, creating one anew if not existing
func (cm *Docker) MustGet(id string) *container.Container { func (cm *DockerContainerSource) MustGet(id string) *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 := collector.NewDocker(cm.client, id) collector := metrics.NewDocker(cm.client, id)
// create container // create container
c = container.New(id, collector) c = NewContainer(id, collector)
cm.lock.Lock() cm.lock.Lock()
cm.containers[id] = c cm.containers[id] = c
cm.lock.Unlock() cm.lock.Unlock()
@@ -138,7 +123,7 @@ func (cm *Docker) MustGet(id string) *container.Container {
} }
// Get a single container, by ID // Get a single container, by ID
func (cm *Docker) Get(id string) (*container.Container, bool) { func (cm *DockerContainerSource) Get(id string) (*Container, bool) {
cm.lock.Lock() cm.lock.Lock()
c, ok := cm.containers[id] c, ok := cm.containers[id]
cm.lock.Unlock() cm.lock.Unlock()
@@ -146,7 +131,7 @@ func (cm *Docker) Get(id string) (*container.Container, bool) {
} }
// Remove containers by ID // Remove containers by ID
func (cm *Docker) delByID(id string) { func (cm *DockerContainerSource) delByID(id string) {
cm.lock.Lock() cm.lock.Lock()
delete(cm.containers, id) delete(cm.containers, id)
cm.lock.Unlock() cm.lock.Unlock()
@@ -154,14 +139,14 @@ func (cm *Docker) delByID(id string) {
} }
// Return array of all containers, sorted by field // Return array of all containers, sorted by field
func (cm *Docker) All() (containers container.Containers) { func (cm *DockerContainerSource) All() (containers 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)
} }
containers.Sort()
containers.Filter()
cm.lock.Unlock() cm.lock.Unlock()
sort.Sort(containers)
containers.Filter()
return containers return containers
} }

54
glide.lock generated
View File

@@ -1,22 +1,12 @@
hash: 0d550b01b3a1c4751a8f5c3fba0c43f62252055e231712729628e514bb494da8 hash: c13011881e895378f374b68596a59a0ea6def372b4f5239b2d8aa342eaa46a4b
updated: 2017-06-09T18:11:10.930196504-03:00 updated: 2017-03-12T09:53:35.212073637+07: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: 90d35abf7b3535c1c319c872900fbd76374e521c version: ce07fb6b0f1b8765b92022e45f96bd4349812e06
subpackages: subpackages:
- api/types - api/types
- api/types/blkiodev - api/types/blkiodev
@@ -37,11 +27,9 @@ 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
@@ -57,12 +45,6 @@ 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
@@ -81,41 +63,15 @@ 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: baf6536d6259209c3edfa2b22237af82942d3dfa version: 31980a53ae7887b2c8f8715d13c3eb486c27b6cf
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: 26709e2714106fb8ad40b773b711ebce25b78914 version: 1deb2db2a6fff8a35532079061b903c3a25eed52
- 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,18 +1,11 @@
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

46
grid.go
View File

@@ -2,7 +2,6 @@ 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"
) )
@@ -35,7 +34,7 @@ func RedrawRows(clr bool) {
ui.Render(cGrid) ui.Render(cGrid)
} }
func ExpandView(c *container.Container) { func ExpandView(c *Container) {
ui.Clear() ui.Clear()
ui.DefaultEvtStream.ResetHandlers() ui.DefaultEvtStream.ResetHandlers()
defer ui.DefaultEvtStream.ResetHandlers() defer ui.DefaultEvtStream.ResetHandlers()
@@ -45,28 +44,25 @@ func ExpandView(c *container.Container) {
ex.Align() ex.Align()
ui.Render(ex) ui.Render(ex)
ui.Handle("/timer/1s", func(ui.Event) {
HandleKeys("up", ex.Up) ui.Render(ex)
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)
} }
func RefreshDisplay() { func RefreshDisplay() {
// skip display refresh during scroll needsClear := cursor.RefreshContainers()
if !cursor.isScrolling { RedrawRows(needsClear)
needsClear := cursor.RefreshContainers()
RedrawRows(needsClear)
}
} }
func Display() bool { func Display() bool {
@@ -81,22 +77,16 @@ func Display() bool {
cursor.RefreshContainers() cursor.RefreshContainers()
RedrawRows(true) RedrawRows(true)
HandleKeys("up", cursor.Up) ui.Handle("/sys/kbd/<up>", func(ui.Event) { cursor.Up() })
HandleKeys("down", cursor.Down) ui.Handle("/sys/kbd/<down>", func(ui.Event) { cursor.Down() })
HandleKeys("pgup", cursor.PgUp)
HandleKeys("pgdown", cursor.PgDown)
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()
@@ -108,6 +98,10 @@ 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)

41
keys.go
View File

@@ -1,41 +0,0 @@
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",
},
"pgup": []string{
"/sys/kbd/<previous>",
"/sys/kbd/C-<up>",
},
"pgdown": []string{
"/sys/kbd/<next>",
"/sys/kbd/C-<down>",
},
"exit": []string{
"/sys/kbd/q",
"/sys/kbd/C-c",
"/sys/kbd/<escape>",
},
"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,7 +1,6 @@
package logging package logging
import ( import (
"os"
"time" "time"
"github.com/op/go-logging" "github.com/op/go-logging"
@@ -14,7 +13,7 @@ const (
var ( var (
Log *CTopLogger Log *CTopLogger
exited bool exited bool
level = logging.INFO // default level level = logging.INFO
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}`,
) )
@@ -34,11 +33,6 @@ 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, "")
@@ -77,6 +71,3 @@ func (log *CTopLogger) Exit() {
exited = true exited = true
StopServer() StopServer()
} }
func debugMode() bool { return os.Getenv("CTOP_DEBUG") == "1" }
func debugModeTCP() bool { return os.Getenv("CTOP_DEBUG_TCP") == "1" }

View File

@@ -7,8 +7,7 @@ import (
) )
const ( const (
socketPath = "./ctop.sock" path = "/tmp/ctop.sock"
socketAddr = "0.0.0.0:9000"
) )
var server struct { var server struct {
@@ -17,13 +16,7 @@ var server struct {
} }
func getListener() net.Listener { func getListener() net.Listener {
var ln net.Listener ln, err := net.Listen("unix", path)
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)
} }

118
main.go
View File

@@ -1,129 +1,84 @@
package main package main
import ( import (
"flag"
"fmt" "fmt"
"os" "os"
"runtime"
"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 (
build = "none" build = "none"
version = "dev-build" version = "dev-build"
goVersion = runtime.Version()
log *logging.CTopLogger log *logging.CTopLogger
cursor *GridCursor cursor *GridCursor
cGrid *compact.CompactGrid cGrid *compact.CompactGrid
header *widgets.CTopHeader header *widgets.CTopHeader
versionStr = fmt.Sprintf("ctop version %v, build %v %v", version, build, goVersion)
) )
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")
var connectorFlag = flag.String("connector", "docker", "container connector to use")
flag.Parse()
if *versionFlag {
fmt.Println(versionStr)
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 Shutdown() // init global config
// init grid, cursor, header config.Init()
conn, err := connector.ByName(*connectorFlag)
if err != nil { // init logger
panic(err) log = logging.Init()
if config.GetSwitchVal("loggingEnabled") {
logging.StartServer()
} }
cursor = &GridCursor{cSource: conn}
// init grid, cursor, header
cursor = NewGridCursor()
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() { func readArgs() {
log.Notice("shutting down") if len(os.Args) < 2 {
log.Exit() return
if tm.IsInit {
ui.Close()
} }
} for _, arg := range os.Args[1:] {
switch arg {
// ensure a given sort field is valid case "-v", "version":
func validSort(s string) { printVersion()
if _, ok := container.Sorters[s]; !ok { os.Exit(0)
fmt.Printf("invalid sort field: %s\n", s) case "-h", "help":
os.Exit(1) printHelp()
os.Exit(0)
default:
fmt.Printf("invalid option or argument: \"%s\"\n", arg)
os.Exit(1)
}
} }
} }
func panicExit() { func panicExit() {
if r := recover(); r != nil { if r := recover(); r != nil {
Shutdown() ui.Clear()
fmt.Printf("error: %s\n", r) fmt.Printf("panic: %s\n", r)
os.Exit(1) os.Exit(1)
} }
} }
@@ -133,9 +88,14 @@ 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() {
fmt.Printf("ctop version %v, build %v\n", version, build)
} }

View File

@@ -2,7 +2,6 @@ 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"
@@ -40,7 +39,6 @@ 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
@@ -54,10 +52,6 @@ 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()
@@ -75,22 +69,18 @@ func SortMenu() {
m.SortItems = true m.SortItems = true
m.BorderLabel = "Sort Field" m.BorderLabel = "Sort Field"
for _, field := range container.SortFields() { for _, field := range SortFields() {
m.AddItems(menu.Item{field, ""}) m.AddItems(menu.Item{field, ""})
} }
// set cursor position to current sort field // set cursor position to current sort field
m.SetCursor(config.GetVal("sortField")) m.SetCursor(config.GetVal("sortField"))
HandleKeys("up", m.Up) ui.Render(m)
HandleKeys("down", m.Down) m.NavigationHandlers()
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

@@ -1,17 +1,16 @@
package collector package metrics
import ( import (
"github.com/bcicen/ctop/models"
api "github.com/fsouza/go-dockerclient" api "github.com/fsouza/go-dockerclient"
) )
// Docker collector // Docker collector
type Docker struct { type Docker struct {
models.Metrics Metrics
id string id string
client *api.Client client *api.Client
running bool running bool
stream chan models.Metrics stream chan Metrics
done chan bool done chan bool
lastCpu float64 lastCpu float64
lastSysCpu float64 lastSysCpu float64
@@ -19,7 +18,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: models.Metrics{}, Metrics: Metrics{},
id: id, id: id,
client: client, client: client,
} }
@@ -27,7 +26,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 models.Metrics) c.stream = make(chan Metrics)
stats := make(chan *api.Stats) stats := make(chan *api.Stats)
go func() { go func() {
@@ -47,7 +46,6 @@ 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)
@@ -61,14 +59,10 @@ func (c *Docker) Running() bool {
return c.running return c.running
} }
func (c *Docker) Stream() chan models.Metrics { func (c *Docker) Stream() chan Metrics {
return c.stream return c.stream
} }
func (c *Docker) Logs() LogCollector {
return &DockerLogs{c.id, c.client, make(chan bool)}
}
// Stop collector // Stop collector
func (c *Docker) Stop() { func (c *Docker) Stop() {
c.done <- true c.done <- true
@@ -85,13 +79,12 @@ 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) {
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 = percent(float64(c.MemUsage), float64(c.MemLimit)) c.MemPercent = round((float64(c.MemUsage) / float64(c.MemLimit)) * 100)
} }
func (c *Docker) ReadNet(stats *api.Stats) { func (c *Docker) ReadNet(stats *api.Stats) {
@@ -102,16 +95,3 @@ 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
}

39
metrics/main.go Normal file
View File

@@ -0,0 +1,39 @@
package metrics
import (
"math"
"github.com/bcicen/ctop/logging"
)
var log = logging.Init()
type Metrics struct {
CPUUtil int
NetTx int64
NetRx int64
MemLimit int64
MemPercent int
MemUsage int64
}
func NewMetrics() Metrics {
return Metrics{
CPUUtil: -1,
NetTx: -1,
NetRx: -1,
MemUsage: -1,
MemPercent: -1,
}
}
type Collector interface {
Stream() chan Metrics
Running() bool
Start()
Stop()
}
func round(num float64) int {
return int(num + math.Copysign(0.5, num))
}

View File

@@ -1,18 +1,16 @@
// +build !release // +build !release
package collector package metrics
import ( import (
"math/rand" "math/rand"
"time" "time"
"github.com/bcicen/ctop/models"
) )
// Mock collector // Mock collector
type Mock struct { type Mock struct {
models.Metrics Metrics
stream chan models.Metrics stream chan Metrics
done bool done bool
running bool running bool
aggression int64 aggression int64
@@ -20,7 +18,7 @@ type Mock struct {
func NewMock(a int64) *Mock { func NewMock(a int64) *Mock {
c := &Mock{ c := &Mock{
Metrics: models.Metrics{}, Metrics: Metrics{},
aggression: a, aggression: a,
} }
c.MemLimit = 2147483648 c.MemLimit = 2147483648
@@ -33,7 +31,7 @@ func (c *Mock) Running() bool {
func (c *Mock) Start() { func (c *Mock) Start() {
c.done = false c.done = false
c.stream = make(chan models.Metrics) c.stream = make(chan Metrics)
go c.run() go c.run()
} }
@@ -41,24 +39,15 @@ func (c *Mock) Stop() {
c.done = true c.done = true
} }
func (c *Mock) Stream() chan models.Metrics { func (c *Mock) Stream() chan Metrics {
return c.stream return c.stream
} }
func (c *Mock) Logs() LogCollector {
return &MockLogs{make(chan bool)}
}
func (c *Mock) run() { func (c *Mock) run() {
c.running = true c.running = true
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 {
@@ -71,7 +60,7 @@ func (c *Mock) run() {
if c.MemUsage > c.MemLimit { if c.MemUsage > c.MemLimit {
c.MemUsage = 0 c.MemUsage = 0
} }
c.MemPercent = percent(float64(c.MemUsage), float64(c.MemLimit)) c.MemPercent = round((float64(c.MemUsage) / float64(c.MemLimit)) * 100)
c.stream <- c.Metrics c.stream <- c.Metrics
if c.done { if c.done {
break break

View File

@@ -1,31 +1,31 @@
// +build !release // +build !release
package connector package main
import ( import (
"math/rand" "math/rand"
"sort"
"strings" "strings"
"time" "time"
"github.com/bcicen/ctop/connector/collector" "github.com/bcicen/ctop/metrics"
"github.com/bcicen/ctop/container"
"github.com/jgautheron/codename-generator" "github.com/jgautheron/codename-generator"
"github.com/nu7hatch/gouuid" "github.com/nu7hatch/gouuid"
) )
type Mock struct { type MockContainerSource struct {
containers container.Containers containers Containers
} }
func NewMock() *Mock { func NewMockContainerSource() *MockContainerSource {
cs := &Mock{} cs := &MockContainerSource{}
go cs.Init() go cs.Init()
go cs.Loop() go cs.Loop()
return cs return cs
} }
// Create Mock containers // Create Mock containers
func (cs *Mock) Init() { func (cs *MockContainerSource) 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 *Mock) Init() {
} }
func (cs *Mock) makeContainer(aggression int64) { func (cs *MockContainerSource) makeContainer(aggression int64) {
collector := collector.NewMock(aggression) collector := metrics.NewMock(aggression)
c := container.New(makeID(), collector) c := NewContainer(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 *Mock) Loop() { func (cs *MockContainerSource) Loop() {
iter := 0 iter := 0
for { for {
// Change state for random container // Change state for random container
@@ -60,7 +60,7 @@ func (cs *Mock) Loop() {
} }
// Get a single container, by ID // Get a single container, by ID
func (cs *Mock) Get(id string) (*container.Container, bool) { func (cs *MockContainerSource) Get(id string) (*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 *Mock) Get(id string) (*container.Container, bool) {
} }
// Return array of all containers, sorted by field // Return array of all containers, sorted by field
func (cs *Mock) All() container.Containers { func (cs *MockContainerSource) All() Containers {
cs.containers.Sort() sort.Sort(cs.containers)
cs.containers.Filter() cs.containers.Filter()
return cs.containers return cs.containers
} }
// Remove containers by ID // Remove containers by ID
func (cs *Mock) delByID(id string) { func (cs *MockContainerSource) 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 *Mock) delByID(id string) {
} }
// Remove one or more containers by index // Remove one or more containers by index
func (cs *Mock) del(idx ...int) { func (cs *MockContainerSource) 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:]...)
} }

View File

@@ -1,33 +0,0 @@
package models
import "time"
type Log struct {
Timestamp time.Time
Message string
}
type Metrics struct {
CPUUtil int
NetTx int64
NetRx int64
MemLimit int64
MemPercent int
MemUsage int64
IOBytesRead int64
IOBytesWrite int64
Pids int
}
func NewMetrics() Metrics {
return Metrics{
CPUUtil: -1,
NetTx: -1,
NetRx: -1,
MemUsage: -1,
MemPercent: -1,
IOBytesRead: -1,
IOBytesWrite: -1,
Pids: -1,
}
}

View File

@@ -1,9 +1,8 @@
package container package main
import ( import (
"fmt" "fmt"
"regexp" "regexp"
"sort"
"github.com/bcicen/ctop/config" "github.com/bcicen/ctop/config"
) )
@@ -54,22 +53,6 @@ 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")
@@ -90,7 +73,6 @@ 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 {
@@ -106,18 +88,16 @@ 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
} }
} }
} }
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 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_." input_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_."
) )
type Padding [2]int // x,y padding type Padding [2]int // x,y padding

View File

@@ -100,20 +100,27 @@ func (m *Menu) Buffer() ui.Buffer {
return buf return buf
} }
func (m *Menu) Up() { func (m *Menu) Up(ui.Event) {
if m.cursorPos > 0 { if m.cursorPos > 0 {
m.cursorPos-- m.cursorPos--
ui.Render(m) ui.Render(m)
} }
} }
func (m *Menu) Down() { func (m *Menu) Down(ui.Event) {
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