mirror of
https://github.com/bcicen/ctop.git
synced 2025-12-06 23:26:45 +08:00
Compare commits
133 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5261444265 | ||
|
|
b3aa291182 | ||
|
|
051b474bf0 | ||
|
|
fac6632459 | ||
|
|
1c7cf98e58 | ||
|
|
44a54e070d | ||
|
|
10b9a6c013 | ||
|
|
a3b67e4607 | ||
|
|
ac1ce18143 | ||
|
|
01a305d326 | ||
|
|
233259be40 | ||
|
|
107def9ccc | ||
|
|
d46ce783c2 | ||
|
|
d743472b16 | ||
|
|
d785b263f4 | ||
|
|
6d37a4e333 | ||
|
|
eb49e51ffb | ||
|
|
734d4bfc0c | ||
|
|
0e75bbda58 | ||
|
|
8b5eb21ac3 | ||
|
|
c958e4c34e | ||
|
|
ab48d830d1 | ||
|
|
915579c0a4 | ||
|
|
0a6e6f02a4 | ||
|
|
a135f45844 | ||
|
|
b39b91774d | ||
|
|
2275248813 | ||
|
|
75f4a91f11 | ||
|
|
d0b5c6c854 | ||
|
|
bd8940ae0a | ||
|
|
1be64f0d11 | ||
|
|
1c8f4b3a35 | ||
|
|
3e5176a79c | ||
|
|
53c0f2a9df | ||
|
|
28389aa38c | ||
|
|
fb5c825cf6 | ||
|
|
a0e0da1da9 | ||
|
|
a826859202 | ||
|
|
71b4a1de94 | ||
|
|
93a2e4b1ca | ||
|
|
436266b1a4 | ||
|
|
19427e33a0 | ||
|
|
48d683be77 | ||
|
|
e1ec264345 | ||
|
|
9aad2efdb0 | ||
|
|
f6595a02c4 | ||
|
|
92ca9bf7eb | ||
|
|
05242a83f0 | ||
|
|
add44c0f18 | ||
|
|
a1ebf3f90e | ||
|
|
626d50d3e9 | ||
|
|
eaa7ad85f8 | ||
|
|
be9be0b2d1 | ||
|
|
f196999c67 | ||
|
|
e674ec4f33 | ||
|
|
f23805550f | ||
|
|
55a356bbec | ||
|
|
954aaeb06b | ||
|
|
27e272c58f | ||
|
|
3ed9912bcb | ||
|
|
e0f6563a39 | ||
|
|
caa64724d0 | ||
|
|
a2011b8bc7 | ||
|
|
40fd9e935a | ||
|
|
b88c143914 | ||
|
|
0a05007c4e | ||
|
|
c47ba3f804 | ||
|
|
79a3f361a7 | ||
|
|
65399a37e5 | ||
|
|
25a3fcf731 | ||
|
|
17e2c2df8e | ||
|
|
240345d527 | ||
|
|
2d284d9277 | ||
|
|
bfa5c5944f | ||
|
|
e1051cd40f | ||
|
|
13029cc7fe | ||
|
|
58d9e4e194 | ||
|
|
4de7036e2f | ||
|
|
617b1b2863 | ||
|
|
e59a360b60 | ||
|
|
91cd53a878 | ||
|
|
c6f2c7b617 | ||
|
|
568bfb2513 | ||
|
|
66ff8ad7ec | ||
|
|
400d9471b6 | ||
|
|
288380ca8d | ||
|
|
429d5b9101 | ||
|
|
1be452d7c0 | ||
|
|
671c944272 | ||
|
|
a48a9031cc | ||
|
|
aff6943d07 | ||
|
|
f56ff96b88 | ||
|
|
b5361c2a28 | ||
|
|
e71b6cacce | ||
|
|
8a1297d3c5 | ||
|
|
bdea7d5853 | ||
|
|
389dee0f3c | ||
|
|
b49e174483 | ||
|
|
53b612ab07 | ||
|
|
446708e456 | ||
|
|
1233ff0ead | ||
|
|
af3f1e2a85 | ||
|
|
4dbc5653ff | ||
|
|
e8d9f3327c | ||
|
|
d372043a17 | ||
|
|
eeac65da8c | ||
|
|
c1780ae30a | ||
|
|
fb39d69fa7 | ||
|
|
6392d63ff8 | ||
|
|
b009a260a4 | ||
|
|
44379cd9fd | ||
|
|
b85ca680f0 | ||
|
|
8fb7a8988f | ||
|
|
6d097c2085 | ||
|
|
f9d68d688d | ||
|
|
bc08b85191 | ||
|
|
f3d26e038d | ||
|
|
b4e1fbf290 | ||
|
|
58d5fba945 | ||
|
|
c76036a6f2 | ||
|
|
6a8848d1e2 | ||
|
|
02d1050130 | ||
|
|
ccb44c964c | ||
|
|
b2165b6a29 | ||
|
|
d81d10ec27 | ||
|
|
9529c04680 | ||
|
|
6a89c9af38 | ||
|
|
06a29fc912 | ||
|
|
2cba7253fc | ||
|
|
47d60fe51b | ||
|
|
28f16c9a17 | ||
|
|
6560768e08 | ||
|
|
084c0c4ec8 |
19
.circleci/config.yml
Normal file
19
.circleci/config.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
version: 2
|
||||
jobs:
|
||||
build:
|
||||
working_directory: ~/build
|
||||
docker:
|
||||
- image: circleci/golang:latest
|
||||
steps:
|
||||
- checkout
|
||||
- setup_remote_docker:
|
||||
version: 17.05.0-ce
|
||||
- run: make image
|
||||
- deploy:
|
||||
command: |
|
||||
if [[ "$CIRCLE_BRANCH" == "master" ]]; then
|
||||
docker tag ctop quay.io/vektorlab/ctop:latest
|
||||
docker tag ctop quay.io/vektorlab/ctop:$(cat VERSION)
|
||||
docker login -u $DOCKER_USER -p $DOCKER_PASS quay.io
|
||||
docker push quay.io/vektorlab/ctop
|
||||
fi
|
||||
15
Dockerfile
15
Dockerfile
@@ -1,3 +1,16 @@
|
||||
FROM quay.io/vektorcloud/go:1.10
|
||||
|
||||
RUN apk add --no-cache make
|
||||
|
||||
COPY Gopkg.* /go/src/github.com/bcicen/ctop/
|
||||
WORKDIR /go/src/github.com/bcicen/ctop/
|
||||
RUN dep ensure -vendor-only
|
||||
|
||||
COPY . /go/src/github.com/bcicen/ctop
|
||||
RUN make build && \
|
||||
mkdir -p /go/bin && \
|
||||
mv -v ctop /go/bin/
|
||||
|
||||
FROM scratch
|
||||
COPY ./ctop /ctop
|
||||
COPY --from=0 /go/bin/ctop /ctop
|
||||
ENTRYPOINT ["/ctop"]
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
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/
|
||||
165
Gopkg.lock
generated
Normal file
165
Gopkg.lock
generated
Normal file
@@ -0,0 +1,165 @@
|
||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/Azure/go-ansiterm"
|
||||
packages = [".","winterm"]
|
||||
revision = "fa152c58bc15761d0200cb75fe958b89a9d4888e"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/BurntSushi/toml"
|
||||
packages = ["."]
|
||||
revision = "b26d9c308763d68093482582cea63d69be07a0f0"
|
||||
version = "v0.3.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/Microsoft/go-winio"
|
||||
packages = ["."]
|
||||
revision = "fff283ad5116362ca252298cfc9b95828956d85d"
|
||||
version = "v0.3.8"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/Nvveen/Gotty"
|
||||
packages = ["."]
|
||||
revision = "cd527374f1e5bff4938207604a14f2e38a9cf512"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/Sirupsen/logrus"
|
||||
packages = ["."]
|
||||
revision = "26709e2714106fb8ad40b773b711ebce25b78914"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/c9s/goprocinfo"
|
||||
packages = ["linux"]
|
||||
revision = "b34328d6e0cd139894ea7347d2624ccf31fa3c58"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/coreos/go-systemd"
|
||||
packages = ["dbus","util"]
|
||||
revision = "b4a58d95188dd092ae20072bac14cece0e67c388"
|
||||
version = "v4"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/docker/docker"
|
||||
packages = ["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"]
|
||||
revision = "90d35abf7b3535c1c319c872900fbd76374e521c"
|
||||
version = "v17.05.0-ce-rc3"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/docker/go-connections"
|
||||
packages = ["nat"]
|
||||
revision = "a2afab9802043837035592f1c24827fb70766de9"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/docker/go-units"
|
||||
packages = ["."]
|
||||
revision = "0dadbb0345b35ec7ef35e228dabb8de89a65bf52"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/fsouza/go-dockerclient"
|
||||
packages = ["."]
|
||||
revision = "318513eb1ab27495afbc67f671ba1080513d8aa0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/gizak/termui"
|
||||
packages = ["."]
|
||||
revision = "cdc199d7ea432fd8187db35f0247285d6f5b0267"
|
||||
source = "https://github.com/bcicen/termui"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/godbus/dbus"
|
||||
packages = ["."]
|
||||
revision = "c7fdd8b5cd55e87b4e1f4e372cdb1db61dd6c66f"
|
||||
version = "v3"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/golang/protobuf"
|
||||
packages = ["proto"]
|
||||
revision = "0a4f71a498b7c4812f64969510bcb4eca251e33a"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/hashicorp/go-cleanhttp"
|
||||
packages = ["."]
|
||||
revision = "3573b8b52aa7b37b9358d966a898feb387f62437"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/jgautheron/codename-generator"
|
||||
packages = ["."]
|
||||
revision = "16d037c7cc3c9b552fe4af9828b7338d752dbaf9"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/maruel/panicparse"
|
||||
packages = ["stack"]
|
||||
revision = "25bcac0d793cf4109483505a0d66e066a3a90a80"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/mattn/go-runewidth"
|
||||
packages = ["."]
|
||||
revision = "14207d285c6c197daabb5c9793d63e7af9ab2d50"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/mitchellh/go-wordwrap"
|
||||
packages = ["."]
|
||||
revision = "ad45545899c7b13c020ea92b2072220eefad42b8"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/nsf/termbox-go"
|
||||
packages = ["."]
|
||||
revision = "e2050e41c8847748ec5288741c0b19a8cb26d084"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/nu7hatch/gouuid"
|
||||
packages = ["."]
|
||||
revision = "179d4d0c4d8d407a32af483c2354df1d2c91e6c3"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/op/go-logging"
|
||||
packages = ["."]
|
||||
revision = "b2cb9fa56473e98db8caba80237377e83fe44db5"
|
||||
version = "v1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/opencontainers/runc"
|
||||
packages = ["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"]
|
||||
revision = "baf6536d6259209c3edfa2b22237af82942d3dfa"
|
||||
version = "v0.1.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/seccomp/libseccomp-golang"
|
||||
packages = ["."]
|
||||
revision = "1b506fc7c24eec5a3693cdcbed40d9c226cfc6a1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/syndtr/gocapability"
|
||||
packages = ["capability"]
|
||||
revision = "2c00daeb6c3b45114c80ac44119e7b8801fdd852"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/vishvananda/netlink"
|
||||
packages = [".","nl"]
|
||||
revision = "1e2e08e8a2dcdacaae3f14ac44c5cfa31361f270"
|
||||
|
||||
[[projects]]
|
||||
name = "golang.org/x/net"
|
||||
packages = ["context","context/ctxhttp"]
|
||||
revision = "a6577fac2d73be281a500b310739095313165611"
|
||||
|
||||
[[projects]]
|
||||
name = "golang.org/x/sys"
|
||||
packages = ["unix","windows"]
|
||||
revision = "99f16d856c9836c42d24e7ab64ea72916925fa97"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "f46f5c696ecb0b0c42a38dac512df21fc1f5fb2bfda888434e005e69d1b6273b"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
47
Gopkg.toml
Normal file
47
Gopkg.toml
Normal file
@@ -0,0 +1,47 @@
|
||||
|
||||
# Gopkg.toml example
|
||||
#
|
||||
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
|
||||
# for detailed Gopkg.toml documentation.
|
||||
#
|
||||
# required = ["github.com/user/thing/cmd/thing"]
|
||||
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project"
|
||||
# version = "1.0.0"
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project2"
|
||||
# branch = "dev"
|
||||
# source = "github.com/myfork/project2"
|
||||
#
|
||||
# [[override]]
|
||||
# name = "github.com/x/y"
|
||||
# version = "2.4.0"
|
||||
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/fsouza/go-dockerclient"
|
||||
revision = "318513eb1ab27495afbc67f671ba1080513d8aa0"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/gizak/termui"
|
||||
source = "https://github.com/bcicen/termui"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/jgautheron/codename-generator"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/nu7hatch/gouuid"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/op/go-logging"
|
||||
version = "1.0.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/opencontainers/runc"
|
||||
version = "0.1.1"
|
||||
25
Makefile
25
Makefile
@@ -1,33 +1,36 @@
|
||||
NAME=ctop
|
||||
VERSION=$(shell cat VERSION)
|
||||
BUILD=$(shell git rev-parse --short HEAD)
|
||||
LD_FLAGS="-w -X main.version=$(VERSION) -X main.build=$(BUILD)"
|
||||
EXT_LD_FLAGS="-Wl,--allow-multiple-definition"
|
||||
LD_FLAGS="-w -X main.version=$(VERSION) -X main.build=$(BUILD) -extldflags=$(EXT_LD_FLAGS)"
|
||||
|
||||
clean:
|
||||
rm -rf build/ release/
|
||||
rm -rf _build/ release/
|
||||
|
||||
build:
|
||||
glide install
|
||||
dep ensure
|
||||
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)"
|
||||
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
|
||||
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
|
||||
GOOS=windows GOARCH=amd64 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-windows-amd64
|
||||
cd _build; sha256sum * > sha256sums.txt
|
||||
|
||||
image:
|
||||
docker build -t ctop_build -f Dockerfile_build .
|
||||
docker run -ti --rm -v $(shell pwd):/target ctop_build cp -v /go/bin/ctop /target/
|
||||
docker build -t ctop -f Dockerfile .
|
||||
|
||||
release:
|
||||
mkdir release
|
||||
go get github.com/progrium/gh-release/...
|
||||
cp build/* release
|
||||
cp _build/* release
|
||||
cd release; sha256sum --quiet --check sha256sums.txt
|
||||
gh-release create bcicen/$(NAME) $(VERSION) \
|
||||
$(shell git rev-parse --abbrev-ref HEAD) $(VERSION)
|
||||
|
||||
|
||||
40
README.md
40
README.md
@@ -2,14 +2,16 @@
|
||||
|
||||
#
|
||||
|
||||
![release][release] ![homebrew][homebrew]
|
||||
|
||||
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.
|
||||
as well as an [single container view][single_view] for inspecting a specific container.
|
||||
|
||||
`ctop` currently comes with built-in support for Docker; connectors for other container and cluster systems are planned for future releases.
|
||||
`ctop` comes with built-in support for Docker and runC; connectors for other container and cluster systems are planned for future releases.
|
||||
|
||||
## Install
|
||||
|
||||
@@ -18,7 +20,7 @@ Fetch the [latest release](https://github.com/bcicen/ctop/releases) for your pla
|
||||
#### Linux
|
||||
|
||||
```bash
|
||||
sudo wget https://github.com/bcicen/ctop/releases/download/v0.5/ctop-0.5-linux-amd64 -O /usr/local/bin/ctop
|
||||
sudo wget https://github.com/bcicen/ctop/releases/download/v0.7/ctop-0.7-linux-amd64 -O /usr/local/bin/ctop
|
||||
sudo chmod +x /usr/local/bin/ctop
|
||||
```
|
||||
|
||||
@@ -29,13 +31,17 @@ brew install ctop
|
||||
```
|
||||
or
|
||||
```bash
|
||||
sudo curl -Lo /usr/local/bin/ctop https://github.com/bcicen/ctop/releases/download/v0.5/ctop-0.5-darwin-amd64
|
||||
sudo curl -Lo /usr/local/bin/ctop https://github.com/bcicen/ctop/releases/download/v0.7/ctop-0.7-darwin-amd64
|
||||
sudo chmod +x /usr/local/bin/ctop
|
||||
```
|
||||
|
||||
or run via Docker:
|
||||
#### Docker
|
||||
|
||||
```bash
|
||||
docker run -ti --name ctop --rm -v /var/run/docker.sock:/var/run/docker.sock quay.io/vektorlab/ctop:latest
|
||||
docker run --rm -ti \
|
||||
--name=ctop \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
quay.io/vektorlab/ctop:latest
|
||||
```
|
||||
|
||||
`ctop` is also available for Arch in the [AUR](https://aur.archlinux.org/packages/ctop-bin/)
|
||||
@@ -46,35 +52,43 @@ Build steps can be found [here][build].
|
||||
|
||||
## Usage
|
||||
|
||||
`ctop` requires no arguments and will configure itself using the `DOCKER_HOST` environment variable
|
||||
```bash
|
||||
export DOCKER_HOST=tcp://127.0.0.1:4243
|
||||
ctop
|
||||
```
|
||||
`ctop` requires no arguments and uses Docker host variables by default. See [connectors][connectors] for further configuration options.
|
||||
|
||||
### Config file
|
||||
|
||||
While running, use `S` to save the current filters, sort field, and other options to a default config path. These settings will be loaded and applied the next time `ctop` is started.
|
||||
|
||||
### Options
|
||||
|
||||
Option | Description
|
||||
--- | ---
|
||||
-a | show active containers only
|
||||
-f <string> | set an initial filter string
|
||||
-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
|
||||
-scale-cpu | show cpu as % of system total
|
||||
-v | output version information and exit
|
||||
|
||||
### Keybindings
|
||||
|
||||
Key | Action
|
||||
--- | ---
|
||||
\<enter\> | Open container menu
|
||||
a | Toggle display of all (running and non-running) containers
|
||||
f | Filter displayed containers (`esc` to clear when open)
|
||||
H | Toggle ctop header
|
||||
h | Open help dialog
|
||||
s | Select container sort field
|
||||
r | Reverse container sort order
|
||||
o | Open single view
|
||||
l | View container logs (`t` to toggle timestamp when open)
|
||||
S | Save current configuration to file
|
||||
q | Quit ctop
|
||||
|
||||
[build]: _docs/build.md
|
||||
[expanded_view]: _docs/expanded.md
|
||||
[connectors]: _docs/connectors.md
|
||||
[single_view]: _docs/single.md
|
||||
[release]: https://img.shields.io/github/release/bcicen/ctop.svg "ctop"
|
||||
[homebrew]: https://img.shields.io/homebrew/v/ctop.svg "ctop"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Build
|
||||
|
||||
To build `ctop` from source, ensure you have a recent version of [glide](https://github.com/Masterminds/glide) installed and run:
|
||||
To build `ctop` from source, ensure you have [dep](https://github.com/golang/dep) installed and run:
|
||||
|
||||
```bash
|
||||
go get github.com/bcicen/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 of container state (default: `/run/runc`)
|
||||
|
||||
#### Options
|
||||
|
||||
Var | Description
|
||||
--- | ---
|
||||
RUNC_ROOT | path to runc root for container state (default: `/run/runc`)
|
||||
RUNC_SYSTEMD_CGROUP | if set, enable systemd cgroups
|
||||
@@ -1,14 +1,23 @@
|
||||
# Debug Mode
|
||||
|
||||
`ctop` comes with a built-in logging facility and local socket server to simplify debugging at run time. Debug mode can be enabled via the `CTOP_DEBUG` environment variable:
|
||||
`ctop` comes with a built-in logging facility and local socket server to simplify debugging at run time.
|
||||
|
||||
## Quick Start
|
||||
|
||||
If running `ctop` via Docker, debug logging can be most easily enabled as below:
|
||||
```bash
|
||||
CTOP_DEBUG=1 ./ctop
|
||||
docker run -ti --rm \
|
||||
--name=ctop \
|
||||
-e CTOP_DEBUG=1 \
|
||||
-e CTOP_DEBUG_TCP=1 \
|
||||
-p 9000:9000 \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
quay.io/vektorlab/ctop:latest
|
||||
```
|
||||
|
||||
While `ctop` is running, you can connect to the logging socket via socat or similar tools:
|
||||
Log messages can be followed by connecting to the default listen address:
|
||||
```bash
|
||||
socat unix-connect:/tmp/ctop.sock stdio
|
||||
curl -s localhost:9000
|
||||
```
|
||||
|
||||
example output:
|
||||
@@ -22,3 +31,26 @@ example output:
|
||||
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`)
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
# 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>
|
||||
|
Before Width: | Height: | Size: 549 KiB After Width: | Height: | Size: 549 KiB |
4
_docs/single.md
Normal file
4
_docs/single.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# Single Container View
|
||||
|
||||
ctop provides a rolling, single container view for following metrics
|
||||
<p align="center"><img width="80%" src="img/single.gif" alt="ctop"/></p>
|
||||
23
circle.yml
23
circle.yml
@@ -1,23 +0,0 @@
|
||||
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}
|
||||
@@ -45,6 +45,9 @@ var ColorMap = map[string]ui.Attribute{
|
||||
"par.text.hi": ui.ColorBlack,
|
||||
"sparkline.line.fg": ui.ColorGreen,
|
||||
"sparkline.title.fg": ui.ColorWhite,
|
||||
"status.ok": ui.ColorGreen,
|
||||
"status.warn": ui.ColorYellow,
|
||||
"status.danger": ui.ColorRed,
|
||||
}
|
||||
|
||||
func InvertColorMap() {
|
||||
|
||||
119
config/file.go
Normal file
119
config/file.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
)
|
||||
|
||||
var (
|
||||
xdgRe = regexp.MustCompile("^XDG_*")
|
||||
)
|
||||
|
||||
type ConfigFile struct {
|
||||
Options map[string]string `toml:"options"`
|
||||
Toggles map[string]bool `toml:"toggles"`
|
||||
}
|
||||
|
||||
func exportConfig() ConfigFile {
|
||||
c := ConfigFile{
|
||||
Options: make(map[string]string),
|
||||
Toggles: make(map[string]bool),
|
||||
}
|
||||
for _, p := range GlobalParams {
|
||||
c.Options[p.Key] = p.Val
|
||||
}
|
||||
for _, sw := range GlobalSwitches {
|
||||
c.Toggles[sw.Key] = sw.Val
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func Read() error {
|
||||
var config ConfigFile
|
||||
|
||||
path, err := getConfigPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := toml.DecodeFile(path, &config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for k, v := range config.Options {
|
||||
Update(k, v)
|
||||
}
|
||||
for k, v := range config.Toggles {
|
||||
UpdateSwitch(k, v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Write() (path string, err error) {
|
||||
path, err = getConfigPath()
|
||||
if err != nil {
|
||||
return path, err
|
||||
}
|
||||
|
||||
cfgdir := basedir(path)
|
||||
// create config dir if not exist
|
||||
if _, err := os.Stat(cfgdir); err != nil {
|
||||
err = os.MkdirAll(cfgdir, 0755)
|
||||
if err != nil {
|
||||
return path, fmt.Errorf("failed to create config dir [%s]: %s", cfgdir, err)
|
||||
}
|
||||
}
|
||||
|
||||
file, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
return path, fmt.Errorf("failed to open config for writing: %s", err)
|
||||
}
|
||||
|
||||
writer := toml.NewEncoder(file)
|
||||
err = writer.Encode(exportConfig())
|
||||
if err != nil {
|
||||
return path, fmt.Errorf("failed to write config: %s", err)
|
||||
}
|
||||
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// determine config path from environment
|
||||
func getConfigPath() (path string, err error) {
|
||||
homeDir, ok := os.LookupEnv("HOME")
|
||||
if !ok {
|
||||
return path, fmt.Errorf("$HOME not set")
|
||||
}
|
||||
|
||||
// use xdg config home if possible
|
||||
if xdgSupport() {
|
||||
xdgHome, ok := os.LookupEnv("XDG_CONFIG_HOME")
|
||||
if !ok {
|
||||
xdgHome = fmt.Sprintf("%s/.config", homeDir)
|
||||
}
|
||||
path = fmt.Sprintf("%s/ctop/config", xdgHome)
|
||||
} else {
|
||||
path = fmt.Sprintf("%s/.ctop", homeDir)
|
||||
}
|
||||
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// test for environemnt supporting XDG spec
|
||||
func xdgSupport() bool {
|
||||
for _, e := range os.Environ() {
|
||||
if xdgRe.FindAllString(e, 1) != nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func basedir(path string) string {
|
||||
parts := strings.Split(path, "/")
|
||||
return strings.Join((parts[0 : len(parts)-1]), "/")
|
||||
}
|
||||
@@ -17,6 +17,11 @@ var switches = []*Switch{
|
||||
Val: true,
|
||||
Label: "Enable Status Header",
|
||||
},
|
||||
&Switch{
|
||||
Key: "scaleCpu",
|
||||
Val: false,
|
||||
Label: "Show CPU as %% of system total",
|
||||
},
|
||||
}
|
||||
|
||||
type Switch struct {
|
||||
@@ -40,6 +45,14 @@ func GetSwitchVal(k string) bool {
|
||||
return GetSwitch(k).Val
|
||||
}
|
||||
|
||||
func UpdateSwitch(k string, val bool) {
|
||||
sw := GetSwitch(k)
|
||||
if sw.Val != val {
|
||||
log.Noticef("config change: %s: %t -> %t", k, sw.Val, val)
|
||||
sw.Val = val
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle a boolean switch
|
||||
func Toggle(k string) {
|
||||
sw := GetSwitch(k)
|
||||
|
||||
@@ -1,32 +1,36 @@
|
||||
package metrics
|
||||
package collector
|
||||
|
||||
import (
|
||||
"github.com/bcicen/ctop/config"
|
||||
"github.com/bcicen/ctop/models"
|
||||
api "github.com/fsouza/go-dockerclient"
|
||||
)
|
||||
|
||||
// Docker collector
|
||||
type Docker struct {
|
||||
Metrics
|
||||
models.Metrics
|
||||
id string
|
||||
client *api.Client
|
||||
running bool
|
||||
stream chan Metrics
|
||||
stream chan models.Metrics
|
||||
done chan bool
|
||||
lastCpu float64
|
||||
lastSysCpu float64
|
||||
scaleCpu bool
|
||||
}
|
||||
|
||||
func NewDocker(client *api.Client, id string) *Docker {
|
||||
return &Docker{
|
||||
Metrics: Metrics{},
|
||||
Metrics: models.Metrics{},
|
||||
id: id,
|
||||
client: client,
|
||||
scaleCpu: config.GetSwitchVal("scaleCpu"),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Docker) Start() {
|
||||
c.done = make(chan bool)
|
||||
c.stream = make(chan Metrics)
|
||||
c.stream = make(chan models.Metrics)
|
||||
stats := make(chan *api.Stats)
|
||||
|
||||
go func() {
|
||||
@@ -60,10 +64,14 @@ func (c *Docker) Running() bool {
|
||||
return c.running
|
||||
}
|
||||
|
||||
func (c *Docker) Stream() chan Metrics {
|
||||
func (c *Docker) Stream() chan models.Metrics {
|
||||
return c.stream
|
||||
}
|
||||
|
||||
func (c *Docker) Logs() LogCollector {
|
||||
return NewDockerLogs(c.id, c.client)
|
||||
}
|
||||
|
||||
// Stop collector
|
||||
func (c *Docker) Stop() {
|
||||
c.done <- true
|
||||
@@ -77,16 +85,20 @@ func (c *Docker) ReadCPU(stats *api.Stats) {
|
||||
cpudiff := total - c.lastCpu
|
||||
syscpudiff := system - c.lastSysCpu
|
||||
|
||||
if c.scaleCpu {
|
||||
c.CPUUtil = round((cpudiff / syscpudiff * 100))
|
||||
} else {
|
||||
c.CPUUtil = round((cpudiff / syscpudiff * 100) * ncpus)
|
||||
}
|
||||
c.lastCpu = total
|
||||
c.lastSysCpu = system
|
||||
c.Pids = int(stats.PidsStats.Current)
|
||||
}
|
||||
|
||||
func (c *Docker) ReadMem(stats *api.Stats) {
|
||||
c.MemUsage = int64(stats.MemoryStats.Usage)
|
||||
c.MemUsage = int64(stats.MemoryStats.Usage - stats.MemoryStats.Stats.Cache)
|
||||
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) {
|
||||
84
connector/collector/docker_logs.go
Normal file
84
connector/collector/docker_logs.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bcicen/ctop/models"
|
||||
api "github.com/fsouza/go-dockerclient"
|
||||
)
|
||||
|
||||
type DockerLogs struct {
|
||||
id string
|
||||
client *api.Client
|
||||
done chan bool
|
||||
}
|
||||
|
||||
func NewDockerLogs(id string, client *api.Client) *DockerLogs {
|
||||
return &DockerLogs{
|
||||
id: id,
|
||||
client: client,
|
||||
done: make(chan bool),
|
||||
}
|
||||
}
|
||||
|
||||
func (l *DockerLogs) Stream() chan models.Log {
|
||||
r, w := io.Pipe()
|
||||
logCh := make(chan models.Log)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
opts := api.LogsOptions{
|
||||
Context: ctx,
|
||||
Container: l.id,
|
||||
OutputStream: w,
|
||||
ErrorStream: w,
|
||||
Stdout: true,
|
||||
Stderr: true,
|
||||
Tail: "10",
|
||||
Follow: true,
|
||||
Timestamps: true,
|
||||
}
|
||||
|
||||
// read io pipe into channel
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(r)
|
||||
for scanner.Scan() {
|
||||
parts := strings.Split(scanner.Text(), " ")
|
||||
ts := l.parseTime(parts[0])
|
||||
logCh <- models.Log{ts, strings.Join(parts[1:], " ")}
|
||||
}
|
||||
}()
|
||||
|
||||
// connect to container log stream
|
||||
go func() {
|
||||
err := l.client.Logs(opts)
|
||||
if err != nil {
|
||||
log.Errorf("error reading container logs: %s", err)
|
||||
}
|
||||
log.Infof("log reader stopped for container: %s", l.id)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
select {
|
||||
case <-l.done:
|
||||
cancel()
|
||||
}
|
||||
}()
|
||||
|
||||
log.Infof("log reader started for container: %s", l.id)
|
||||
return logCh
|
||||
}
|
||||
|
||||
func (l *DockerLogs) Stop() { l.done <- true }
|
||||
|
||||
func (l *DockerLogs) parseTime(s string) time.Time {
|
||||
ts, err := time.Parse("2006-01-02T15:04:05.000000000Z", s)
|
||||
if err != nil {
|
||||
log.Errorf("failed to parse container log: %s", err)
|
||||
ts = time.Now()
|
||||
}
|
||||
return ts
|
||||
}
|
||||
35
connector/collector/main.go
Normal file
35
connector/collector/main.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/bcicen/ctop/logging"
|
||||
"github.com/bcicen/ctop/models"
|
||||
)
|
||||
|
||||
var log = logging.Init()
|
||||
|
||||
type LogCollector interface {
|
||||
Stream() chan models.Log
|
||||
Stop()
|
||||
}
|
||||
|
||||
type Collector interface {
|
||||
Stream() chan models.Metrics
|
||||
Logs() LogCollector
|
||||
Running() bool
|
||||
Start()
|
||||
Stop()
|
||||
}
|
||||
|
||||
func round(num float64) int {
|
||||
return int(num + math.Copysign(0.5, num))
|
||||
}
|
||||
|
||||
// return rounded percentage
|
||||
func percent(val float64, total float64) int {
|
||||
if total <= 0 {
|
||||
return 0
|
||||
}
|
||||
return round((val / total) * 100)
|
||||
}
|
||||
@@ -1,16 +1,18 @@
|
||||
// +build !release
|
||||
|
||||
package metrics
|
||||
package collector
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/bcicen/ctop/models"
|
||||
)
|
||||
|
||||
// Mock collector
|
||||
type Mock struct {
|
||||
Metrics
|
||||
stream chan Metrics
|
||||
models.Metrics
|
||||
stream chan models.Metrics
|
||||
done bool
|
||||
running bool
|
||||
aggression int64
|
||||
@@ -18,7 +20,7 @@ type Mock struct {
|
||||
|
||||
func NewMock(a int64) *Mock {
|
||||
c := &Mock{
|
||||
Metrics: Metrics{},
|
||||
Metrics: models.Metrics{},
|
||||
aggression: a,
|
||||
}
|
||||
c.MemLimit = 2147483648
|
||||
@@ -31,7 +33,7 @@ func (c *Mock) Running() bool {
|
||||
|
||||
func (c *Mock) Start() {
|
||||
c.done = false
|
||||
c.stream = make(chan Metrics)
|
||||
c.stream = make(chan models.Metrics)
|
||||
go c.run()
|
||||
}
|
||||
|
||||
@@ -39,15 +41,24 @@ func (c *Mock) Stop() {
|
||||
c.done = true
|
||||
}
|
||||
|
||||
func (c *Mock) Stream() chan Metrics {
|
||||
func (c *Mock) Stream() chan models.Metrics {
|
||||
return c.stream
|
||||
}
|
||||
|
||||
func (c *Mock) Logs() LogCollector {
|
||||
return &MockLogs{make(chan bool)}
|
||||
}
|
||||
|
||||
func (c *Mock) run() {
|
||||
c.running = true
|
||||
rand.Seed(int64(time.Now().Nanosecond()))
|
||||
defer close(c.stream)
|
||||
|
||||
// set to random static value, once
|
||||
c.Pids = rand.Intn(12)
|
||||
c.IOBytesRead = rand.Int63n(8098) * c.aggression
|
||||
c.IOBytesWrite = rand.Int63n(8098) * c.aggression
|
||||
|
||||
for {
|
||||
c.CPUUtil += rand.Intn(2) * int(c.aggression)
|
||||
if c.CPUUtil >= 100 {
|
||||
@@ -60,7 +71,7 @@ func (c *Mock) run() {
|
||||
if c.MemUsage > c.MemLimit {
|
||||
c.MemUsage = 0
|
||||
}
|
||||
c.MemPercent = round((float64(c.MemUsage) / float64(c.MemLimit)) * 100)
|
||||
c.MemPercent = percent(float64(c.MemUsage), float64(c.MemLimit))
|
||||
c.stream <- c.Metrics
|
||||
if c.done {
|
||||
break
|
||||
31
connector/collector/mock_logs.go
Normal file
31
connector/collector/mock_logs.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/bcicen/ctop/models"
|
||||
)
|
||||
|
||||
const mockLog = "Cura ob pro qui tibi inveni dum qua fit donec amare illic mea, regem falli contexo pro peregrinorum heremo absconditi araneae meminerim deliciosas actionibus facere modico dura sonuerunt psalmi contra rerum, tempus mala anima volebant dura quae o modis."
|
||||
|
||||
type MockLogs struct {
|
||||
done chan bool
|
||||
}
|
||||
|
||||
func (l *MockLogs) Stream() chan models.Log {
|
||||
logCh := make(chan models.Log)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-l.done:
|
||||
break
|
||||
default:
|
||||
logCh <- models.Log{time.Now(), mockLog}
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
}()
|
||||
return logCh
|
||||
}
|
||||
|
||||
func (l *MockLogs) Stop() { l.done <- true }
|
||||
44
connector/collector/proc.go
Normal file
44
connector/collector/proc.go
Normal file
@@ -0,0 +1,44 @@
|
||||
// +build linux
|
||||
|
||||
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
|
||||
}
|
||||
135
connector/collector/runc.go
Normal file
135
connector/collector/runc.go
Normal file
@@ -0,0 +1,135 @@
|
||||
// +build linux
|
||||
|
||||
package collector
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/bcicen/ctop/config"
|
||||
"github.com/bcicen/ctop/models"
|
||||
"github.com/opencontainers/runc/libcontainer"
|
||||
"github.com/opencontainers/runc/libcontainer/cgroups"
|
||||
)
|
||||
|
||||
// Runc collector
|
||||
type Runc struct {
|
||||
models.Metrics
|
||||
id string
|
||||
libc libcontainer.Container
|
||||
stream chan models.Metrics
|
||||
done bool
|
||||
running bool
|
||||
interval int // collection interval, in seconds
|
||||
lastCpu float64
|
||||
lastSysCpu float64
|
||||
scaleCpu bool
|
||||
}
|
||||
|
||||
func NewRunc(libc libcontainer.Container) *Runc {
|
||||
c := &Runc{
|
||||
Metrics: models.Metrics{},
|
||||
id: libc.ID(),
|
||||
libc: libc,
|
||||
interval: 1,
|
||||
scaleCpu: config.GetSwitchVal("scaleCpu"),
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Runc) Running() bool {
|
||||
return c.running
|
||||
}
|
||||
|
||||
func (c *Runc) Start() {
|
||||
c.done = false
|
||||
c.stream = make(chan models.Metrics)
|
||||
go c.run()
|
||||
}
|
||||
|
||||
func (c *Runc) Stop() {
|
||||
c.done = true
|
||||
}
|
||||
|
||||
func (c *Runc) Stream() chan models.Metrics {
|
||||
return c.stream
|
||||
}
|
||||
|
||||
func (c *Runc) Logs() LogCollector {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Runc) run() {
|
||||
c.running = true
|
||||
defer close(c.stream)
|
||||
log.Debugf("collector started for container: %s", c.id)
|
||||
|
||||
for {
|
||||
stats, err := c.libc.Stats()
|
||||
if err != nil {
|
||||
log.Errorf("failed to collect stats for container %s:\n%s", c.id, err)
|
||||
break
|
||||
}
|
||||
|
||||
c.ReadCPU(stats.CgroupStats)
|
||||
c.ReadMem(stats.CgroupStats)
|
||||
c.ReadNet(stats.Interfaces)
|
||||
|
||||
c.stream <- c.Metrics
|
||||
if c.done {
|
||||
break
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
c.running = false
|
||||
}
|
||||
|
||||
func (c *Runc) ReadCPU(stats *cgroups.Stats) {
|
||||
u := stats.CpuStats.CpuUsage
|
||||
ncpus := float64(len(u.PercpuUsage))
|
||||
total := float64(u.TotalUsage)
|
||||
system := float64(getSysCPUUsage())
|
||||
|
||||
cpudiff := total - c.lastCpu
|
||||
syscpudiff := system - c.lastSysCpu
|
||||
|
||||
if c.scaleCpu {
|
||||
c.CPUUtil = round((cpudiff / syscpudiff * 100))
|
||||
} else {
|
||||
c.CPUUtil = round((cpudiff / syscpudiff * 100) * ncpus)
|
||||
}
|
||||
c.lastCpu = total
|
||||
c.lastSysCpu = system
|
||||
c.Pids = int(stats.PidsStats.Current)
|
||||
}
|
||||
|
||||
func (c *Runc) ReadMem(stats *cgroups.Stats) {
|
||||
c.MemUsage = int64(stats.MemoryStats.Usage.Usage)
|
||||
c.MemLimit = int64(stats.MemoryStats.Usage.Limit)
|
||||
if c.MemLimit > sysMemTotal && sysMemTotal > 0 {
|
||||
c.MemLimit = sysMemTotal
|
||||
}
|
||||
c.MemPercent = percent(float64(c.MemUsage), float64(c.MemLimit))
|
||||
}
|
||||
|
||||
func (c *Runc) ReadNet(interfaces []*libcontainer.NetworkInterface) {
|
||||
var rx, tx int64
|
||||
for _, network := range interfaces {
|
||||
rx += int64(network.RxBytes)
|
||||
tx += int64(network.TxBytes)
|
||||
}
|
||||
c.NetRx, c.NetTx = rx, tx
|
||||
}
|
||||
|
||||
func (c *Runc) ReadIO(stats *cgroups.Stats) {
|
||||
var read, write int64
|
||||
for _, blk := range stats.BlkioStats.IoServiceBytesRecursive {
|
||||
if blk.Op == "Read" {
|
||||
read = int64(blk.Value)
|
||||
}
|
||||
if blk.Op == "Write" {
|
||||
write = int64(blk.Value)
|
||||
}
|
||||
}
|
||||
c.IOBytesRead, c.IOBytesWrite = read, write
|
||||
}
|
||||
@@ -1,35 +1,34 @@
|
||||
package main
|
||||
package connector
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/bcicen/ctop/metrics"
|
||||
"github.com/fsouza/go-dockerclient"
|
||||
"github.com/bcicen/ctop/connector/collector"
|
||||
"github.com/bcicen/ctop/connector/manager"
|
||||
"github.com/bcicen/ctop/container"
|
||||
api "github.com/fsouza/go-dockerclient"
|
||||
)
|
||||
|
||||
type ContainerSource interface {
|
||||
All() Containers
|
||||
Get(string) (*Container, bool)
|
||||
}
|
||||
func init() { enabled["docker"] = NewDocker }
|
||||
|
||||
type DockerContainerSource struct {
|
||||
client *docker.Client
|
||||
containers map[string]*Container
|
||||
type Docker struct {
|
||||
client *api.Client
|
||||
containers map[string]*container.Container
|
||||
needsRefresh chan string // container IDs requiring refresh
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
func NewDockerContainerSource() *DockerContainerSource {
|
||||
func NewDocker() Connector {
|
||||
// init docker client
|
||||
client, err := docker.NewClientFromEnv()
|
||||
client, err := api.NewClientFromEnv()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
cm := &DockerContainerSource{
|
||||
cm := &Docker{
|
||||
client: client,
|
||||
containers: make(map[string]*Container),
|
||||
containers: make(map[string]*container.Container),
|
||||
needsRefresh: make(chan string, 60),
|
||||
lock: sync.RWMutex{},
|
||||
}
|
||||
@@ -40,17 +39,20 @@ func NewDockerContainerSource() *DockerContainerSource {
|
||||
}
|
||||
|
||||
// Docker events watcher
|
||||
func (cm *DockerContainerSource) watchEvents() {
|
||||
func (cm *Docker) watchEvents() {
|
||||
log.Info("docker event listener starting")
|
||||
events := make(chan *docker.APIEvents)
|
||||
events := make(chan *api.APIEvents)
|
||||
cm.client.AddEventListener(events)
|
||||
|
||||
for e := range events {
|
||||
if e.Type != "container" {
|
||||
continue
|
||||
}
|
||||
switch e.Action {
|
||||
case "start", "die", "pause", "unpause":
|
||||
|
||||
actionName := strings.Split(e.Action, ":")[0]
|
||||
|
||||
switch actionName {
|
||||
case "start", "die", "pause", "unpause", "health_status":
|
||||
log.Debugf("handling docker event: action=%s id=%s", e.Action, e.ID)
|
||||
cm.needsRefresh <- e.ID
|
||||
case "destroy":
|
||||
@@ -60,7 +62,25 @@ func (cm *DockerContainerSource) watchEvents() {
|
||||
}
|
||||
}
|
||||
|
||||
func (cm *DockerContainerSource) refresh(c *Container) {
|
||||
func portsFormat(ports map[api.Port][]api.PortBinding) string {
|
||||
var exposed []string
|
||||
var published []string
|
||||
|
||||
for k, v := range ports {
|
||||
if len(v) == 0 {
|
||||
exposed = append(exposed, string(k))
|
||||
continue
|
||||
}
|
||||
for _, binding := range v {
|
||||
s := fmt.Sprintf("%s:%s -> %s", binding.HostIP, binding.HostPort, k)
|
||||
published = append(published, s)
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(append(exposed, published...), "\n")
|
||||
}
|
||||
|
||||
func (cm *Docker) refresh(c *container.Container) {
|
||||
insp := cm.inspect(c.Id)
|
||||
// remove container if no longer exists
|
||||
if insp == nil {
|
||||
@@ -69,14 +89,16 @@ func (cm *DockerContainerSource) refresh(c *Container) {
|
||||
}
|
||||
c.SetMeta("name", shortName(insp.Name))
|
||||
c.SetMeta("image", insp.Config.Image)
|
||||
c.SetMeta("ports", portsFormat(insp.NetworkSettings.Ports))
|
||||
c.SetMeta("created", insp.Created.Format("Mon Jan 2 15:04:05 2006"))
|
||||
c.SetMeta("health", insp.State.Health.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)
|
||||
if err != nil {
|
||||
if _, ok := err.(*docker.NoSuchContainer); ok == false {
|
||||
if _, ok := err.(*api.NoSuchContainer); ok == false {
|
||||
log.Errorf(err.Error())
|
||||
}
|
||||
}
|
||||
@@ -84,8 +106,8 @@ func (cm *DockerContainerSource) inspect(id string) *docker.Container {
|
||||
}
|
||||
|
||||
// Mark all container IDs for refresh
|
||||
func (cm *DockerContainerSource) refreshAll() {
|
||||
opts := docker.ListContainersOptions{All: true}
|
||||
func (cm *Docker) refreshAll() {
|
||||
opts := api.ListContainersOptions{All: true}
|
||||
allContainers, err := cm.client.ListContainers(opts)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -99,7 +121,7 @@ func (cm *DockerContainerSource) refreshAll() {
|
||||
}
|
||||
}
|
||||
|
||||
func (cm *DockerContainerSource) Loop() {
|
||||
func (cm *Docker) Loop() {
|
||||
for id := range cm.needsRefresh {
|
||||
c := cm.MustGet(id)
|
||||
cm.refresh(c)
|
||||
@@ -107,14 +129,16 @@ func (cm *DockerContainerSource) Loop() {
|
||||
}
|
||||
|
||||
// Get a single container, creating one anew if not existing
|
||||
func (cm *DockerContainerSource) MustGet(id string) *Container {
|
||||
func (cm *Docker) MustGet(id string) *container.Container {
|
||||
c, ok := cm.Get(id)
|
||||
// append container struct for new containers
|
||||
if !ok {
|
||||
// create collector
|
||||
collector := metrics.NewDocker(cm.client, id)
|
||||
collector := collector.NewDocker(cm.client, id)
|
||||
// create manager
|
||||
manager := manager.NewDocker(cm.client, id)
|
||||
// create container
|
||||
c = NewContainer(id, collector)
|
||||
c = container.New(id, collector, manager)
|
||||
cm.lock.Lock()
|
||||
cm.containers[id] = c
|
||||
cm.lock.Unlock()
|
||||
@@ -123,7 +147,7 @@ func (cm *DockerContainerSource) MustGet(id string) *Container {
|
||||
}
|
||||
|
||||
// Get a single container, by ID
|
||||
func (cm *DockerContainerSource) Get(id string) (*Container, bool) {
|
||||
func (cm *Docker) Get(id string) (*container.Container, bool) {
|
||||
cm.lock.Lock()
|
||||
c, ok := cm.containers[id]
|
||||
cm.lock.Unlock()
|
||||
@@ -131,7 +155,7 @@ func (cm *DockerContainerSource) Get(id string) (*Container, bool) {
|
||||
}
|
||||
|
||||
// Remove containers by ID
|
||||
func (cm *DockerContainerSource) delByID(id string) {
|
||||
func (cm *Docker) delByID(id string) {
|
||||
cm.lock.Lock()
|
||||
delete(cm.containers, id)
|
||||
cm.lock.Unlock()
|
||||
@@ -139,14 +163,15 @@ func (cm *DockerContainerSource) delByID(id string) {
|
||||
}
|
||||
|
||||
// Return array of all containers, sorted by field
|
||||
func (cm *DockerContainerSource) All() (containers Containers) {
|
||||
func (cm *Docker) All() (containers container.Containers) {
|
||||
cm.lock.Lock()
|
||||
for _, c := range cm.containers {
|
||||
containers = append(containers, c)
|
||||
}
|
||||
cm.lock.Unlock()
|
||||
sort.Sort(containers)
|
||||
|
||||
containers.Sort()
|
||||
containers.Filter()
|
||||
cm.lock.Unlock()
|
||||
return containers
|
||||
}
|
||||
|
||||
35
connector/main.go
Normal file
35
connector/main.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package connector
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/bcicen/ctop/container"
|
||||
"github.com/bcicen/ctop/logging"
|
||||
)
|
||||
|
||||
var (
|
||||
log = logging.Init()
|
||||
enabled = make(map[string]func() Connector)
|
||||
)
|
||||
|
||||
// return names for all enabled connectors on the current platform
|
||||
func Enabled() (a []string) {
|
||||
for k, _ := range enabled {
|
||||
a = append(a, k)
|
||||
}
|
||||
sort.Strings(a)
|
||||
return a
|
||||
}
|
||||
|
||||
func ByName(s string) (Connector, error) {
|
||||
if cfn, ok := enabled[s]; ok {
|
||||
return cfn(), nil
|
||||
}
|
||||
return nil, fmt.Errorf("invalid connector type \"%s\"", s)
|
||||
}
|
||||
|
||||
type Connector interface {
|
||||
All() container.Containers
|
||||
Get(string) (*container.Container, bool)
|
||||
}
|
||||
44
connector/manager/docker.go
Normal file
44
connector/manager/docker.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
api "github.com/fsouza/go-dockerclient"
|
||||
)
|
||||
|
||||
type Docker struct {
|
||||
id string
|
||||
client *api.Client
|
||||
}
|
||||
|
||||
func NewDocker(client *api.Client, id string) *Docker {
|
||||
return &Docker{
|
||||
id: id,
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
func (dc *Docker) Start() error {
|
||||
c, err := dc.client.InspectContainer(dc.id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot inspect container: %v", err)
|
||||
}
|
||||
|
||||
if err := dc.client.StartContainer(c.ID, c.HostConfig); err != nil {
|
||||
return fmt.Errorf("cannot start container: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dc *Docker) Stop() error {
|
||||
if err := dc.client.StopContainer(dc.id, 3); err != nil {
|
||||
return fmt.Errorf("cannot stop container: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dc *Docker) Remove() error {
|
||||
if err := dc.client.RemoveContainer(api.RemoveContainerOptions{ID: dc.id}); err != nil {
|
||||
return fmt.Errorf("cannot remove container: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
7
connector/manager/main.go
Normal file
7
connector/manager/main.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package manager
|
||||
|
||||
type Manager interface {
|
||||
Start() error
|
||||
Stop() error
|
||||
Remove() error
|
||||
}
|
||||
19
connector/manager/mock.go
Normal file
19
connector/manager/mock.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package manager
|
||||
|
||||
type Mock struct{}
|
||||
|
||||
func NewMock() *Mock {
|
||||
return &Mock{}
|
||||
}
|
||||
|
||||
func (m *Mock) Start() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Mock) Stop() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Mock) Remove() error {
|
||||
return nil
|
||||
}
|
||||
19
connector/manager/runc.go
Normal file
19
connector/manager/runc.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package manager
|
||||
|
||||
type Runc struct{}
|
||||
|
||||
func NewRunc() *Runc {
|
||||
return &Runc{}
|
||||
}
|
||||
|
||||
func (rc *Runc) Start() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rc *Runc) Stop() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rc *Runc) Remove() error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,31 +1,34 @@
|
||||
// +build !release
|
||||
|
||||
package main
|
||||
package connector
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bcicen/ctop/metrics"
|
||||
"github.com/bcicen/ctop/connector/collector"
|
||||
"github.com/bcicen/ctop/connector/manager"
|
||||
"github.com/bcicen/ctop/container"
|
||||
"github.com/jgautheron/codename-generator"
|
||||
"github.com/nu7hatch/gouuid"
|
||||
)
|
||||
|
||||
type MockContainerSource struct {
|
||||
containers Containers
|
||||
func init() { enabled["mock"] = NewMock }
|
||||
|
||||
type Mock struct {
|
||||
containers container.Containers
|
||||
}
|
||||
|
||||
func NewMockContainerSource() *MockContainerSource {
|
||||
cs := &MockContainerSource{}
|
||||
func NewMock() Connector {
|
||||
cs := &Mock{}
|
||||
go cs.Init()
|
||||
go cs.Loop()
|
||||
return cs
|
||||
}
|
||||
|
||||
// Create Mock containers
|
||||
func (cs *MockContainerSource) Init() {
|
||||
func (cs *Mock) Init() {
|
||||
rand.Seed(int64(time.Now().Nanosecond()))
|
||||
|
||||
for i := 0; i < 4; i++ {
|
||||
@@ -38,15 +41,16 @@ func (cs *MockContainerSource) Init() {
|
||||
|
||||
}
|
||||
|
||||
func (cs *MockContainerSource) makeContainer(aggression int64) {
|
||||
collector := metrics.NewMock(aggression)
|
||||
c := NewContainer(makeID(), collector)
|
||||
func (cs *Mock) makeContainer(aggression int64) {
|
||||
collector := collector.NewMock(aggression)
|
||||
manager := manager.NewMock()
|
||||
c := container.New(makeID(), collector, manager)
|
||||
c.SetMeta("name", makeName())
|
||||
c.SetState(makeState())
|
||||
cs.containers = append(cs.containers, c)
|
||||
}
|
||||
|
||||
func (cs *MockContainerSource) Loop() {
|
||||
func (cs *Mock) Loop() {
|
||||
iter := 0
|
||||
for {
|
||||
// Change state for random container
|
||||
@@ -60,7 +64,7 @@ func (cs *MockContainerSource) Loop() {
|
||||
}
|
||||
|
||||
// Get a single container, by ID
|
||||
func (cs *MockContainerSource) Get(id string) (*Container, bool) {
|
||||
func (cs *Mock) Get(id string) (*container.Container, bool) {
|
||||
for _, c := range cs.containers {
|
||||
if c.Id == id {
|
||||
return c, true
|
||||
@@ -70,14 +74,14 @@ func (cs *MockContainerSource) Get(id string) (*Container, bool) {
|
||||
}
|
||||
|
||||
// Return array of all containers, sorted by field
|
||||
func (cs *MockContainerSource) All() Containers {
|
||||
sort.Sort(cs.containers)
|
||||
func (cs *Mock) All() container.Containers {
|
||||
cs.containers.Sort()
|
||||
cs.containers.Filter()
|
||||
return cs.containers
|
||||
}
|
||||
|
||||
// Remove containers by ID
|
||||
func (cs *MockContainerSource) delByID(id string) {
|
||||
func (cs *Mock) delByID(id string) {
|
||||
for n, c := range cs.containers {
|
||||
if c.Id == id {
|
||||
cs.del(n)
|
||||
@@ -87,7 +91,7 @@ func (cs *MockContainerSource) delByID(id string) {
|
||||
}
|
||||
|
||||
// Remove one or more containers by index
|
||||
func (cs *MockContainerSource) del(idx ...int) {
|
||||
func (cs *Mock) del(idx ...int) {
|
||||
for _, i := range idx {
|
||||
cs.containers = append(cs.containers[:i], cs.containers[i+1:]...)
|
||||
}
|
||||
247
connector/runc.go
Normal file
247
connector/runc.go
Normal file
@@ -0,0 +1,247 @@
|
||||
// +build linux
|
||||
|
||||
package connector
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bcicen/ctop/connector/collector"
|
||||
"github.com/bcicen/ctop/connector/manager"
|
||||
"github.com/bcicen/ctop/container"
|
||||
"github.com/opencontainers/runc/libcontainer"
|
||||
"github.com/opencontainers/runc/libcontainer/cgroups/systemd"
|
||||
)
|
||||
|
||||
func init() { enabled["runc"] = NewRunc }
|
||||
|
||||
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
|
||||
manager := manager.NewRunc()
|
||||
c = container.New(id, collector, manager)
|
||||
|
||||
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,31 +1,40 @@
|
||||
package main
|
||||
package container
|
||||
|
||||
import (
|
||||
"github.com/bcicen/ctop/connector/collector"
|
||||
"github.com/bcicen/ctop/connector/manager"
|
||||
"github.com/bcicen/ctop/cwidgets"
|
||||
"github.com/bcicen/ctop/cwidgets/compact"
|
||||
"github.com/bcicen/ctop/metrics"
|
||||
"github.com/bcicen/ctop/logging"
|
||||
"github.com/bcicen/ctop/models"
|
||||
)
|
||||
|
||||
var (
|
||||
log = logging.Init()
|
||||
)
|
||||
|
||||
// Metrics and metadata representing a container
|
||||
type Container struct {
|
||||
metrics.Metrics
|
||||
models.Metrics
|
||||
Id string
|
||||
Meta map[string]string
|
||||
Widgets *compact.Compact
|
||||
Display bool // display this container in compact view
|
||||
updater cwidgets.WidgetUpdater
|
||||
collector metrics.Collector
|
||||
display bool // display this container in compact view
|
||||
collector collector.Collector
|
||||
manager manager.Manager
|
||||
}
|
||||
|
||||
func NewContainer(id string, collector metrics.Collector) *Container {
|
||||
func New(id string, collector collector.Collector, manager manager.Manager) *Container {
|
||||
widgets := compact.NewCompact(id)
|
||||
return &Container{
|
||||
Metrics: metrics.NewMetrics(),
|
||||
Metrics: models.NewMetrics(),
|
||||
Id: id,
|
||||
Meta: make(map[string]string),
|
||||
Widgets: widgets,
|
||||
updater: widgets,
|
||||
collector: collector,
|
||||
manager: manager,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,16 +70,50 @@ func (c *Container) SetState(s string) {
|
||||
}
|
||||
}
|
||||
|
||||
// Return container log collector
|
||||
func (c *Container) Logs() collector.LogCollector {
|
||||
return c.collector.Logs()
|
||||
}
|
||||
|
||||
// Read metric stream, updating widgets
|
||||
func (c *Container) Read(stream chan metrics.Metrics) {
|
||||
func (c *Container) Read(stream chan models.Metrics) {
|
||||
go func() {
|
||||
for metrics := range stream {
|
||||
c.Metrics = metrics
|
||||
c.updater.SetMetrics(metrics)
|
||||
}
|
||||
log.Infof("reader stopped for container: %s", c.Id)
|
||||
c.Metrics = metrics.NewMetrics()
|
||||
c.Metrics = models.NewMetrics()
|
||||
c.Widgets.Reset()
|
||||
}()
|
||||
log.Infof("reader started for container: %s", c.Id)
|
||||
}
|
||||
|
||||
func (c *Container) Start() {
|
||||
if c.Meta["state"] != "running" {
|
||||
if err := c.manager.Start(); err != nil {
|
||||
log.Warningf("container %s: %v", c.Id, err)
|
||||
log.StatusErr(err)
|
||||
return
|
||||
}
|
||||
c.SetState("running")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Container) Stop() {
|
||||
if c.Meta["state"] == "running" {
|
||||
if err := c.manager.Stop(); err != nil {
|
||||
log.Warningf("container %s: %v", c.Id, err)
|
||||
log.StatusErr(err)
|
||||
return
|
||||
}
|
||||
c.SetState("exited")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Container) Remove() {
|
||||
if err := c.manager.Remove(); err != nil {
|
||||
log.Warningf("container %s: %v", c.Id, err)
|
||||
log.StatusErr(err)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
package main
|
||||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
|
||||
"github.com/bcicen/ctop/config"
|
||||
)
|
||||
@@ -89,6 +90,7 @@ func SortFields() (fields []string) {
|
||||
|
||||
type Containers []*Container
|
||||
|
||||
func (a Containers) Sort() { sort.Sort(a) }
|
||||
func (a Containers) Len() int { return len(a) }
|
||||
func (a Containers) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a Containers) Less(i, j int) bool {
|
||||
@@ -104,14 +106,14 @@ func (a Containers) Filter() {
|
||||
re := regexp.MustCompile(fmt.Sprintf(".*%s", filter))
|
||||
|
||||
for _, c := range a {
|
||||
c.display = true
|
||||
c.Display = true
|
||||
// Apply name filter
|
||||
if re.FindAllString(c.GetMeta("name"), 1) == nil {
|
||||
c.display = false
|
||||
c.Display = false
|
||||
}
|
||||
// Apply state filter
|
||||
if !config.GetSwitchVal("allContainers") && c.GetMeta("state") != "running" {
|
||||
c.display = false
|
||||
c.Display = false
|
||||
}
|
||||
}
|
||||
}
|
||||
82
cursor.go
82
cursor.go
@@ -1,24 +1,23 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/bcicen/ctop/connector"
|
||||
"github.com/bcicen/ctop/container"
|
||||
ui "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
type GridCursor struct {
|
||||
selectedID string // id of currently selected container
|
||||
filtered Containers
|
||||
cSource ContainerSource
|
||||
}
|
||||
|
||||
func NewGridCursor() *GridCursor {
|
||||
return &GridCursor{
|
||||
cSource: NewDockerContainerSource(),
|
||||
}
|
||||
filtered container.Containers
|
||||
cSource connector.Connector
|
||||
isScrolling bool // toggled when actively scrolling
|
||||
}
|
||||
|
||||
func (gc *GridCursor) Len() int { return len(gc.filtered) }
|
||||
|
||||
func (gc *GridCursor) Selected() *Container {
|
||||
func (gc *GridCursor) Selected() *container.Container {
|
||||
idx := gc.Idx()
|
||||
if idx < gc.Len() {
|
||||
return gc.filtered[idx]
|
||||
@@ -31,10 +30,10 @@ func (gc *GridCursor) RefreshContainers() (lenChanged bool) {
|
||||
oldLen := gc.Len()
|
||||
|
||||
// Containers filtered by display bool
|
||||
gc.filtered = Containers{}
|
||||
gc.filtered = container.Containers{}
|
||||
var cursorVisible bool
|
||||
for _, c := range gc.cSource.All() {
|
||||
if c.display {
|
||||
if c.Display {
|
||||
if c.Id == gc.selectedID {
|
||||
cursorVisible = true
|
||||
}
|
||||
@@ -100,6 +99,9 @@ func (gc *GridCursor) ScrollPage() {
|
||||
}
|
||||
|
||||
func (gc *GridCursor) Up() {
|
||||
gc.isScrolling = true
|
||||
defer func() { gc.isScrolling = false }()
|
||||
|
||||
idx := gc.Idx()
|
||||
if idx <= 0 { // already at top
|
||||
return
|
||||
@@ -116,6 +118,9 @@ func (gc *GridCursor) Up() {
|
||||
}
|
||||
|
||||
func (gc *GridCursor) Down() {
|
||||
gc.isScrolling = true
|
||||
defer func() { gc.isScrolling = false }()
|
||||
|
||||
idx := gc.Idx()
|
||||
if idx >= gc.Len()-1 { // already at bottom
|
||||
return
|
||||
@@ -130,3 +135,58 @@ func (gc *GridCursor) Down() {
|
||||
gc.ScrollPage()
|
||||
ui.Render(cGrid)
|
||||
}
|
||||
|
||||
func (gc *GridCursor) PgUp() {
|
||||
idx := gc.Idx()
|
||||
if idx <= 0 { // already at top
|
||||
return
|
||||
}
|
||||
|
||||
nextidx := int(math.Max(0.0, float64(idx-cGrid.MaxRows())))
|
||||
if gc.pgCount() > 0 {
|
||||
cGrid.Offset = int(math.Max(float64(cGrid.Offset-cGrid.MaxRows()),
|
||||
float64(0)))
|
||||
}
|
||||
|
||||
active := gc.filtered[idx]
|
||||
next := gc.filtered[nextidx]
|
||||
|
||||
active.Widgets.Name.UnHighlight()
|
||||
gc.selectedID = next.Id
|
||||
next.Widgets.Name.Highlight()
|
||||
|
||||
cGrid.Align()
|
||||
ui.Render(cGrid)
|
||||
}
|
||||
|
||||
func (gc *GridCursor) PgDown() {
|
||||
idx := gc.Idx()
|
||||
if idx >= gc.Len()-1 { // already at bottom
|
||||
return
|
||||
}
|
||||
|
||||
nextidx := int(math.Min(float64(gc.Len()-1), float64(idx+cGrid.MaxRows())))
|
||||
if gc.pgCount() > 0 {
|
||||
cGrid.Offset = int(math.Min(float64(cGrid.Offset+cGrid.MaxRows()),
|
||||
float64(gc.Len()-cGrid.MaxRows())))
|
||||
}
|
||||
|
||||
active := gc.filtered[idx]
|
||||
next := gc.filtered[nextidx]
|
||||
|
||||
active.Widgets.Name.UnHighlight()
|
||||
gc.selectedID = next.Id
|
||||
next.Widgets.Name.Highlight()
|
||||
|
||||
cGrid.Align()
|
||||
ui.Render(cGrid)
|
||||
}
|
||||
|
||||
// number of pages at current row count and term height
|
||||
func (gc *GridCursor) pgCount() int {
|
||||
pages := gc.Len() / cGrid.MaxRows()
|
||||
if gc.Len()%cGrid.MaxRows() > 0 {
|
||||
pages++
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
@@ -25,10 +25,10 @@ func (w *GaugeCol) Reset() {
|
||||
|
||||
func colorScale(n int) ui.Attribute {
|
||||
if n > 70 {
|
||||
return ui.ColorRed
|
||||
return ui.ThemeAttr("status.danger")
|
||||
}
|
||||
if n > 30 {
|
||||
return ui.ColorYellow
|
||||
return ui.ThemeAttr("status.warn")
|
||||
}
|
||||
return ui.ColorGreen
|
||||
return ui.ThemeAttr("status.ok")
|
||||
}
|
||||
|
||||
@@ -22,9 +22,11 @@ func NewCompactGrid() *CompactGrid {
|
||||
|
||||
func (cg *CompactGrid) Align() {
|
||||
y := cg.Y
|
||||
if cg.Offset >= len(cg.Rows) {
|
||||
|
||||
if cg.Offset >= len(cg.Rows) || cg.Offset < 0 {
|
||||
cg.Offset = 0
|
||||
}
|
||||
|
||||
// update row ypos, width recursively
|
||||
for _, r := range cg.pageRows() {
|
||||
r.SetY(y)
|
||||
|
||||
@@ -2,7 +2,7 @@ package compact
|
||||
|
||||
import (
|
||||
"github.com/bcicen/ctop/logging"
|
||||
"github.com/bcicen/ctop/metrics"
|
||||
"github.com/bcicen/ctop/models"
|
||||
ui "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
@@ -13,7 +13,7 @@ type Compact struct {
|
||||
Name *TextCol
|
||||
Cid *TextCol
|
||||
Cpu *GaugeCol
|
||||
Memory *GaugeCol
|
||||
Mem *GaugeCol
|
||||
Net *TextCol
|
||||
IO *TextCol
|
||||
Pids *TextCol
|
||||
@@ -32,7 +32,7 @@ func NewCompact(id string) *Compact {
|
||||
Name: NewTextCol("-"),
|
||||
Cid: NewTextCol(id),
|
||||
Cpu: NewGaugeCol(),
|
||||
Memory: NewGaugeCol(),
|
||||
Mem: NewGaugeCol(),
|
||||
Net: NewTextCol("-"),
|
||||
IO: NewTextCol("-"),
|
||||
Pids: NewTextCol("-"),
|
||||
@@ -56,10 +56,12 @@ func (row *Compact) SetMeta(k, v string) {
|
||||
row.Name.Set(v)
|
||||
case "state":
|
||||
row.Status.Set(v)
|
||||
case "health":
|
||||
row.Status.SetHealth(v)
|
||||
}
|
||||
}
|
||||
|
||||
func (row *Compact) SetMetrics(m metrics.Metrics) {
|
||||
func (row *Compact) SetMetrics(m models.Metrics) {
|
||||
row.SetCPU(m.CPUUtil)
|
||||
row.SetNet(m.NetRx, m.NetTx)
|
||||
row.SetMem(m.MemUsage, m.MemLimit, m.MemPercent)
|
||||
@@ -70,7 +72,7 @@ func (row *Compact) SetMetrics(m metrics.Metrics) {
|
||||
// Set gauges, counters to default unread values
|
||||
func (row *Compact) Reset() {
|
||||
row.Cpu.Reset()
|
||||
row.Memory.Reset()
|
||||
row.Mem.Reset()
|
||||
row.Net.Reset()
|
||||
row.IO.Reset()
|
||||
row.Pids.Reset()
|
||||
@@ -121,7 +123,7 @@ func (row *Compact) Buffer() ui.Buffer {
|
||||
buf.Merge(row.Name.Buffer())
|
||||
buf.Merge(row.Cid.Buffer())
|
||||
buf.Merge(row.Cpu.Buffer())
|
||||
buf.Merge(row.Memory.Buffer())
|
||||
buf.Merge(row.Mem.Buffer())
|
||||
buf.Merge(row.Net.Buffer())
|
||||
buf.Merge(row.IO.Buffer())
|
||||
buf.Merge(row.Pids.Buffer())
|
||||
@@ -134,7 +136,7 @@ func (row *Compact) all() []ui.GridBufferer {
|
||||
row.Name,
|
||||
row.Cid,
|
||||
row.Cpu,
|
||||
row.Memory,
|
||||
row.Mem,
|
||||
row.Net,
|
||||
row.IO,
|
||||
row.Pids,
|
||||
|
||||
@@ -37,12 +37,12 @@ func (row *Compact) SetCPU(val int) {
|
||||
}
|
||||
|
||||
func (row *Compact) SetMem(val int64, limit int64, percent int) {
|
||||
row.Memory.Label = fmt.Sprintf("%s / %s", cwidgets.ByteFormat(val), cwidgets.ByteFormat(limit))
|
||||
row.Mem.Label = fmt.Sprintf("%s / %s", cwidgets.ByteFormat(val), cwidgets.ByteFormat(limit))
|
||||
if percent < 5 {
|
||||
percent = 5
|
||||
row.Memory.BarColor = ui.ColorBlack
|
||||
row.Mem.BarColor = ui.ColorBlack
|
||||
} else {
|
||||
row.Memory.BarColor = ui.ThemeAttr("gauge.bar.bg")
|
||||
row.Mem.BarColor = ui.ThemeAttr("gauge.bar.bg")
|
||||
}
|
||||
row.Memory.Percent = percent
|
||||
row.Mem.Percent = percent
|
||||
}
|
||||
|
||||
@@ -1,28 +1,42 @@
|
||||
package compact
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
ui "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
const (
|
||||
mark = string('\u25C9')
|
||||
vBar = string('\u25AE')
|
||||
statusWidth = 3
|
||||
healthMark = string('\u207A')
|
||||
vBar = string('\u25AE') + string('\u25AE')
|
||||
)
|
||||
|
||||
// Status indicator
|
||||
type Status struct {
|
||||
*ui.Par
|
||||
*ui.Block
|
||||
status []ui.Cell
|
||||
health []ui.Cell
|
||||
}
|
||||
|
||||
func NewStatus() *Status {
|
||||
p := ui.NewPar(mark)
|
||||
p.Border = false
|
||||
p.Height = 1
|
||||
p.Width = statusWidth
|
||||
return &Status{p}
|
||||
s := &Status{Block: ui.NewBlock()}
|
||||
s.Height = 1
|
||||
s.Border = false
|
||||
s.Set("")
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Status) Buffer() ui.Buffer {
|
||||
buf := s.Block.Buffer()
|
||||
x := 0
|
||||
for _, c := range s.status {
|
||||
buf.Set(s.InnerX()+x, s.InnerY(), c)
|
||||
x += c.Width()
|
||||
}
|
||||
for _, c := range s.health {
|
||||
buf.Set(s.InnerX()+x, s.InnerY(), c)
|
||||
x += c.Width()
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
func (s *Status) Set(val string) {
|
||||
@@ -32,13 +46,38 @@ func (s *Status) Set(val string) {
|
||||
|
||||
switch val {
|
||||
case "running":
|
||||
color = ui.ColorGreen
|
||||
color = ui.ThemeAttr("status.ok")
|
||||
case "exited":
|
||||
color = ui.ColorRed
|
||||
color = ui.ThemeAttr("status.danger")
|
||||
case "paused":
|
||||
text = fmt.Sprintf("%s%s", vBar, vBar)
|
||||
text = vBar
|
||||
}
|
||||
|
||||
s.Text = text
|
||||
s.TextFgColor = color
|
||||
var cells []ui.Cell
|
||||
for _, ch := range text {
|
||||
cells = append(cells, ui.Cell{Ch: ch, Fg: color})
|
||||
}
|
||||
s.status = cells
|
||||
}
|
||||
|
||||
func (s *Status) SetHealth(val string) {
|
||||
if val == "" {
|
||||
return
|
||||
}
|
||||
color := ui.ColorDefault
|
||||
|
||||
switch val {
|
||||
case "healthy":
|
||||
color = ui.ThemeAttr("status.ok")
|
||||
case "unhealthy":
|
||||
color = ui.ThemeAttr("status.danger")
|
||||
case "starting":
|
||||
color = ui.ThemeAttr("status.warn")
|
||||
}
|
||||
|
||||
var cells []ui.Cell
|
||||
for _, ch := range healthMark {
|
||||
cells = append(cells, ui.Cell{Ch: ch, Fg: color})
|
||||
}
|
||||
s.health = cells
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
package expanded
|
||||
|
||||
import (
|
||||
ui "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
var displayInfo = []string{"id", "name", "image", "state"}
|
||||
|
||||
type Info struct {
|
||||
*ui.Table
|
||||
data map[string]string
|
||||
}
|
||||
|
||||
func NewInfo(id string) *Info {
|
||||
p := ui.NewTable()
|
||||
p.Height = 4
|
||||
p.Width = colWidth[0]
|
||||
p.FgColor = ui.ThemeAttr("par.text.fg")
|
||||
p.Separator = false
|
||||
i := &Info{p, make(map[string]string)}
|
||||
i.Set("id", id)
|
||||
return i
|
||||
}
|
||||
|
||||
func (w *Info) Set(k, v string) {
|
||||
w.data[k] = v
|
||||
// rebuild rows
|
||||
w.Rows = [][]string{}
|
||||
for _, k := range displayInfo {
|
||||
if v, ok := w.data[k]; ok {
|
||||
w.Rows = append(w.Rows, []string{k, v})
|
||||
}
|
||||
}
|
||||
w.Height = len(w.Rows) + 2
|
||||
}
|
||||
@@ -2,12 +2,12 @@ package cwidgets
|
||||
|
||||
import (
|
||||
"github.com/bcicen/ctop/logging"
|
||||
"github.com/bcicen/ctop/metrics"
|
||||
"github.com/bcicen/ctop/models"
|
||||
)
|
||||
|
||||
var log = logging.Init()
|
||||
|
||||
type WidgetUpdater interface {
|
||||
SetMeta(string, string)
|
||||
SetMetrics(metrics.Metrics)
|
||||
SetMetrics(models.Metrics)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package expanded
|
||||
package single
|
||||
|
||||
import (
|
||||
ui "github.com/gizak/termui"
|
||||
@@ -1,4 +1,4 @@
|
||||
package expanded
|
||||
package single
|
||||
|
||||
type IntHist struct {
|
||||
Val int // most current data point
|
||||
58
cwidgets/single/info.go
Normal file
58
cwidgets/single/info.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package single
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
ui "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
var displayInfo = []string{"id", "name", "image", "ports", "state", "created", "health"}
|
||||
|
||||
type Info struct {
|
||||
*ui.Table
|
||||
data map[string]string
|
||||
}
|
||||
|
||||
func NewInfo(id string) *Info {
|
||||
p := ui.NewTable()
|
||||
p.Height = 4
|
||||
p.Width = colWidth[0]
|
||||
p.FgColor = ui.ThemeAttr("par.text.fg")
|
||||
p.Separator = false
|
||||
i := &Info{p, make(map[string]string)}
|
||||
i.Set("id", id)
|
||||
return i
|
||||
}
|
||||
|
||||
func (w *Info) Set(k, v string) {
|
||||
w.data[k] = v
|
||||
|
||||
// rebuild rows
|
||||
w.Rows = [][]string{}
|
||||
for _, k := range displayInfo {
|
||||
if v, ok := w.data[k]; ok {
|
||||
w.Rows = append(w.Rows, mkInfoRows(k, v)...)
|
||||
}
|
||||
}
|
||||
|
||||
w.Height = len(w.Rows) + 2
|
||||
}
|
||||
|
||||
// Build row(s) from a key and value string
|
||||
func mkInfoRows(k, v string) (rows [][]string) {
|
||||
lines := strings.Split(v, "\n")
|
||||
|
||||
// initial row with field name
|
||||
rows = append(rows, []string{k, lines[0]})
|
||||
|
||||
// append any additional lines in seperate row
|
||||
if len(lines) > 1 {
|
||||
for _, line := range lines[1:] {
|
||||
if line != "" {
|
||||
rows = append(rows, []string{"", line})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rows
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package expanded
|
||||
package single
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
83
cwidgets/single/logs.go
Normal file
83
cwidgets/single/logs.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package single
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/bcicen/ctop/models"
|
||||
ui "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
type LogLines struct {
|
||||
ts []time.Time
|
||||
data []string
|
||||
}
|
||||
|
||||
func NewLogLines(max int) *LogLines {
|
||||
ll := &LogLines{
|
||||
ts: make([]time.Time, max),
|
||||
data: make([]string, max),
|
||||
}
|
||||
return ll
|
||||
}
|
||||
|
||||
func (ll *LogLines) tail(n int) []string {
|
||||
lines := make([]string, n)
|
||||
for i := 0; i < n; i++ {
|
||||
lines = append(lines, ll.data[len(ll.data)-i])
|
||||
}
|
||||
return lines
|
||||
}
|
||||
func (ll *LogLines) getLines(start, end int) []string {
|
||||
if end < 0 {
|
||||
return ll.data[start:]
|
||||
}
|
||||
return ll.data[start:end]
|
||||
}
|
||||
|
||||
func (ll *LogLines) add(l models.Log) {
|
||||
if len(ll.data) == cap(ll.data) {
|
||||
ll.data = append(ll.data[:0], ll.data[1:]...)
|
||||
ll.ts = append(ll.ts[:0], ll.ts[1:]...)
|
||||
}
|
||||
ll.ts = append(ll.ts, l.Timestamp)
|
||||
ll.data = append(ll.data, l.Message)
|
||||
log.Debugf("recorded log line: %v", l)
|
||||
}
|
||||
|
||||
type Logs struct {
|
||||
*ui.List
|
||||
lines *LogLines
|
||||
}
|
||||
|
||||
func NewLogs(stream chan models.Log) *Logs {
|
||||
p := ui.NewList()
|
||||
p.Y = ui.TermHeight() / 2
|
||||
p.X = 0
|
||||
p.Height = ui.TermHeight() - p.Y
|
||||
p.Width = ui.TermWidth()
|
||||
//p.Overflow = "wrap"
|
||||
p.ItemFgColor = ui.ThemeAttr("par.text.fg")
|
||||
i := &Logs{p, NewLogLines(4098)}
|
||||
go func() {
|
||||
for line := range stream {
|
||||
i.lines.add(line)
|
||||
ui.Render(i)
|
||||
}
|
||||
}()
|
||||
return i
|
||||
}
|
||||
|
||||
func (w *Logs) Align() {
|
||||
w.X = colWidth[0]
|
||||
w.List.Align()
|
||||
}
|
||||
|
||||
func (w *Logs) Buffer() ui.Buffer {
|
||||
maxLines := w.Height - 2
|
||||
offset := len(w.lines.data) - maxLines
|
||||
w.Items = w.lines.getLines(offset, -1)
|
||||
return w.List.Buffer()
|
||||
}
|
||||
|
||||
// number of rows a line will occupy at current panel width
|
||||
func (w *Logs) lineHeight(s string) int { return (len(s) / w.InnerWidth()) + 1 }
|
||||
@@ -1,8 +1,8 @@
|
||||
package expanded
|
||||
package single
|
||||
|
||||
import (
|
||||
"github.com/bcicen/ctop/logging"
|
||||
"github.com/bcicen/ctop/metrics"
|
||||
"github.com/bcicen/ctop/models"
|
||||
ui "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
@@ -12,7 +12,7 @@ var (
|
||||
colWidth = [2]int{65, 0} // left,right column width
|
||||
)
|
||||
|
||||
type Expanded struct {
|
||||
type Single struct {
|
||||
Info *Info
|
||||
Net *Net
|
||||
Cpu *Cpu
|
||||
@@ -22,11 +22,11 @@ type Expanded struct {
|
||||
Width int
|
||||
}
|
||||
|
||||
func NewExpanded(id string) *Expanded {
|
||||
func NewSingle(id string) *Single {
|
||||
if len(id) > 12 {
|
||||
id = id[:12]
|
||||
}
|
||||
return &Expanded{
|
||||
return &Single{
|
||||
Info: NewInfo(id),
|
||||
Net: NewNet(),
|
||||
Cpu: NewCpu(),
|
||||
@@ -36,7 +36,7 @@ func NewExpanded(id string) *Expanded {
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Expanded) Up() {
|
||||
func (e *Single) Up() {
|
||||
if e.Y < 0 {
|
||||
e.Y++
|
||||
e.Align()
|
||||
@@ -44,7 +44,7 @@ func (e *Expanded) Up() {
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Expanded) Down() {
|
||||
func (e *Single) Down() {
|
||||
if e.Y > (ui.TermHeight() - e.GetHeight()) {
|
||||
e.Y--
|
||||
e.Align()
|
||||
@@ -52,10 +52,10 @@ func (e *Expanded) Down() {
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Expanded) SetWidth(w int) { e.Width = w }
|
||||
func (e *Expanded) SetMeta(k, v string) { e.Info.Set(k, v) }
|
||||
func (e *Single) SetWidth(w int) { e.Width = w }
|
||||
func (e *Single) SetMeta(k, v string) { e.Info.Set(k, v) }
|
||||
|
||||
func (e *Expanded) SetMetrics(m metrics.Metrics) {
|
||||
func (e *Single) SetMetrics(m models.Metrics) {
|
||||
e.Cpu.Update(m.CPUUtil)
|
||||
e.Net.Update(m.NetRx, m.NetTx)
|
||||
e.Mem.Update(int(m.MemUsage), int(m.MemLimit))
|
||||
@@ -63,7 +63,7 @@ func (e *Expanded) SetMetrics(m metrics.Metrics) {
|
||||
}
|
||||
|
||||
// Return total column height
|
||||
func (e *Expanded) GetHeight() (h int) {
|
||||
func (e *Single) GetHeight() (h int) {
|
||||
h += e.Info.Height
|
||||
h += e.Net.Height
|
||||
h += e.Cpu.Height
|
||||
@@ -72,7 +72,7 @@ func (e *Expanded) GetHeight() (h int) {
|
||||
return h
|
||||
}
|
||||
|
||||
func (e *Expanded) Align() {
|
||||
func (e *Single) Align() {
|
||||
// reset offset if needed
|
||||
if e.GetHeight() <= ui.TermHeight() {
|
||||
e.Y = 0
|
||||
@@ -91,10 +91,7 @@ func (e *Expanded) Align() {
|
||||
log.Debugf("align: width=%v left-col=%v right-col=%v", e.Width, colWidth[0], colWidth[1])
|
||||
}
|
||||
|
||||
func calcWidth(w int) {
|
||||
}
|
||||
|
||||
func (e *Expanded) Buffer() ui.Buffer {
|
||||
func (e *Single) Buffer() ui.Buffer {
|
||||
buf := ui.NewBuffer()
|
||||
if e.Width < (colWidth[0] + colWidth[1]) {
|
||||
ui.Clear()
|
||||
@@ -109,7 +106,7 @@ func (e *Expanded) Buffer() ui.Buffer {
|
||||
return buf
|
||||
}
|
||||
|
||||
func (e *Expanded) all() []ui.GridBufferer {
|
||||
func (e *Single) all() []ui.GridBufferer {
|
||||
return []ui.GridBufferer{
|
||||
e.Info,
|
||||
e.Cpu,
|
||||
@@ -1,4 +1,4 @@
|
||||
package expanded
|
||||
package single
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -1,4 +1,4 @@
|
||||
package expanded
|
||||
package single
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -9,6 +9,7 @@ const (
|
||||
kb = 1024
|
||||
mb = kb * 1024
|
||||
gb = mb * 1024
|
||||
tb = gb * 1024
|
||||
)
|
||||
|
||||
// convenience method
|
||||
@@ -28,8 +29,12 @@ func ByteFormat(n int64) string {
|
||||
n = n / mb
|
||||
return fmt.Sprintf("%sM", strconv.FormatInt(n, 10))
|
||||
}
|
||||
if n < tb {
|
||||
nf := float64(n) / gb
|
||||
return fmt.Sprintf("%sG", unpadFloat(nf))
|
||||
}
|
||||
nf := float64(n) / tb
|
||||
return fmt.Sprintf("%sT", unpadFloat(nf))
|
||||
}
|
||||
|
||||
func unpadFloat(f float64) string {
|
||||
|
||||
22
debug.go
22
debug.go
@@ -3,10 +3,14 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"runtime"
|
||||
|
||||
"github.com/bcicen/ctop/container"
|
||||
ui "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
var mstats = &runtime.MemStats{}
|
||||
|
||||
func logEvent(e ui.Event) {
|
||||
var s string
|
||||
s += fmt.Sprintf("Type=%s", quote(e.Type))
|
||||
@@ -18,8 +22,24 @@ func logEvent(e ui.Event) {
|
||||
log.Debugf("new event: %s", s)
|
||||
}
|
||||
|
||||
func runtimeStats() {
|
||||
var msg string
|
||||
msg += fmt.Sprintf("cgo calls=%v", runtime.NumCgoCall())
|
||||
msg += fmt.Sprintf(" routines=%v", runtime.NumGoroutine())
|
||||
runtime.ReadMemStats(mstats)
|
||||
msg += fmt.Sprintf(" numgc=%v", mstats.NumGC)
|
||||
msg += fmt.Sprintf(" alloc=%v", mstats.Alloc)
|
||||
log.Debugf("runtime: %v", msg)
|
||||
}
|
||||
|
||||
func runtimeStack() {
|
||||
buf := make([]byte, 32768)
|
||||
buf = buf[:runtime.Stack(buf, true)]
|
||||
log.Infof(fmt.Sprintf("stack:\n%v", string(buf)))
|
||||
}
|
||||
|
||||
// log container, metrics, and widget state
|
||||
func dumpContainer(c *Container) {
|
||||
func dumpContainer(c *container.Container) {
|
||||
msg := fmt.Sprintf("logging state for container: %s\n", c.Id)
|
||||
for k, v := range c.Meta {
|
||||
msg += fmt.Sprintf("Meta.%s = %s\n", k, v)
|
||||
|
||||
85
glide.lock
generated
85
glide.lock
generated
@@ -1,85 +0,0 @@
|
||||
hash: c13011881e895378f374b68596a59a0ea6def372b4f5239b2d8aa342eaa46a4b
|
||||
updated: 2017-03-12T09:53:35.212073637+07:00
|
||||
imports:
|
||||
- name: github.com/Azure/go-ansiterm
|
||||
version: fa152c58bc15761d0200cb75fe958b89a9d4888e
|
||||
subpackages:
|
||||
- winterm
|
||||
- name: github.com/docker/docker
|
||||
version: ce07fb6b0f1b8765b92022e45f96bd4349812e06
|
||||
subpackages:
|
||||
- api/types
|
||||
- api/types/blkiodev
|
||||
- api/types/container
|
||||
- api/types/filters
|
||||
- api/types/mount
|
||||
- api/types/network
|
||||
- api/types/registry
|
||||
- api/types/strslice
|
||||
- api/types/swarm
|
||||
- api/types/versions
|
||||
- opts
|
||||
- pkg/archive
|
||||
- pkg/fileutils
|
||||
- pkg/homedir
|
||||
- pkg/idtools
|
||||
- pkg/ioutils
|
||||
- pkg/jsonlog
|
||||
- pkg/jsonmessage
|
||||
- pkg/longpath
|
||||
- pkg/pools
|
||||
- pkg/promise
|
||||
- pkg/stdcopy
|
||||
- pkg/system
|
||||
- pkg/term
|
||||
- pkg/term/windows
|
||||
- name: github.com/docker/go-connections
|
||||
version: a2afab9802043837035592f1c24827fb70766de9
|
||||
subpackages:
|
||||
- nat
|
||||
- name: github.com/docker/go-units
|
||||
version: 0dadbb0345b35ec7ef35e228dabb8de89a65bf52
|
||||
- name: github.com/fsouza/go-dockerclient
|
||||
version: 318513eb1ab27495afbc67f671ba1080513d8aa0
|
||||
- name: github.com/gizak/termui
|
||||
version: ea10e6ccee219e572ffad0ac1909f1a17f6db7d6
|
||||
repo: https://github.com/bcicen/termui
|
||||
vcs: git
|
||||
- name: github.com/hashicorp/go-cleanhttp
|
||||
version: 3573b8b52aa7b37b9358d966a898feb387f62437
|
||||
- name: github.com/jgautheron/codename-generator
|
||||
version: 16d037c7cc3c9b552fe4af9828b7338d752dbaf9
|
||||
- name: github.com/maruel/panicparse
|
||||
version: 25bcac0d793cf4109483505a0d66e066a3a90a80
|
||||
subpackages:
|
||||
- stack
|
||||
- name: github.com/mattn/go-runewidth
|
||||
version: 14207d285c6c197daabb5c9793d63e7af9ab2d50
|
||||
- name: github.com/Microsoft/go-winio
|
||||
version: fff283ad5116362ca252298cfc9b95828956d85d
|
||||
- name: github.com/mitchellh/go-wordwrap
|
||||
version: ad45545899c7b13c020ea92b2072220eefad42b8
|
||||
- name: github.com/nsf/termbox-go
|
||||
version: 91bae1bb5fa9ee504905ecbe7043fa30e92feaa3
|
||||
- name: github.com/nu7hatch/gouuid
|
||||
version: 179d4d0c4d8d407a32af483c2354df1d2c91e6c3
|
||||
- name: github.com/op/go-logging
|
||||
version: b2cb9fa56473e98db8caba80237377e83fe44db5
|
||||
- name: github.com/opencontainers/runc
|
||||
version: 31980a53ae7887b2c8f8715d13c3eb486c27b6cf
|
||||
subpackages:
|
||||
- libcontainer/system
|
||||
- libcontainer/user
|
||||
- name: github.com/Sirupsen/logrus
|
||||
version: 1deb2db2a6fff8a35532079061b903c3a25eed52
|
||||
- name: golang.org/x/net
|
||||
version: a6577fac2d73be281a500b310739095313165611
|
||||
subpackages:
|
||||
- context
|
||||
- context/ctxhttp
|
||||
- name: golang.org/x/sys
|
||||
version: 99f16d856c9836c42d24e7ab64ea72916925fa97
|
||||
subpackages:
|
||||
- unix
|
||||
- windows
|
||||
testImports: []
|
||||
11
glide.yaml
11
glide.yaml
@@ -1,11 +0,0 @@
|
||||
package: github.com/bcicen/ctop
|
||||
import:
|
||||
- package: github.com/fsouza/go-dockerclient
|
||||
- package: github.com/gizak/termui
|
||||
version: barchart-numfmt
|
||||
repo: https://github.com/bcicen/termui
|
||||
vcs: git
|
||||
- package: github.com/jgautheron/codename-generator
|
||||
- package: github.com/nu7hatch/gouuid
|
||||
- package: github.com/op/go-logging
|
||||
version: ^1.0.0
|
||||
70
grid.go
70
grid.go
@@ -2,7 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"github.com/bcicen/ctop/config"
|
||||
"github.com/bcicen/ctop/cwidgets/expanded"
|
||||
"github.com/bcicen/ctop/cwidgets/single"
|
||||
ui "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
@@ -17,6 +17,7 @@ func RedrawRows(clr bool) {
|
||||
header.SetFilter(config.GetVal("filterStr"))
|
||||
y += header.Height()
|
||||
}
|
||||
|
||||
cGrid.SetY(y)
|
||||
|
||||
for _, c := range cursor.filtered {
|
||||
@@ -32,14 +33,20 @@ func RedrawRows(clr bool) {
|
||||
}
|
||||
cGrid.Align()
|
||||
ui.Render(cGrid)
|
||||
|
||||
}
|
||||
|
||||
func ExpandView(c *Container) {
|
||||
func SingleView() MenuFn {
|
||||
c := cursor.Selected()
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ui.Clear()
|
||||
ui.DefaultEvtStream.ResetHandlers()
|
||||
defer ui.DefaultEvtStream.ResetHandlers()
|
||||
|
||||
ex := expanded.NewExpanded(c.Id)
|
||||
ex := single.NewSingle(c.Id)
|
||||
c.SetUpdater(ex)
|
||||
|
||||
ex.Align()
|
||||
@@ -58,27 +65,35 @@ func ExpandView(c *Container) {
|
||||
|
||||
ui.Loop()
|
||||
c.SetUpdater(c.Widgets)
|
||||
return nil
|
||||
}
|
||||
|
||||
func RefreshDisplay() {
|
||||
// skip display refresh during scroll
|
||||
if !cursor.isScrolling {
|
||||
needsClear := cursor.RefreshContainers()
|
||||
RedrawRows(needsClear)
|
||||
}
|
||||
}
|
||||
|
||||
func Display() bool {
|
||||
var menu func()
|
||||
var expand bool
|
||||
var menu MenuFn
|
||||
|
||||
cGrid.SetWidth(ui.TermWidth())
|
||||
ui.DefaultEvtStream.Hook(logEvent)
|
||||
|
||||
// initial draw
|
||||
header.Align()
|
||||
status.Align()
|
||||
cursor.RefreshContainers()
|
||||
RedrawRows(true)
|
||||
|
||||
HandleKeys("up", cursor.Up)
|
||||
HandleKeys("down", cursor.Down)
|
||||
|
||||
HandleKeys("pgup", cursor.PgUp)
|
||||
HandleKeys("pgdown", cursor.PgDown)
|
||||
|
||||
HandleKeys("exit", ui.StopLoop)
|
||||
HandleKeys("help", func() {
|
||||
menu = HelpMenu
|
||||
@@ -86,7 +101,15 @@ func Display() bool {
|
||||
})
|
||||
|
||||
ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
|
||||
expand = true
|
||||
menu = ContainerMenu
|
||||
ui.StopLoop()
|
||||
})
|
||||
ui.Handle("/sys/kbd/l", func(ui.Event) {
|
||||
menu = LogMenu
|
||||
ui.StopLoop()
|
||||
})
|
||||
ui.Handle("/sys/kbd/o", func(ui.Event) {
|
||||
menu = SingleView
|
||||
ui.StopLoop()
|
||||
})
|
||||
ui.Handle("/sys/kbd/a", func(ui.Event) {
|
||||
@@ -111,13 +134,26 @@ func Display() bool {
|
||||
menu = SortMenu
|
||||
ui.StopLoop()
|
||||
})
|
||||
ui.Handle("/sys/kbd/S", func(ui.Event) {
|
||||
path, err := config.Write()
|
||||
if err == nil {
|
||||
log.Statusf("wrote config to %s", path)
|
||||
} else {
|
||||
log.StatusErr(err)
|
||||
}
|
||||
ui.StopLoop()
|
||||
})
|
||||
|
||||
ui.Handle("/timer/1s", func(e ui.Event) {
|
||||
if log.StatusQueued() {
|
||||
ui.StopLoop()
|
||||
}
|
||||
RefreshDisplay()
|
||||
})
|
||||
|
||||
ui.Handle("/sys/wnd/resize", func(e ui.Event) {
|
||||
header.Align()
|
||||
status.Align()
|
||||
cursor.ScrollPage()
|
||||
cGrid.SetWidth(ui.TermWidth())
|
||||
log.Infof("resize: width=%v max-rows=%v", cGrid.Width, cGrid.MaxRows())
|
||||
@@ -125,16 +161,24 @@ func Display() bool {
|
||||
})
|
||||
|
||||
ui.Loop()
|
||||
|
||||
if log.StatusQueued() {
|
||||
for sm := range log.FlushStatus() {
|
||||
if sm.IsError {
|
||||
status.ShowErr(sm.Text)
|
||||
} else {
|
||||
status.Show(sm.Text)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if menu != nil {
|
||||
menu()
|
||||
return false
|
||||
}
|
||||
if expand {
|
||||
c := cursor.Selected()
|
||||
if c != nil {
|
||||
ExpandView(c)
|
||||
for menu != nil {
|
||||
menu = menu()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
60
install.sh
Executable file
60
install.sh
Executable file
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env bash
|
||||
# a simple install script for ctop
|
||||
|
||||
KERNEL=$(uname -s)
|
||||
|
||||
function output() { echo -e "\033[32mctop-install\033[0m $@"; }
|
||||
|
||||
# extract github download url matching pattern
|
||||
function extract_url() {
|
||||
match=$1; shift
|
||||
echo "$@" | while read line; do
|
||||
case $line in
|
||||
*browser_download_url*${match}*)
|
||||
url=$(echo $line | sed -e 's/^.*"browser_download_url":[ ]*"//' -e 's/".*//;s/\ //g')
|
||||
echo $url
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
case $KERNEL in
|
||||
Linux) MATCH_BUILD="linux-amd64" ;;
|
||||
Darwin) MATCH_BUILD="darwin-amd64" ;;
|
||||
*)
|
||||
echo "platform not supported by this install script"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
TMP=$(mktemp -d "${TMPDIR:-/tmp}/ctop.XXXXX")
|
||||
cd ${TMP}
|
||||
|
||||
output "fetching latest release info"
|
||||
resp=$(curl -s https://api.github.com/repos/bcicen/ctop/releases/latest)
|
||||
|
||||
output "fetching release checksums"
|
||||
checksum_url=$(extract_url sha256sums.txt "$resp")
|
||||
wget -q $checksum_url -O sha256sums.txt
|
||||
|
||||
# skip if latest already installed
|
||||
cur_ctop=$(which ctop 2> /dev/null)
|
||||
if [[ -n "$cur_ctop" ]]; then
|
||||
cur_sum=$(sha256sum $cur_ctop | sed 's/ .*//')
|
||||
(grep -q $cur_sum sha256sums.txt) && {
|
||||
output "already up-to-date"
|
||||
exit 0
|
||||
}
|
||||
fi
|
||||
|
||||
output "fetching latest ctop"
|
||||
url=$(extract_url $MATCH_BUILD "$resp")
|
||||
wget -q --show-progress $url
|
||||
(sha256sum -c --quiet --ignore-missing sha256sums.txt) || exit 1
|
||||
|
||||
output "installing to /usr/local/bin"
|
||||
chmod +x ctop-*
|
||||
sudo mv ctop-* /usr/local/bin/ctop
|
||||
|
||||
output "done!"
|
||||
9
keys.go
9
keys.go
@@ -14,9 +14,18 @@ var keyMap = map[string][]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",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
@@ -20,11 +21,36 @@ var (
|
||||
)
|
||||
)
|
||||
|
||||
type statusMsg struct {
|
||||
Text string
|
||||
IsError bool
|
||||
}
|
||||
|
||||
type CTopLogger struct {
|
||||
*logging.Logger
|
||||
backend *logging.MemoryBackend
|
||||
sLog []statusMsg
|
||||
}
|
||||
|
||||
func (c *CTopLogger) FlushStatus() chan statusMsg {
|
||||
ch := make(chan statusMsg)
|
||||
go func() {
|
||||
for _, sm := range c.sLog {
|
||||
ch <- sm
|
||||
}
|
||||
close(ch)
|
||||
c.sLog = []statusMsg{}
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
func (c *CTopLogger) StatusQueued() bool { return len(c.sLog) > 0 }
|
||||
func (c *CTopLogger) Status(s string) { c.addStatus(statusMsg{s, false}) }
|
||||
func (c *CTopLogger) StatusErr(err error) { c.addStatus(statusMsg{err.Error(), true}) }
|
||||
func (c *CTopLogger) addStatus(sm statusMsg) { c.sLog = append(c.sLog, sm) }
|
||||
|
||||
func (c *CTopLogger) Statusf(s string, a ...interface{}) { c.Status(fmt.Sprintf(s, a...)) }
|
||||
|
||||
func Init() *CTopLogger {
|
||||
if Log == nil {
|
||||
logging.SetFormatter(format) // setup default formatter
|
||||
@@ -32,6 +58,7 @@ func Init() *CTopLogger {
|
||||
Log = &CTopLogger{
|
||||
logging.MustGetLogger("ctop"),
|
||||
logging.NewMemoryBackend(size),
|
||||
[]statusMsg{},
|
||||
}
|
||||
|
||||
if debugMode() {
|
||||
@@ -79,3 +106,4 @@ func (log *CTopLogger) Exit() {
|
||||
}
|
||||
|
||||
func debugMode() bool { return os.Getenv("CTOP_DEBUG") == "1" }
|
||||
func debugModeTCP() bool { return os.Getenv("CTOP_DEBUG_TCP") == "1" }
|
||||
|
||||
@@ -7,7 +7,8 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
path = "/tmp/ctop.sock"
|
||||
socketPath = "./ctop.sock"
|
||||
socketAddr = "0.0.0.0:9000"
|
||||
)
|
||||
|
||||
var server struct {
|
||||
@@ -16,7 +17,13 @@ var server struct {
|
||||
}
|
||||
|
||||
func getListener() net.Listener {
|
||||
ln, err := net.Listen("unix", path)
|
||||
var ln net.Listener
|
||||
var err error
|
||||
if debugModeTCP() {
|
||||
ln, err = net.Listen("tcp", socketAddr)
|
||||
} else {
|
||||
ln, err = net.Listen("unix", socketPath)
|
||||
}
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
69
main.go
69
main.go
@@ -4,39 +4,52 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/bcicen/ctop/config"
|
||||
"github.com/bcicen/ctop/connector"
|
||||
"github.com/bcicen/ctop/container"
|
||||
"github.com/bcicen/ctop/cwidgets/compact"
|
||||
"github.com/bcicen/ctop/logging"
|
||||
"github.com/bcicen/ctop/widgets"
|
||||
ui "github.com/gizak/termui"
|
||||
tm "github.com/nsf/termbox-go"
|
||||
)
|
||||
|
||||
var (
|
||||
build = "none"
|
||||
version = "dev-build"
|
||||
goVersion = runtime.Version()
|
||||
|
||||
log *logging.CTopLogger
|
||||
cursor *GridCursor
|
||||
cGrid *compact.CompactGrid
|
||||
header *widgets.CTopHeader
|
||||
status *widgets.StatusLine
|
||||
|
||||
versionStr = fmt.Sprintf("ctop version %v, build %v %v", version, build, goVersion)
|
||||
)
|
||||
|
||||
func main() {
|
||||
defer panicExit()
|
||||
|
||||
// parse command line arguments
|
||||
var versionFlag = flag.Bool("v", false, "output version information and exit")
|
||||
var helpFlag = flag.Bool("h", false, "display this help dialog")
|
||||
var filterFlag = flag.String("f", "", "filter containers")
|
||||
var activeOnlyFlag = flag.Bool("a", false, "show active containers only")
|
||||
var sortFieldFlag = flag.String("s", "", "select container sort field")
|
||||
var reverseSortFlag = flag.Bool("r", false, "reverse container sort order")
|
||||
var invertFlag = flag.Bool("i", false, "invert default colors")
|
||||
var (
|
||||
versionFlag = flag.Bool("v", false, "output version information and exit")
|
||||
helpFlag = flag.Bool("h", false, "display this help dialog")
|
||||
filterFlag = flag.String("f", "", "filter containers")
|
||||
activeOnlyFlag = flag.Bool("a", false, "show active containers only")
|
||||
sortFieldFlag = flag.String("s", "", "select container sort field")
|
||||
reverseSortFlag = flag.Bool("r", false, "reverse container sort order")
|
||||
invertFlag = flag.Bool("i", false, "invert default colors")
|
||||
scaleCpu = flag.Bool("scale-cpu", false, "show cpu as % of system total")
|
||||
connectorFlag = flag.String("connector", "docker", "container connector to use")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
if *versionFlag {
|
||||
printVersion()
|
||||
fmt.Println(versionStr)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
@@ -48,8 +61,9 @@ func main() {
|
||||
// init logger
|
||||
log = logging.Init()
|
||||
|
||||
// init global config
|
||||
// init global config and read config file if exists
|
||||
config.Init()
|
||||
config.Read()
|
||||
|
||||
// override default config values with command line flags
|
||||
if *filterFlag != "" {
|
||||
@@ -69,6 +83,10 @@ func main() {
|
||||
config.Toggle("sortReversed")
|
||||
}
|
||||
|
||||
if *scaleCpu {
|
||||
config.Toggle("scaleCpu")
|
||||
}
|
||||
|
||||
// init ui
|
||||
if *invertFlag {
|
||||
InvertColorMap()
|
||||
@@ -77,26 +95,37 @@ func main() {
|
||||
if err := ui.Init(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer ui.Close()
|
||||
|
||||
defer Shutdown()
|
||||
// init grid, cursor, header
|
||||
cursor = NewGridCursor()
|
||||
conn, err := connector.ByName(*connectorFlag)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
cursor = &GridCursor{cSource: conn}
|
||||
cGrid = compact.NewCompactGrid()
|
||||
header = widgets.NewCTopHeader()
|
||||
status = widgets.NewStatusLine()
|
||||
|
||||
for {
|
||||
exit := Display()
|
||||
if exit {
|
||||
log.Notice("shutting down")
|
||||
log.Exit()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Shutdown() {
|
||||
log.Notice("shutting down")
|
||||
log.Exit()
|
||||
if tm.IsInit {
|
||||
ui.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// ensure a given sort field is valid
|
||||
func validSort(s string) {
|
||||
if _, ok := Sorters[s]; !ok {
|
||||
if _, ok := container.Sorters[s]; !ok {
|
||||
fmt.Printf("invalid sort field: %s\n", s)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -104,13 +133,13 @@ func validSort(s string) {
|
||||
|
||||
func panicExit() {
|
||||
if r := recover(); r != nil {
|
||||
ui.Clear()
|
||||
fmt.Printf("panic: %s\n", r)
|
||||
Shutdown()
|
||||
fmt.Printf("error: %s\n", r)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
var helpMsg = `ctop - container metric viewer
|
||||
var helpMsg = `ctop - interactive container viewer
|
||||
|
||||
usage: ctop [options]
|
||||
|
||||
@@ -120,8 +149,6 @@ options:
|
||||
func printHelp() {
|
||||
fmt.Println(helpMsg)
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
|
||||
func printVersion() {
|
||||
fmt.Printf("ctop version %v, build %v\n", version, build)
|
||||
fmt.Printf("\navailable connectors: ")
|
||||
fmt.Println(strings.Join(connector.Enabled(), ", "))
|
||||
}
|
||||
|
||||
216
menus.go
216
menus.go
@@ -1,23 +1,35 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/bcicen/ctop/config"
|
||||
"github.com/bcicen/ctop/container"
|
||||
"github.com/bcicen/ctop/widgets"
|
||||
"github.com/bcicen/ctop/widgets/menu"
|
||||
ui "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
// MenuFn executes a menu window, returning the next menu or nil
|
||||
type MenuFn func() MenuFn
|
||||
|
||||
var helpDialog = []menu.Item{
|
||||
menu.Item{"[a] - toggle display of all containers", ""},
|
||||
menu.Item{"[f] - filter displayed containers", ""},
|
||||
menu.Item{"[h] - open this help dialog", ""},
|
||||
menu.Item{"[H] - toggle ctop header", ""},
|
||||
menu.Item{"[s] - select container sort field", ""},
|
||||
menu.Item{"[r] - reverse container sort order", ""},
|
||||
menu.Item{"[q] - exit ctop", ""},
|
||||
{"<enter> - open container menu", ""},
|
||||
{"", ""},
|
||||
{"[a] - toggle display of all containers", ""},
|
||||
{"[f] - filter displayed containers", ""},
|
||||
{"[h] - open this help dialog", ""},
|
||||
{"[H] - toggle ctop header", ""},
|
||||
{"[s] - select container sort field", ""},
|
||||
{"[r] - reverse container sort order", ""},
|
||||
{"[o] - open single view", ""},
|
||||
{"[l] - view container logs ([t] to toggle timestamp when open)", ""},
|
||||
{"[S] - save current configuration to file", ""},
|
||||
{"[q] - exit ctop", ""},
|
||||
}
|
||||
|
||||
func HelpMenu() {
|
||||
func HelpMenu() MenuFn {
|
||||
ui.Clear()
|
||||
ui.DefaultEvtStream.ResetHandlers()
|
||||
defer ui.DefaultEvtStream.ResetHandlers()
|
||||
@@ -25,14 +37,18 @@ func HelpMenu() {
|
||||
m := menu.NewMenu()
|
||||
m.BorderLabel = "Help"
|
||||
m.AddItems(helpDialog...)
|
||||
ui.Handle("/sys/wnd/resize", func(e ui.Event) {
|
||||
ui.Clear()
|
||||
ui.Render(m)
|
||||
})
|
||||
ui.Handle("/sys/kbd/", func(ui.Event) {
|
||||
ui.StopLoop()
|
||||
})
|
||||
ui.Loop()
|
||||
return nil
|
||||
}
|
||||
|
||||
func FilterMenu() {
|
||||
func FilterMenu() MenuFn {
|
||||
ui.DefaultEvtStream.ResetHandlers()
|
||||
defer ui.DefaultEvtStream.ResetHandlers()
|
||||
|
||||
@@ -62,9 +78,10 @@ func FilterMenu() {
|
||||
ui.StopLoop()
|
||||
})
|
||||
ui.Loop()
|
||||
return nil
|
||||
}
|
||||
|
||||
func SortMenu() {
|
||||
func SortMenu() MenuFn {
|
||||
ui.Clear()
|
||||
ui.DefaultEvtStream.ResetHandlers()
|
||||
defer ui.DefaultEvtStream.ResetHandlers()
|
||||
@@ -74,7 +91,7 @@ func SortMenu() {
|
||||
m.SortItems = true
|
||||
m.BorderLabel = "Sort Field"
|
||||
|
||||
for _, field := range SortFields() {
|
||||
for _, field := range container.SortFields() {
|
||||
m.AddItems(menu.Item{field, ""})
|
||||
}
|
||||
|
||||
@@ -92,4 +109,181 @@ func SortMenu() {
|
||||
|
||||
ui.Render(m)
|
||||
ui.Loop()
|
||||
return nil
|
||||
}
|
||||
|
||||
func ContainerMenu() MenuFn {
|
||||
c := cursor.Selected()
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ui.DefaultEvtStream.ResetHandlers()
|
||||
defer ui.DefaultEvtStream.ResetHandlers()
|
||||
|
||||
m := menu.NewMenu()
|
||||
m.Selectable = true
|
||||
m.BorderLabel = "Menu"
|
||||
|
||||
items := []menu.Item{
|
||||
menu.Item{Val: "single", Label: "single view"},
|
||||
menu.Item{Val: "logs", Label: "log view"},
|
||||
}
|
||||
|
||||
if c.Meta["state"] == "running" {
|
||||
items = append(items, menu.Item{Val: "stop", Label: "stop"})
|
||||
}
|
||||
if c.Meta["state"] == "exited" || c.Meta["state"] == "created" {
|
||||
items = append(items, menu.Item{Val: "start", Label: "start"})
|
||||
items = append(items, menu.Item{Val: "remove", Label: "remove"})
|
||||
}
|
||||
items = append(items, menu.Item{Val: "cancel", Label: "cancel"})
|
||||
|
||||
m.AddItems(items...)
|
||||
ui.Render(m)
|
||||
|
||||
var nextMenu MenuFn
|
||||
HandleKeys("up", m.Up)
|
||||
HandleKeys("down", m.Down)
|
||||
ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
|
||||
switch m.SelectedItem().Val {
|
||||
case "single":
|
||||
nextMenu = SingleView
|
||||
case "logs":
|
||||
nextMenu = LogMenu
|
||||
case "start":
|
||||
nextMenu = Confirm(confirmTxt("start", c.GetMeta("name")), c.Start)
|
||||
case "stop":
|
||||
nextMenu = Confirm(confirmTxt("stop", c.GetMeta("name")), c.Stop)
|
||||
case "remove":
|
||||
nextMenu = Confirm(confirmTxt("remove", c.GetMeta("name")), c.Remove)
|
||||
}
|
||||
ui.StopLoop()
|
||||
})
|
||||
ui.Handle("/sys/kbd/", func(ui.Event) {
|
||||
ui.StopLoop()
|
||||
})
|
||||
ui.Loop()
|
||||
return nextMenu
|
||||
}
|
||||
|
||||
func LogMenu() MenuFn {
|
||||
|
||||
c := cursor.Selected()
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ui.DefaultEvtStream.ResetHandlers()
|
||||
defer ui.DefaultEvtStream.ResetHandlers()
|
||||
|
||||
logs, quit := logReader(c)
|
||||
m := widgets.NewTextView(logs)
|
||||
m.BorderLabel = "Logs"
|
||||
ui.Render(m)
|
||||
|
||||
ui.Handle("/sys/wnd/resize", func(e ui.Event) {
|
||||
m.Resize()
|
||||
})
|
||||
ui.Handle("/sys/kbd/t", func(ui.Event) {
|
||||
m.Toggle()
|
||||
})
|
||||
ui.Handle("/sys/kbd/", func(ui.Event) {
|
||||
quit <- true
|
||||
ui.StopLoop()
|
||||
})
|
||||
ui.Loop()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a confirmation dialog with a given description string and
|
||||
// func to perform if confirmed
|
||||
func Confirm(txt string, fn func()) MenuFn {
|
||||
menu := func() MenuFn {
|
||||
ui.DefaultEvtStream.ResetHandlers()
|
||||
defer ui.DefaultEvtStream.ResetHandlers()
|
||||
|
||||
m := menu.NewMenu()
|
||||
m.Selectable = true
|
||||
m.BorderLabel = "Confirm"
|
||||
m.SubText = txt
|
||||
|
||||
items := []menu.Item{
|
||||
menu.Item{Val: "cancel", Label: "[c]ancel"},
|
||||
menu.Item{Val: "yes", Label: "[y]es"},
|
||||
}
|
||||
|
||||
var response bool
|
||||
|
||||
m.AddItems(items...)
|
||||
ui.Render(m)
|
||||
|
||||
yes := func() {
|
||||
response = true
|
||||
ui.StopLoop()
|
||||
}
|
||||
|
||||
no := func() {
|
||||
response = false
|
||||
ui.StopLoop()
|
||||
}
|
||||
|
||||
HandleKeys("up", m.Up)
|
||||
HandleKeys("down", m.Down)
|
||||
HandleKeys("exit", no)
|
||||
ui.Handle("/sys/kbd/c", func(ui.Event) { no() })
|
||||
ui.Handle("/sys/kbd/y", func(ui.Event) { yes() })
|
||||
|
||||
ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
|
||||
switch m.SelectedItem().Val {
|
||||
case "cancel":
|
||||
no()
|
||||
case "yes":
|
||||
yes()
|
||||
}
|
||||
})
|
||||
|
||||
ui.Loop()
|
||||
if response {
|
||||
fn()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return menu
|
||||
}
|
||||
|
||||
type toggleLog struct {
|
||||
timestamp time.Time
|
||||
message string
|
||||
}
|
||||
|
||||
func (t *toggleLog) Toggle(on bool) string {
|
||||
if on {
|
||||
return fmt.Sprintf("%s %s", t.timestamp.Format("2006-01-02T15:04:05.999Z07:00"), t.message)
|
||||
}
|
||||
return t.message
|
||||
}
|
||||
|
||||
func logReader(container *container.Container) (logs chan widgets.ToggleText, quit chan bool) {
|
||||
|
||||
logCollector := container.Logs()
|
||||
stream := logCollector.Stream()
|
||||
logs = make(chan widgets.ToggleText)
|
||||
quit = make(chan bool)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case log := <-stream:
|
||||
logs <- &toggleLog{timestamp: log.Timestamp, message: log.Message}
|
||||
case <-quit:
|
||||
logCollector.Stop()
|
||||
close(logs)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
return
|
||||
}
|
||||
|
||||
func confirmTxt(a, n string) string { return fmt.Sprintf("%s container %s?", a, n) }
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/bcicen/ctop/logging"
|
||||
)
|
||||
|
||||
var log = logging.Init()
|
||||
|
||||
type Metrics struct {
|
||||
CPUUtil int
|
||||
NetTx int64
|
||||
NetRx int64
|
||||
MemLimit int64
|
||||
MemPercent int
|
||||
MemUsage int64
|
||||
IOBytesRead int64
|
||||
IOBytesWrite int64
|
||||
Pids int
|
||||
}
|
||||
|
||||
func NewMetrics() Metrics {
|
||||
return Metrics{
|
||||
CPUUtil: -1,
|
||||
NetTx: -1,
|
||||
NetRx: -1,
|
||||
MemUsage: -1,
|
||||
MemPercent: -1,
|
||||
IOBytesRead: -1,
|
||||
IOBytesWrite: -1,
|
||||
Pids: -1,
|
||||
}
|
||||
}
|
||||
|
||||
type Collector interface {
|
||||
Stream() chan Metrics
|
||||
Running() bool
|
||||
Start()
|
||||
Stop()
|
||||
}
|
||||
|
||||
func round(num float64) int {
|
||||
return int(num + math.Copysign(0.5, num))
|
||||
}
|
||||
33
models/main.go
Normal file
33
models/main.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type Log struct {
|
||||
Timestamp time.Time
|
||||
Message string
|
||||
}
|
||||
|
||||
type Metrics struct {
|
||||
CPUUtil int
|
||||
NetTx int64
|
||||
NetRx int64
|
||||
MemLimit int64
|
||||
MemPercent int
|
||||
MemUsage int64
|
||||
IOBytesRead int64
|
||||
IOBytesWrite int64
|
||||
Pids int
|
||||
}
|
||||
|
||||
func NewMetrics() Metrics {
|
||||
return Metrics{
|
||||
CPUUtil: -1,
|
||||
NetTx: -1,
|
||||
NetRx: -1,
|
||||
MemUsage: -1,
|
||||
MemPercent: -1,
|
||||
IOBytesRead: -1,
|
||||
IOBytesWrite: -1,
|
||||
Pids: -1,
|
||||
}
|
||||
}
|
||||
@@ -17,8 +17,8 @@ type CTopHeader struct {
|
||||
func NewCTopHeader() *CTopHeader {
|
||||
return &CTopHeader{
|
||||
Time: headerPar(2, timeStr()),
|
||||
Count: headerPar(27, "-"),
|
||||
Filter: headerPar(47, ""),
|
||||
Count: headerPar(24, "-"),
|
||||
Filter: headerPar(40, ""),
|
||||
bg: headerBg(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ type Padding [2]int // x,y padding
|
||||
type Menu struct {
|
||||
ui.Block
|
||||
SortItems bool // enable automatic sorting of menu items
|
||||
SubText string // optional text to display before items
|
||||
TextFgColor ui.Attribute
|
||||
TextBgColor ui.Attribute
|
||||
Selectable bool
|
||||
@@ -82,9 +83,19 @@ func (m *Menu) Buffer() ui.Buffer {
|
||||
var cell ui.Cell
|
||||
buf := m.Block.Buffer()
|
||||
|
||||
y := m.Y + m.padding[1]
|
||||
|
||||
if m.SubText != "" {
|
||||
x := m.X + m.padding[0]
|
||||
for i, ch := range m.SubText {
|
||||
cell = ui.Cell{Ch: ch, Fg: m.TextFgColor, Bg: m.TextBgColor}
|
||||
buf.Set(x+i, y, cell)
|
||||
}
|
||||
y += 2
|
||||
}
|
||||
|
||||
for n, item := range m.items {
|
||||
x := m.X + m.padding[0]
|
||||
y := m.Y + m.padding[1]
|
||||
for _, ch := range item.Text() {
|
||||
// invert bg/fg colors on currently selected row
|
||||
if m.Selectable && n == m.cursorPos {
|
||||
@@ -118,14 +129,22 @@ func (m *Menu) Down() {
|
||||
func (m *Menu) calcSize() {
|
||||
m.Width = 7 // minimum width
|
||||
|
||||
items := m.items
|
||||
var height int
|
||||
for _, i := range m.items {
|
||||
s := i.Text()
|
||||
if len(s) > m.Width {
|
||||
m.Width = len(s)
|
||||
}
|
||||
height++
|
||||
}
|
||||
|
||||
if m.SubText != "" {
|
||||
if len(m.SubText) > m.Width {
|
||||
m.Width = len(m.SubText)
|
||||
}
|
||||
height += 2
|
||||
}
|
||||
|
||||
m.Width += (m.padding[0] * 2)
|
||||
m.Height = len(items) + (m.padding[1] * 2)
|
||||
m.Height = height + (m.padding[1] * 2)
|
||||
}
|
||||
|
||||
87
widgets/status.go
Normal file
87
widgets/status.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package widgets
|
||||
|
||||
import (
|
||||
ui "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
var (
|
||||
statusHeight = 1
|
||||
statusIter = 3
|
||||
)
|
||||
|
||||
type StatusLine struct {
|
||||
Message *ui.Par
|
||||
bg *ui.Par
|
||||
}
|
||||
|
||||
func NewStatusLine() *StatusLine {
|
||||
p := ui.NewPar("")
|
||||
p.X = 2
|
||||
p.Border = false
|
||||
p.Height = statusHeight
|
||||
p.Bg = ui.ThemeAttr("header.bg")
|
||||
p.TextFgColor = ui.ThemeAttr("header.fg")
|
||||
p.TextBgColor = ui.ThemeAttr("header.bg")
|
||||
return &StatusLine{
|
||||
Message: p,
|
||||
bg: statusBg(),
|
||||
}
|
||||
}
|
||||
|
||||
func (sl *StatusLine) Display() {
|
||||
ui.DefaultEvtStream.ResetHandlers()
|
||||
defer ui.DefaultEvtStream.ResetHandlers()
|
||||
|
||||
iter := statusIter
|
||||
ui.Handle("/sys/kbd/", func(ui.Event) {
|
||||
ui.StopLoop()
|
||||
})
|
||||
ui.Handle("/timer/1s", func(ui.Event) {
|
||||
iter--
|
||||
if iter <= 0 {
|
||||
ui.StopLoop()
|
||||
}
|
||||
})
|
||||
|
||||
ui.Render(sl)
|
||||
ui.Loop()
|
||||
}
|
||||
|
||||
// change given message on the status line
|
||||
func (sl *StatusLine) Show(s string) {
|
||||
sl.Message.TextFgColor = ui.ThemeAttr("header.fg")
|
||||
sl.Message.Text = s
|
||||
sl.Display()
|
||||
}
|
||||
|
||||
func (sl *StatusLine) ShowErr(s string) {
|
||||
sl.Message.TextFgColor = ui.ThemeAttr("status.danger")
|
||||
sl.Message.Text = s
|
||||
sl.Display()
|
||||
}
|
||||
|
||||
func (sl *StatusLine) Buffer() ui.Buffer {
|
||||
buf := ui.NewBuffer()
|
||||
buf.Merge(sl.bg.Buffer())
|
||||
buf.Merge(sl.Message.Buffer())
|
||||
return buf
|
||||
}
|
||||
|
||||
func (sl *StatusLine) Align() {
|
||||
sl.bg.SetWidth(ui.TermWidth() - 1)
|
||||
sl.Message.SetWidth(ui.TermWidth() - 2)
|
||||
|
||||
sl.bg.Y = ui.TermHeight() - 1
|
||||
sl.Message.Y = ui.TermHeight() - 1
|
||||
}
|
||||
|
||||
func (sl *StatusLine) Height() int { return statusHeight }
|
||||
|
||||
func statusBg() *ui.Par {
|
||||
bg := ui.NewPar("")
|
||||
bg.X = 1
|
||||
bg.Height = statusHeight
|
||||
bg.Border = false
|
||||
bg.Bg = ui.ThemeAttr("header.bg")
|
||||
return bg
|
||||
}
|
||||
124
widgets/view.go
Normal file
124
widgets/view.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package widgets
|
||||
|
||||
import (
|
||||
ui "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
type ToggleText interface {
|
||||
// returns text for toggle on/off
|
||||
Toggle(on bool) string
|
||||
}
|
||||
|
||||
type TextView struct {
|
||||
ui.Block
|
||||
inputStream <-chan ToggleText
|
||||
render chan bool
|
||||
toggleState bool
|
||||
Text []ToggleText // all the text
|
||||
TextOut []string // text to be displayed
|
||||
TextFgColor ui.Attribute
|
||||
TextBgColor ui.Attribute
|
||||
padding Padding
|
||||
}
|
||||
|
||||
func NewTextView(lines <-chan ToggleText) *TextView {
|
||||
t := &TextView{
|
||||
Block: *ui.NewBlock(),
|
||||
inputStream: lines,
|
||||
render: make(chan bool),
|
||||
Text: []ToggleText{},
|
||||
TextOut: []string{},
|
||||
TextFgColor: ui.ThemeAttr("menu.text.fg"),
|
||||
TextBgColor: ui.ThemeAttr("menu.text.bg"),
|
||||
padding: Padding{4, 2},
|
||||
}
|
||||
|
||||
t.BorderFg = ui.ThemeAttr("menu.border.fg")
|
||||
t.BorderLabelFg = ui.ThemeAttr("menu.label.fg")
|
||||
t.Height = ui.TermHeight()
|
||||
t.Width = ui.TermWidth()
|
||||
|
||||
t.readInputLoop()
|
||||
t.renderLoop()
|
||||
return t
|
||||
}
|
||||
|
||||
// Adjusts text inside this view according to the window size. No need to call ui.Render(...)
|
||||
// after calling this method, it is called automatically
|
||||
func (t *TextView) Resize() {
|
||||
ui.Clear()
|
||||
t.Height = ui.TermHeight()
|
||||
t.Width = ui.TermWidth()
|
||||
t.render <- true
|
||||
}
|
||||
|
||||
// Toggles text inside this view. No need to call ui.Render(...) after calling this method,
|
||||
// it is called automatically
|
||||
func (t *TextView) Toggle() {
|
||||
t.toggleState = !t.toggleState
|
||||
t.render <- true
|
||||
}
|
||||
|
||||
func (t *TextView) Buffer() ui.Buffer {
|
||||
var cell ui.Cell
|
||||
buf := t.Block.Buffer()
|
||||
|
||||
x := t.Block.X + t.padding[0]
|
||||
y := t.Block.Y + t.padding[1]
|
||||
|
||||
for _, line := range t.TextOut {
|
||||
for _, ch := range line {
|
||||
cell = ui.Cell{Ch: ch, Fg: t.TextFgColor, Bg: t.TextBgColor}
|
||||
buf.Set(x, y, cell)
|
||||
x++
|
||||
}
|
||||
x = t.Block.X + t.padding[0]
|
||||
y++
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
func (t *TextView) renderLoop() {
|
||||
go func() {
|
||||
for range t.render {
|
||||
maxWidth := t.Width - (t.padding[0] * 2)
|
||||
height := t.Height - (t.padding[1] * 2)
|
||||
t.TextOut = []string{}
|
||||
for i := len(t.Text) - 1; i >= 0; i-- {
|
||||
lines := splitLine(t.Text[i].Toggle(t.toggleState), maxWidth)
|
||||
t.TextOut = append(lines, t.TextOut...)
|
||||
if len(t.TextOut) > height {
|
||||
t.TextOut = t.TextOut[:height]
|
||||
break
|
||||
}
|
||||
}
|
||||
ui.Render(t)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (t *TextView) readInputLoop() {
|
||||
go func() {
|
||||
for line := range t.inputStream {
|
||||
t.Text = append(t.Text, line)
|
||||
t.render <- true
|
||||
}
|
||||
close(t.render)
|
||||
}()
|
||||
}
|
||||
|
||||
func splitLine(line string, lineSize int) []string {
|
||||
if line == "" {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
var lines []string
|
||||
for {
|
||||
if len(line) <= lineSize {
|
||||
lines = append(lines, line)
|
||||
return lines
|
||||
}
|
||||
lines = append(lines, line[:lineSize])
|
||||
line = line[lineSize:]
|
||||
}
|
||||
}
|
||||
35
widgets/view_test.go
Normal file
35
widgets/view_test.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package widgets
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSplitEmptyLine(t *testing.T) {
|
||||
|
||||
result := splitLine("", 5)
|
||||
if len(result) != 0 {
|
||||
t.Errorf("expected: 0 lines, got: %d", len(result))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitLineShorterThanLimit(t *testing.T) {
|
||||
|
||||
result := splitLine("hello", 7)
|
||||
if len(result) != 1 {
|
||||
t.Errorf("expected: 0 lines, got: %d", len(result))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitLineLongerThanLimit(t *testing.T) {
|
||||
|
||||
result := splitLine("hello", 3)
|
||||
if len(result) != 2 {
|
||||
t.Errorf("expected: 0 lines, got: %d", len(result))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitLineSameAsLimit(t *testing.T) {
|
||||
|
||||
result := splitLine("hello", 5)
|
||||
if len(result) != 1 {
|
||||
t.Errorf("expected: 0 lines, got: %d", len(result))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user