mirror of
https://github.com/bcicen/ctop.git
synced 2025-12-06 23:26:45 +08:00
Compare commits
139 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
ecc37a2f99 | ||
|
|
2f17a9d689 | ||
|
|
8a6808c804 | ||
|
|
3ca94b50cd | ||
|
|
0e3fe88bb4 | ||
|
|
b9b904626c | ||
|
|
e195828f92 | ||
|
|
1d176d46c4 | ||
|
|
7026193f8e | ||
|
|
d9b4295176 | ||
|
|
92cc7bc849 | ||
|
|
70790e88ae | ||
|
|
bcf05b7f42 | ||
|
|
9df3ff2aa0 | ||
|
|
2b80832a36 | ||
|
|
a6ee6edb1d | ||
|
|
d7f9f715bb | ||
|
|
2d2d58d47f | ||
|
|
bf4d59c251 | ||
|
|
b8eb386360 | ||
|
|
02610c59da | ||
|
|
71768b498c | ||
|
|
57e49ea2c6 | ||
|
|
5b25f931df | ||
|
|
4af33fdf12 |
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ctop
|
||||||
|
.idea
|
||||||
3
Dockerfile
Normal file
3
Dockerfile
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
FROM scratch
|
||||||
|
COPY ./ctop /ctop
|
||||||
|
ENTRYPOINT ["/ctop"]
|
||||||
12
Dockerfile_build
Normal file
12
Dockerfile_build
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
FROM quay.io/vektorcloud/go:1.8
|
||||||
|
|
||||||
|
RUN apk add --no-cache make
|
||||||
|
|
||||||
|
COPY glide.* /go/src/github.com/bcicen/ctop/
|
||||||
|
WORKDIR /go/src/github.com/bcicen/ctop/
|
||||||
|
RUN glide install
|
||||||
|
|
||||||
|
COPY . /go/src/github.com/bcicen/ctop
|
||||||
|
RUN make build && \
|
||||||
|
mkdir -p /go/bin && \
|
||||||
|
mv -v ctop /go/bin/
|
||||||
22
LICENSE
Normal file
22
LICENSE
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2017 VektorLab
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
37
Makefile
Normal file
37
Makefile
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
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_build -f Dockerfile_build .
|
||||||
|
docker create --name=ctop_built ctop_build ctop -v
|
||||||
|
docker cp ctop_built:/go/bin/ctop .
|
||||||
|
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
|
||||||
68
README.md
68
README.md
@@ -1,7 +1,18 @@
|
|||||||
# ctop
|
<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:
|
||||||
|
<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` comes with built-in support for Docker and runC; connectors for other container and cluster systems are planned for future releases.
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
Fetch the [latest release](https://github.com/bcicen/ctop/releases) for your platform:
|
Fetch the [latest release](https://github.com/bcicen/ctop/releases) for your platform:
|
||||||
@@ -9,35 +20,66 @@ Fetch the [latest release](https://github.com/bcicen/ctop/releases) for your pla
|
|||||||
#### Linux
|
#### Linux
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/bcicen/ctop/releases/download/v0.1/ctop-0.1-linux-amd64 -O ctop
|
sudo wget https://github.com/bcicen/ctop/releases/download/v0.6/ctop-0.6-linux-amd64 -O /usr/local/bin/ctop
|
||||||
sudo mv ctop /usr/local/bin/
|
|
||||||
sudo chmod +x /usr/local/bin/ctop
|
sudo chmod +x /usr/local/bin/ctop
|
||||||
```
|
```
|
||||||
|
|
||||||
#### OS X
|
#### OS X
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -Lo ctop https://github.com/bcicen/ctop/releases/download/v0.1/ctop-0.1-darwin-amd64
|
brew install ctop
|
||||||
sudo mv ctop /usr/local/bin/
|
```
|
||||||
|
or
|
||||||
|
```bash
|
||||||
|
sudo curl -Lo /usr/local/bin/ctop https://github.com/bcicen/ctop/releases/download/v0.6/ctop-0.6-darwin-amd64
|
||||||
sudo chmod +x /usr/local/bin/ctop
|
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
|
## Usage
|
||||||
|
|
||||||
cTop requires no arguments and will configure itself using the `DOCKER_HOST` environment variable
|
`ctop` requires no arguments and uses Docker host variables by default. See [connectors][connectors] for further configuration options.
|
||||||
```bash
|
|
||||||
export DOCKER_HOST=tcp://127.0.0.1:4243
|
### Options
|
||||||
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
|
f | Filter displayed containers (`esc` to clear when open)
|
||||||
H | Toggle cTop header
|
H | Toggle ctop header
|
||||||
h | Open help dialog
|
h | Open help dialog
|
||||||
s | Select container sort field
|
s | Select container sort field
|
||||||
r | Reverse container sort order
|
r | Reverse container sort order
|
||||||
q | Quit cTop
|
q | Quit ctop
|
||||||
|
|
||||||
|
[build]: _docs/build.md
|
||||||
|
[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`)
|
||||||
4
_docs/expanded.md
Normal file
4
_docs/expanded.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Expanded View
|
||||||
|
|
||||||
|
ctop provides an expanded, rolling view for following container metrics
|
||||||
|
<p align="center"><img width="80%" src="img/expanded.gif" alt="ctop"/></p>
|
||||||
BIN
_docs/img/expanded.gif
Normal file
BIN
_docs/img/expanded.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 549 KiB |
BIN
_docs/img/grid.gif
Normal file
BIN
_docs/img/grid.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 676 KiB |
BIN
_docs/img/logo.png
Normal file
BIN
_docs/img/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
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}
|
||||||
58
colors.go
Normal file
58
colors.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
ui "github.com/gizak/termui"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Valid colors:
|
||||||
|
ui.ColorDefault
|
||||||
|
ui.ColorBlack
|
||||||
|
ui.ColorRed
|
||||||
|
ui.ColorGreen
|
||||||
|
ui.ColorYellow
|
||||||
|
ui.ColorBlue
|
||||||
|
ui.ColorMagenta
|
||||||
|
ui.ColorCyan
|
||||||
|
ui.ColorWhite
|
||||||
|
*/
|
||||||
|
|
||||||
|
var ColorMap = map[string]ui.Attribute{
|
||||||
|
"fg": ui.ColorWhite,
|
||||||
|
"bg": ui.ColorDefault,
|
||||||
|
"block.bg": ui.ColorDefault,
|
||||||
|
"border.bg": ui.ColorDefault,
|
||||||
|
"border.fg": ui.ColorWhite,
|
||||||
|
"label.bg": ui.ColorDefault,
|
||||||
|
"label.fg": ui.ColorGreen,
|
||||||
|
"menu.text.fg": ui.ColorWhite,
|
||||||
|
"menu.text.bg": ui.ColorDefault,
|
||||||
|
"menu.border.fg": ui.ColorCyan,
|
||||||
|
"menu.label.fg": ui.ColorGreen,
|
||||||
|
"header.fg": ui.ColorBlack,
|
||||||
|
"header.bg": ui.ColorWhite,
|
||||||
|
"gauge.bar.bg": ui.ColorGreen,
|
||||||
|
"gauge.percent.fg": ui.ColorWhite,
|
||||||
|
"linechart.axes.fg": ui.ColorDefault,
|
||||||
|
"linechart.line.fg": ui.ColorGreen,
|
||||||
|
"mbarchart.bar.bg": ui.ColorGreen,
|
||||||
|
"mbarchart.num.fg": ui.ColorWhite,
|
||||||
|
"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)
|
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)
|
||||||
|
|||||||
@@ -2,11 +2,6 @@ package config
|
|||||||
|
|
||||||
// defaults
|
// defaults
|
||||||
var params = []*Param{
|
var params = []*Param{
|
||||||
&Param{
|
|
||||||
Key: "dockerHost",
|
|
||||||
Val: getEnv("DOCKER_HOST", "unix:///var/run/docker.sock"),
|
|
||||||
Label: "Docker API URL",
|
|
||||||
},
|
|
||||||
&Param{
|
&Param{
|
||||||
Key: "filterStr",
|
Key: "filterStr",
|
||||||
Val: "",
|
Val: "",
|
||||||
|
|||||||
@@ -15,12 +15,7 @@ var switches = []*Switch{
|
|||||||
&Switch{
|
&Switch{
|
||||||
Key: "enableHeader",
|
Key: "enableHeader",
|
||||||
Val: true,
|
Val: true,
|
||||||
Label: "Enable cTop Status Line",
|
Label: "Enable Status Header",
|
||||||
},
|
|
||||||
&Switch{
|
|
||||||
Key: "loggingEnabled",
|
|
||||||
Val: false,
|
|
||||||
Label: "Enable Logging Server",
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
package metrics
|
package collector
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/bcicen/ctop/metrics"
|
||||||
api "github.com/fsouza/go-dockerclient"
|
api "github.com/fsouza/go-dockerclient"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Docker collector
|
// Docker collector
|
||||||
type Docker struct {
|
type Docker struct {
|
||||||
Metrics
|
metrics.Metrics
|
||||||
id string
|
id string
|
||||||
client *api.Client
|
client *api.Client
|
||||||
running bool
|
running bool
|
||||||
stream chan Metrics
|
stream chan metrics.Metrics
|
||||||
done chan bool
|
done chan bool
|
||||||
lastCpu float64
|
lastCpu float64
|
||||||
lastSysCpu float64
|
lastSysCpu float64
|
||||||
@@ -18,7 +19,7 @@ type Docker struct {
|
|||||||
|
|
||||||
func NewDocker(client *api.Client, id string) *Docker {
|
func NewDocker(client *api.Client, id string) *Docker {
|
||||||
return &Docker{
|
return &Docker{
|
||||||
Metrics: Metrics{},
|
Metrics: metrics.Metrics{},
|
||||||
id: id,
|
id: id,
|
||||||
client: client,
|
client: client,
|
||||||
}
|
}
|
||||||
@@ -26,7 +27,7 @@ func NewDocker(client *api.Client, id string) *Docker {
|
|||||||
|
|
||||||
func (c *Docker) Start() {
|
func (c *Docker) Start() {
|
||||||
c.done = make(chan bool)
|
c.done = make(chan bool)
|
||||||
c.stream = make(chan Metrics)
|
c.stream = make(chan metrics.Metrics)
|
||||||
stats := make(chan *api.Stats)
|
stats := make(chan *api.Stats)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
@@ -46,6 +47,7 @@ func (c *Docker) Start() {
|
|||||||
c.ReadCPU(s)
|
c.ReadCPU(s)
|
||||||
c.ReadMem(s)
|
c.ReadMem(s)
|
||||||
c.ReadNet(s)
|
c.ReadNet(s)
|
||||||
|
c.ReadIO(s)
|
||||||
c.stream <- c.Metrics
|
c.stream <- c.Metrics
|
||||||
}
|
}
|
||||||
log.Infof("collector stopped for container: %s", c.id)
|
log.Infof("collector stopped for container: %s", c.id)
|
||||||
@@ -59,7 +61,7 @@ func (c *Docker) Running() bool {
|
|||||||
return c.running
|
return c.running
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Docker) Stream() chan Metrics {
|
func (c *Docker) Stream() chan metrics.Metrics {
|
||||||
return c.stream
|
return c.stream
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,12 +81,13 @@ 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 = round((float64(c.MemUsage) / float64(c.MemLimit)) * 100)
|
c.MemPercent = percent(float64(c.MemUsage), float64(c.MemLimit))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Docker) ReadNet(stats *api.Stats) {
|
func (c *Docker) ReadNet(stats *api.Stats) {
|
||||||
@@ -95,3 +98,16 @@ func (c *Docker) ReadNet(stats *api.Stats) {
|
|||||||
}
|
}
|
||||||
c.NetRx, c.NetTx = rx, tx
|
c.NetRx, c.NetTx = rx, tx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Docker) ReadIO(stats *api.Stats) {
|
||||||
|
var read, write int64
|
||||||
|
for _, blk := range stats.BlkioStats.IOServiceBytesRecursive {
|
||||||
|
if blk.Op == "Read" {
|
||||||
|
read = int64(blk.Value)
|
||||||
|
}
|
||||||
|
if blk.Op == "Write" {
|
||||||
|
write = int64(blk.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.IOBytesRead, c.IOBytesWrite = read, write
|
||||||
|
}
|
||||||
21
connector/collector/main.go
Normal file
21
connector/collector/main.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package collector
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/bcicen/ctop/logging"
|
||||||
|
)
|
||||||
|
|
||||||
|
var log = logging.Init()
|
||||||
|
|
||||||
|
func round(num float64) int {
|
||||||
|
return int(num + math.Copysign(0.5, num))
|
||||||
|
}
|
||||||
|
|
||||||
|
// return rounded percentage
|
||||||
|
func percent(val float64, total float64) int {
|
||||||
|
if total <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return round((val / total) * 100)
|
||||||
|
}
|
||||||
79
connector/collector/mock.go
Normal file
79
connector/collector/mock.go
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
// +build !release
|
||||||
|
|
||||||
|
package collector
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bcicen/ctop/metrics"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mock collector
|
||||||
|
type Mock struct {
|
||||||
|
metrics.Metrics
|
||||||
|
stream chan metrics.Metrics
|
||||||
|
done bool
|
||||||
|
running bool
|
||||||
|
aggression int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMock(a int64) *Mock {
|
||||||
|
c := &Mock{
|
||||||
|
Metrics: metrics.Metrics{},
|
||||||
|
aggression: a,
|
||||||
|
}
|
||||||
|
c.MemLimit = 2147483648
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Mock) Running() bool {
|
||||||
|
return c.running
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Mock) Start() {
|
||||||
|
c.done = false
|
||||||
|
c.stream = make(chan metrics.Metrics)
|
||||||
|
go c.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Mock) Stop() {
|
||||||
|
c.done = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Mock) Stream() chan metrics.Metrics {
|
||||||
|
return c.stream
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
c.CPUUtil = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
c.NetTx += rand.Int63n(60) * c.aggression
|
||||||
|
c.NetRx += rand.Int63n(60) * c.aggression
|
||||||
|
c.MemUsage += rand.Int63n(c.MemLimit/512) * c.aggression
|
||||||
|
if c.MemUsage > c.MemLimit {
|
||||||
|
c.MemUsage = 0
|
||||||
|
}
|
||||||
|
c.MemPercent = percent(float64(c.MemUsage), float64(c.MemLimit))
|
||||||
|
c.stream <- c.Metrics
|
||||||
|
if c.done {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.running = false
|
||||||
|
}
|
||||||
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
|
||||||
|
}
|
||||||
124
connector/collector/runc.go
Normal file
124
connector/collector/runc.go
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
// +build !darwin
|
||||||
|
|
||||||
|
package collector
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bcicen/ctop/metrics"
|
||||||
|
"github.com/opencontainers/runc/libcontainer"
|
||||||
|
"github.com/opencontainers/runc/libcontainer/cgroups"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Runc collector
|
||||||
|
type Runc struct {
|
||||||
|
metrics.Metrics
|
||||||
|
id string
|
||||||
|
libc libcontainer.Container
|
||||||
|
stream chan metrics.Metrics
|
||||||
|
done bool
|
||||||
|
running bool
|
||||||
|
interval int // collection interval, in seconds
|
||||||
|
lastCpu float64
|
||||||
|
lastSysCpu float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRunc(libc libcontainer.Container) *Runc {
|
||||||
|
c := &Runc{
|
||||||
|
Metrics: metrics.Metrics{},
|
||||||
|
id: libc.ID(),
|
||||||
|
libc: libc,
|
||||||
|
interval: 1,
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Runc) Running() bool {
|
||||||
|
return c.running
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Runc) Start() {
|
||||||
|
c.done = false
|
||||||
|
c.stream = make(chan metrics.Metrics)
|
||||||
|
go c.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Runc) Stop() {
|
||||||
|
c.done = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Runc) Stream() chan metrics.Metrics {
|
||||||
|
return c.stream
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Runc) run() {
|
||||||
|
c.running = true
|
||||||
|
defer close(c.stream)
|
||||||
|
log.Debugf("collector started for container: %s", c.id)
|
||||||
|
|
||||||
|
for {
|
||||||
|
stats, err := c.libc.Stats()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to collect stats for container %s:\n%s", c.id, err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
c.ReadCPU(stats.CgroupStats)
|
||||||
|
c.ReadMem(stats.CgroupStats)
|
||||||
|
c.ReadNet(stats.Interfaces)
|
||||||
|
|
||||||
|
c.stream <- c.Metrics
|
||||||
|
if c.done {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.running = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Runc) ReadCPU(stats *cgroups.Stats) {
|
||||||
|
u := stats.CpuStats.CpuUsage
|
||||||
|
ncpus := float64(len(u.PercpuUsage))
|
||||||
|
total := float64(u.TotalUsage)
|
||||||
|
system := float64(getSysCPUUsage())
|
||||||
|
|
||||||
|
cpudiff := total - c.lastCpu
|
||||||
|
syscpudiff := system - c.lastSysCpu
|
||||||
|
|
||||||
|
c.CPUUtil = round((cpudiff / syscpudiff * 100) * ncpus)
|
||||||
|
c.lastCpu = total
|
||||||
|
c.lastSysCpu = system
|
||||||
|
c.Pids = int(stats.PidsStats.Current)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Runc) ReadMem(stats *cgroups.Stats) {
|
||||||
|
c.MemUsage = int64(stats.MemoryStats.Usage.Usage)
|
||||||
|
c.MemLimit = int64(stats.MemoryStats.Usage.Limit)
|
||||||
|
if c.MemLimit > sysMemTotal && sysMemTotal > 0 {
|
||||||
|
c.MemLimit = sysMemTotal
|
||||||
|
}
|
||||||
|
c.MemPercent = percent(float64(c.MemUsage), float64(c.MemLimit))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Runc) ReadNet(interfaces []*libcontainer.NetworkInterface) {
|
||||||
|
var rx, tx int64
|
||||||
|
for _, network := range interfaces {
|
||||||
|
rx += int64(network.RxBytes)
|
||||||
|
tx += int64(network.TxBytes)
|
||||||
|
}
|
||||||
|
c.NetRx, c.NetTx = rx, tx
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Runc) ReadIO(stats *cgroups.Stats) {
|
||||||
|
var read, write int64
|
||||||
|
for _, blk := range stats.BlkioStats.IoServiceBytesRecursive {
|
||||||
|
if blk.Op == "Read" {
|
||||||
|
read = int64(blk.Value)
|
||||||
|
}
|
||||||
|
if blk.Op == "Write" {
|
||||||
|
write = int64(blk.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.IOBytesRead, c.IOBytesWrite = read, write
|
||||||
|
}
|
||||||
@@ -1,46 +1,44 @@
|
|||||||
package main
|
package connector
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sort"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/bcicen/ctop/config"
|
"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 ContainerSource interface {
|
type Docker struct {
|
||||||
All() Containers
|
client *api.Client
|
||||||
Get(string) (*Container, bool)
|
containers map[string]*container.Container
|
||||||
}
|
|
||||||
|
|
||||||
type DockerContainerSource struct {
|
|
||||||
client *docker.Client
|
|
||||||
containers map[string]*Container
|
|
||||||
needsRefresh chan string // container IDs requiring refresh
|
needsRefresh chan string // container IDs requiring refresh
|
||||||
|
lock sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDockerContainerSource() *DockerContainerSource {
|
func NewDocker() Connector {
|
||||||
// init docker client
|
// init docker client
|
||||||
client, err := docker.NewClient(config.GetVal("dockerHost"))
|
client, err := api.NewClientFromEnv()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
cm := &DockerContainerSource{
|
cm := &Docker{
|
||||||
client: client,
|
client: client,
|
||||||
containers: make(map[string]*Container),
|
containers: make(map[string]*container.Container),
|
||||||
needsRefresh: make(chan string, 60),
|
needsRefresh: make(chan string, 60),
|
||||||
|
lock: sync.RWMutex{},
|
||||||
}
|
}
|
||||||
cm.refreshAll()
|
|
||||||
go cm.Loop()
|
go cm.Loop()
|
||||||
|
cm.refreshAll()
|
||||||
go cm.watchEvents()
|
go cm.watchEvents()
|
||||||
return cm
|
return cm
|
||||||
}
|
}
|
||||||
|
|
||||||
// Docker events watcher
|
// Docker events watcher
|
||||||
func (cm *DockerContainerSource) watchEvents() {
|
func (cm *Docker) watchEvents() {
|
||||||
log.Info("docker event listener starting")
|
log.Info("docker event listener starting")
|
||||||
events := make(chan *docker.APIEvents)
|
events := make(chan *api.APIEvents)
|
||||||
cm.client.AddEventListener(events)
|
cm.client.AddEventListener(events)
|
||||||
|
|
||||||
for e := range events {
|
for e := range events {
|
||||||
@@ -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", k, binding.HostIP, binding.HostPort)
|
||||||
|
published = append(published, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(append(exposed, published...), "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cm *Docker) refresh(c *container.Container) {
|
||||||
insp := cm.inspect(c.Id)
|
insp := cm.inspect(c.Id)
|
||||||
// remove container if no longer exists
|
// remove container if no longer exists
|
||||||
if insp == nil {
|
if insp == nil {
|
||||||
@@ -67,14 +83,15 @@ func (cm *DockerContainerSource) refresh(c *Container) {
|
|||||||
}
|
}
|
||||||
c.SetMeta("name", shortName(insp.Name))
|
c.SetMeta("name", shortName(insp.Name))
|
||||||
c.SetMeta("image", insp.Config.Image)
|
c.SetMeta("image", insp.Config.Image)
|
||||||
|
c.SetMeta("ports", portsFormat(insp.NetworkSettings.Ports))
|
||||||
c.SetMeta("created", insp.Created.Format("Mon Jan 2 15:04:05 2006"))
|
c.SetMeta("created", insp.Created.Format("Mon Jan 2 15:04:05 2006"))
|
||||||
c.SetState(insp.State.Status)
|
c.SetState(insp.State.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cm *DockerContainerSource) inspect(id string) *docker.Container {
|
func (cm *Docker) inspect(id string) *api.Container {
|
||||||
c, err := cm.client.InspectContainer(id)
|
c, err := cm.client.InspectContainer(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if _, ok := err.(*docker.NoSuchContainer); ok == false {
|
if _, ok := err.(*api.NoSuchContainer); ok == false {
|
||||||
log.Errorf(err.Error())
|
log.Errorf(err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,8 +99,8 @@ func (cm *DockerContainerSource) inspect(id string) *docker.Container {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Mark all container IDs for refresh
|
// Mark all container IDs for refresh
|
||||||
func (cm *DockerContainerSource) refreshAll() {
|
func (cm *Docker) refreshAll() {
|
||||||
opts := docker.ListContainersOptions{All: true}
|
opts := api.ListContainersOptions{All: true}
|
||||||
allContainers, err := cm.client.ListContainers(opts)
|
allContainers, err := cm.client.ListContainers(opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@@ -97,7 +114,7 @@ func (cm *DockerContainerSource) refreshAll() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cm *DockerContainerSource) Loop() {
|
func (cm *Docker) Loop() {
|
||||||
for id := range cm.needsRefresh {
|
for id := range cm.needsRefresh {
|
||||||
c := cm.MustGet(id)
|
c := cm.MustGet(id)
|
||||||
cm.refresh(c)
|
cm.refresh(c)
|
||||||
@@ -105,37 +122,46 @@ func (cm *DockerContainerSource) Loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get a single container, creating one anew if not existing
|
// Get a single container, creating one anew if not existing
|
||||||
func (cm *DockerContainerSource) MustGet(id string) *Container {
|
func (cm *Docker) MustGet(id string) *container.Container {
|
||||||
c, ok := cm.Get(id)
|
c, ok := cm.Get(id)
|
||||||
// append container struct for new containers
|
// append container struct for new containers
|
||||||
if !ok {
|
if !ok {
|
||||||
// create collector
|
// create collector
|
||||||
collector := metrics.NewDocker(cm.client, id)
|
collector := collector.NewDocker(cm.client, id)
|
||||||
// create container
|
// create container
|
||||||
c = NewContainer(id, collector)
|
c = container.New(id, collector)
|
||||||
|
cm.lock.Lock()
|
||||||
cm.containers[id] = c
|
cm.containers[id] = c
|
||||||
|
cm.lock.Unlock()
|
||||||
}
|
}
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get a single container, by ID
|
// Get a single container, by ID
|
||||||
func (cm *DockerContainerSource) Get(id string) (*Container, bool) {
|
func (cm *Docker) Get(id string) (*container.Container, bool) {
|
||||||
|
cm.lock.Lock()
|
||||||
c, ok := cm.containers[id]
|
c, ok := cm.containers[id]
|
||||||
|
cm.lock.Unlock()
|
||||||
return c, ok
|
return c, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove containers by ID
|
// Remove containers by ID
|
||||||
func (cm *DockerContainerSource) delByID(id string) {
|
func (cm *Docker) delByID(id string) {
|
||||||
|
cm.lock.Lock()
|
||||||
delete(cm.containers, id)
|
delete(cm.containers, id)
|
||||||
|
cm.lock.Unlock()
|
||||||
log.Infof("removed dead container: %s", id)
|
log.Infof("removed dead container: %s", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return array of all containers, sorted by field
|
// Return array of all containers, sorted by field
|
||||||
func (cm *DockerContainerSource) All() (containers Containers) {
|
func (cm *Docker) All() (containers container.Containers) {
|
||||||
|
cm.lock.Lock()
|
||||||
for _, c := range cm.containers {
|
for _, c := range cm.containers {
|
||||||
containers = append(containers, c)
|
containers = append(containers, c)
|
||||||
}
|
}
|
||||||
sort.Sort(containers)
|
containers.Sort()
|
||||||
|
containers.Filter()
|
||||||
|
cm.lock.Unlock()
|
||||||
return containers
|
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,46 +1,52 @@
|
|||||||
// +build !release
|
// +build !release
|
||||||
|
|
||||||
package main
|
package connector
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bcicen/ctop/metrics"
|
"github.com/bcicen/ctop/connector/collector"
|
||||||
|
"github.com/bcicen/ctop/container"
|
||||||
"github.com/jgautheron/codename-generator"
|
"github.com/jgautheron/codename-generator"
|
||||||
"github.com/nu7hatch/gouuid"
|
"github.com/nu7hatch/gouuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MockContainerSource struct {
|
type Mock struct {
|
||||||
containers Containers
|
containers container.Containers
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMockContainerSource() *MockContainerSource {
|
func NewMock() *Mock {
|
||||||
cs := &MockContainerSource{}
|
cs := &Mock{}
|
||||||
go cs.Init()
|
go cs.Init()
|
||||||
go cs.Loop()
|
go cs.Loop()
|
||||||
return cs
|
return cs
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create Mock containers
|
// Create Mock containers
|
||||||
func (cs *MockContainerSource) Init() {
|
func (cs *Mock) Init() {
|
||||||
total := 20
|
|
||||||
rand.Seed(int64(time.Now().Nanosecond()))
|
rand.Seed(int64(time.Now().Nanosecond()))
|
||||||
|
|
||||||
for i := 0; i < total; i++ {
|
for i := 0; i < 4; i++ {
|
||||||
//time.Sleep(1 * time.Second)
|
cs.makeContainer(3)
|
||||||
collector := metrics.NewMock()
|
}
|
||||||
c := NewContainer(makeID(), collector)
|
|
||||||
c.SetMeta("name", makeName())
|
for i := 0; i < 16; i++ {
|
||||||
c.SetState(makeState())
|
cs.makeContainer(1)
|
||||||
cs.containers = append(cs.containers, c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cs *MockContainerSource) Loop() {
|
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 *Mock) Loop() {
|
||||||
iter := 0
|
iter := 0
|
||||||
for {
|
for {
|
||||||
// Change state for random container
|
// Change state for random container
|
||||||
@@ -54,7 +60,7 @@ func (cs *MockContainerSource) Loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get a single container, by ID
|
// Get a single container, by ID
|
||||||
func (cs *MockContainerSource) Get(id string) (*Container, bool) {
|
func (cs *Mock) Get(id string) (*container.Container, bool) {
|
||||||
for _, c := range cs.containers {
|
for _, c := range cs.containers {
|
||||||
if c.Id == id {
|
if c.Id == id {
|
||||||
return c, true
|
return c, true
|
||||||
@@ -64,13 +70,14 @@ func (cs *MockContainerSource) Get(id string) (*Container, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Return array of all containers, sorted by field
|
// Return array of all containers, sorted by field
|
||||||
func (cs *MockContainerSource) All() Containers {
|
func (cs *Mock) All() container.Containers {
|
||||||
sort.Sort(cs.containers)
|
cs.containers.Sort()
|
||||||
|
cs.containers.Filter()
|
||||||
return cs.containers
|
return cs.containers
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove containers by ID
|
// Remove containers by ID
|
||||||
func (cs *MockContainerSource) delByID(id string) {
|
func (cs *Mock) delByID(id string) {
|
||||||
for n, c := range cs.containers {
|
for n, c := range cs.containers {
|
||||||
if c.Id == id {
|
if c.Id == id {
|
||||||
cs.del(n)
|
cs.del(n)
|
||||||
@@ -80,7 +87,7 @@ func (cs *MockContainerSource) delByID(id string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Remove one or more containers by index
|
// Remove one or more containers by index
|
||||||
func (cs *MockContainerSource) del(idx ...int) {
|
func (cs *Mock) del(idx ...int) {
|
||||||
for _, i := range idx {
|
for _, i := range idx {
|
||||||
cs.containers = append(cs.containers[:i], cs.containers[i+1:]...)
|
cs.containers = append(cs.containers[:i], cs.containers[i+1:]...)
|
||||||
}
|
}
|
||||||
243
connector/runc.go
Normal file
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,22 +1,28 @@
|
|||||||
package main
|
package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/bcicen/ctop/cwidgets"
|
"github.com/bcicen/ctop/cwidgets"
|
||||||
"github.com/bcicen/ctop/cwidgets/compact"
|
"github.com/bcicen/ctop/cwidgets/compact"
|
||||||
|
"github.com/bcicen/ctop/logging"
|
||||||
"github.com/bcicen/ctop/metrics"
|
"github.com/bcicen/ctop/metrics"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
log = logging.Init()
|
||||||
|
)
|
||||||
|
|
||||||
// Metrics and metadata representing a container
|
// Metrics and metadata representing a container
|
||||||
type Container struct {
|
type Container struct {
|
||||||
metrics.Metrics
|
metrics.Metrics
|
||||||
Id string
|
Id string
|
||||||
Meta map[string]string
|
Meta map[string]string
|
||||||
Widgets *compact.Compact
|
Widgets *compact.Compact
|
||||||
|
Display bool // display this container in compact view
|
||||||
updater cwidgets.WidgetUpdater
|
updater cwidgets.WidgetUpdater
|
||||||
collector metrics.Collector
|
collector metrics.Collector
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewContainer(id string, collector metrics.Collector) *Container {
|
func New(id string, collector metrics.Collector) *Container {
|
||||||
widgets := compact.NewCompact(id)
|
widgets := compact.NewCompact(id)
|
||||||
return &Container{
|
return &Container{
|
||||||
Metrics: metrics.NewMetrics(),
|
Metrics: metrics.NewMetrics(),
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
package main
|
package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
|
||||||
"github.com/bcicen/ctop/config"
|
"github.com/bcicen/ctop/config"
|
||||||
)
|
)
|
||||||
@@ -53,6 +54,22 @@ var Sorters = map[string]sortMethod{
|
|||||||
}
|
}
|
||||||
return sum1 > sum2
|
return sum1 > sum2
|
||||||
},
|
},
|
||||||
|
"pids": func(c1, c2 *Container) bool {
|
||||||
|
// Use secondary sort method if equal values
|
||||||
|
if c1.Pids == c2.Pids {
|
||||||
|
return nameSorter(c1, c2)
|
||||||
|
}
|
||||||
|
return c1.Pids > c2.Pids
|
||||||
|
},
|
||||||
|
"io": func(c1, c2 *Container) bool {
|
||||||
|
sum1 := sumIO(c1)
|
||||||
|
sum2 := sumIO(c2)
|
||||||
|
// Use secondary sort method if equal values
|
||||||
|
if sum1 == sum2 {
|
||||||
|
return nameSorter(c1, c2)
|
||||||
|
}
|
||||||
|
return sum1 > sum2
|
||||||
|
},
|
||||||
"state": func(c1, c2 *Container) bool {
|
"state": func(c1, c2 *Container) bool {
|
||||||
// Use secondary sort method if equal values
|
// Use secondary sort method if equal values
|
||||||
c1state := c1.GetMeta("state")
|
c1state := c1.GetMeta("state")
|
||||||
@@ -73,6 +90,7 @@ func SortFields() (fields []string) {
|
|||||||
|
|
||||||
type Containers []*Container
|
type Containers []*Container
|
||||||
|
|
||||||
|
func (a Containers) Sort() { sort.Sort(a) }
|
||||||
func (a Containers) Len() int { return len(a) }
|
func (a Containers) Len() int { return len(a) }
|
||||||
func (a Containers) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
func (a Containers) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||||
func (a Containers) Less(i, j int) bool {
|
func (a Containers) Less(i, j int) bool {
|
||||||
@@ -83,23 +101,23 @@ func (a Containers) Less(i, j int) bool {
|
|||||||
return f(a[i], a[j])
|
return f(a[i], a[j])
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a Containers) Filter() (filtered []*Container) {
|
func (a Containers) Filter() {
|
||||||
filter := config.GetVal("filterStr")
|
filter := config.GetVal("filterStr")
|
||||||
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
|
||||||
// Apply name filter
|
// Apply name filter
|
||||||
if re.FindAllString(c.GetMeta("name"), 1) == nil {
|
if re.FindAllString(c.GetMeta("name"), 1) == nil {
|
||||||
continue
|
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" {
|
||||||
continue
|
c.Display = false
|
||||||
}
|
}
|
||||||
filtered = append(filtered, c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return filtered
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 }
|
||||||
155
cursor.go
155
cursor.go
@@ -1,79 +1,182 @@
|
|||||||
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
|
||||||
containers Containers
|
filtered container.Containers
|
||||||
cSource ContainerSource
|
cSource connector.Connector
|
||||||
|
isScrolling bool // toggled when actively scrolling
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewGridCursor() *GridCursor {
|
func (gc *GridCursor) Len() int { return len(gc.filtered) }
|
||||||
return &GridCursor{
|
|
||||||
cSource: NewDockerContainerSource(),
|
func (gc *GridCursor) Selected() *container.Container {
|
||||||
|
idx := gc.Idx()
|
||||||
|
if idx < gc.Len() {
|
||||||
|
return gc.filtered[idx]
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gc *GridCursor) Len() int { return len(gc.containers) }
|
// Refresh containers from source
|
||||||
func (gc *GridCursor) Selected() *Container { return gc.containers[gc.Idx()] }
|
func (gc *GridCursor) RefreshContainers() (lenChanged bool) {
|
||||||
|
oldLen := gc.Len()
|
||||||
|
|
||||||
func (gc *GridCursor) RefreshContainers() {
|
// Containers filtered by display bool
|
||||||
gc.containers = gc.cSource.All().Filter()
|
gc.filtered = container.Containers{}
|
||||||
|
var cursorVisible bool
|
||||||
|
for _, c := range gc.cSource.All() {
|
||||||
|
if c.Display {
|
||||||
|
if c.Id == gc.selectedID {
|
||||||
|
cursorVisible = true
|
||||||
|
}
|
||||||
|
gc.filtered = append(gc.filtered, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if oldLen != gc.Len() {
|
||||||
|
lenChanged = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cursorVisible {
|
||||||
|
gc.Reset()
|
||||||
|
}
|
||||||
if gc.selectedID == "" {
|
if gc.selectedID == "" {
|
||||||
gc.Reset()
|
gc.Reset()
|
||||||
}
|
}
|
||||||
|
return lenChanged
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set an initial cursor position, if possible
|
// Set an initial cursor position, if possible
|
||||||
func (gc *GridCursor) Reset() {
|
func (gc *GridCursor) Reset() {
|
||||||
|
for _, c := range gc.cSource.All() {
|
||||||
|
c.Widgets.Name.UnHighlight()
|
||||||
|
}
|
||||||
if gc.Len() > 0 {
|
if gc.Len() > 0 {
|
||||||
gc.selectedID = gc.containers[0].Id
|
gc.selectedID = gc.filtered[0].Id
|
||||||
gc.containers[0].Widgets.Name.Highlight()
|
gc.filtered[0].Widgets.Name.Highlight()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return current cursor index
|
// Return current cursor index
|
||||||
func (gc *GridCursor) Idx() int {
|
func (gc *GridCursor) Idx() int {
|
||||||
for n, c := range gc.containers {
|
for n, c := range gc.filtered {
|
||||||
if c.Id == gc.selectedID {
|
if c.Id == gc.selectedID {
|
||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
gc.Reset()
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gc *GridCursor) Up() {
|
func (gc *GridCursor) ScrollPage() {
|
||||||
idx := gc.Idx()
|
// skip scroll if no need to page
|
||||||
// decrement if possible
|
if gc.Len() < cGrid.MaxRows() {
|
||||||
if idx <= 0 {
|
cGrid.Offset = 0
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
active := gc.containers[idx]
|
|
||||||
next := gc.containers[idx-1]
|
idx := gc.Idx()
|
||||||
|
|
||||||
|
// page down
|
||||||
|
if idx >= cGrid.Offset+cGrid.MaxRows() {
|
||||||
|
cGrid.Offset++
|
||||||
|
cGrid.Align()
|
||||||
|
}
|
||||||
|
// page up
|
||||||
|
if idx < cGrid.Offset {
|
||||||
|
cGrid.Offset--
|
||||||
|
cGrid.Align()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gc *GridCursor) Up() {
|
||||||
|
gc.isScrolling = true
|
||||||
|
defer func() { gc.isScrolling = false }()
|
||||||
|
|
||||||
|
idx := gc.Idx()
|
||||||
|
if idx <= 0 { // already at top
|
||||||
|
return
|
||||||
|
}
|
||||||
|
active := gc.filtered[idx]
|
||||||
|
next := gc.filtered[idx-1]
|
||||||
|
|
||||||
active.Widgets.Name.UnHighlight()
|
active.Widgets.Name.UnHighlight()
|
||||||
gc.selectedID = next.Id
|
gc.selectedID = next.Id
|
||||||
next.Widgets.Name.Highlight()
|
next.Widgets.Name.Highlight()
|
||||||
|
|
||||||
|
gc.ScrollPage()
|
||||||
ui.Render(cGrid)
|
ui.Render(cGrid)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gc *GridCursor) Down() {
|
func (gc *GridCursor) Down() {
|
||||||
|
gc.isScrolling = true
|
||||||
|
defer func() { gc.isScrolling = false }()
|
||||||
|
|
||||||
idx := gc.Idx()
|
idx := gc.Idx()
|
||||||
// increment if possible
|
if idx >= gc.Len()-1 { // already at bottom
|
||||||
if idx >= (gc.Len() - 1) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if idx >= maxRows()-1 {
|
active := gc.filtered[idx]
|
||||||
return
|
next := gc.filtered[idx+1]
|
||||||
}
|
|
||||||
active := gc.containers[idx]
|
|
||||||
next := gc.containers[idx+1]
|
|
||||||
|
|
||||||
active.Widgets.Name.UnHighlight()
|
active.Widgets.Name.UnHighlight()
|
||||||
gc.selectedID = next.Id
|
gc.selectedID = next.Id
|
||||||
next.Widgets.Name.Highlight()
|
next.Widgets.Name.Highlight()
|
||||||
|
|
||||||
|
gc.ScrollPage()
|
||||||
|
ui.Render(cGrid)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gc *GridCursor) PgUp() {
|
||||||
|
idx := gc.Idx()
|
||||||
|
if idx <= 0 { // already at top
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var nextidx int
|
||||||
|
nextidx = int(math.Max(0.0, float64(idx-cGrid.MaxRows())))
|
||||||
|
cGrid.Offset = int(math.Max(float64(cGrid.Offset-cGrid.MaxRows()),
|
||||||
|
float64(0)))
|
||||||
|
|
||||||
|
active := gc.filtered[idx]
|
||||||
|
next := gc.filtered[nextidx]
|
||||||
|
|
||||||
|
active.Widgets.Name.UnHighlight()
|
||||||
|
gc.selectedID = next.Id
|
||||||
|
next.Widgets.Name.Highlight()
|
||||||
|
|
||||||
|
cGrid.Align()
|
||||||
|
ui.Render(cGrid)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gc *GridCursor) PgDown() {
|
||||||
|
idx := gc.Idx()
|
||||||
|
if idx >= gc.Len()-1 { // already at bottom
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var nextidx int
|
||||||
|
nextidx = int(math.Min(float64(gc.Len()-1),
|
||||||
|
float64(idx+cGrid.MaxRows())))
|
||||||
|
cGrid.Offset = int(math.Min(float64(cGrid.Offset+cGrid.MaxRows()),
|
||||||
|
float64(gc.Len()-cGrid.MaxRows())))
|
||||||
|
|
||||||
|
active := gc.filtered[idx]
|
||||||
|
next := gc.filtered[nextidx]
|
||||||
|
|
||||||
|
active.Widgets.Name.UnHighlight()
|
||||||
|
gc.selectedID = next.Id
|
||||||
|
next.Widgets.Name.Highlight()
|
||||||
|
|
||||||
|
cGrid.Align()
|
||||||
ui.Render(cGrid)
|
ui.Render(cGrid)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ func NewGaugeCol() *GaugeCol {
|
|||||||
g.Border = false
|
g.Border = false
|
||||||
g.Percent = 0
|
g.Percent = 0
|
||||||
g.PaddingBottom = 0
|
g.PaddingBottom = 0
|
||||||
g.BarColor = ui.ColorGreen
|
|
||||||
g.Label = "-"
|
g.Label = "-"
|
||||||
return &GaugeCol{g}
|
return &GaugeCol{g}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,44 +4,51 @@ import (
|
|||||||
ui "github.com/gizak/termui"
|
ui "github.com/gizak/termui"
|
||||||
)
|
)
|
||||||
|
|
||||||
var header = NewCompactHeader()
|
var header *CompactHeader
|
||||||
|
|
||||||
type CompactGrid struct {
|
type CompactGrid struct {
|
||||||
ui.GridBufferer
|
ui.GridBufferer
|
||||||
Rows []ui.GridBufferer
|
Rows []ui.GridBufferer
|
||||||
X, Y int
|
X, Y int
|
||||||
Width int
|
Width int
|
||||||
Height int
|
Height int
|
||||||
cursorID string
|
Offset int // starting row offset
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCompactGrid() *CompactGrid {
|
func NewCompactGrid() *CompactGrid {
|
||||||
|
header = NewCompactHeader() // init column header
|
||||||
return &CompactGrid{}
|
return &CompactGrid{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cg *CompactGrid) Align() {
|
func (cg *CompactGrid) Align() {
|
||||||
// update row y pos recursively
|
|
||||||
y := cg.Y
|
y := cg.Y
|
||||||
for _, r := range cg.Rows {
|
if cg.Offset >= len(cg.Rows) {
|
||||||
|
cg.Offset = 0
|
||||||
|
}
|
||||||
|
// update row ypos, width recursively
|
||||||
|
for _, r := range cg.pageRows() {
|
||||||
r.SetY(y)
|
r.SetY(y)
|
||||||
y += r.GetHeight()
|
y += r.GetHeight()
|
||||||
}
|
|
||||||
|
|
||||||
// update row width recursively
|
|
||||||
for _, r := range cg.Rows {
|
|
||||||
r.SetWidth(cg.Width)
|
r.SetWidth(cg.Width)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cg *CompactGrid) Clear() { cg.Rows = []ui.GridBufferer{header} }
|
func (cg *CompactGrid) Clear() { cg.Rows = []ui.GridBufferer{} }
|
||||||
func (cg *CompactGrid) GetHeight() int { return len(cg.Rows) }
|
func (cg *CompactGrid) GetHeight() int { return len(cg.Rows) + header.Height }
|
||||||
func (cg *CompactGrid) SetX(x int) { cg.X = x }
|
func (cg *CompactGrid) SetX(x int) { cg.X = x }
|
||||||
func (cg *CompactGrid) SetY(y int) { cg.Y = y }
|
func (cg *CompactGrid) SetY(y int) { cg.Y = y }
|
||||||
func (cg *CompactGrid) SetWidth(w int) { cg.Width = w }
|
func (cg *CompactGrid) SetWidth(w int) { cg.Width = w }
|
||||||
|
func (cg *CompactGrid) MaxRows() int { return ui.TermHeight() - header.Height - cg.Y }
|
||||||
|
|
||||||
|
func (cg *CompactGrid) pageRows() (rows []ui.GridBufferer) {
|
||||||
|
rows = append(rows, header)
|
||||||
|
rows = append(rows, cg.Rows[cg.Offset:]...)
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
func (cg *CompactGrid) Buffer() ui.Buffer {
|
func (cg *CompactGrid) Buffer() ui.Buffer {
|
||||||
buf := ui.NewBuffer()
|
buf := ui.NewBuffer()
|
||||||
for _, r := range cg.Rows {
|
for _, r := range cg.pageRows() {
|
||||||
buf.Merge(r.Buffer())
|
buf.Merge(r.Buffer())
|
||||||
}
|
}
|
||||||
return buf
|
return buf
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ type CompactHeader struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewCompactHeader() *CompactHeader {
|
func NewCompactHeader() *CompactHeader {
|
||||||
fields := []string{"", "NAME", "CID", "CPU", "MEM", "NET RX/TX"}
|
fields := []string{"", "NAME", "CID", "CPU", "MEM", "NET RX/TX", "IO R/W", "PIDS"}
|
||||||
ch := &CompactHeader{}
|
ch := &CompactHeader{}
|
||||||
ch.Height = 2
|
ch.Height = 2
|
||||||
for _, f := range fields {
|
for _, f := range fields {
|
||||||
@@ -27,13 +27,13 @@ func (ch *CompactHeader) GetHeight() int {
|
|||||||
|
|
||||||
func (ch *CompactHeader) SetWidth(w int) {
|
func (ch *CompactHeader) SetWidth(w int) {
|
||||||
x := ch.X
|
x := ch.X
|
||||||
autoWidth := calcWidth(w, 5)
|
autoWidth := calcWidth(w)
|
||||||
for n, col := range ch.pars {
|
for n, col := range ch.pars {
|
||||||
// set status column to static width
|
// set column to static width
|
||||||
if n == 0 {
|
if colWidths[n] != 0 {
|
||||||
col.SetX(x)
|
col.SetX(x)
|
||||||
col.SetWidth(statusWidth)
|
col.SetWidth(colWidths[n])
|
||||||
x += statusWidth
|
x += colWidths[n]
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
col.SetX(x)
|
col.SetX(x)
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ type Compact struct {
|
|||||||
Cpu *GaugeCol
|
Cpu *GaugeCol
|
||||||
Memory *GaugeCol
|
Memory *GaugeCol
|
||||||
Net *TextCol
|
Net *TextCol
|
||||||
|
IO *TextCol
|
||||||
|
Pids *TextCol
|
||||||
X, Y int
|
X, Y int
|
||||||
Width int
|
Width int
|
||||||
Height int
|
Height int
|
||||||
@@ -32,6 +34,8 @@ func NewCompact(id string) *Compact {
|
|||||||
Cpu: NewGaugeCol(),
|
Cpu: NewGaugeCol(),
|
||||||
Memory: NewGaugeCol(),
|
Memory: NewGaugeCol(),
|
||||||
Net: NewTextCol("-"),
|
Net: NewTextCol("-"),
|
||||||
|
IO: NewTextCol("-"),
|
||||||
|
Pids: NewTextCol("-"),
|
||||||
X: 1,
|
X: 1,
|
||||||
Height: 1,
|
Height: 1,
|
||||||
}
|
}
|
||||||
@@ -59,6 +63,8 @@ func (row *Compact) SetMetrics(m metrics.Metrics) {
|
|||||||
row.SetCPU(m.CPUUtil)
|
row.SetCPU(m.CPUUtil)
|
||||||
row.SetNet(m.NetRx, m.NetTx)
|
row.SetNet(m.NetRx, m.NetTx)
|
||||||
row.SetMem(m.MemUsage, m.MemLimit, m.MemPercent)
|
row.SetMem(m.MemUsage, m.MemLimit, m.MemPercent)
|
||||||
|
row.SetIO(m.IOBytesRead, m.IOBytesWrite)
|
||||||
|
row.SetPids(m.Pids)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set gauges, counters to default unread values
|
// Set gauges, counters to default unread values
|
||||||
@@ -66,6 +72,8 @@ func (row *Compact) Reset() {
|
|||||||
row.Cpu.Reset()
|
row.Cpu.Reset()
|
||||||
row.Memory.Reset()
|
row.Memory.Reset()
|
||||||
row.Net.Reset()
|
row.Net.Reset()
|
||||||
|
row.IO.Reset()
|
||||||
|
row.Pids.Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (row *Compact) GetHeight() int {
|
func (row *Compact) GetHeight() int {
|
||||||
@@ -91,13 +99,12 @@ func (row *Compact) SetWidth(width int) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
x := row.X
|
x := row.X
|
||||||
autoWidth := calcWidth(width, 5)
|
autoWidth := calcWidth(width)
|
||||||
for n, col := range row.all() {
|
for n, col := range row.all() {
|
||||||
// set status column to static width
|
if colWidths[n] != 0 {
|
||||||
if n == 0 {
|
|
||||||
col.SetX(x)
|
col.SetX(x)
|
||||||
col.SetWidth(statusWidth)
|
col.SetWidth(colWidths[n])
|
||||||
x += statusWidth
|
x += colWidths[n]
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
col.SetX(x)
|
col.SetX(x)
|
||||||
@@ -116,7 +123,8 @@ func (row *Compact) Buffer() ui.Buffer {
|
|||||||
buf.Merge(row.Cpu.Buffer())
|
buf.Merge(row.Cpu.Buffer())
|
||||||
buf.Merge(row.Memory.Buffer())
|
buf.Merge(row.Memory.Buffer())
|
||||||
buf.Merge(row.Net.Buffer())
|
buf.Merge(row.Net.Buffer())
|
||||||
|
buf.Merge(row.IO.Buffer())
|
||||||
|
buf.Merge(row.Pids.Buffer())
|
||||||
return buf
|
return buf
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,5 +136,7 @@ func (row *Compact) all() []ui.GridBufferer {
|
|||||||
row.Cpu,
|
row.Cpu,
|
||||||
row.Memory,
|
row.Memory,
|
||||||
row.Net,
|
row.Net,
|
||||||
|
row.IO,
|
||||||
|
row.Pids,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,12 +13,25 @@ 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))
|
||||||
if val < 5 {
|
if val < 5 {
|
||||||
val = 5
|
val = 5
|
||||||
row.Cpu.BarColor = ui.ColorBlack
|
row.Cpu.BarColor = ui.ThemeAttr("gauge.bar.bg")
|
||||||
|
}
|
||||||
|
if val > 100 {
|
||||||
|
val = 100
|
||||||
}
|
}
|
||||||
row.Cpu.Percent = val
|
row.Cpu.Percent = val
|
||||||
}
|
}
|
||||||
@@ -29,7 +42,7 @@ func (row *Compact) SetMem(val int64, limit int64, percent int) {
|
|||||||
percent = 5
|
percent = 5
|
||||||
row.Memory.BarColor = ui.ColorBlack
|
row.Memory.BarColor = ui.ColorBlack
|
||||||
} else {
|
} else {
|
||||||
row.Memory.BarColor = ui.ColorGreen
|
row.Memory.BarColor = ui.ThemeAttr("gauge.bar.bg")
|
||||||
}
|
}
|
||||||
row.Memory.Percent = percent
|
row.Memory.Percent = percent
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ func NewTextCol(s string) *TextCol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *TextCol) Highlight() {
|
func (w *TextCol) Highlight() {
|
||||||
w.TextFgColor = ui.ThemeAttr("par.text.bg")
|
w.TextFgColor = ui.ThemeAttr("par.text.hi")
|
||||||
w.TextBgColor = ui.ThemeAttr("par.text.fg")
|
w.TextBgColor = ui.ThemeAttr("par.text.fg")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,10 +9,29 @@ import (
|
|||||||
|
|
||||||
const colSpacing = 1
|
const colSpacing = 1
|
||||||
|
|
||||||
// Calculate per-column width, given total width and number of items
|
// per-column width. 0 == auto width
|
||||||
func calcWidth(width, items int) int {
|
var colWidths = []int{
|
||||||
spacing := colSpacing * items
|
3, // status
|
||||||
return (width - statusWidth - spacing) / items
|
0, // name
|
||||||
|
0, // cid
|
||||||
|
0, // cpu
|
||||||
|
0, // memory
|
||||||
|
0, // net
|
||||||
|
0, // io
|
||||||
|
4, // pids
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate per-column width, given total width
|
||||||
|
func calcWidth(width int) int {
|
||||||
|
spacing := colSpacing * len(colWidths)
|
||||||
|
var staticCols int
|
||||||
|
for _, w := range colWidths {
|
||||||
|
width -= w
|
||||||
|
if w == 0 {
|
||||||
|
staticCols += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (width - spacing) / staticCols
|
||||||
}
|
}
|
||||||
|
|
||||||
func centerParText(p *ui.Par) {
|
func centerParText(p *ui.Par) {
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ func NewCpu() *Cpu {
|
|||||||
cpu.Width = colWidth[0]
|
cpu.Width = colWidth[0]
|
||||||
cpu.X = 0
|
cpu.X = 0
|
||||||
cpu.DataLabels = cpu.hist.Labels
|
cpu.DataLabels = cpu.hist.Labels
|
||||||
cpu.AxesColor = ui.ColorDefault
|
|
||||||
cpu.LineColor = ui.ColorGreen
|
|
||||||
|
|
||||||
// hack to force the default minY scale to 0
|
// hack to force the default minY scale to 0
|
||||||
tmpData := []float64{20}
|
tmpData := []float64{20}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package expanded
|
package expanded
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
ui "github.com/gizak/termui"
|
ui "github.com/gizak/termui"
|
||||||
)
|
)
|
||||||
|
|
||||||
var displayInfo = []string{"id", "name", "image", "state"}
|
var displayInfo = []string{"id", "name", "image", "ports", "state", "created"}
|
||||||
|
|
||||||
type Info struct {
|
type Info struct {
|
||||||
*ui.Table
|
*ui.Table
|
||||||
@@ -15,8 +17,8 @@ func NewInfo(id string) *Info {
|
|||||||
p := ui.NewTable()
|
p := ui.NewTable()
|
||||||
p.Height = 4
|
p.Height = 4
|
||||||
p.Width = colWidth[0]
|
p.Width = colWidth[0]
|
||||||
p.FgColor = ui.ColorWhite
|
p.FgColor = ui.ThemeAttr("par.text.fg")
|
||||||
p.Seperator = false
|
p.Separator = false
|
||||||
i := &Info{p, make(map[string]string)}
|
i := &Info{p, make(map[string]string)}
|
||||||
i.Set("id", id)
|
i.Set("id", id)
|
||||||
return i
|
return i
|
||||||
@@ -24,12 +26,33 @@ func NewInfo(id string) *Info {
|
|||||||
|
|
||||||
func (w *Info) Set(k, v string) {
|
func (w *Info) Set(k, v string) {
|
||||||
w.data[k] = v
|
w.data[k] = v
|
||||||
|
|
||||||
// rebuild rows
|
// rebuild rows
|
||||||
w.Rows = [][]string{}
|
w.Rows = [][]string{}
|
||||||
for _, k := range displayInfo {
|
for _, k := range displayInfo {
|
||||||
if v, ok := w.data[k]; ok {
|
if v, ok := w.data[k]; ok {
|
||||||
w.Rows = append(w.Rows, []string{k, v})
|
w.Rows = append(w.Rows, mkInfoRows(k, v)...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Height = len(w.Rows) + 2
|
w.Height = len(w.Rows) + 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build row(s) from a key and value string
|
||||||
|
func mkInfoRows(k, v string) (rows [][]string) {
|
||||||
|
lines := strings.Split(v, "\n")
|
||||||
|
|
||||||
|
// initial row with field name
|
||||||
|
rows = append(rows, []string{k, lines[0]})
|
||||||
|
|
||||||
|
// append any additional lines in seperate row
|
||||||
|
if len(lines) > 1 {
|
||||||
|
for _, line := range lines[1:] {
|
||||||
|
if line != "" {
|
||||||
|
rows = append(rows, []string{"", line})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|||||||
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)
|
||||||
|
}
|
||||||
@@ -17,6 +17,8 @@ type Expanded struct {
|
|||||||
Net *Net
|
Net *Net
|
||||||
Cpu *Cpu
|
Cpu *Cpu
|
||||||
Mem *Mem
|
Mem *Mem
|
||||||
|
IO *IO
|
||||||
|
X, Y int
|
||||||
Width int
|
Width int
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,30 +31,59 @@ func NewExpanded(id string) *Expanded {
|
|||||||
Net: NewNet(),
|
Net: NewNet(),
|
||||||
Cpu: NewCpu(),
|
Cpu: NewCpu(),
|
||||||
Mem: NewMem(),
|
Mem: NewMem(),
|
||||||
|
IO: NewIO(),
|
||||||
Width: ui.TermWidth(),
|
Width: ui.TermWidth(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Expanded) SetWidth(w int) {
|
func (e *Expanded) Up() {
|
||||||
e.Width = w
|
if e.Y < 0 {
|
||||||
|
e.Y++
|
||||||
|
e.Align()
|
||||||
|
ui.Render(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Expanded) SetMeta(k, v string) {
|
func (e *Expanded) Down() {
|
||||||
e.Info.Set(k, v)
|
if e.Y > (ui.TermHeight() - e.GetHeight()) {
|
||||||
|
e.Y--
|
||||||
|
e.Align()
|
||||||
|
ui.Render(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *Expanded) SetWidth(w int) { e.Width = w }
|
||||||
|
func (e *Expanded) SetMeta(k, v string) { e.Info.Set(k, v) }
|
||||||
|
|
||||||
func (e *Expanded) SetMetrics(m metrics.Metrics) {
|
func (e *Expanded) SetMetrics(m metrics.Metrics) {
|
||||||
e.Cpu.Update(m.CPUUtil)
|
e.Cpu.Update(m.CPUUtil)
|
||||||
e.Net.Update(m.NetRx, m.NetTx)
|
e.Net.Update(m.NetRx, m.NetTx)
|
||||||
e.Mem.Update(int(m.MemUsage), int(m.MemLimit))
|
e.Mem.Update(int(m.MemUsage), int(m.MemLimit))
|
||||||
|
e.IO.Update(m.IOBytesRead, m.IOBytesWrite)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return total column height
|
||||||
|
func (e *Expanded) GetHeight() (h int) {
|
||||||
|
h += e.Info.Height
|
||||||
|
h += e.Net.Height
|
||||||
|
h += e.Cpu.Height
|
||||||
|
h += e.Mem.Height
|
||||||
|
h += e.IO.Height
|
||||||
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Expanded) Align() {
|
func (e *Expanded) Align() {
|
||||||
y := 0
|
// reset offset if needed
|
||||||
|
if e.GetHeight() <= ui.TermHeight() {
|
||||||
|
e.Y = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
y := e.Y
|
||||||
for _, i := range e.all() {
|
for _, i := range e.all() {
|
||||||
i.SetY(y)
|
i.SetY(y)
|
||||||
y += i.GetHeight()
|
y += i.GetHeight()
|
||||||
}
|
}
|
||||||
|
|
||||||
if e.Width > colWidth[0] {
|
if e.Width > colWidth[0] {
|
||||||
colWidth[1] = e.Width - (colWidth[0] + 1)
|
colWidth[1] = e.Width - (colWidth[0] + 1)
|
||||||
}
|
}
|
||||||
@@ -74,6 +105,7 @@ func (e *Expanded) Buffer() ui.Buffer {
|
|||||||
buf.Merge(e.Cpu.Buffer())
|
buf.Merge(e.Cpu.Buffer())
|
||||||
buf.Merge(e.Mem.Buffer())
|
buf.Merge(e.Mem.Buffer())
|
||||||
buf.Merge(e.Net.Buffer())
|
buf.Merge(e.Net.Buffer())
|
||||||
|
buf.Merge(e.IO.Buffer())
|
||||||
return buf
|
return buf
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,6 +115,7 @@ func (e *Expanded) all() []ui.GridBufferer {
|
|||||||
e.Cpu,
|
e.Cpu,
|
||||||
e.Mem,
|
e.Mem,
|
||||||
e.Net,
|
e.Net,
|
||||||
|
e.IO,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ func newMemLabel() *ui.Par {
|
|||||||
p.Border = false
|
p.Border = false
|
||||||
p.Height = 1
|
p.Height = 1
|
||||||
p.Width = 20
|
p.Width = 20
|
||||||
p.TextFgColor = ui.ColorDefault
|
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,9 +66,6 @@ func newMemChart() *ui.MBarChart {
|
|||||||
mbar.Border = false
|
mbar.Border = false
|
||||||
mbar.BarGap = 1
|
mbar.BarGap = 1
|
||||||
mbar.BarWidth = 6
|
mbar.BarWidth = 6
|
||||||
mbar.TextColor = ui.ColorDefault
|
|
||||||
|
|
||||||
mbar.BarColor[0] = ui.ColorGreen
|
|
||||||
|
|
||||||
mbar.BarColor[1] = ui.ColorBlack
|
mbar.BarColor[1] = ui.ColorBlack
|
||||||
mbar.NumColor[1] = ui.ColorBlack
|
mbar.NumColor[1] = ui.ColorBlack
|
||||||
|
|||||||
@@ -26,14 +26,12 @@ func NewNet() *Net {
|
|||||||
rx.Title = "RX"
|
rx.Title = "RX"
|
||||||
rx.Height = 1
|
rx.Height = 1
|
||||||
rx.Data = net.rxHist.Data
|
rx.Data = net.rxHist.Data
|
||||||
rx.TitleColor = ui.ColorDefault
|
|
||||||
rx.LineColor = ui.ColorGreen
|
rx.LineColor = ui.ColorGreen
|
||||||
|
|
||||||
tx := ui.NewSparkline()
|
tx := ui.NewSparkline()
|
||||||
tx.Title = "TX"
|
tx.Title = "TX"
|
||||||
tx.Height = 1
|
tx.Height = 1
|
||||||
tx.Data = net.txHist.Data
|
tx.Data = net.txHist.Data
|
||||||
tx.TitleColor = ui.ColorDefault
|
|
||||||
tx.LineColor = ui.ColorYellow
|
tx.LineColor = ui.ColorYellow
|
||||||
|
|
||||||
net.Lines = []ui.Sparkline{rx, tx}
|
net.Lines = []ui.Sparkline{rx, tx}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const (
|
|||||||
kb = 1024
|
kb = 1024
|
||||||
mb = kb * 1024
|
mb = kb * 1024
|
||||||
gb = mb * 1024
|
gb = mb * 1024
|
||||||
|
tb = gb * 1024
|
||||||
)
|
)
|
||||||
|
|
||||||
// convenience method
|
// convenience method
|
||||||
@@ -28,8 +29,12 @@ 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))
|
||||||
}
|
}
|
||||||
nf := float64(n) / gb
|
if n < tb {
|
||||||
return fmt.Sprintf("%sG", unpadFloat(nf))
|
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 {
|
func unpadFloat(f float64) string {
|
||||||
|
|||||||
3
debug.go
3
debug.go
@@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/bcicen/ctop/container"
|
||||||
ui "github.com/gizak/termui"
|
ui "github.com/gizak/termui"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,7 +20,7 @@ func logEvent(e ui.Event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// log container, metrics, and widget state
|
// log container, metrics, and widget state
|
||||||
func dumpContainer(c *Container) {
|
func dumpContainer(c *container.Container) {
|
||||||
msg := fmt.Sprintf("logging state for container: %s\n", c.Id)
|
msg := fmt.Sprintf("logging state for container: %s\n", c.Id)
|
||||||
for k, v := range c.Meta {
|
for k, v := range c.Meta {
|
||||||
msg += fmt.Sprintf("Meta.%s = %s\n", k, v)
|
msg += fmt.Sprintf("Meta.%s = %s\n", k, v)
|
||||||
|
|||||||
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
|
||||||
96
grid.go
96
grid.go
@@ -2,15 +2,12 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
func maxRows() int {
|
func RedrawRows(clr bool) {
|
||||||
return ui.TermHeight() - 2 - cGrid.Y
|
|
||||||
}
|
|
||||||
|
|
||||||
func RedrawRows() {
|
|
||||||
// reinit body rows
|
// reinit body rows
|
||||||
cGrid.Clear()
|
cGrid.Clear()
|
||||||
|
|
||||||
@@ -23,31 +20,22 @@ func RedrawRows() {
|
|||||||
}
|
}
|
||||||
cGrid.SetY(y)
|
cGrid.SetY(y)
|
||||||
|
|
||||||
var cursorVisible bool
|
for _, c := range cursor.filtered {
|
||||||
max := maxRows()
|
|
||||||
for n, c := range cursor.containers {
|
|
||||||
if n >= max {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
cGrid.AddRows(c.Widgets)
|
cGrid.AddRows(c.Widgets)
|
||||||
if c.Id == cursor.selectedID {
|
|
||||||
cursorVisible = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !cursorVisible {
|
if clr {
|
||||||
cursor.Reset()
|
ui.Clear()
|
||||||
|
log.Debugf("screen cleared")
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.Clear()
|
|
||||||
if config.GetSwitchVal("enableHeader") {
|
if config.GetSwitchVal("enableHeader") {
|
||||||
header.Render()
|
ui.Render(header)
|
||||||
}
|
}
|
||||||
cGrid.Align()
|
cGrid.Align()
|
||||||
ui.Render(cGrid)
|
ui.Render(cGrid)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExpandView(c *Container) {
|
func ExpandView(c *container.Container) {
|
||||||
ui.Clear()
|
ui.Clear()
|
||||||
ui.DefaultEvtStream.ResetHandlers()
|
ui.DefaultEvtStream.ResetHandlers()
|
||||||
defer ui.DefaultEvtStream.ResetHandlers()
|
defer ui.DefaultEvtStream.ResetHandlers()
|
||||||
@@ -57,22 +45,30 @@ func ExpandView(c *Container) {
|
|||||||
|
|
||||||
ex.Align()
|
ex.Align()
|
||||||
ui.Render(ex)
|
ui.Render(ex)
|
||||||
ui.Handle("/timer/1s", func(ui.Event) {
|
|
||||||
ui.Render(ex)
|
HandleKeys("up", ex.Up)
|
||||||
})
|
HandleKeys("down", ex.Down)
|
||||||
|
ui.Handle("/sys/kbd/", func(ui.Event) { ui.StopLoop() })
|
||||||
|
|
||||||
|
ui.Handle("/timer/1s", func(ui.Event) { ui.Render(ex) })
|
||||||
ui.Handle("/sys/wnd/resize", func(e ui.Event) {
|
ui.Handle("/sys/wnd/resize", func(e ui.Event) {
|
||||||
ex.SetWidth(ui.TermWidth())
|
ex.SetWidth(ui.TermWidth())
|
||||||
ex.Align()
|
ex.Align()
|
||||||
log.Infof("resize: width=%v max-rows=%v", ex.Width, 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() {
|
||||||
|
// skip display refresh during scroll
|
||||||
|
if !cursor.isScrolling {
|
||||||
|
needsClear := cursor.RefreshContainers()
|
||||||
|
RedrawRows(needsClear)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func Display() bool {
|
func Display() bool {
|
||||||
var menu func()
|
var menu func()
|
||||||
var expand bool
|
var expand bool
|
||||||
@@ -83,23 +79,27 @@ func Display() bool {
|
|||||||
// initial draw
|
// initial draw
|
||||||
header.Align()
|
header.Align()
|
||||||
cursor.RefreshContainers()
|
cursor.RefreshContainers()
|
||||||
RedrawRows()
|
RedrawRows(true)
|
||||||
|
|
||||||
ui.Handle("/sys/kbd/<up>", func(ui.Event) {
|
HandleKeys("up", cursor.Up)
|
||||||
cursor.Up()
|
HandleKeys("down", cursor.Down)
|
||||||
})
|
|
||||||
ui.Handle("/sys/kbd/<down>", func(ui.Event) {
|
HandleKeys("pgup", cursor.PgUp)
|
||||||
cursor.Down()
|
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/a", func(ui.Event) {
|
ui.Handle("/sys/kbd/a", func(ui.Event) {
|
||||||
config.Toggle("allContainers")
|
config.Toggle("allContainers")
|
||||||
cursor.RefreshContainers()
|
RefreshDisplay()
|
||||||
RedrawRows()
|
|
||||||
})
|
})
|
||||||
ui.Handle("/sys/kbd/D", func(ui.Event) {
|
ui.Handle("/sys/kbd/D", func(ui.Event) {
|
||||||
dumpContainer(cursor.Selected())
|
dumpContainer(cursor.Selected())
|
||||||
@@ -108,16 +108,9 @@ 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()
|
RedrawRows(true)
|
||||||
})
|
|
||||||
ui.Handle("/sys/kbd/q", func(ui.Event) {
|
|
||||||
ui.StopLoop()
|
|
||||||
})
|
})
|
||||||
ui.Handle("/sys/kbd/r", func(e ui.Event) {
|
ui.Handle("/sys/kbd/r", func(e ui.Event) {
|
||||||
config.Toggle("sortReversed")
|
config.Toggle("sortReversed")
|
||||||
@@ -128,15 +121,15 @@ func Display() bool {
|
|||||||
})
|
})
|
||||||
|
|
||||||
ui.Handle("/timer/1s", func(e ui.Event) {
|
ui.Handle("/timer/1s", func(e ui.Event) {
|
||||||
cursor.RefreshContainers()
|
RefreshDisplay()
|
||||||
RedrawRows()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
ui.Handle("/sys/wnd/resize", func(e ui.Event) {
|
ui.Handle("/sys/wnd/resize", func(e ui.Event) {
|
||||||
header.Align()
|
header.Align()
|
||||||
|
cursor.ScrollPage()
|
||||||
cGrid.SetWidth(ui.TermWidth())
|
cGrid.SetWidth(ui.TermWidth())
|
||||||
log.Infof("resize: width=%v max-rows=%v", cGrid.Width, maxRows())
|
log.Infof("resize: width=%v max-rows=%v", cGrid.Width, cGrid.MaxRows())
|
||||||
RedrawRows()
|
RedrawRows(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
ui.Loop()
|
ui.Loop()
|
||||||
@@ -145,7 +138,10 @@ func Display() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if expand {
|
if expand {
|
||||||
ExpandView(cursor.Selected())
|
c := cursor.Selected()
|
||||||
|
if c != nil {
|
||||||
|
ExpandView(c)
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
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
|
package logging
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/op/go-logging"
|
"github.com/op/go-logging"
|
||||||
@@ -13,7 +14,7 @@ const (
|
|||||||
var (
|
var (
|
||||||
Log *CTopLogger
|
Log *CTopLogger
|
||||||
exited bool
|
exited bool
|
||||||
level = logging.INFO
|
level = logging.INFO // default level
|
||||||
format = logging.MustStringFormatter(
|
format = logging.MustStringFormatter(
|
||||||
`%{color}%{time:15:04:05.000} ▶ %{level:.4s} %{id:03x}%{color:reset} %{message}`,
|
`%{color}%{time:15:04:05.000} ▶ %{level:.4s} %{id:03x}%{color:reset} %{message}`,
|
||||||
)
|
)
|
||||||
@@ -33,6 +34,11 @@ func Init() *CTopLogger {
|
|||||||
logging.NewMemoryBackend(size),
|
logging.NewMemoryBackend(size),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if debugMode() {
|
||||||
|
level = logging.DEBUG
|
||||||
|
StartServer()
|
||||||
|
}
|
||||||
|
|
||||||
backendLvl := logging.AddModuleLevel(Log.backend)
|
backendLvl := logging.AddModuleLevel(Log.backend)
|
||||||
backendLvl.SetLevel(level, "")
|
backendLvl.SetLevel(level, "")
|
||||||
|
|
||||||
@@ -71,3 +77,6 @@ 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" }
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
path = "/tmp/ctop.sock"
|
socketPath = "./ctop.sock"
|
||||||
|
socketAddr = "0.0.0.0:9000"
|
||||||
)
|
)
|
||||||
|
|
||||||
var server struct {
|
var server struct {
|
||||||
@@ -16,7 +17,13 @@ var server struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getListener() net.Listener {
|
func getListener() net.Listener {
|
||||||
ln, err := net.Listen("unix", path)
|
var ln net.Listener
|
||||||
|
var err error
|
||||||
|
if debugModeTCP() {
|
||||||
|
ln, err = net.Listen("tcp", socketAddr)
|
||||||
|
} else {
|
||||||
|
ln, err = net.Listen("unix", socketPath)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|||||||
111
main.go
111
main.go
@@ -1,14 +1,18 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/bcicen/ctop/config"
|
"github.com/bcicen/ctop/config"
|
||||||
|
"github.com/bcicen/ctop/connector"
|
||||||
|
"github.com/bcicen/ctop/container"
|
||||||
"github.com/bcicen/ctop/cwidgets/compact"
|
"github.com/bcicen/ctop/cwidgets/compact"
|
||||||
"github.com/bcicen/ctop/logging"
|
"github.com/bcicen/ctop/logging"
|
||||||
"github.com/bcicen/ctop/widgets"
|
"github.com/bcicen/ctop/widgets"
|
||||||
ui "github.com/gizak/termui"
|
ui "github.com/gizak/termui"
|
||||||
|
tm "github.com/nsf/termbox-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -19,82 +23,117 @@ var (
|
|||||||
cursor *GridCursor
|
cursor *GridCursor
|
||||||
cGrid *compact.CompactGrid
|
cGrid *compact.CompactGrid
|
||||||
header *widgets.CTopHeader
|
header *widgets.CTopHeader
|
||||||
|
|
||||||
|
versionStr = fmt.Sprintf("ctop version %v, build %v", version, build)
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
readArgs()
|
|
||||||
defer panicExit()
|
defer panicExit()
|
||||||
|
|
||||||
// init ui
|
// parse command line arguments
|
||||||
if err := ui.Init(); err != nil {
|
var versionFlag = flag.Bool("v", false, "output version information and exit")
|
||||||
panic(err)
|
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
|
// init global config
|
||||||
config.Init()
|
config.Init()
|
||||||
|
|
||||||
// init logger
|
// override default config values with command line flags
|
||||||
log = logging.Init()
|
if *filterFlag != "" {
|
||||||
if config.GetSwitchVal("loggingEnabled") {
|
config.Update("filterStr", *filterFlag)
|
||||||
logging.StartServer()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// init grid, cursor, header
|
||||||
cursor = NewGridCursor()
|
conn, err := connector.ByName(*connectorFlag)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
cursor = &GridCursor{cSource: conn}
|
||||||
cGrid = compact.NewCompactGrid()
|
cGrid = compact.NewCompactGrid()
|
||||||
header = widgets.NewCTopHeader()
|
header = widgets.NewCTopHeader()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
exit := Display()
|
exit := Display()
|
||||||
if exit {
|
if exit {
|
||||||
log.Notice("shutting down")
|
|
||||||
log.Exit()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func readArgs() {
|
func Shutdown() {
|
||||||
if len(os.Args) < 2 {
|
log.Notice("shutting down")
|
||||||
return
|
log.Exit()
|
||||||
|
if tm.IsInit {
|
||||||
|
ui.Close()
|
||||||
}
|
}
|
||||||
for _, arg := range os.Args[1:] {
|
}
|
||||||
switch arg {
|
|
||||||
case "-v", "version":
|
// ensure a given sort field is valid
|
||||||
printVersion()
|
func validSort(s string) {
|
||||||
os.Exit(0)
|
if _, ok := container.Sorters[s]; !ok {
|
||||||
case "-h", "help":
|
fmt.Printf("invalid sort field: %s\n", s)
|
||||||
printHelp()
|
os.Exit(1)
|
||||||
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 {
|
||||||
ui.Clear()
|
Shutdown()
|
||||||
fmt.Printf("panic: %s\n", r)
|
fmt.Printf("error: %s\n", r)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var helpMsg = `cTop - container metric viewer
|
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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
27
menus.go
27
menus.go
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/bcicen/ctop/config"
|
"github.com/bcicen/ctop/config"
|
||||||
|
"github.com/bcicen/ctop/container"
|
||||||
"github.com/bcicen/ctop/widgets"
|
"github.com/bcicen/ctop/widgets"
|
||||||
"github.com/bcicen/ctop/widgets/menu"
|
"github.com/bcicen/ctop/widgets/menu"
|
||||||
ui "github.com/gizak/termui"
|
ui "github.com/gizak/termui"
|
||||||
@@ -11,7 +12,7 @@ var helpDialog = []menu.Item{
|
|||||||
menu.Item{"[a] - toggle display of all containers", ""},
|
menu.Item{"[a] - toggle display of all containers", ""},
|
||||||
menu.Item{"[f] - filter displayed containers", ""},
|
menu.Item{"[f] - filter displayed containers", ""},
|
||||||
menu.Item{"[h] - open this help dialog", ""},
|
menu.Item{"[h] - open this help dialog", ""},
|
||||||
menu.Item{"[H] - toggle cTop header", ""},
|
menu.Item{"[H] - toggle ctop header", ""},
|
||||||
menu.Item{"[s] - select container sort field", ""},
|
menu.Item{"[s] - select container sort field", ""},
|
||||||
menu.Item{"[r] - reverse container sort order", ""},
|
menu.Item{"[r] - reverse container sort order", ""},
|
||||||
menu.Item{"[q] - exit ctop", ""},
|
menu.Item{"[q] - exit ctop", ""},
|
||||||
@@ -23,9 +24,7 @@ func HelpMenu() {
|
|||||||
defer ui.DefaultEvtStream.ResetHandlers()
|
defer ui.DefaultEvtStream.ResetHandlers()
|
||||||
|
|
||||||
m := menu.NewMenu()
|
m := menu.NewMenu()
|
||||||
m.TextFgColor = ui.ColorWhite
|
|
||||||
m.BorderLabel = "Help"
|
m.BorderLabel = "Help"
|
||||||
m.BorderFg = ui.ColorCyan
|
|
||||||
m.AddItems(helpDialog...)
|
m.AddItems(helpDialog...)
|
||||||
ui.Render(m)
|
ui.Render(m)
|
||||||
ui.Handle("/sys/kbd/", func(ui.Event) {
|
ui.Handle("/sys/kbd/", func(ui.Event) {
|
||||||
@@ -39,10 +38,9 @@ func FilterMenu() {
|
|||||||
defer ui.DefaultEvtStream.ResetHandlers()
|
defer ui.DefaultEvtStream.ResetHandlers()
|
||||||
|
|
||||||
i := widgets.NewInput()
|
i := widgets.NewInput()
|
||||||
i.TextFgColor = ui.ColorWhite
|
|
||||||
i.BorderLabel = "Filter"
|
i.BorderLabel = "Filter"
|
||||||
i.BorderFg = ui.ColorCyan
|
|
||||||
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
|
||||||
@@ -50,13 +48,16 @@ func FilterMenu() {
|
|||||||
go func() {
|
go func() {
|
||||||
for s := range stream {
|
for s := range stream {
|
||||||
config.Update("filterStr", s)
|
config.Update("filterStr", s)
|
||||||
cursor.RefreshContainers()
|
RefreshDisplay()
|
||||||
RedrawRows()
|
|
||||||
ui.Render(i)
|
ui.Render(i)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
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()
|
||||||
@@ -72,22 +73,24 @@ func SortMenu() {
|
|||||||
m := menu.NewMenu()
|
m := menu.NewMenu()
|
||||||
m.Selectable = true
|
m.Selectable = true
|
||||||
m.SortItems = true
|
m.SortItems = true
|
||||||
m.TextFgColor = ui.ColorWhite
|
|
||||||
m.BorderLabel = "Sort Field"
|
m.BorderLabel = "Sort Field"
|
||||||
m.BorderFg = ui.ColorCyan
|
|
||||||
|
|
||||||
for _, field := range SortFields() {
|
for _, field := range container.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"))
|
||||||
|
|
||||||
ui.Render(m)
|
HandleKeys("up", m.Up)
|
||||||
m.NavigationHandlers()
|
HandleKeys("down", m.Down)
|
||||||
|
HandleKeys("exit", ui.StopLoop)
|
||||||
|
|
||||||
ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
|
ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
|
||||||
config.Update("sortField", m.SelectedItem().Val)
|
config.Update("sortField", m.SelectedItem().Val)
|
||||||
ui.StopLoop()
|
ui.StopLoop()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ui.Render(m)
|
||||||
ui.Loop()
|
ui.Loop()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,27 @@
|
|||||||
package metrics
|
package metrics
|
||||||
|
|
||||||
import (
|
|
||||||
"math"
|
|
||||||
|
|
||||||
"github.com/bcicen/ctop/logging"
|
|
||||||
)
|
|
||||||
|
|
||||||
var log = logging.Init()
|
|
||||||
|
|
||||||
type Metrics struct {
|
type Metrics struct {
|
||||||
CPUUtil int
|
CPUUtil int
|
||||||
NetTx int64
|
NetTx int64
|
||||||
NetRx int64
|
NetRx int64
|
||||||
MemLimit int64
|
MemLimit int64
|
||||||
MemPercent int
|
MemPercent int
|
||||||
MemUsage int64
|
MemUsage int64
|
||||||
|
IOBytesRead int64
|
||||||
|
IOBytesWrite int64
|
||||||
|
Pids int
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMetrics() Metrics {
|
func NewMetrics() Metrics {
|
||||||
return Metrics{
|
return Metrics{
|
||||||
CPUUtil: -1,
|
CPUUtil: -1,
|
||||||
NetTx: -1,
|
NetTx: -1,
|
||||||
NetRx: -1,
|
NetRx: -1,
|
||||||
MemUsage: -1,
|
MemUsage: -1,
|
||||||
MemPercent: -1,
|
MemPercent: -1,
|
||||||
|
IOBytesRead: -1,
|
||||||
|
IOBytesWrite: -1,
|
||||||
|
Pids: -1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,7 +31,3 @@ type Collector interface {
|
|||||||
Start()
|
Start()
|
||||||
Stop()
|
Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
func round(num float64) int {
|
|
||||||
return int(num + math.Copysign(0.5, num))
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
// +build !release
|
|
||||||
|
|
||||||
package metrics
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math/rand"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Mock collector
|
|
||||||
type Mock struct {
|
|
||||||
Metrics
|
|
||||||
stream chan Metrics
|
|
||||||
done bool
|
|
||||||
running bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMock() *Mock {
|
|
||||||
c := &Mock{
|
|
||||||
Metrics: Metrics{},
|
|
||||||
}
|
|
||||||
c.MemLimit = 2147483648
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Mock) Running() bool {
|
|
||||||
return c.running
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Mock) Start() {
|
|
||||||
c.done = false
|
|
||||||
c.stream = make(chan Metrics)
|
|
||||||
go c.run()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Mock) Stop() {
|
|
||||||
c.done = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Mock) Stream() chan Metrics {
|
|
||||||
return c.stream
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Mock) run() {
|
|
||||||
c.running = true
|
|
||||||
rand.Seed(int64(time.Now().Nanosecond()))
|
|
||||||
defer close(c.stream)
|
|
||||||
|
|
||||||
for {
|
|
||||||
c.CPUUtil += rand.Intn(2)
|
|
||||||
if c.CPUUtil > 100 {
|
|
||||||
c.CPUUtil = 0
|
|
||||||
}
|
|
||||||
c.NetTx += rand.Int63n(600)
|
|
||||||
c.NetRx += rand.Int63n(600)
|
|
||||||
c.MemUsage += rand.Int63n(c.MemLimit / 32)
|
|
||||||
if c.MemUsage > c.MemLimit {
|
|
||||||
c.MemUsage = 0
|
|
||||||
}
|
|
||||||
c.MemPercent = round((float64(c.MemUsage) / float64(c.MemLimit)) * 100)
|
|
||||||
c.stream <- c.Metrics
|
|
||||||
if c.done {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.running = false
|
|
||||||
}
|
|
||||||
@@ -23,10 +23,14 @@ func NewCTopHeader() *CTopHeader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CTopHeader) Render() {
|
func (c *CTopHeader) Buffer() ui.Buffer {
|
||||||
|
buf := ui.NewBuffer()
|
||||||
c.Time.Text = timeStr()
|
c.Time.Text = timeStr()
|
||||||
ui.Render(c.bg)
|
buf.Merge(c.bg.Buffer())
|
||||||
ui.Render(c.Time, c.Count, c.Filter)
|
buf.Merge(c.Time.Buffer())
|
||||||
|
buf.Merge(c.Count.Buffer())
|
||||||
|
buf.Merge(c.Filter.Buffer())
|
||||||
|
return buf
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CTopHeader) Align() {
|
func (c *CTopHeader) Align() {
|
||||||
@@ -41,7 +45,7 @@ func headerBgBordered() *ui.Par {
|
|||||||
bg := ui.NewPar("")
|
bg := ui.NewPar("")
|
||||||
bg.X = 1
|
bg.X = 1
|
||||||
bg.Height = 3
|
bg.Height = 3
|
||||||
bg.Bg = ui.ColorWhite
|
bg.Bg = ui.ThemeAttr("header.bg")
|
||||||
return bg
|
return bg
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +54,7 @@ func headerBg() *ui.Par {
|
|||||||
bg.X = 1
|
bg.X = 1
|
||||||
bg.Height = 1
|
bg.Height = 1
|
||||||
bg.Border = false
|
bg.Border = false
|
||||||
bg.Bg = ui.ColorWhite
|
bg.Bg = ui.ThemeAttr("header.bg")
|
||||||
return bg
|
return bg
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,7 +72,7 @@ func (c *CTopHeader) SetFilter(val string) {
|
|||||||
|
|
||||||
func timeStr() string {
|
func timeStr() string {
|
||||||
ts := time.Now().Local().Format("15:04:05 MST")
|
ts := time.Now().Local().Format("15:04:05 MST")
|
||||||
return fmt.Sprintf("cTop - %s", ts)
|
return fmt.Sprintf("ctop - %s", ts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func headerPar(x int, s string) *ui.Par {
|
func headerPar(x int, s string) *ui.Par {
|
||||||
@@ -77,8 +81,8 @@ func headerPar(x int, s string) *ui.Par {
|
|||||||
p.Border = false
|
p.Border = false
|
||||||
p.Height = 1
|
p.Height = 1
|
||||||
p.Width = 20
|
p.Width = 20
|
||||||
p.TextFgColor = ui.ColorDefault
|
p.Bg = ui.ThemeAttr("header.bg")
|
||||||
p.TextBgColor = ui.ColorWhite
|
p.TextFgColor = ui.ThemeAttr("header.fg")
|
||||||
p.Bg = ui.ColorWhite
|
p.TextBgColor = ui.ThemeAttr("header.bg")
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
input_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_."
|
input_chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_."
|
||||||
)
|
)
|
||||||
|
|
||||||
type Padding [2]int // x,y padding
|
type Padding [2]int // x,y padding
|
||||||
@@ -28,10 +28,12 @@ func NewInput() *Input {
|
|||||||
Block: *ui.NewBlock(),
|
Block: *ui.NewBlock(),
|
||||||
Label: "input",
|
Label: "input",
|
||||||
MaxLen: 20,
|
MaxLen: 20,
|
||||||
TextFgColor: ui.ThemeAttr("par.text.fg"),
|
TextFgColor: ui.ThemeAttr("menu.text.fg"),
|
||||||
TextBgColor: ui.ThemeAttr("par.text.bg"),
|
TextBgColor: ui.ThemeAttr("menu.text.bg"),
|
||||||
padding: Padding{4, 2},
|
padding: Padding{4, 2},
|
||||||
}
|
}
|
||||||
|
i.BorderFg = ui.ThemeAttr("menu.border.fg")
|
||||||
|
i.BorderLabelFg = ui.ThemeAttr("menu.label.fg")
|
||||||
i.calcSize()
|
i.calcSize()
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,11 +22,13 @@ type Menu struct {
|
|||||||
func NewMenu() *Menu {
|
func NewMenu() *Menu {
|
||||||
m := &Menu{
|
m := &Menu{
|
||||||
Block: *ui.NewBlock(),
|
Block: *ui.NewBlock(),
|
||||||
TextFgColor: ui.ThemeAttr("par.text.fg"),
|
TextFgColor: ui.ThemeAttr("menu.text.fg"),
|
||||||
TextBgColor: ui.ThemeAttr("par.text.bg"),
|
TextBgColor: ui.ThemeAttr("menu.text.bg"),
|
||||||
cursorPos: 0,
|
cursorPos: 0,
|
||||||
padding: Padding{4, 2},
|
padding: Padding{4, 2},
|
||||||
}
|
}
|
||||||
|
m.BorderFg = ui.ThemeAttr("menu.border.fg")
|
||||||
|
m.BorderLabelFg = ui.ThemeAttr("menu.label.fg")
|
||||||
m.X = 1
|
m.X = 1
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
@@ -86,7 +88,7 @@ func (m *Menu) Buffer() ui.Buffer {
|
|||||||
for _, ch := range item.Text() {
|
for _, ch := range item.Text() {
|
||||||
// invert bg/fg colors on currently selected row
|
// invert bg/fg colors on currently selected row
|
||||||
if m.Selectable && n == m.cursorPos {
|
if m.Selectable && n == m.cursorPos {
|
||||||
cell = ui.Cell{Ch: ch, Fg: m.TextBgColor, Bg: m.TextFgColor}
|
cell = ui.Cell{Ch: ch, Fg: ui.ColorBlack, Bg: m.TextFgColor}
|
||||||
} else {
|
} else {
|
||||||
cell = ui.Cell{Ch: ch, Fg: m.TextFgColor, Bg: m.TextBgColor}
|
cell = ui.Cell{Ch: ch, Fg: m.TextFgColor, Bg: m.TextBgColor}
|
||||||
}
|
}
|
||||||
@@ -98,27 +100,20 @@ func (m *Menu) Buffer() ui.Buffer {
|
|||||||
return buf
|
return buf
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Menu) Up(ui.Event) {
|
func (m *Menu) Up() {
|
||||||
if m.cursorPos > 0 {
|
if m.cursorPos > 0 {
|
||||||
m.cursorPos--
|
m.cursorPos--
|
||||||
ui.Render(m)
|
ui.Render(m)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Menu) Down(ui.Event) {
|
func (m *Menu) Down() {
|
||||||
if m.cursorPos < (len(m.items) - 1) {
|
if m.cursorPos < (len(m.items) - 1) {
|
||||||
m.cursorPos++
|
m.cursorPos++
|
||||||
ui.Render(m)
|
ui.Render(m)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup some default handlers for menu navigation
|
|
||||||
func (m *Menu) NavigationHandlers() {
|
|
||||||
ui.Handle("/sys/kbd/<up>", m.Up)
|
|
||||||
ui.Handle("/sys/kbd/<down>", m.Down)
|
|
||||||
ui.Handle("/sys/kbd/q", func(ui.Event) { ui.StopLoop() })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set width and height based on menu items
|
// Set width and height based on menu items
|
||||||
func (m *Menu) calcSize() {
|
func (m *Menu) calcSize() {
|
||||||
m.Width = 7 // minimum width
|
m.Width = 7 // minimum width
|
||||||
|
|||||||
Reference in New Issue
Block a user