mirror of
https://github.com/bcicen/ctop.git
synced 2025-12-06 23:26:45 +08:00
Compare commits
130 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2011b8bc7 | ||
|
|
40fd9e935a | ||
|
|
b88c143914 | ||
|
|
0a05007c4e | ||
|
|
c47ba3f804 | ||
|
|
79a3f361a7 | ||
|
|
65399a37e5 | ||
|
|
25a3fcf731 | ||
|
|
17e2c2df8e | ||
|
|
240345d527 | ||
|
|
2d284d9277 | ||
|
|
bfa5c5944f | ||
|
|
e1051cd40f | ||
|
|
13029cc7fe | ||
|
|
58d9e4e194 | ||
|
|
4de7036e2f | ||
|
|
617b1b2863 | ||
|
|
e59a360b60 | ||
|
|
91cd53a878 | ||
|
|
c6f2c7b617 | ||
|
|
568bfb2513 | ||
|
|
66ff8ad7ec | ||
|
|
400d9471b6 | ||
|
|
288380ca8d | ||
|
|
429d5b9101 | ||
|
|
1be452d7c0 | ||
|
|
671c944272 | ||
|
|
a48a9031cc | ||
|
|
aff6943d07 | ||
|
|
f56ff96b88 | ||
|
|
b5361c2a28 | ||
|
|
e71b6cacce | ||
|
|
8a1297d3c5 | ||
|
|
bdea7d5853 | ||
|
|
389dee0f3c | ||
|
|
b49e174483 | ||
|
|
53b612ab07 | ||
|
|
446708e456 | ||
|
|
1233ff0ead | ||
|
|
af3f1e2a85 | ||
|
|
4dbc5653ff | ||
|
|
e8d9f3327c | ||
|
|
d372043a17 | ||
|
|
eeac65da8c | ||
|
|
c1780ae30a | ||
|
|
fb39d69fa7 | ||
|
|
6392d63ff8 | ||
|
|
b009a260a4 | ||
|
|
44379cd9fd | ||
|
|
b85ca680f0 | ||
|
|
8fb7a8988f | ||
|
|
6d097c2085 | ||
|
|
f9d68d688d | ||
|
|
bc08b85191 | ||
|
|
f3d26e038d | ||
|
|
b4e1fbf290 | ||
|
|
58d5fba945 | ||
|
|
c76036a6f2 | ||
|
|
6a8848d1e2 | ||
|
|
02d1050130 | ||
|
|
ccb44c964c | ||
|
|
b2165b6a29 | ||
|
|
d81d10ec27 | ||
|
|
9529c04680 | ||
|
|
6a89c9af38 | ||
|
|
06a29fc912 | ||
|
|
2cba7253fc | ||
|
|
47d60fe51b | ||
|
|
28f16c9a17 | ||
|
|
6560768e08 | ||
|
|
084c0c4ec8 | ||
|
|
5db90f31dc | ||
|
|
82677d52ef | ||
|
|
2b2338805b | ||
|
|
60213f1551 | ||
|
|
8aa932b29f | ||
|
|
35cc8d095d | ||
|
|
30530bc2a1 | ||
|
|
2c282923c0 | ||
|
|
d0d39749de | ||
|
|
26b88a9790 | ||
|
|
a135a67c06 | ||
|
|
19b212f45d | ||
|
|
34987df010 | ||
|
|
e2bc4d0a08 | ||
|
|
4ac1348fbb | ||
|
|
66d78a7d74 | ||
|
|
e62a8881a2 | ||
|
|
a5b2e7b074 | ||
|
|
a87bdce0fe | ||
|
|
2228188ebf | ||
|
|
e94a9c0cc2 | ||
|
|
e82d77ecb0 | ||
|
|
50b4181866 | ||
|
|
1285288b9e | ||
|
|
2a709577bd | ||
|
|
38599bbd19 | ||
|
|
b3cdb33efc | ||
|
|
0ac70c96eb | ||
|
|
36a5bbdfe1 | ||
|
|
3553b0af9d | ||
|
|
ca61ec712e | ||
|
|
06c4b24212 | ||
|
|
12fa716825 | ||
|
|
8327406069 | ||
|
|
2134110224 | ||
|
|
77c3d00e67 | ||
|
|
85eb5228ae | ||
|
|
3a3950e395 | ||
|
|
eaac079b15 | ||
|
|
ab1ccb3cd8 | ||
|
|
dbaebe0192 | ||
|
|
d5ef818c8d | ||
|
|
8203d0b883 | ||
|
|
b28beed3ee | ||
|
|
2e51406d00 | ||
|
|
c84b52ce40 | ||
|
|
4ee8cf621a | ||
|
|
192298c045 | ||
|
|
258536740d | ||
|
|
ef69744249 | ||
|
|
07f95a04b0 | ||
|
|
b2184bbc6d | ||
|
|
96b01eb3b9 | ||
|
|
03d4869361 | ||
|
|
4b7257908f | ||
|
|
1875013a76 | ||
|
|
dab2f926b9 | ||
|
|
ddce54f991 | ||
|
|
168e8f3aae |
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
ctop
|
||||
.idea
|
||||
16
Dockerfile
Normal file
16
Dockerfile
Normal file
@@ -0,0 +1,16 @@
|
||||
FROM quay.io/vektorcloud/go:1.8
|
||||
|
||||
RUN apk add --no-cache make
|
||||
|
||||
COPY glide.* /go/src/github.com/bcicen/ctop/
|
||||
WORKDIR /go/src/github.com/bcicen/ctop/
|
||||
RUN glide install
|
||||
|
||||
COPY . /go/src/github.com/bcicen/ctop
|
||||
RUN make build && \
|
||||
mkdir -p /go/bin && \
|
||||
mv -v ctop /go/bin/
|
||||
|
||||
FROM scratch
|
||||
COPY --from=0 /go/bin/ctop /ctop
|
||||
ENTRYPOINT ["/ctop"]
|
||||
34
Makefile
Normal file
34
Makefile
Normal file
@@ -0,0 +1,34 @@
|
||||
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
|
||||
56
README.md
56
README.md
@@ -1,14 +1,17 @@
|
||||
<p align="center"><img width="200px" src="/_docs/img/logo.png" alt="ctop"/></p>
|
||||
|
||||
#
|
||||
|
||||
![release][release] ![homebrew][homebrew]
|
||||
|
||||
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:
|
||||
<p align="center"><img src="_docs/img/grid.gif" alt="ctop"/></p>
|
||||
|
||||
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
|
||||
|
||||
@@ -17,37 +20,66 @@ Fetch the [latest release](https://github.com/bcicen/ctop/releases) for your pla
|
||||
#### Linux
|
||||
|
||||
```bash
|
||||
wget https://github.com/bcicen/ctop/releases/download/v0.4/ctop-0.4-linux-amd64 -O ctop
|
||||
sudo mv ctop /usr/local/bin/
|
||||
sudo wget https://github.com/bcicen/ctop/releases/download/v0.6.0/ctop-0.6.0-linux-amd64 -O /usr/local/bin/ctop
|
||||
sudo chmod +x /usr/local/bin/ctop
|
||||
```
|
||||
|
||||
#### OS X
|
||||
|
||||
```bash
|
||||
curl -Lo ctop https://github.com/bcicen/ctop/releases/download/v0.4/ctop-0.4-darwin-amd64
|
||||
sudo mv ctop /usr/local/bin/
|
||||
brew install ctop
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
#### Docker
|
||||
|
||||
```bash
|
||||
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/)
|
||||
|
||||
## Building
|
||||
|
||||
Build steps can be found [here][build].
|
||||
|
||||
## Usage
|
||||
|
||||
ctop requires no arguments and will configure itself using the `DOCKER_HOST` environment variable
|
||||
```bash
|
||||
export DOCKER_HOST=tcp://127.0.0.1:4243
|
||||
ctop
|
||||
```
|
||||
`ctop` requires no arguments and uses Docker host variables by default. See [connectors][connectors] for further configuration options.
|
||||
|
||||
### Options
|
||||
|
||||
Option | Description
|
||||
--- | ---
|
||||
-a | show active containers only
|
||||
-f <string> | set an initial filter string
|
||||
-h | display help dialog
|
||||
-i | invert default colors
|
||||
-r | reverse container sort order
|
||||
-s | select initial container sort field
|
||||
-v | output version information and exit
|
||||
|
||||
### Keybindings
|
||||
|
||||
Key | Action
|
||||
--- | ---
|
||||
a | Toggle display of all (running and non-running) containers
|
||||
f | Filter displayed containers
|
||||
f | Filter displayed containers (`esc` to clear when open)
|
||||
H | Toggle ctop header
|
||||
h | Open help dialog
|
||||
s | Select container sort field
|
||||
r | Reverse container sort order
|
||||
q | Quit ctop
|
||||
|
||||
[build]: _docs/build.md
|
||||
[connectors]: _docs/connectors.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"
|
||||
|
||||
20
_docs/build.md
Normal file
20
_docs/build.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# Build
|
||||
|
||||
To build `ctop` from source, ensure you have a recent version of [glide](https://github.com/Masterminds/glide) installed and run:
|
||||
|
||||
```bash
|
||||
go get github.com/bcicen/ctop && \
|
||||
cd $GOPATH/src/github.com/bcicen/ctop && \
|
||||
make build
|
||||
```
|
||||
|
||||
To build a minimal Docker image containing only `ctop`:
|
||||
```bash
|
||||
make image
|
||||
```
|
||||
|
||||
Now you can run your local image:
|
||||
|
||||
```bash
|
||||
docker run -ti --name ctop --rm -v /var/run/docker.sock:/var/run/docker.sock ctop
|
||||
```
|
||||
26
_docs/connectors.md
Normal file
26
_docs/connectors.md
Normal 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
|
||||
56
_docs/debug.md
Normal file
56
_docs/debug.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# 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`)
|
||||
23
circle.yml
Normal file
23
circle.yml
Normal file
@@ -0,0 +1,23 @@
|
||||
machine:
|
||||
services:
|
||||
- docker
|
||||
environment:
|
||||
IMAGE_NAME: quay.io/vektorlab/ctop
|
||||
|
||||
dependencies:
|
||||
override:
|
||||
- docker info
|
||||
- make image
|
||||
|
||||
test:
|
||||
override:
|
||||
- docker run -ti ctop -v
|
||||
|
||||
deployment:
|
||||
hub:
|
||||
branch: master
|
||||
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 push ${IMAGE_NAME}
|
||||
17
colors.go
17
colors.go
@@ -1,6 +1,10 @@
|
||||
package main
|
||||
|
||||
import ui "github.com/gizak/termui"
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
ui "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
/*
|
||||
Valid colors:
|
||||
@@ -38,6 +42,17 @@ var ColorMap = map[string]ui.Attribute{
|
||||
"mbarchart.text.fg": ui.ColorWhite,
|
||||
"par.text.fg": ui.ColorWhite,
|
||||
"par.text.bg": ui.ColorDefault,
|
||||
"par.text.hi": ui.ColorBlack,
|
||||
"sparkline.line.fg": ui.ColorGreen,
|
||||
"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
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ func Init() {
|
||||
GlobalParams = append(GlobalParams, p)
|
||||
log.Infof("loaded config param: %s: %s", quote(p.Key), quote(p.Val))
|
||||
}
|
||||
|
||||
for _, s := range switches {
|
||||
GlobalSwitches = append(GlobalSwitches, s)
|
||||
log.Infof("loaded config switch: %s: %t", quote(s.Key), s.Val)
|
||||
|
||||
@@ -2,11 +2,6 @@ package config
|
||||
|
||||
// defaults
|
||||
var params = []*Param{
|
||||
&Param{
|
||||
Key: "dockerHost",
|
||||
Val: getEnv("DOCKER_HOST", "unix:///var/run/docker.sock"),
|
||||
Label: "Docker API URL",
|
||||
},
|
||||
&Param{
|
||||
Key: "filterStr",
|
||||
Val: "",
|
||||
|
||||
@@ -17,11 +17,6 @@ var switches = []*Switch{
|
||||
Val: true,
|
||||
Label: "Enable Status Header",
|
||||
},
|
||||
&Switch{
|
||||
Key: "loggingEnabled",
|
||||
Val: false,
|
||||
Label: "Enable Logging Server",
|
||||
},
|
||||
}
|
||||
|
||||
type Switch struct {
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
package metrics
|
||||
package collector
|
||||
|
||||
import (
|
||||
"github.com/bcicen/ctop/models"
|
||||
api "github.com/fsouza/go-dockerclient"
|
||||
)
|
||||
|
||||
// Docker collector
|
||||
type Docker struct {
|
||||
Metrics
|
||||
models.Metrics
|
||||
id string
|
||||
client *api.Client
|
||||
running bool
|
||||
stream chan Metrics
|
||||
stream chan models.Metrics
|
||||
done chan bool
|
||||
lastCpu float64
|
||||
lastSysCpu float64
|
||||
@@ -18,7 +19,7 @@ type Docker struct {
|
||||
|
||||
func NewDocker(client *api.Client, id string) *Docker {
|
||||
return &Docker{
|
||||
Metrics: Metrics{},
|
||||
Metrics: models.Metrics{},
|
||||
id: id,
|
||||
client: client,
|
||||
}
|
||||
@@ -26,7 +27,7 @@ func NewDocker(client *api.Client, id string) *Docker {
|
||||
|
||||
func (c *Docker) Start() {
|
||||
c.done = make(chan bool)
|
||||
c.stream = make(chan Metrics)
|
||||
c.stream = make(chan models.Metrics)
|
||||
stats := make(chan *api.Stats)
|
||||
|
||||
go func() {
|
||||
@@ -46,6 +47,7 @@ func (c *Docker) Start() {
|
||||
c.ReadCPU(s)
|
||||
c.ReadMem(s)
|
||||
c.ReadNet(s)
|
||||
c.ReadIO(s)
|
||||
c.stream <- c.Metrics
|
||||
}
|
||||
log.Infof("collector stopped for container: %s", c.id)
|
||||
@@ -59,10 +61,14 @@ func (c *Docker) Running() bool {
|
||||
return c.running
|
||||
}
|
||||
|
||||
func (c *Docker) Stream() chan Metrics {
|
||||
func (c *Docker) Stream() chan models.Metrics {
|
||||
return c.stream
|
||||
}
|
||||
|
||||
func (c *Docker) Logs() LogCollector {
|
||||
return &DockerLogs{c.id, c.client, make(chan bool)}
|
||||
}
|
||||
|
||||
// Stop collector
|
||||
func (c *Docker) Stop() {
|
||||
c.done <- true
|
||||
@@ -79,12 +85,13 @@ func (c *Docker) ReadCPU(stats *api.Stats) {
|
||||
c.CPUUtil = round((cpudiff / syscpudiff * 100) * ncpus)
|
||||
c.lastCpu = total
|
||||
c.lastSysCpu = system
|
||||
c.Pids = int(stats.PidsStats.Current)
|
||||
}
|
||||
|
||||
func (c *Docker) ReadMem(stats *api.Stats) {
|
||||
c.MemUsage = int64(stats.MemoryStats.Usage)
|
||||
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) {
|
||||
@@ -95,3 +102,16 @@ func (c *Docker) ReadNet(stats *api.Stats) {
|
||||
}
|
||||
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
|
||||
}
|
||||
74
connector/collector/docker_logs.go
Normal file
74
connector/collector/docker_logs.go
Normal file
@@ -0,0 +1,74 @@
|
||||
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
|
||||
}
|
||||
35
connector/collector/main.go
Normal file
35
connector/collector/main.go
Normal file
@@ -0,0 +1,35 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,16 +1,18 @@
|
||||
// +build !release
|
||||
|
||||
package metrics
|
||||
package collector
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/bcicen/ctop/models"
|
||||
)
|
||||
|
||||
// Mock collector
|
||||
type Mock struct {
|
||||
Metrics
|
||||
stream chan Metrics
|
||||
models.Metrics
|
||||
stream chan models.Metrics
|
||||
done bool
|
||||
running bool
|
||||
aggression int64
|
||||
@@ -18,7 +20,7 @@ type Mock struct {
|
||||
|
||||
func NewMock(a int64) *Mock {
|
||||
c := &Mock{
|
||||
Metrics: Metrics{},
|
||||
Metrics: models.Metrics{},
|
||||
aggression: a,
|
||||
}
|
||||
c.MemLimit = 2147483648
|
||||
@@ -31,7 +33,7 @@ func (c *Mock) Running() bool {
|
||||
|
||||
func (c *Mock) Start() {
|
||||
c.done = false
|
||||
c.stream = make(chan Metrics)
|
||||
c.stream = make(chan models.Metrics)
|
||||
go c.run()
|
||||
}
|
||||
|
||||
@@ -39,15 +41,24 @@ func (c *Mock) Stop() {
|
||||
c.done = true
|
||||
}
|
||||
|
||||
func (c *Mock) Stream() chan Metrics {
|
||||
func (c *Mock) Stream() chan models.Metrics {
|
||||
return c.stream
|
||||
}
|
||||
|
||||
func (c *Mock) Logs() LogCollector {
|
||||
return &MockLogs{make(chan bool)}
|
||||
}
|
||||
|
||||
func (c *Mock) run() {
|
||||
c.running = true
|
||||
rand.Seed(int64(time.Now().Nanosecond()))
|
||||
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 {
|
||||
c.CPUUtil += rand.Intn(2) * int(c.aggression)
|
||||
if c.CPUUtil >= 100 {
|
||||
@@ -60,7 +71,7 @@ func (c *Mock) run() {
|
||||
if c.MemUsage > c.MemLimit {
|
||||
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
|
||||
if c.done {
|
||||
break
|
||||
31
connector/collector/mock_logs.go
Normal file
31
connector/collector/mock_logs.go
Normal file
@@ -0,0 +1,31 @@
|
||||
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 }
|
||||
44
connector/collector/proc.go
Normal file
44
connector/collector/proc.go
Normal 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
|
||||
}
|
||||
128
connector/collector/runc.go
Normal file
128
connector/collector/runc.go
Normal file
@@ -0,0 +1,128 @@
|
||||
// +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
|
||||
}
|
||||
@@ -1,35 +1,33 @@
|
||||
package main
|
||||
package connector
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/bcicen/ctop/config"
|
||||
"github.com/bcicen/ctop/metrics"
|
||||
"github.com/fsouza/go-dockerclient"
|
||||
"github.com/bcicen/ctop/connector/collector"
|
||||
"github.com/bcicen/ctop/container"
|
||||
api "github.com/fsouza/go-dockerclient"
|
||||
)
|
||||
|
||||
type ContainerSource interface {
|
||||
All() Containers
|
||||
Get(string) (*Container, bool)
|
||||
}
|
||||
|
||||
type DockerContainerSource struct {
|
||||
client *docker.Client
|
||||
containers map[string]*Container
|
||||
type Docker struct {
|
||||
client *api.Client
|
||||
containers map[string]*container.Container
|
||||
needsRefresh chan string // container IDs requiring refresh
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
func NewDockerContainerSource() *DockerContainerSource {
|
||||
func NewDocker() Connector {
|
||||
// init docker client
|
||||
client, err := docker.NewClient(config.GetVal("dockerHost"))
|
||||
client, err := api.NewClientFromEnv()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
cm := &DockerContainerSource{
|
||||
cm := &Docker{
|
||||
client: client,
|
||||
containers: make(map[string]*Container),
|
||||
containers: make(map[string]*container.Container),
|
||||
needsRefresh: make(chan string, 60),
|
||||
lock: sync.RWMutex{},
|
||||
}
|
||||
go cm.Loop()
|
||||
cm.refreshAll()
|
||||
@@ -38,9 +36,9 @@ func NewDockerContainerSource() *DockerContainerSource {
|
||||
}
|
||||
|
||||
// Docker events watcher
|
||||
func (cm *DockerContainerSource) watchEvents() {
|
||||
func (cm *Docker) watchEvents() {
|
||||
log.Info("docker event listener starting")
|
||||
events := make(chan *docker.APIEvents)
|
||||
events := make(chan *api.APIEvents)
|
||||
cm.client.AddEventListener(events)
|
||||
|
||||
for e := range events {
|
||||
@@ -58,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", 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)
|
||||
// remove container if no longer exists
|
||||
if insp == nil {
|
||||
@@ -67,14 +83,15 @@ func (cm *DockerContainerSource) refresh(c *Container) {
|
||||
}
|
||||
c.SetMeta("name", shortName(insp.Name))
|
||||
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.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)
|
||||
if err != nil {
|
||||
if _, ok := err.(*docker.NoSuchContainer); ok == false {
|
||||
if _, ok := err.(*api.NoSuchContainer); ok == false {
|
||||
log.Errorf(err.Error())
|
||||
}
|
||||
}
|
||||
@@ -82,8 +99,8 @@ func (cm *DockerContainerSource) inspect(id string) *docker.Container {
|
||||
}
|
||||
|
||||
// Mark all container IDs for refresh
|
||||
func (cm *DockerContainerSource) refreshAll() {
|
||||
opts := docker.ListContainersOptions{All: true}
|
||||
func (cm *Docker) refreshAll() {
|
||||
opts := api.ListContainersOptions{All: true}
|
||||
allContainers, err := cm.client.ListContainers(opts)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -97,7 +114,7 @@ func (cm *DockerContainerSource) refreshAll() {
|
||||
}
|
||||
}
|
||||
|
||||
func (cm *DockerContainerSource) Loop() {
|
||||
func (cm *Docker) Loop() {
|
||||
for id := range cm.needsRefresh {
|
||||
c := cm.MustGet(id)
|
||||
cm.refresh(c)
|
||||
@@ -105,38 +122,46 @@ func (cm *DockerContainerSource) Loop() {
|
||||
}
|
||||
|
||||
// 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)
|
||||
// append container struct for new containers
|
||||
if !ok {
|
||||
// create collector
|
||||
collector := metrics.NewDocker(cm.client, id)
|
||||
collector := collector.NewDocker(cm.client, id)
|
||||
// create container
|
||||
c = NewContainer(id, collector)
|
||||
c = container.New(id, collector)
|
||||
cm.lock.Lock()
|
||||
cm.containers[id] = c
|
||||
cm.lock.Unlock()
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// 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()
|
||||
c, ok := cm.containers[id]
|
||||
cm.lock.Unlock()
|
||||
return c, ok
|
||||
}
|
||||
|
||||
// Remove containers by ID
|
||||
func (cm *DockerContainerSource) delByID(id string) {
|
||||
func (cm *Docker) delByID(id string) {
|
||||
cm.lock.Lock()
|
||||
delete(cm.containers, id)
|
||||
cm.lock.Unlock()
|
||||
log.Infof("removed dead container: %s", id)
|
||||
}
|
||||
|
||||
// Return array of all containers, sorted by field
|
||||
func (cm *DockerContainerSource) All() (containers Containers) {
|
||||
func (cm *Docker) All() (containers container.Containers) {
|
||||
cm.lock.Lock()
|
||||
for _, c := range cm.containers {
|
||||
containers = append(containers, c)
|
||||
}
|
||||
sort.Sort(containers)
|
||||
containers.Sort()
|
||||
containers.Filter()
|
||||
cm.lock.Unlock()
|
||||
return containers
|
||||
}
|
||||
|
||||
7
connector/enabled_darwin.go
Normal file
7
connector/enabled_darwin.go
Normal file
@@ -0,0 +1,7 @@
|
||||
// +build !linux
|
||||
|
||||
package connector
|
||||
|
||||
var enabled = map[string]func() Connector{
|
||||
"docker": NewDocker,
|
||||
}
|
||||
8
connector/enabled_linux.go
Normal file
8
connector/enabled_linux.go
Normal 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
26
connector/main.go
Normal 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)
|
||||
}
|
||||
@@ -1,31 +1,31 @@
|
||||
// +build !release
|
||||
|
||||
package main
|
||||
package connector
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bcicen/ctop/metrics"
|
||||
"github.com/bcicen/ctop/connector/collector"
|
||||
"github.com/bcicen/ctop/container"
|
||||
"github.com/jgautheron/codename-generator"
|
||||
"github.com/nu7hatch/gouuid"
|
||||
)
|
||||
|
||||
type MockContainerSource struct {
|
||||
containers Containers
|
||||
type Mock struct {
|
||||
containers container.Containers
|
||||
}
|
||||
|
||||
func NewMockContainerSource() *MockContainerSource {
|
||||
cs := &MockContainerSource{}
|
||||
func NewMock() *Mock {
|
||||
cs := &Mock{}
|
||||
go cs.Init()
|
||||
go cs.Loop()
|
||||
return cs
|
||||
}
|
||||
|
||||
// Create Mock containers
|
||||
func (cs *MockContainerSource) Init() {
|
||||
func (cs *Mock) Init() {
|
||||
rand.Seed(int64(time.Now().Nanosecond()))
|
||||
|
||||
for i := 0; i < 4; i++ {
|
||||
@@ -38,15 +38,15 @@ func (cs *MockContainerSource) Init() {
|
||||
|
||||
}
|
||||
|
||||
func (cs *MockContainerSource) makeContainer(aggression int64) {
|
||||
collector := metrics.NewMock(aggression)
|
||||
c := NewContainer(makeID(), collector)
|
||||
func (cs *Mock) makeContainer(aggression int64) {
|
||||
collector := collector.NewMock(aggression)
|
||||
c := container.New(makeID(), collector)
|
||||
c.SetMeta("name", makeName())
|
||||
c.SetState(makeState())
|
||||
cs.containers = append(cs.containers, c)
|
||||
}
|
||||
|
||||
func (cs *MockContainerSource) Loop() {
|
||||
func (cs *Mock) Loop() {
|
||||
iter := 0
|
||||
for {
|
||||
// Change state for random container
|
||||
@@ -60,7 +60,7 @@ func (cs *MockContainerSource) Loop() {
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if c.Id == id {
|
||||
return c, true
|
||||
@@ -70,14 +70,14 @@ func (cs *MockContainerSource) Get(id string) (*Container, bool) {
|
||||
}
|
||||
|
||||
// Return array of all containers, sorted by field
|
||||
func (cs *MockContainerSource) All() Containers {
|
||||
sort.Sort(cs.containers)
|
||||
func (cs *Mock) All() container.Containers {
|
||||
cs.containers.Sort()
|
||||
cs.containers.Filter()
|
||||
return cs.containers
|
||||
}
|
||||
|
||||
// Remove containers by ID
|
||||
func (cs *MockContainerSource) delByID(id string) {
|
||||
func (cs *Mock) delByID(id string) {
|
||||
for n, c := range cs.containers {
|
||||
if c.Id == id {
|
||||
cs.del(n)
|
||||
@@ -87,7 +87,7 @@ func (cs *MockContainerSource) delByID(id string) {
|
||||
}
|
||||
|
||||
// Remove one or more containers by index
|
||||
func (cs *MockContainerSource) del(idx ...int) {
|
||||
func (cs *Mock) del(idx ...int) {
|
||||
for _, i := range idx {
|
||||
cs.containers = append(cs.containers[:i], cs.containers[i+1:]...)
|
||||
}
|
||||
243
connector/runc.go
Normal file
243
connector/runc.go
Normal 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))
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,32 @@
|
||||
package main
|
||||
package container
|
||||
|
||||
import (
|
||||
"github.com/bcicen/ctop/connector/collector"
|
||||
"github.com/bcicen/ctop/cwidgets"
|
||||
"github.com/bcicen/ctop/cwidgets/compact"
|
||||
"github.com/bcicen/ctop/metrics"
|
||||
"github.com/bcicen/ctop/logging"
|
||||
"github.com/bcicen/ctop/models"
|
||||
)
|
||||
|
||||
var (
|
||||
log = logging.Init()
|
||||
)
|
||||
|
||||
// Metrics and metadata representing a container
|
||||
type Container struct {
|
||||
metrics.Metrics
|
||||
models.Metrics
|
||||
Id string
|
||||
Meta map[string]string
|
||||
Widgets *compact.Compact
|
||||
Display bool // display this container in compact view
|
||||
updater cwidgets.WidgetUpdater
|
||||
collector metrics.Collector
|
||||
display bool // display this container in compact view
|
||||
collector collector.Collector
|
||||
}
|
||||
|
||||
func NewContainer(id string, collector metrics.Collector) *Container {
|
||||
func New(id string, collector collector.Collector) *Container {
|
||||
widgets := compact.NewCompact(id)
|
||||
return &Container{
|
||||
Metrics: metrics.NewMetrics(),
|
||||
Metrics: models.NewMetrics(),
|
||||
Id: id,
|
||||
Meta: make(map[string]string),
|
||||
Widgets: widgets,
|
||||
@@ -61,15 +67,20 @@ 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
|
||||
func (c *Container) Read(stream chan metrics.Metrics) {
|
||||
func (c *Container) Read(stream chan models.Metrics) {
|
||||
go func() {
|
||||
for metrics := range stream {
|
||||
c.Metrics = metrics
|
||||
c.updater.SetMetrics(metrics)
|
||||
}
|
||||
log.Infof("reader stopped for container: %s", c.Id)
|
||||
c.Metrics = metrics.NewMetrics()
|
||||
c.Metrics = models.NewMetrics()
|
||||
c.Widgets.Reset()
|
||||
}()
|
||||
log.Infof("reader started for container: %s", c.Id)
|
||||
@@ -1,8 +1,9 @@
|
||||
package main
|
||||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
|
||||
"github.com/bcicen/ctop/config"
|
||||
)
|
||||
@@ -53,6 +54,22 @@ var Sorters = map[string]sortMethod{
|
||||
}
|
||||
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 {
|
||||
// Use secondary sort method if equal values
|
||||
c1state := c1.GetMeta("state")
|
||||
@@ -73,6 +90,7 @@ func SortFields() (fields []string) {
|
||||
|
||||
type Containers []*Container
|
||||
|
||||
func (a Containers) Sort() { sort.Sort(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) Less(i, j int) bool {
|
||||
@@ -88,16 +106,18 @@ func (a Containers) Filter() {
|
||||
re := regexp.MustCompile(fmt.Sprintf(".*%s", filter))
|
||||
|
||||
for _, c := range a {
|
||||
c.display = true
|
||||
c.Display = true
|
||||
// Apply name filter
|
||||
if re.FindAllString(c.GetMeta("name"), 1) == nil {
|
||||
c.display = false
|
||||
c.Display = false
|
||||
}
|
||||
// Apply state filter
|
||||
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 sumIO(c *Container) int64 { return c.IOBytesRead + c.IOBytesWrite }
|
||||
89
cursor.go
89
cursor.go
@@ -1,33 +1,39 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/bcicen/ctop/connector"
|
||||
"github.com/bcicen/ctop/container"
|
||||
ui "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
type GridCursor struct {
|
||||
selectedID string // id of currently selected container
|
||||
filtered Containers
|
||||
cSource ContainerSource
|
||||
}
|
||||
|
||||
func NewGridCursor() *GridCursor {
|
||||
return &GridCursor{
|
||||
cSource: NewDockerContainerSource(),
|
||||
}
|
||||
filtered container.Containers
|
||||
cSource connector.Connector
|
||||
isScrolling bool // toggled when actively scrolling
|
||||
}
|
||||
|
||||
func (gc *GridCursor) Len() int { return len(gc.filtered) }
|
||||
func (gc *GridCursor) Selected() *Container { return gc.filtered[gc.Idx()] }
|
||||
|
||||
func (gc *GridCursor) Selected() *container.Container {
|
||||
idx := gc.Idx()
|
||||
if idx < gc.Len() {
|
||||
return gc.filtered[idx]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Refresh containers from source
|
||||
func (gc *GridCursor) RefreshContainers() (lenChanged bool) {
|
||||
oldLen := gc.Len()
|
||||
|
||||
// Containers filtered by display bool
|
||||
gc.filtered = Containers{}
|
||||
gc.filtered = container.Containers{}
|
||||
var cursorVisible bool
|
||||
for _, c := range gc.cSource.All() {
|
||||
if c.display {
|
||||
if c.Display {
|
||||
if c.Id == gc.selectedID {
|
||||
cursorVisible = true
|
||||
}
|
||||
@@ -93,6 +99,9 @@ func (gc *GridCursor) ScrollPage() {
|
||||
}
|
||||
|
||||
func (gc *GridCursor) Up() {
|
||||
gc.isScrolling = true
|
||||
defer func() { gc.isScrolling = false }()
|
||||
|
||||
idx := gc.Idx()
|
||||
if idx <= 0 { // already at top
|
||||
return
|
||||
@@ -109,6 +118,9 @@ func (gc *GridCursor) Up() {
|
||||
}
|
||||
|
||||
func (gc *GridCursor) Down() {
|
||||
gc.isScrolling = true
|
||||
defer func() { gc.isScrolling = false }()
|
||||
|
||||
idx := gc.Idx()
|
||||
if idx >= gc.Len()-1 { // already at bottom
|
||||
return
|
||||
@@ -123,3 +135,58 @@ func (gc *GridCursor) Down() {
|
||||
gc.ScrollPage()
|
||||
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
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
ui "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
var header = NewCompactHeader()
|
||||
var header *CompactHeader
|
||||
|
||||
type CompactGrid struct {
|
||||
ui.GridBufferer
|
||||
@@ -16,14 +16,20 @@ type CompactGrid struct {
|
||||
}
|
||||
|
||||
func NewCompactGrid() *CompactGrid {
|
||||
header = NewCompactHeader() // init column header
|
||||
return &CompactGrid{}
|
||||
}
|
||||
|
||||
func (cg *CompactGrid) Align() {
|
||||
y := cg.Y
|
||||
|
||||
if cg.Offset >= len(cg.Rows) {
|
||||
cg.Offset = 0
|
||||
}
|
||||
if cg.Offset < 0 {
|
||||
cg.Offset = 0
|
||||
}
|
||||
|
||||
// update row ypos, width recursively
|
||||
for _, r := range cg.pageRows() {
|
||||
r.SetY(y)
|
||||
|
||||
@@ -12,7 +12,7 @@ type CompactHeader struct {
|
||||
}
|
||||
|
||||
func NewCompactHeader() *CompactHeader {
|
||||
fields := []string{"", "NAME", "CID", "CPU", "MEM", "NET RX/TX"}
|
||||
fields := []string{"", "NAME", "CID", "CPU", "MEM", "NET RX/TX", "IO R/W", "PIDS"}
|
||||
ch := &CompactHeader{}
|
||||
ch.Height = 2
|
||||
for _, f := range fields {
|
||||
@@ -27,13 +27,13 @@ func (ch *CompactHeader) GetHeight() int {
|
||||
|
||||
func (ch *CompactHeader) SetWidth(w int) {
|
||||
x := ch.X
|
||||
autoWidth := calcWidth(w, 5)
|
||||
autoWidth := calcWidth(w)
|
||||
for n, col := range ch.pars {
|
||||
// set status column to static width
|
||||
if n == 0 {
|
||||
// set column to static width
|
||||
if colWidths[n] != 0 {
|
||||
col.SetX(x)
|
||||
col.SetWidth(statusWidth)
|
||||
x += statusWidth
|
||||
col.SetWidth(colWidths[n])
|
||||
x += colWidths[n]
|
||||
continue
|
||||
}
|
||||
col.SetX(x)
|
||||
|
||||
@@ -2,7 +2,7 @@ package compact
|
||||
|
||||
import (
|
||||
"github.com/bcicen/ctop/logging"
|
||||
"github.com/bcicen/ctop/metrics"
|
||||
"github.com/bcicen/ctop/models"
|
||||
ui "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
@@ -15,6 +15,8 @@ type Compact struct {
|
||||
Cpu *GaugeCol
|
||||
Memory *GaugeCol
|
||||
Net *TextCol
|
||||
IO *TextCol
|
||||
Pids *TextCol
|
||||
X, Y int
|
||||
Width int
|
||||
Height int
|
||||
@@ -32,6 +34,8 @@ func NewCompact(id string) *Compact {
|
||||
Cpu: NewGaugeCol(),
|
||||
Memory: NewGaugeCol(),
|
||||
Net: NewTextCol("-"),
|
||||
IO: NewTextCol("-"),
|
||||
Pids: NewTextCol("-"),
|
||||
X: 1,
|
||||
Height: 1,
|
||||
}
|
||||
@@ -55,10 +59,12 @@ func (row *Compact) SetMeta(k, v string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (row *Compact) SetMetrics(m metrics.Metrics) {
|
||||
func (row *Compact) SetMetrics(m models.Metrics) {
|
||||
row.SetCPU(m.CPUUtil)
|
||||
row.SetNet(m.NetRx, m.NetTx)
|
||||
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
|
||||
@@ -66,6 +72,8 @@ func (row *Compact) Reset() {
|
||||
row.Cpu.Reset()
|
||||
row.Memory.Reset()
|
||||
row.Net.Reset()
|
||||
row.IO.Reset()
|
||||
row.Pids.Reset()
|
||||
}
|
||||
|
||||
func (row *Compact) GetHeight() int {
|
||||
@@ -91,13 +99,12 @@ func (row *Compact) SetWidth(width int) {
|
||||
return
|
||||
}
|
||||
x := row.X
|
||||
autoWidth := calcWidth(width, 5)
|
||||
autoWidth := calcWidth(width)
|
||||
for n, col := range row.all() {
|
||||
// set status column to static width
|
||||
if n == 0 {
|
||||
if colWidths[n] != 0 {
|
||||
col.SetX(x)
|
||||
col.SetWidth(statusWidth)
|
||||
x += statusWidth
|
||||
col.SetWidth(colWidths[n])
|
||||
x += colWidths[n]
|
||||
continue
|
||||
}
|
||||
col.SetX(x)
|
||||
@@ -116,7 +123,8 @@ func (row *Compact) Buffer() ui.Buffer {
|
||||
buf.Merge(row.Cpu.Buffer())
|
||||
buf.Merge(row.Memory.Buffer())
|
||||
buf.Merge(row.Net.Buffer())
|
||||
|
||||
buf.Merge(row.IO.Buffer())
|
||||
buf.Merge(row.Pids.Buffer())
|
||||
return buf
|
||||
}
|
||||
|
||||
@@ -128,5 +136,7 @@ func (row *Compact) all() []ui.GridBufferer {
|
||||
row.Cpu,
|
||||
row.Memory,
|
||||
row.Net,
|
||||
row.IO,
|
||||
row.Pids,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,16 @@ func (row *Compact) SetNet(rx int64, tx int64) {
|
||||
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) {
|
||||
row.Cpu.BarColor = colorScale(val)
|
||||
row.Cpu.Label = fmt.Sprintf("%s%%", strconv.Itoa(val))
|
||||
@@ -20,6 +30,9 @@ func (row *Compact) SetCPU(val int) {
|
||||
val = 5
|
||||
row.Cpu.BarColor = ui.ThemeAttr("gauge.bar.bg")
|
||||
}
|
||||
if val > 100 {
|
||||
val = 100
|
||||
}
|
||||
row.Cpu.Percent = val
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ func NewTextCol(s string) *TextCol {
|
||||
}
|
||||
|
||||
func (w *TextCol) Highlight() {
|
||||
w.TextFgColor = ui.ColorBlack
|
||||
w.TextFgColor = ui.ThemeAttr("par.text.hi")
|
||||
w.TextBgColor = ui.ThemeAttr("par.text.fg")
|
||||
}
|
||||
|
||||
|
||||
@@ -9,10 +9,29 @@ import (
|
||||
|
||||
const colSpacing = 1
|
||||
|
||||
// Calculate per-column width, given total width and number of items
|
||||
func calcWidth(width, items int) int {
|
||||
spacing := colSpacing * items
|
||||
return (width - statusWidth - spacing) / items
|
||||
// per-column width. 0 == auto width
|
||||
var colWidths = []int{
|
||||
3, // status
|
||||
0, // name
|
||||
0, // cid
|
||||
0, // cpu
|
||||
0, // memory
|
||||
0, // net
|
||||
0, // io
|
||||
4, // pids
|
||||
}
|
||||
|
||||
// Calculate per-column width, given total width
|
||||
func calcWidth(width int) int {
|
||||
spacing := colSpacing * len(colWidths)
|
||||
var staticCols int
|
||||
for _, w := range colWidths {
|
||||
width -= w
|
||||
if w == 0 {
|
||||
staticCols += 1
|
||||
}
|
||||
}
|
||||
return (width - spacing) / staticCols
|
||||
}
|
||||
|
||||
func centerParText(p *ui.Par) {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package expanded
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
ui "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
var displayInfo = []string{"id", "name", "image", "state"}
|
||||
var displayInfo = []string{"id", "name", "image", "ports", "state", "created"}
|
||||
|
||||
type Info struct {
|
||||
*ui.Table
|
||||
@@ -16,7 +18,7 @@ func NewInfo(id string) *Info {
|
||||
p.Height = 4
|
||||
p.Width = colWidth[0]
|
||||
p.FgColor = ui.ThemeAttr("par.text.fg")
|
||||
p.Seperator = false
|
||||
p.Separator = false
|
||||
i := &Info{p, make(map[string]string)}
|
||||
i.Set("id", id)
|
||||
return i
|
||||
@@ -24,12 +26,33 @@ func NewInfo(id string) *Info {
|
||||
|
||||
func (w *Info) Set(k, v string) {
|
||||
w.data[k] = v
|
||||
|
||||
// rebuild rows
|
||||
w.Rows = [][]string{}
|
||||
for _, k := range displayInfo {
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
51
cwidgets/expanded/io.go
Normal file
51
cwidgets/expanded/io.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package expanded
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/bcicen/ctop/cwidgets"
|
||||
ui "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
type IO struct {
|
||||
*ui.Sparklines
|
||||
readHist *DiffHist
|
||||
writeHist *DiffHist
|
||||
}
|
||||
|
||||
func NewIO() *IO {
|
||||
io := &IO{ui.NewSparklines(), NewDiffHist(60), NewDiffHist(60)}
|
||||
io.BorderLabel = "IO"
|
||||
io.Height = 6
|
||||
io.Width = colWidth[0]
|
||||
io.X = 0
|
||||
io.Y = 24
|
||||
|
||||
read := ui.NewSparkline()
|
||||
read.Title = "READ"
|
||||
read.Height = 1
|
||||
read.Data = io.readHist.Data
|
||||
read.LineColor = ui.ColorGreen
|
||||
|
||||
write := ui.NewSparkline()
|
||||
write.Title = "WRITE"
|
||||
write.Height = 1
|
||||
write.Data = io.writeHist.Data
|
||||
write.LineColor = ui.ColorYellow
|
||||
|
||||
io.Lines = []ui.Sparkline{read, write}
|
||||
return io
|
||||
}
|
||||
|
||||
func (w *IO) Update(read int64, write int64) {
|
||||
var rate string
|
||||
|
||||
w.readHist.Append(int(read))
|
||||
rate = strings.ToLower(cwidgets.ByteFormatInt(w.readHist.Val))
|
||||
w.Lines[0].Title = fmt.Sprintf("read [%s/s]", rate)
|
||||
|
||||
w.writeHist.Append(int(write))
|
||||
rate = strings.ToLower(cwidgets.ByteFormatInt(w.writeHist.Val))
|
||||
w.Lines[1].Title = fmt.Sprintf("write [%s/s]", rate)
|
||||
}
|
||||
83
cwidgets/expanded/logs.go
Normal file
83
cwidgets/expanded/logs.go
Normal file
@@ -0,0 +1,83 @@
|
||||
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 }
|
||||
@@ -2,7 +2,7 @@ package expanded
|
||||
|
||||
import (
|
||||
"github.com/bcicen/ctop/logging"
|
||||
"github.com/bcicen/ctop/metrics"
|
||||
"github.com/bcicen/ctop/models"
|
||||
ui "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
@@ -17,6 +17,8 @@ type Expanded struct {
|
||||
Net *Net
|
||||
Cpu *Cpu
|
||||
Mem *Mem
|
||||
IO *IO
|
||||
X, Y int
|
||||
Width int
|
||||
}
|
||||
|
||||
@@ -29,30 +31,59 @@ func NewExpanded(id string) *Expanded {
|
||||
Net: NewNet(),
|
||||
Cpu: NewCpu(),
|
||||
Mem: NewMem(),
|
||||
IO: NewIO(),
|
||||
Width: ui.TermWidth(),
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Expanded) SetWidth(w int) {
|
||||
e.Width = w
|
||||
func (e *Expanded) Up() {
|
||||
if e.Y < 0 {
|
||||
e.Y++
|
||||
e.Align()
|
||||
ui.Render(e)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Expanded) SetMeta(k, v string) {
|
||||
e.Info.Set(k, v)
|
||||
func (e *Expanded) Down() {
|
||||
if e.Y > (ui.TermHeight() - e.GetHeight()) {
|
||||
e.Y--
|
||||
e.Align()
|
||||
ui.Render(e)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Expanded) SetMetrics(m metrics.Metrics) {
|
||||
func (e *Expanded) SetWidth(w int) { e.Width = w }
|
||||
func (e *Expanded) SetMeta(k, v string) { e.Info.Set(k, v) }
|
||||
|
||||
func (e *Expanded) SetMetrics(m models.Metrics) {
|
||||
e.Cpu.Update(m.CPUUtil)
|
||||
e.Net.Update(m.NetRx, m.NetTx)
|
||||
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() {
|
||||
y := 0
|
||||
// reset offset if needed
|
||||
if e.GetHeight() <= ui.TermHeight() {
|
||||
e.Y = 0
|
||||
}
|
||||
|
||||
y := e.Y
|
||||
for _, i := range e.all() {
|
||||
i.SetY(y)
|
||||
y += i.GetHeight()
|
||||
}
|
||||
|
||||
if e.Width > colWidth[0] {
|
||||
colWidth[1] = e.Width - (colWidth[0] + 1)
|
||||
}
|
||||
@@ -74,6 +105,7 @@ func (e *Expanded) Buffer() ui.Buffer {
|
||||
buf.Merge(e.Cpu.Buffer())
|
||||
buf.Merge(e.Mem.Buffer())
|
||||
buf.Merge(e.Net.Buffer())
|
||||
buf.Merge(e.IO.Buffer())
|
||||
return buf
|
||||
}
|
||||
|
||||
@@ -83,6 +115,7 @@ func (e *Expanded) all() []ui.GridBufferer {
|
||||
e.Cpu,
|
||||
e.Mem,
|
||||
e.Net,
|
||||
e.IO,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,12 @@ package cwidgets
|
||||
|
||||
import (
|
||||
"github.com/bcicen/ctop/logging"
|
||||
"github.com/bcicen/ctop/metrics"
|
||||
"github.com/bcicen/ctop/models"
|
||||
)
|
||||
|
||||
var log = logging.Init()
|
||||
|
||||
type WidgetUpdater interface {
|
||||
SetMeta(string, string)
|
||||
SetMetrics(metrics.Metrics)
|
||||
SetMetrics(models.Metrics)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ const (
|
||||
kb = 1024
|
||||
mb = kb * 1024
|
||||
gb = mb * 1024
|
||||
tb = gb * 1024
|
||||
)
|
||||
|
||||
// convenience method
|
||||
@@ -28,8 +29,12 @@ func ByteFormat(n int64) string {
|
||||
n = n / mb
|
||||
return fmt.Sprintf("%sM", strconv.FormatInt(n, 10))
|
||||
}
|
||||
if n < tb {
|
||||
nf := float64(n) / gb
|
||||
return fmt.Sprintf("%sG", unpadFloat(nf))
|
||||
}
|
||||
nf := float64(n) / tb
|
||||
return fmt.Sprintf("%sT", unpadFloat(nf))
|
||||
}
|
||||
|
||||
func unpadFloat(f float64) string {
|
||||
|
||||
22
debug.go
22
debug.go
@@ -3,10 +3,14 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"runtime"
|
||||
|
||||
"github.com/bcicen/ctop/container"
|
||||
ui "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
var mstats = &runtime.MemStats{}
|
||||
|
||||
func logEvent(e ui.Event) {
|
||||
var s string
|
||||
s += fmt.Sprintf("Type=%s", quote(e.Type))
|
||||
@@ -18,8 +22,24 @@ func logEvent(e ui.Event) {
|
||||
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
|
||||
func dumpContainer(c *Container) {
|
||||
func dumpContainer(c *container.Container) {
|
||||
msg := fmt.Sprintf("logging state for container: %s\n", c.Id)
|
||||
for k, v := range c.Meta {
|
||||
msg += fmt.Sprintf("Meta.%s = %s\n", k, v)
|
||||
|
||||
129
glide.lock
generated
Normal file
129
glide.lock
generated
Normal file
@@ -0,0 +1,129 @@
|
||||
hash: 0d550b01b3a1c4751a8f5c3fba0c43f62252055e231712729628e514bb494da8
|
||||
updated: 2017-06-09T18:11:10.930196504-03:00
|
||||
imports:
|
||||
- name: github.com/Azure/go-ansiterm
|
||||
version: fa152c58bc15761d0200cb75fe958b89a9d4888e
|
||||
subpackages:
|
||||
- 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
|
||||
version: 90d35abf7b3535c1c319c872900fbd76374e521c
|
||||
subpackages:
|
||||
- api/types
|
||||
- api/types/blkiodev
|
||||
- api/types/container
|
||||
- api/types/filters
|
||||
- api/types/mount
|
||||
- api/types/network
|
||||
- api/types/registry
|
||||
- api/types/strslice
|
||||
- api/types/swarm
|
||||
- api/types/versions
|
||||
- opts
|
||||
- pkg/archive
|
||||
- pkg/fileutils
|
||||
- pkg/homedir
|
||||
- pkg/idtools
|
||||
- pkg/ioutils
|
||||
- pkg/jsonlog
|
||||
- pkg/jsonmessage
|
||||
- pkg/longpath
|
||||
- pkg/mount
|
||||
- pkg/pools
|
||||
- pkg/promise
|
||||
- pkg/stdcopy
|
||||
- pkg/symlink
|
||||
- pkg/system
|
||||
- pkg/term
|
||||
- pkg/term/windows
|
||||
- name: github.com/docker/go-connections
|
||||
version: a2afab9802043837035592f1c24827fb70766de9
|
||||
subpackages:
|
||||
- nat
|
||||
- name: github.com/docker/go-units
|
||||
version: 0dadbb0345b35ec7ef35e228dabb8de89a65bf52
|
||||
- name: github.com/fsouza/go-dockerclient
|
||||
version: 318513eb1ab27495afbc67f671ba1080513d8aa0
|
||||
- name: github.com/gizak/termui
|
||||
version: ea10e6ccee219e572ffad0ac1909f1a17f6db7d6
|
||||
repo: https://github.com/bcicen/termui
|
||||
vcs: git
|
||||
- name: github.com/godbus/dbus
|
||||
version: c7fdd8b5cd55e87b4e1f4e372cdb1db61dd6c66f
|
||||
- name: github.com/golang/protobuf
|
||||
version: f7137ae6b19afbfd61a94b746fda3b3fe0491874
|
||||
subpackages:
|
||||
- proto
|
||||
- name: github.com/hashicorp/go-cleanhttp
|
||||
version: 3573b8b52aa7b37b9358d966a898feb387f62437
|
||||
- name: github.com/jgautheron/codename-generator
|
||||
version: 16d037c7cc3c9b552fe4af9828b7338d752dbaf9
|
||||
- name: github.com/maruel/panicparse
|
||||
version: 25bcac0d793cf4109483505a0d66e066a3a90a80
|
||||
subpackages:
|
||||
- stack
|
||||
- name: github.com/mattn/go-runewidth
|
||||
version: 14207d285c6c197daabb5c9793d63e7af9ab2d50
|
||||
- name: github.com/Microsoft/go-winio
|
||||
version: fff283ad5116362ca252298cfc9b95828956d85d
|
||||
- name: github.com/mitchellh/go-wordwrap
|
||||
version: ad45545899c7b13c020ea92b2072220eefad42b8
|
||||
- name: github.com/nsf/termbox-go
|
||||
version: 91bae1bb5fa9ee504905ecbe7043fa30e92feaa3
|
||||
- name: github.com/nu7hatch/gouuid
|
||||
version: 179d4d0c4d8d407a32af483c2354df1d2c91e6c3
|
||||
- name: github.com/Nvveen/Gotty
|
||||
version: cd527374f1e5bff4938207604a14f2e38a9cf512
|
||||
- name: github.com/op/go-logging
|
||||
version: b2cb9fa56473e98db8caba80237377e83fe44db5
|
||||
- name: github.com/opencontainers/runc
|
||||
version: baf6536d6259209c3edfa2b22237af82942d3dfa
|
||||
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/user
|
||||
- libcontainer/utils
|
||||
- name: github.com/seccomp/libseccomp-golang
|
||||
version: 1b506fc7c24eec5a3693cdcbed40d9c226cfc6a1
|
||||
- name: github.com/Sirupsen/logrus
|
||||
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
|
||||
version: a6577fac2d73be281a500b310739095313165611
|
||||
subpackages:
|
||||
- context
|
||||
- context/ctxhttp
|
||||
- name: golang.org/x/sys
|
||||
version: 99f16d856c9836c42d24e7ab64ea72916925fa97
|
||||
subpackages:
|
||||
- unix
|
||||
- windows
|
||||
testImports: []
|
||||
18
glide.yaml
Normal file
18
glide.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
package: github.com/bcicen/ctop
|
||||
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
|
||||
version: 318513eb1ab27495afbc67f671ba1080513d8aa0
|
||||
- package: github.com/gizak/termui
|
||||
version: barchart-numfmt
|
||||
repo: https://github.com/bcicen/termui
|
||||
vcs: git
|
||||
- package: github.com/jgautheron/codename-generator
|
||||
- package: github.com/nu7hatch/gouuid
|
||||
- package: github.com/c9s/goprocinfo/linux
|
||||
- package: github.com/op/go-logging
|
||||
version: ^1.0.0
|
||||
49
grid.go
49
grid.go
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"github.com/bcicen/ctop/config"
|
||||
"github.com/bcicen/ctop/container"
|
||||
"github.com/bcicen/ctop/cwidgets/expanded"
|
||||
ui "github.com/gizak/termui"
|
||||
)
|
||||
@@ -28,13 +29,13 @@ func RedrawRows(clr bool) {
|
||||
log.Debugf("screen cleared")
|
||||
}
|
||||
if config.GetSwitchVal("enableHeader") {
|
||||
header.Render()
|
||||
ui.Render(header)
|
||||
}
|
||||
cGrid.Align()
|
||||
ui.Render(cGrid)
|
||||
}
|
||||
|
||||
func ExpandView(c *Container) {
|
||||
func ExpandView(c *container.Container) {
|
||||
ui.Clear()
|
||||
ui.DefaultEvtStream.ResetHandlers()
|
||||
defer ui.DefaultEvtStream.ResetHandlers()
|
||||
@@ -44,25 +45,28 @@ func ExpandView(c *Container) {
|
||||
|
||||
ex.Align()
|
||||
ui.Render(ex)
|
||||
ui.Handle("/timer/1s", func(ui.Event) {
|
||||
ui.Render(ex)
|
||||
})
|
||||
|
||||
HandleKeys("up", ex.Up)
|
||||
HandleKeys("down", ex.Down)
|
||||
ui.Handle("/sys/kbd/", func(ui.Event) { ui.StopLoop() })
|
||||
|
||||
ui.Handle("/timer/1s", func(ui.Event) { ui.Render(ex) })
|
||||
ui.Handle("/sys/wnd/resize", func(e ui.Event) {
|
||||
ex.SetWidth(ui.TermWidth())
|
||||
ex.Align()
|
||||
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)
|
||||
}
|
||||
|
||||
func RefreshDisplay() {
|
||||
// skip display refresh during scroll
|
||||
if !cursor.isScrolling {
|
||||
needsClear := cursor.RefreshContainers()
|
||||
RedrawRows(needsClear)
|
||||
}
|
||||
}
|
||||
|
||||
func Display() bool {
|
||||
@@ -77,16 +81,22 @@ func Display() bool {
|
||||
cursor.RefreshContainers()
|
||||
RedrawRows(true)
|
||||
|
||||
ui.Handle("/sys/kbd/<up>", func(ui.Event) { cursor.Up() })
|
||||
ui.Handle("/sys/kbd/<down>", func(ui.Event) { cursor.Down() })
|
||||
HandleKeys("up", cursor.Up)
|
||||
HandleKeys("down", 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) {
|
||||
expand = true
|
||||
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) {
|
||||
config.Toggle("allContainers")
|
||||
RefreshDisplay()
|
||||
@@ -98,10 +108,6 @@ func Display() bool {
|
||||
menu = FilterMenu
|
||||
ui.StopLoop()
|
||||
})
|
||||
ui.Handle("/sys/kbd/h", func(ui.Event) {
|
||||
menu = HelpMenu
|
||||
ui.StopLoop()
|
||||
})
|
||||
ui.Handle("/sys/kbd/H", func(ui.Event) {
|
||||
config.Toggle("enableHeader")
|
||||
RedrawRows(true)
|
||||
@@ -132,7 +138,10 @@ func Display() bool {
|
||||
return false
|
||||
}
|
||||
if expand {
|
||||
ExpandView(cursor.Selected())
|
||||
c := cursor.Selected()
|
||||
if c != nil {
|
||||
ExpandView(c)
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
||||
41
keys.go
Normal file
41
keys.go
Normal file
@@ -0,0 +1,41 @@
|
||||
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() })
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/op/go-logging"
|
||||
@@ -13,7 +14,7 @@ const (
|
||||
var (
|
||||
Log *CTopLogger
|
||||
exited bool
|
||||
level = logging.INFO
|
||||
level = logging.INFO // default level
|
||||
format = logging.MustStringFormatter(
|
||||
`%{color}%{time:15:04:05.000} ▶ %{level:.4s} %{id:03x}%{color:reset} %{message}`,
|
||||
)
|
||||
@@ -33,6 +34,11 @@ func Init() *CTopLogger {
|
||||
logging.NewMemoryBackend(size),
|
||||
}
|
||||
|
||||
if debugMode() {
|
||||
level = logging.DEBUG
|
||||
StartServer()
|
||||
}
|
||||
|
||||
backendLvl := logging.AddModuleLevel(Log.backend)
|
||||
backendLvl.SetLevel(level, "")
|
||||
|
||||
@@ -71,3 +77,6 @@ func (log *CTopLogger) Exit() {
|
||||
exited = true
|
||||
StopServer()
|
||||
}
|
||||
|
||||
func debugMode() bool { return os.Getenv("CTOP_DEBUG") == "1" }
|
||||
func debugModeTCP() bool { return os.Getenv("CTOP_DEBUG_TCP") == "1" }
|
||||
|
||||
@@ -7,7 +7,8 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
path = "/tmp/ctop.sock"
|
||||
socketPath = "./ctop.sock"
|
||||
socketAddr = "0.0.0.0:9000"
|
||||
)
|
||||
|
||||
var server struct {
|
||||
@@ -16,7 +17,13 @@ var server struct {
|
||||
}
|
||||
|
||||
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 {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
110
main.go
110
main.go
@@ -1,84 +1,129 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"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/logging"
|
||||
"github.com/bcicen/ctop/widgets"
|
||||
ui "github.com/gizak/termui"
|
||||
tm "github.com/nsf/termbox-go"
|
||||
)
|
||||
|
||||
var (
|
||||
build = "none"
|
||||
version = "dev-build"
|
||||
goVersion = runtime.Version()
|
||||
|
||||
log *logging.CTopLogger
|
||||
cursor *GridCursor
|
||||
cGrid *compact.CompactGrid
|
||||
header *widgets.CTopHeader
|
||||
|
||||
versionStr = fmt.Sprintf("ctop version %v, build %v %v", version, build, goVersion)
|
||||
)
|
||||
|
||||
func main() {
|
||||
readArgs()
|
||||
defer panicExit()
|
||||
|
||||
// init ui
|
||||
ui.ColorMap = ColorMap // override default colormap
|
||||
if err := ui.Init(); err != nil {
|
||||
panic(err)
|
||||
// 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)
|
||||
}
|
||||
defer ui.Close()
|
||||
|
||||
if *helpFlag {
|
||||
printHelp()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// init logger
|
||||
log = logging.Init()
|
||||
|
||||
// init global config
|
||||
config.Init()
|
||||
|
||||
// init logger
|
||||
log = logging.Init()
|
||||
if config.GetSwitchVal("loggingEnabled") {
|
||||
logging.StartServer()
|
||||
// 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
|
||||
if *invertFlag {
|
||||
InvertColorMap()
|
||||
}
|
||||
ui.ColorMap = ColorMap // override default colormap
|
||||
if err := ui.Init(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
defer Shutdown()
|
||||
// init grid, cursor, header
|
||||
cursor = NewGridCursor()
|
||||
conn, err := connector.ByName(*connectorFlag)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
cursor = &GridCursor{cSource: conn}
|
||||
cGrid = compact.NewCompactGrid()
|
||||
header = widgets.NewCTopHeader()
|
||||
|
||||
for {
|
||||
exit := Display()
|
||||
if exit {
|
||||
log.Notice("shutting down")
|
||||
log.Exit()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func readArgs() {
|
||||
if len(os.Args) < 2 {
|
||||
return
|
||||
func Shutdown() {
|
||||
log.Notice("shutting down")
|
||||
log.Exit()
|
||||
if tm.IsInit {
|
||||
ui.Close()
|
||||
}
|
||||
for _, arg := range os.Args[1:] {
|
||||
switch arg {
|
||||
case "-v", "version":
|
||||
printVersion()
|
||||
os.Exit(0)
|
||||
case "-h", "help":
|
||||
printHelp()
|
||||
os.Exit(0)
|
||||
default:
|
||||
fmt.Printf("invalid option or argument: \"%s\"\n", arg)
|
||||
}
|
||||
|
||||
// ensure a given sort field is valid
|
||||
func validSort(s string) {
|
||||
if _, ok := container.Sorters[s]; !ok {
|
||||
fmt.Printf("invalid sort field: %s\n", s)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func panicExit() {
|
||||
if r := recover(); r != nil {
|
||||
ui.Clear()
|
||||
fmt.Printf("panic: %s\n", r)
|
||||
Shutdown()
|
||||
fmt.Printf("error: %s\n", r)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -88,14 +133,9 @@ var helpMsg = `ctop - container metric viewer
|
||||
usage: ctop [options]
|
||||
|
||||
options:
|
||||
-h display this help dialog
|
||||
-v output version information and exit
|
||||
`
|
||||
|
||||
func printHelp() {
|
||||
fmt.Println(helpMsg)
|
||||
}
|
||||
|
||||
func printVersion() {
|
||||
fmt.Printf("ctop version %v, build %v\n", version, build)
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
|
||||
16
menus.go
16
menus.go
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"github.com/bcicen/ctop/config"
|
||||
"github.com/bcicen/ctop/container"
|
||||
"github.com/bcicen/ctop/widgets"
|
||||
"github.com/bcicen/ctop/widgets/menu"
|
||||
ui "github.com/gizak/termui"
|
||||
@@ -39,6 +40,7 @@ func FilterMenu() {
|
||||
i := widgets.NewInput()
|
||||
i.BorderLabel = "Filter"
|
||||
i.SetY(ui.TermHeight() - i.Height)
|
||||
i.Data = config.GetVal("filterStr")
|
||||
ui.Render(i)
|
||||
|
||||
// refresh container rows on input
|
||||
@@ -52,6 +54,10 @@ func FilterMenu() {
|
||||
}()
|
||||
|
||||
i.InputHandlers()
|
||||
ui.Handle("/sys/kbd/<escape>", func(ui.Event) {
|
||||
config.Update("filterStr", "")
|
||||
ui.StopLoop()
|
||||
})
|
||||
ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
|
||||
config.Update("filterStr", i.Data)
|
||||
ui.StopLoop()
|
||||
@@ -69,18 +75,22 @@ func SortMenu() {
|
||||
m.SortItems = true
|
||||
m.BorderLabel = "Sort Field"
|
||||
|
||||
for _, field := range SortFields() {
|
||||
for _, field := range container.SortFields() {
|
||||
m.AddItems(menu.Item{field, ""})
|
||||
}
|
||||
|
||||
// set cursor position to current sort field
|
||||
m.SetCursor(config.GetVal("sortField"))
|
||||
|
||||
ui.Render(m)
|
||||
m.NavigationHandlers()
|
||||
HandleKeys("up", m.Up)
|
||||
HandleKeys("down", m.Down)
|
||||
HandleKeys("exit", ui.StopLoop)
|
||||
|
||||
ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
|
||||
config.Update("sortField", m.SelectedItem().Val)
|
||||
ui.StopLoop()
|
||||
})
|
||||
|
||||
ui.Render(m)
|
||||
ui.Loop()
|
||||
}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
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))
|
||||
}
|
||||
33
models/main.go
Normal file
33
models/main.go
Normal file
@@ -0,0 +1,33 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -23,10 +23,14 @@ func NewCTopHeader() *CTopHeader {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CTopHeader) Render() {
|
||||
func (c *CTopHeader) Buffer() ui.Buffer {
|
||||
buf := ui.NewBuffer()
|
||||
c.Time.Text = timeStr()
|
||||
ui.Render(c.bg)
|
||||
ui.Render(c.Time, c.Count, c.Filter)
|
||||
buf.Merge(c.bg.Buffer())
|
||||
buf.Merge(c.Time.Buffer())
|
||||
buf.Merge(c.Count.Buffer())
|
||||
buf.Merge(c.Filter.Buffer())
|
||||
return buf
|
||||
}
|
||||
|
||||
func (c *CTopHeader) Align() {
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
input_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_."
|
||||
input_chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_."
|
||||
)
|
||||
|
||||
type Padding [2]int // x,y padding
|
||||
|
||||
@@ -100,27 +100,20 @@ func (m *Menu) Buffer() ui.Buffer {
|
||||
return buf
|
||||
}
|
||||
|
||||
func (m *Menu) Up(ui.Event) {
|
||||
func (m *Menu) Up() {
|
||||
if m.cursorPos > 0 {
|
||||
m.cursorPos--
|
||||
ui.Render(m)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Menu) Down(ui.Event) {
|
||||
func (m *Menu) Down() {
|
||||
if m.cursorPos < (len(m.items) - 1) {
|
||||
m.cursorPos++
|
||||
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
|
||||
func (m *Menu) calcSize() {
|
||||
m.Width = 7 // minimum width
|
||||
|
||||
Reference in New Issue
Block a user