mirror of
https://github.com/bcicen/ctop.git
synced 2025-12-06 23:26:45 +08:00
Compare commits
258 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae62c388bc | ||
|
|
523d6c7277 | ||
|
|
47a0d7e495 | ||
|
|
9dec6b4c67 | ||
|
|
7f6ff0b599 | ||
|
|
187adf0540 | ||
|
|
665e8fdd06 | ||
|
|
a39b7a3a3e | ||
|
|
77f5e6b735 | ||
|
|
3c83b7576b | ||
|
|
8a0bd3cf8a | ||
|
|
78caad2dbd | ||
|
|
8d8f1e72eb | ||
|
|
93556a1754 | ||
|
|
4d247f5272 | ||
|
|
db3d7e8927 | ||
|
|
efef345665 | ||
|
|
f158fa742f | ||
|
|
4d48245d7d | ||
|
|
6bee1b7f31 | ||
|
|
7118e45f3a | ||
|
|
3405d19be8 | ||
|
|
9a185b2388 | ||
|
|
caf6fc63c1 | ||
|
|
cf352f7c8a | ||
|
|
ac5bed210f | ||
|
|
a72d43526f | ||
|
|
9eb2457aa4 | ||
|
|
b83402b886 | ||
|
|
078564bd38 | ||
|
|
a2c08d312e | ||
|
|
f7a3d38d6b | ||
|
|
a3b8585697 | ||
|
|
2e526e9b86 | ||
|
|
541fe70b78 | ||
|
|
c786b697bf | ||
|
|
aa6c00b083 | ||
|
|
17855e3d8e | ||
|
|
842809bef5 | ||
|
|
4e567ee007 | ||
|
|
56700e120b | ||
|
|
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 | ||
|
|
5db90f31dc | ||
|
|
82677d52ef | ||
|
|
2b2338805b | ||
|
|
60213f1551 | ||
|
|
8aa932b29f | ||
|
|
35cc8d095d | ||
|
|
30530bc2a1 | ||
|
|
2c282923c0 | ||
|
|
d0d39749de | ||
|
|
26b88a9790 | ||
|
|
a135a67c06 | ||
|
|
19b212f45d | ||
|
|
34987df010 | ||
|
|
e2bc4d0a08 | ||
|
|
4ac1348fbb | ||
|
|
66d78a7d74 | ||
|
|
e62a8881a2 | ||
|
|
a5b2e7b074 | ||
|
|
a87bdce0fe | ||
|
|
2228188ebf | ||
|
|
e94a9c0cc2 | ||
|
|
e82d77ecb0 | ||
|
|
50b4181866 | ||
|
|
1285288b9e | ||
|
|
2a709577bd | ||
|
|
38599bbd19 | ||
|
|
b3cdb33efc | ||
|
|
0ac70c96eb | ||
|
|
36a5bbdfe1 | ||
|
|
3553b0af9d | ||
|
|
ca61ec712e | ||
|
|
06c4b24212 | ||
|
|
12fa716825 | ||
|
|
8327406069 | ||
|
|
2134110224 | ||
|
|
77c3d00e67 | ||
|
|
85eb5228ae | ||
|
|
3a3950e395 | ||
|
|
eaac079b15 | ||
|
|
ab1ccb3cd8 | ||
|
|
dbaebe0192 | ||
|
|
d5ef818c8d | ||
|
|
8203d0b883 | ||
|
|
b28beed3ee | ||
|
|
2e51406d00 | ||
|
|
c84b52ce40 | ||
|
|
4ee8cf621a | ||
|
|
192298c045 | ||
|
|
258536740d | ||
|
|
ef69744249 | ||
|
|
07f95a04b0 | ||
|
|
b2184bbc6d | ||
|
|
96b01eb3b9 | ||
|
|
03d4869361 | ||
|
|
4b7257908f | ||
|
|
1875013a76 | ||
|
|
dab2f926b9 | ||
|
|
ddce54f991 | ||
|
|
168e8f3aae | ||
|
|
ecc37a2f99 | ||
|
|
2f17a9d689 | ||
|
|
8a6808c804 | ||
|
|
3ca94b50cd | ||
|
|
0e3fe88bb4 | ||
|
|
b9b904626c | ||
|
|
e195828f92 | ||
|
|
1d176d46c4 | ||
|
|
7026193f8e | ||
|
|
d9b4295176 | ||
|
|
92cc7bc849 | ||
|
|
70790e88ae | ||
|
|
bcf05b7f42 | ||
|
|
9df3ff2aa0 | ||
|
|
2b80832a36 | ||
|
|
a6ee6edb1d | ||
|
|
d7f9f715bb | ||
|
|
2d2d58d47f | ||
|
|
bf4d59c251 | ||
|
|
b8eb386360 | ||
|
|
02610c59da | ||
|
|
71768b498c | ||
|
|
57e49ea2c6 | ||
|
|
5b25f931df | ||
|
|
4af33fdf12 |
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
|
||||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
ctop
|
||||||
|
.idea
|
||||||
|
/vendor/
|
||||||
17
Dockerfile
Normal file
17
Dockerfile
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
FROM quay.io/vektorcloud/go:1.11
|
||||||
|
|
||||||
|
RUN apk add --no-cache make
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY go.mod .
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN make build && \
|
||||||
|
mkdir -p /go/bin && \
|
||||||
|
mv -v ctop /go/bin/
|
||||||
|
|
||||||
|
FROM scratch
|
||||||
|
ENV TERM=linux
|
||||||
|
COPY --from=0 /go/bin/ctop /ctop
|
||||||
|
ENTRYPOINT ["/ctop"]
|
||||||
22
LICENSE
Normal file
22
LICENSE
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2017 VektorLab
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
37
Makefile
Normal file
37
Makefile
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
NAME=ctop
|
||||||
|
VERSION=$(shell cat VERSION)
|
||||||
|
BUILD=$(shell git rev-parse --short HEAD)
|
||||||
|
EXT_LD_FLAGS="-Wl,--allow-multiple-definition"
|
||||||
|
LD_FLAGS="-w -X main.version=$(VERSION) -X main.build=$(BUILD) -extldflags=$(EXT_LD_FLAGS)"
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf _build/ release/
|
||||||
|
|
||||||
|
build:
|
||||||
|
go mod download
|
||||||
|
CGO_ENABLED=0 go build -tags release -ldflags $(LD_FLAGS) -o ctop
|
||||||
|
|
||||||
|
build-dev:
|
||||||
|
go build -ldflags "-w -X main.version=$(VERSION)-dev -X main.build=$(BUILD) -extldflags=$(EXT_LD_FLAGS)"
|
||||||
|
|
||||||
|
build-all:
|
||||||
|
mkdir -p _build
|
||||||
|
GOOS=darwin GOARCH=amd64 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-darwin-amd64
|
||||||
|
GOOS=linux GOARCH=amd64 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-linux-amd64
|
||||||
|
GOOS=linux GOARCH=arm go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-linux-arm
|
||||||
|
GOOS=linux GOARCH=arm64 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-linux-arm64
|
||||||
|
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 -f Dockerfile .
|
||||||
|
|
||||||
|
release:
|
||||||
|
mkdir release
|
||||||
|
go get github.com/progrium/gh-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)
|
||||||
|
|
||||||
|
.PHONY: build
|
||||||
77
README.md
77
README.md
@@ -1,7 +1,18 @@
|
|||||||
# ctop
|
<p align="center"><img width="200px" src="/_docs/img/logo.png" alt="ctop"/></p>
|
||||||
|
|
||||||
|
#
|
||||||
|
|
||||||
|
![release][release] ![homebrew][homebrew]
|
||||||
|
|
||||||
Top-like interface for container metrics
|
Top-like interface for container metrics
|
||||||
|
|
||||||
|
`ctop` provides a concise and condensed overview of real-time metrics for multiple containers:
|
||||||
|
<p align="center"><img src="_docs/img/grid.gif" alt="ctop"/></p>
|
||||||
|
|
||||||
|
as well as an [single container view][single_view] for inspecting a specific container.
|
||||||
|
|
||||||
|
`ctop` comes with built-in support for Docker and runC; connectors for other container and cluster systems are planned for future releases.
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
Fetch the [latest release](https://github.com/bcicen/ctop/releases) for your platform:
|
Fetch the [latest release](https://github.com/bcicen/ctop/releases) for your platform:
|
||||||
@@ -9,35 +20,75 @@ Fetch the [latest release](https://github.com/bcicen/ctop/releases) for your pla
|
|||||||
#### Linux
|
#### Linux
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/bcicen/ctop/releases/download/v0.1/ctop-0.1-linux-amd64 -O ctop
|
sudo wget https://github.com/bcicen/ctop/releases/download/v0.7.1/ctop-0.7.1-linux-amd64 -O /usr/local/bin/ctop
|
||||||
sudo mv ctop /usr/local/bin/
|
|
||||||
sudo chmod +x /usr/local/bin/ctop
|
sudo chmod +x /usr/local/bin/ctop
|
||||||
```
|
```
|
||||||
|
|
||||||
#### OS X
|
#### OS X
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -Lo ctop https://github.com/bcicen/ctop/releases/download/v0.1/ctop-0.1-darwin-amd64
|
brew install ctop
|
||||||
sudo mv ctop /usr/local/bin/
|
```
|
||||||
|
or
|
||||||
|
```bash
|
||||||
|
sudo curl -Lo /usr/local/bin/ctop https://github.com/bcicen/ctop/releases/download/v0.7.1/ctop-0.7.1-darwin-amd64
|
||||||
sudo chmod +x /usr/local/bin/ctop
|
sudo chmod +x /usr/local/bin/ctop
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm -ti \
|
||||||
|
--name=ctop \
|
||||||
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
quay.io/vektorlab/ctop:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
`ctop` is also available for Arch in the [AUR](https://aur.archlinux.org/packages/ctop-bin/)
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
Build steps can be found [here][build].
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
cTop requires no arguments and will configure itself using the `DOCKER_HOST` environment variable
|
`ctop` requires no arguments and uses Docker host variables by default. See [connectors][connectors] for further configuration options.
|
||||||
```bash
|
|
||||||
export DOCKER_HOST=tcp://127.0.0.1:4243
|
### Config file
|
||||||
ctop
|
|
||||||
```
|
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
|
||||||
|
-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
|
### Keybindings
|
||||||
|
|
||||||
Key | Action
|
Key | Action
|
||||||
--- | ---
|
--- | ---
|
||||||
|
\<enter\> | Open container menu
|
||||||
a | Toggle display of all (running and non-running) containers
|
a | Toggle display of all (running and non-running) containers
|
||||||
f | Filter displayed containers
|
f | Filter displayed containers (`esc` to clear when open)
|
||||||
H | Toggle cTop header
|
H | Toggle ctop header
|
||||||
h | Open help dialog
|
h | Open help dialog
|
||||||
s | Select container sort field
|
s | Select container sort field
|
||||||
r | Reverse container sort order
|
r | Reverse container sort order
|
||||||
q | Quit cTop
|
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
|
||||||
|
[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"
|
||||||
|
|||||||
20
_docs/build.md
Normal file
20
_docs/build.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Build
|
||||||
|
|
||||||
|
To build `ctop` from source, ensure you have [dep](https://github.com/golang/dep) installed and run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get github.com/bcicen/ctop && \
|
||||||
|
cd $GOPATH/src/github.com/bcicen/ctop && \
|
||||||
|
make build
|
||||||
|
```
|
||||||
|
|
||||||
|
To build a minimal Docker image containing only `ctop`:
|
||||||
|
```bash
|
||||||
|
make image
|
||||||
|
```
|
||||||
|
|
||||||
|
Now you can run your local image:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -ti --name ctop --rm -v /var/run/docker.sock:/var/run/docker.sock ctop
|
||||||
|
```
|
||||||
26
_docs/connectors.md
Normal file
26
_docs/connectors.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# connectors
|
||||||
|
|
||||||
|
`ctop` comes with the below native connectors, enabled via the `--connector` option.
|
||||||
|
|
||||||
|
Default connector behavior can be changed by setting the relevant environment variables.
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
Default connector, configurable via standard [Docker commandline varaibles](https://docs.docker.com/engine/reference/commandline/cli/#environment-variables)
|
||||||
|
|
||||||
|
#### Options
|
||||||
|
|
||||||
|
Var | Description
|
||||||
|
--- | ---
|
||||||
|
DOCKER_HOST | Daemon socket to connect to (default: `unix://var/run/docker.sock`)
|
||||||
|
|
||||||
|
## RunC
|
||||||
|
|
||||||
|
Using this connector requires full privileges to the local runC root dir 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
|
||||||
56
_docs/debug.md
Normal file
56
_docs/debug.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# Debug Mode
|
||||||
|
|
||||||
|
`ctop` comes with a built-in logging facility and local socket server to simplify debugging at run time.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
If running `ctop` via Docker, debug logging can be most easily enabled as below:
|
||||||
|
```bash
|
||||||
|
docker run -ti --rm \
|
||||||
|
--name=ctop \
|
||||||
|
-e CTOP_DEBUG=1 \
|
||||||
|
-e CTOP_DEBUG_TCP=1 \
|
||||||
|
-p 9000:9000 \
|
||||||
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
quay.io/vektorlab/ctop:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Log messages can be followed by connecting to the default listen address:
|
||||||
|
```bash
|
||||||
|
curl -s localhost:9000
|
||||||
|
```
|
||||||
|
|
||||||
|
example output:
|
||||||
|
```
|
||||||
|
15:06:43.881 ▶ NOTI 002 logger initialized
|
||||||
|
15:06:43.881 ▶ INFO 003 loaded config param: "filterStr": ""
|
||||||
|
15:06:43.881 ▶ INFO 004 loaded config param: "sortField": "state"
|
||||||
|
15:06:43.881 ▶ INFO 005 loaded config switch: "sortReversed": false
|
||||||
|
15:06:43.881 ▶ INFO 006 loaded config switch: "allContainers": true
|
||||||
|
15:06:43.881 ▶ INFO 007 loaded config switch: "enableHeader": true
|
||||||
|
15:06:43.883 ▶ INFO 008 collector started for container: 7120f83ca...
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Unix Socket
|
||||||
|
|
||||||
|
Debug mode is enabled via the `CTOP_DEBUG` environment variable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CTOP_DEBUG=1 ./ctop
|
||||||
|
```
|
||||||
|
|
||||||
|
While `ctop` is running, you can connect to the logging socket via socat or similar tools:
|
||||||
|
```bash
|
||||||
|
socat unix-connect:./ctop.sock stdio
|
||||||
|
```
|
||||||
|
|
||||||
|
## TCP Logging Socket
|
||||||
|
|
||||||
|
In lieu of using a local unix socket, TCP logging can be enabled via the `CTOP_DEBUG_TCP` environment variable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CTOP_DEBUG=1 CTOP_DEBUG_TCP=1 ./ctop
|
||||||
|
```
|
||||||
|
|
||||||
|
A TCP listener for streaming log messages will be started on the default listen address(`0.0.0.0:9000`)
|
||||||
BIN
_docs/img/grid.gif
Normal file
BIN
_docs/img/grid.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 676 KiB |
BIN
_docs/img/logo.png
Normal file
BIN
_docs/img/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
BIN
_docs/img/single.gif
Normal file
BIN
_docs/img/single.gif
Normal file
Binary file not shown.
|
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>
|
||||||
61
colors.go
Normal file
61
colors.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
ui "github.com/gizak/termui"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Valid colors:
|
||||||
|
ui.ColorDefault
|
||||||
|
ui.ColorBlack
|
||||||
|
ui.ColorRed
|
||||||
|
ui.ColorGreen
|
||||||
|
ui.ColorYellow
|
||||||
|
ui.ColorBlue
|
||||||
|
ui.ColorMagenta
|
||||||
|
ui.ColorCyan
|
||||||
|
ui.ColorWhite
|
||||||
|
*/
|
||||||
|
|
||||||
|
var ColorMap = map[string]ui.Attribute{
|
||||||
|
"fg": ui.ColorWhite,
|
||||||
|
"bg": ui.ColorDefault,
|
||||||
|
"block.bg": ui.ColorDefault,
|
||||||
|
"border.bg": ui.ColorDefault,
|
||||||
|
"border.fg": ui.ColorWhite,
|
||||||
|
"label.bg": ui.ColorDefault,
|
||||||
|
"label.fg": ui.ColorGreen,
|
||||||
|
"menu.text.fg": ui.ColorWhite,
|
||||||
|
"menu.text.bg": ui.ColorDefault,
|
||||||
|
"menu.border.fg": ui.ColorCyan,
|
||||||
|
"menu.label.fg": ui.ColorGreen,
|
||||||
|
"header.fg": ui.ColorBlack,
|
||||||
|
"header.bg": ui.ColorWhite,
|
||||||
|
"gauge.bar.bg": ui.ColorGreen,
|
||||||
|
"gauge.percent.fg": ui.ColorWhite,
|
||||||
|
"linechart.axes.fg": ui.ColorDefault,
|
||||||
|
"linechart.line.fg": ui.ColorGreen,
|
||||||
|
"mbarchart.bar.bg": ui.ColorGreen,
|
||||||
|
"mbarchart.num.fg": ui.ColorWhite,
|
||||||
|
"mbarchart.text.fg": ui.ColorWhite,
|
||||||
|
"par.text.fg": ui.ColorWhite,
|
||||||
|
"par.text.bg": ui.ColorDefault,
|
||||||
|
"par.text.hi": ui.ColorBlack,
|
||||||
|
"sparkline.line.fg": ui.ColorGreen,
|
||||||
|
"sparkline.title.fg": ui.ColorWhite,
|
||||||
|
"status.ok": ui.ColorGreen,
|
||||||
|
"status.warn": ui.ColorYellow,
|
||||||
|
"status.danger": ui.ColorRed,
|
||||||
|
}
|
||||||
|
|
||||||
|
func InvertColorMap() {
|
||||||
|
re := regexp.MustCompile(".*.fg")
|
||||||
|
for k := range ColorMap {
|
||||||
|
if re.FindAllString(k, 1) != nil {
|
||||||
|
ColorMap[k] = ui.ColorBlack
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ColorMap["par.text.hi"] = ui.ColorWhite
|
||||||
|
}
|
||||||
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 File struct {
|
||||||
|
Options map[string]string `toml:"options"`
|
||||||
|
Toggles map[string]bool `toml:"toggles"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func exportConfig() File {
|
||||||
|
c := File{
|
||||||
|
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 File
|
||||||
|
|
||||||
|
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]), "/")
|
||||||
|
}
|
||||||
@@ -18,7 +18,6 @@ func Init() {
|
|||||||
GlobalParams = append(GlobalParams, p)
|
GlobalParams = append(GlobalParams, p)
|
||||||
log.Infof("loaded config param: %s: %s", quote(p.Key), quote(p.Val))
|
log.Infof("loaded config param: %s: %s", quote(p.Key), quote(p.Val))
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, s := range switches {
|
for _, s := range switches {
|
||||||
GlobalSwitches = append(GlobalSwitches, s)
|
GlobalSwitches = append(GlobalSwitches, s)
|
||||||
log.Infof("loaded config switch: %s: %t", quote(s.Key), s.Val)
|
log.Infof("loaded config switch: %s: %t", quote(s.Key), s.Val)
|
||||||
|
|||||||
@@ -2,11 +2,6 @@ package config
|
|||||||
|
|
||||||
// defaults
|
// defaults
|
||||||
var params = []*Param{
|
var params = []*Param{
|
||||||
&Param{
|
|
||||||
Key: "dockerHost",
|
|
||||||
Val: getEnv("DOCKER_HOST", "unix:///var/run/docker.sock"),
|
|
||||||
Label: "Docker API URL",
|
|
||||||
},
|
|
||||||
&Param{
|
&Param{
|
||||||
Key: "filterStr",
|
Key: "filterStr",
|
||||||
Val: "",
|
Val: "",
|
||||||
@@ -17,6 +12,11 @@ var params = []*Param{
|
|||||||
Val: "state",
|
Val: "state",
|
||||||
Label: "Container Sort Field",
|
Label: "Container Sort Field",
|
||||||
},
|
},
|
||||||
|
&Param{
|
||||||
|
Key: "namespace",
|
||||||
|
Val: "state",
|
||||||
|
Label: "Kubernetes namespace for monitoring",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
type Param struct {
|
type Param struct {
|
||||||
|
|||||||
@@ -5,22 +5,27 @@ var switches = []*Switch{
|
|||||||
&Switch{
|
&Switch{
|
||||||
Key: "sortReversed",
|
Key: "sortReversed",
|
||||||
Val: false,
|
Val: false,
|
||||||
Label: "Reverse Sort Order",
|
Label: "Reverse sort order",
|
||||||
},
|
},
|
||||||
&Switch{
|
&Switch{
|
||||||
Key: "allContainers",
|
Key: "allContainers",
|
||||||
Val: true,
|
Val: true,
|
||||||
Label: "Show All Containers",
|
Label: "Show all containers",
|
||||||
|
},
|
||||||
|
&Switch{
|
||||||
|
Key: "fullRowCursor",
|
||||||
|
Val: true,
|
||||||
|
Label: "Highlight entire cursor row (vs. name only)",
|
||||||
},
|
},
|
||||||
&Switch{
|
&Switch{
|
||||||
Key: "enableHeader",
|
Key: "enableHeader",
|
||||||
Val: true,
|
Val: true,
|
||||||
Label: "Enable cTop Status Line",
|
Label: "Enable status header",
|
||||||
},
|
},
|
||||||
&Switch{
|
&Switch{
|
||||||
Key: "loggingEnabled",
|
Key: "scaleCpu",
|
||||||
Val: false,
|
Val: false,
|
||||||
Label: "Enable Logging Server",
|
Label: "Show CPU as %% of system total",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,10 +50,18 @@ func GetSwitchVal(k string) bool {
|
|||||||
return GetSwitch(k).Val
|
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
|
// Toggle a boolean switch
|
||||||
func Toggle(k string) {
|
func Toggle(k string) {
|
||||||
sw := GetSwitch(k)
|
sw := GetSwitch(k)
|
||||||
newVal := sw.Val != true
|
newVal := !sw.Val
|
||||||
log.Noticef("config change: %s: %t -> %t", k, sw.Val, newVal)
|
log.Noticef("config change: %s: %t -> %t", k, sw.Val, newVal)
|
||||||
sw.Val = newVal
|
sw.Val = newVal
|
||||||
//log.Errorf("ignoring toggle for non-existant switch: %s", k)
|
//log.Errorf("ignoring toggle for non-existant switch: %s", k)
|
||||||
|
|||||||
@@ -1,32 +1,36 @@
|
|||||||
package metrics
|
package collector
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/bcicen/ctop/config"
|
||||||
|
"github.com/bcicen/ctop/models"
|
||||||
api "github.com/fsouza/go-dockerclient"
|
api "github.com/fsouza/go-dockerclient"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Docker collector
|
// Docker collector
|
||||||
type Docker struct {
|
type Docker struct {
|
||||||
Metrics
|
models.Metrics
|
||||||
id string
|
id string
|
||||||
client *api.Client
|
client *api.Client
|
||||||
running bool
|
running bool
|
||||||
stream chan Metrics
|
stream chan models.Metrics
|
||||||
done chan bool
|
done chan bool
|
||||||
lastCpu float64
|
lastCpu float64
|
||||||
lastSysCpu float64
|
lastSysCpu float64
|
||||||
|
scaleCpu bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDocker(client *api.Client, id string) *Docker {
|
func NewDocker(client *api.Client, id string) *Docker {
|
||||||
return &Docker{
|
return &Docker{
|
||||||
Metrics: Metrics{},
|
Metrics: models.Metrics{},
|
||||||
id: id,
|
id: id,
|
||||||
client: client,
|
client: client,
|
||||||
|
scaleCpu: config.GetSwitchVal("scaleCpu"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Docker) Start() {
|
func (c *Docker) Start() {
|
||||||
c.done = make(chan bool)
|
c.done = make(chan bool)
|
||||||
c.stream = make(chan Metrics)
|
c.stream = make(chan models.Metrics)
|
||||||
stats := make(chan *api.Stats)
|
stats := make(chan *api.Stats)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
@@ -46,6 +50,7 @@ func (c *Docker) Start() {
|
|||||||
c.ReadCPU(s)
|
c.ReadCPU(s)
|
||||||
c.ReadMem(s)
|
c.ReadMem(s)
|
||||||
c.ReadNet(s)
|
c.ReadNet(s)
|
||||||
|
c.ReadIO(s)
|
||||||
c.stream <- c.Metrics
|
c.stream <- c.Metrics
|
||||||
}
|
}
|
||||||
log.Infof("collector stopped for container: %s", c.id)
|
log.Infof("collector stopped for container: %s", c.id)
|
||||||
@@ -59,10 +64,14 @@ func (c *Docker) Running() bool {
|
|||||||
return c.running
|
return c.running
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Docker) Stream() chan Metrics {
|
func (c *Docker) Stream() chan models.Metrics {
|
||||||
return c.stream
|
return c.stream
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Docker) Logs() LogCollector {
|
||||||
|
return NewDockerLogs(c.id, c.client)
|
||||||
|
}
|
||||||
|
|
||||||
// Stop collector
|
// Stop collector
|
||||||
func (c *Docker) Stop() {
|
func (c *Docker) Stop() {
|
||||||
c.done <- true
|
c.done <- true
|
||||||
@@ -76,15 +85,20 @@ func (c *Docker) ReadCPU(stats *api.Stats) {
|
|||||||
cpudiff := total - c.lastCpu
|
cpudiff := total - c.lastCpu
|
||||||
syscpudiff := system - c.lastSysCpu
|
syscpudiff := system - c.lastSysCpu
|
||||||
|
|
||||||
c.CPUUtil = round((cpudiff / syscpudiff * 100) * ncpus)
|
if c.scaleCpu {
|
||||||
|
c.CPUUtil = round((cpudiff / syscpudiff * 100))
|
||||||
|
} else {
|
||||||
|
c.CPUUtil = round((cpudiff / syscpudiff * 100) * ncpus)
|
||||||
|
}
|
||||||
c.lastCpu = total
|
c.lastCpu = total
|
||||||
c.lastSysCpu = system
|
c.lastSysCpu = system
|
||||||
|
c.Pids = int(stats.PidsStats.Current)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Docker) ReadMem(stats *api.Stats) {
|
func (c *Docker) ReadMem(stats *api.Stats) {
|
||||||
c.MemUsage = int64(stats.MemoryStats.Usage)
|
c.MemUsage = int64(stats.MemoryStats.Usage - stats.MemoryStats.Stats.Cache)
|
||||||
c.MemLimit = int64(stats.MemoryStats.Limit)
|
c.MemLimit = int64(stats.MemoryStats.Limit)
|
||||||
c.MemPercent = round((float64(c.MemUsage) / float64(c.MemLimit)) * 100)
|
c.MemPercent = percent(float64(c.MemUsage), float64(c.MemLimit))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Docker) ReadNet(stats *api.Stats) {
|
func (c *Docker) ReadNet(stats *api.Stats) {
|
||||||
@@ -95,3 +109,16 @@ func (c *Docker) ReadNet(stats *api.Stats) {
|
|||||||
}
|
}
|
||||||
c.NetRx, c.NetTx = rx, tx
|
c.NetRx, c.NetTx = rx, tx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Docker) ReadIO(stats *api.Stats) {
|
||||||
|
var read, write int64
|
||||||
|
for _, blk := range stats.BlkioStats.IOServiceBytesRecursive {
|
||||||
|
if blk.Op == "Read" {
|
||||||
|
read = int64(blk.Value)
|
||||||
|
}
|
||||||
|
if blk.Op == "Write" {
|
||||||
|
write = int64(blk.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.IOBytesRead, c.IOBytesWrite = read, write
|
||||||
|
}
|
||||||
82
connector/collector/docker_logs.go
Normal file
82
connector/collector/docker_logs.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
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{Timestamp: ts, Message: 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() {
|
||||||
|
<-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
|
||||||
|
}
|
||||||
128
connector/collector/kubernetes.go
Normal file
128
connector/collector/kubernetes.go
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
package collector
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"k8s.io/metrics/pkg/apis/metrics/v1alpha1"
|
||||||
|
clientset "k8s.io/metrics/pkg/client/clientset/versioned"
|
||||||
|
|
||||||
|
"github.com/bcicen/ctop/config"
|
||||||
|
"github.com/bcicen/ctop/models"
|
||||||
|
"k8s.io/api/core/v1"
|
||||||
|
|
||||||
|
"k8s.io/client-go/kubernetes"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Kubernetes collector
|
||||||
|
type Kubernetes struct {
|
||||||
|
models.Metrics
|
||||||
|
name string
|
||||||
|
client clientset.Interface
|
||||||
|
clientset *kubernetes.Clientset
|
||||||
|
running bool
|
||||||
|
stream chan models.Metrics
|
||||||
|
done chan bool
|
||||||
|
lastCpu float64
|
||||||
|
lastSysCpu float64
|
||||||
|
scaleCpu bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewKubernetes(client *kubernetes.Clientset, name string) *Kubernetes {
|
||||||
|
return &Kubernetes{
|
||||||
|
Metrics: models.Metrics{},
|
||||||
|
name: name,
|
||||||
|
client: clientset.New(client.RESTClient()),
|
||||||
|
clientset: client,
|
||||||
|
scaleCpu: config.GetSwitchVal("scaleCpu"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Kubernetes) Start() {
|
||||||
|
k.done = make(chan bool)
|
||||||
|
k.stream = make(chan models.Metrics)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
k.running = false
|
||||||
|
for {
|
||||||
|
|
||||||
|
result := &v1alpha1.PodMetrics{}
|
||||||
|
err := k.clientset.RESTClient().Get().AbsPath("/api/v1/namespaces/kube-system/services/http:heapster:/proxy/apis/metrics/v1alpha1/namespaces/" + config.GetVal("namespace") + "/pods/" + k.name).Do().Into(result)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("has error %s here %s", k.name, err.Error())
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
k.ReadCPU(result)
|
||||||
|
k.ReadMem(result)
|
||||||
|
k.stream <- k.Metrics
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
k.running = true
|
||||||
|
log.Infof("collector started for container: %s", k.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Kubernetes) Running() bool {
|
||||||
|
return c.running
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Kubernetes) Stream() chan models.Metrics {
|
||||||
|
return c.stream
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Kubernetes) Logs() LogCollector {
|
||||||
|
return NewKubernetesLogs(c.name, c.clientset)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop collector
|
||||||
|
func (c *Kubernetes) Stop() {
|
||||||
|
c.done <- true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Kubernetes) ReadCPU(metrics *v1alpha1.PodMetrics) {
|
||||||
|
all := int64(0)
|
||||||
|
for _, c := range metrics.Containers {
|
||||||
|
v := c.Usage[v1.ResourceCPU]
|
||||||
|
all += v.Value()
|
||||||
|
}
|
||||||
|
if all != 0 {
|
||||||
|
k.CPUUtil = round(float64(all))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Kubernetes) ReadMem(metrics *v1alpha1.PodMetrics) {
|
||||||
|
all := int64(0)
|
||||||
|
for _, c := range metrics.Containers {
|
||||||
|
v := c.Usage[v1.ResourceMemory]
|
||||||
|
a, ok := v.AsInt64()
|
||||||
|
if ok {
|
||||||
|
all += a
|
||||||
|
}
|
||||||
|
}
|
||||||
|
k.MemUsage = all
|
||||||
|
k.MemLimit = int64(0)
|
||||||
|
//k.MemPercent = percent(float64(k.MemUsage), float64(k.MemLimit))
|
||||||
|
}
|
||||||
|
|
||||||
|
//func (c *Kubernetes) ReadNet(stats *api.Stats) {
|
||||||
|
// var rx, tx int64
|
||||||
|
// for _, network := range stats.Networks {
|
||||||
|
// rx += int64(network.RxBytes)
|
||||||
|
// tx += int64(network.TxBytes)
|
||||||
|
// }
|
||||||
|
// c.NetRx, c.NetTx = rx, tx
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//func (c *Kubernetes) ReadIO(stats *api.Stats) {
|
||||||
|
// var read, write int64
|
||||||
|
// for _, blk := range stats.BlkioStats.IOServiceBytesRecursive {
|
||||||
|
// if blk.Op == "Read" {
|
||||||
|
// read = int64(blk.Value)
|
||||||
|
// }
|
||||||
|
// if blk.Op == "Write" {
|
||||||
|
// write = int64(blk.Value)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// c.IOBytesRead, c.IOBytesWrite = read, write
|
||||||
|
//}
|
||||||
78
connector/collector/kubernetes_logs.go
Normal file
78
connector/collector/kubernetes_logs.go
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
package collector
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bcicen/ctop/models"
|
||||||
|
"k8s.io/client-go/kubernetes"
|
||||||
|
)
|
||||||
|
|
||||||
|
type KubernetesLogs struct {
|
||||||
|
id string
|
||||||
|
client *kubernetes.Clientset
|
||||||
|
done chan bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewKubernetesLogs(id string, client *kubernetes.Clientset) *KubernetesLogs {
|
||||||
|
return &KubernetesLogs{
|
||||||
|
id: id,
|
||||||
|
client: client,
|
||||||
|
done: make(chan bool),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *KubernetesLogs) 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{Timestamp: ts, Message: 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() {
|
||||||
|
// <-l.done
|
||||||
|
// cancel()
|
||||||
|
//}()
|
||||||
|
|
||||||
|
log.Infof("log reader started for container: %s", l.id)
|
||||||
|
return logCh
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *KubernetesLogs) Stop() { l.done <- true }
|
||||||
|
|
||||||
|
func (l *KubernetesLogs) 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)
|
||||||
|
}
|
||||||
83
connector/collector/mock.go
Normal file
83
connector/collector/mock.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
// +build !release
|
||||||
|
|
||||||
|
package collector
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bcicen/ctop/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mock collector
|
||||||
|
type Mock struct {
|
||||||
|
models.Metrics
|
||||||
|
stream chan models.Metrics
|
||||||
|
done bool
|
||||||
|
running bool
|
||||||
|
aggression int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMock(a int64) *Mock {
|
||||||
|
c := &Mock{
|
||||||
|
Metrics: models.Metrics{},
|
||||||
|
aggression: a,
|
||||||
|
}
|
||||||
|
c.MemLimit = 2147483648
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Mock) Running() bool {
|
||||||
|
return c.running
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Mock) Start() {
|
||||||
|
c.done = false
|
||||||
|
c.stream = make(chan models.Metrics)
|
||||||
|
go c.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Mock) Stop() {
|
||||||
|
c.done = true
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
c.CPUUtil = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
c.NetTx += rand.Int63n(60) * c.aggression
|
||||||
|
c.NetRx += rand.Int63n(60) * c.aggression
|
||||||
|
c.MemUsage += rand.Int63n(c.MemLimit/512) * c.aggression
|
||||||
|
if c.MemUsage > c.MemLimit {
|
||||||
|
c.MemUsage = 0
|
||||||
|
}
|
||||||
|
c.MemPercent = percent(float64(c.MemUsage), float64(c.MemLimit))
|
||||||
|
c.stream <- c.Metrics
|
||||||
|
if c.done {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.running = false
|
||||||
|
}
|
||||||
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{Timestamp: time.Now(), Message: 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
|
||||||
|
}
|
||||||
196
connector/docker.go
Normal file
196
connector/docker.go
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
package connector
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/bcicen/ctop/connector/collector"
|
||||||
|
"github.com/bcicen/ctop/connector/manager"
|
||||||
|
"github.com/bcicen/ctop/container"
|
||||||
|
api "github.com/fsouza/go-dockerclient"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() { enabled["docker"] = NewDocker }
|
||||||
|
|
||||||
|
type Docker struct {
|
||||||
|
client *api.Client
|
||||||
|
containers map[string]*container.Container
|
||||||
|
needsRefresh chan string // container IDs requiring refresh
|
||||||
|
lock sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDocker() Connector {
|
||||||
|
// init docker client
|
||||||
|
client, err := api.NewClientFromEnv()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
cm := &Docker{
|
||||||
|
client: client,
|
||||||
|
containers: make(map[string]*container.Container),
|
||||||
|
needsRefresh: make(chan string, 60),
|
||||||
|
lock: sync.RWMutex{},
|
||||||
|
}
|
||||||
|
go cm.Loop()
|
||||||
|
cm.refreshAll()
|
||||||
|
go cm.watchEvents()
|
||||||
|
return cm
|
||||||
|
}
|
||||||
|
|
||||||
|
// Docker events watcher
|
||||||
|
func (cm *Docker) watchEvents() {
|
||||||
|
log.Info("docker event listener starting")
|
||||||
|
events := make(chan *api.APIEvents)
|
||||||
|
cm.client.AddEventListener(events)
|
||||||
|
|
||||||
|
for e := range events {
|
||||||
|
if e.Type != "container" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
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":
|
||||||
|
log.Debugf("handling docker event: action=%s id=%s", e.Action, e.ID)
|
||||||
|
cm.delByID(e.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ipsFormat(networks map[string]api.ContainerNetwork) string {
|
||||||
|
var ips []string
|
||||||
|
|
||||||
|
for k, v := range networks {
|
||||||
|
s := fmt.Sprintf("%s:%s", k, v.IPAddress)
|
||||||
|
ips = append(ips, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(ips, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cm *Docker) refresh(c *container.Container) {
|
||||||
|
insp := cm.inspect(c.Id)
|
||||||
|
// remove container if no longer exists
|
||||||
|
if insp == nil {
|
||||||
|
cm.delByID(c.Id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.SetMeta("name", shortName(insp.Name))
|
||||||
|
c.SetMeta("image", insp.Config.Image)
|
||||||
|
c.SetMeta("IPs", ipsFormat(insp.NetworkSettings.Networks))
|
||||||
|
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)
|
||||||
|
for _, env := range insp.Config.Env {
|
||||||
|
c.SetMeta("[ENV-VAR]", env)
|
||||||
|
}
|
||||||
|
c.SetState(insp.State.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cm *Docker) inspect(id string) *api.Container {
|
||||||
|
c, err := cm.client.InspectContainer(id)
|
||||||
|
if err != nil {
|
||||||
|
if _, ok := err.(*api.NoSuchContainer); !ok {
|
||||||
|
log.Errorf(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark all container IDs for refresh
|
||||||
|
func (cm *Docker) refreshAll() {
|
||||||
|
opts := api.ListContainersOptions{All: true}
|
||||||
|
allContainers, err := cm.client.ListContainers(opts)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, i := range allContainers {
|
||||||
|
c := cm.MustGet(i.ID)
|
||||||
|
c.SetMeta("name", shortName(i.Names[0]))
|
||||||
|
c.SetState(i.State)
|
||||||
|
cm.needsRefresh <- c.Id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cm *Docker) Loop() {
|
||||||
|
for id := range cm.needsRefresh {
|
||||||
|
c := cm.MustGet(id)
|
||||||
|
cm.refresh(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a single container, creating one anew if not existing
|
||||||
|
func (cm *Docker) MustGet(id string) *container.Container {
|
||||||
|
c, ok := cm.Get(id)
|
||||||
|
// append container struct for new containers
|
||||||
|
if !ok {
|
||||||
|
// create collector
|
||||||
|
collector := collector.NewDocker(cm.client, id)
|
||||||
|
// create manager
|
||||||
|
manager := manager.NewDocker(cm.client, id)
|
||||||
|
// create container
|
||||||
|
c = container.New(id, collector, manager)
|
||||||
|
cm.lock.Lock()
|
||||||
|
cm.containers[id] = c
|
||||||
|
cm.lock.Unlock()
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a single container, by ID
|
||||||
|
func (cm *Docker) Get(id string) (*container.Container, bool) {
|
||||||
|
cm.lock.Lock()
|
||||||
|
c, ok := cm.containers[id]
|
||||||
|
cm.lock.Unlock()
|
||||||
|
return c, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove containers by ID
|
||||||
|
func (cm *Docker) delByID(id string) {
|
||||||
|
cm.lock.Lock()
|
||||||
|
delete(cm.containers, id)
|
||||||
|
cm.lock.Unlock()
|
||||||
|
log.Infof("removed dead container: %s", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return array of all containers, sorted by field
|
||||||
|
func (cm *Docker) 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// use primary container name
|
||||||
|
func shortName(name string) string {
|
||||||
|
return strings.Replace(name, "/", "", 1)
|
||||||
|
}
|
||||||
218
connector/kubernetes.go
Normal file
218
connector/kubernetes.go
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
package connector
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
bcfg "github.com/bcicen/ctop/config"
|
||||||
|
"github.com/bcicen/ctop/connector/collector"
|
||||||
|
"github.com/bcicen/ctop/connector/manager"
|
||||||
|
"github.com/bcicen/ctop/container"
|
||||||
|
api "github.com/fsouza/go-dockerclient"
|
||||||
|
"k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/client-go/kubernetes"
|
||||||
|
"k8s.io/client-go/tools/clientcmd"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() { enabled["kubernetes"] = NewKubernetes }
|
||||||
|
|
||||||
|
type Kubernetes struct {
|
||||||
|
namespace string
|
||||||
|
clientset *kubernetes.Clientset
|
||||||
|
containers map[string]*container.Container
|
||||||
|
needsRefresh chan string // container IDs requiring refresh
|
||||||
|
lock sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewKubernetes() Connector {
|
||||||
|
var kubeconfig string
|
||||||
|
//if home := homeDir(); home != "" {
|
||||||
|
// kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file")
|
||||||
|
//} else {
|
||||||
|
// kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file")
|
||||||
|
//}
|
||||||
|
//flag.Parse()
|
||||||
|
kubeconfig = filepath.Join(homeDir(), ".kube", "config")
|
||||||
|
|
||||||
|
// use the current context in kubeconfig
|
||||||
|
config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err.Error())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the clientset
|
||||||
|
clientset, err := kubernetes.NewForConfig(config)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err.Error())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// init docker client
|
||||||
|
k := &Kubernetes{
|
||||||
|
clientset: clientset,
|
||||||
|
containers: make(map[string]*container.Container),
|
||||||
|
needsRefresh: make(chan string, 60),
|
||||||
|
lock: sync.RWMutex{},
|
||||||
|
namespace: bcfg.GetVal("namespace"),
|
||||||
|
}
|
||||||
|
go k.Loop()
|
||||||
|
k.refreshAll()
|
||||||
|
go k.watchEvents()
|
||||||
|
return k
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Kubernetes) watchEvents() {
|
||||||
|
for {
|
||||||
|
log.Info("kubernetes event listener starting")
|
||||||
|
allEvents, err := k.clientset.CoreV1().Events(k.namespace).List(metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, e := range allEvents.Items {
|
||||||
|
if e.Kind != "pod" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
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.UID)
|
||||||
|
k.needsRefresh <- e.Name
|
||||||
|
case "destroy":
|
||||||
|
log.Debugf("handling docker event: action=%s id=%s", e.Action, e.UID)
|
||||||
|
k.delByID(e.Name)
|
||||||
|
default:
|
||||||
|
log.Debugf("handling docker event: %v", e)
|
||||||
|
k.needsRefresh <- e.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (k *Kubernetes) Loop() {
|
||||||
|
for id := range k.needsRefresh {
|
||||||
|
c := k.MustGet(id)
|
||||||
|
k.refresh(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a single container, creating one anew if not existing
|
||||||
|
func (k *Kubernetes) MustGet(name string) *container.Container {
|
||||||
|
c, ok := k.Get(name)
|
||||||
|
// append container struct for new containers
|
||||||
|
if !ok {
|
||||||
|
// create collector
|
||||||
|
collector := collector.NewKubernetes(k.clientset, name)
|
||||||
|
// create manager
|
||||||
|
manager := manager.NewKubernetes(k.clientset, name)
|
||||||
|
// create container
|
||||||
|
c = container.New(name, collector, manager)
|
||||||
|
k.lock.Lock()
|
||||||
|
k.containers[name] = c
|
||||||
|
k.lock.Unlock()
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Kubernetes) refresh(c *container.Container) {
|
||||||
|
insp := k.inspect(c.Id)
|
||||||
|
// remove container if no longer exists
|
||||||
|
if insp == nil {
|
||||||
|
k.delByID(c.Id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.SetMeta("name", insp.Name)
|
||||||
|
if len(insp.Spec.Containers) >= 1 {
|
||||||
|
c.SetMeta("image", insp.Spec.Containers[0].Image)
|
||||||
|
c.SetMeta("ports", k8sPort(insp.Spec.Containers[0].Ports))
|
||||||
|
for _, env := range insp.Spec.Containers[0].Env {
|
||||||
|
c.SetMeta("[ENV-VAR]", env.Name+"="+env.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.SetMeta("IPs", insp.Status.PodIP)
|
||||||
|
c.SetMeta("created", insp.CreationTimestamp.Format("Mon Jan 2 15:04:05 2006"))
|
||||||
|
c.SetMeta("health", string(insp.Status.Phase))
|
||||||
|
c.SetState("running")
|
||||||
|
}
|
||||||
|
|
||||||
|
func k8sPort(ports []v1.ContainerPort) string {
|
||||||
|
str := []string{}
|
||||||
|
for _, p := range ports {
|
||||||
|
str = append(str, fmt.Sprintf("%s:%d -> %d", p.HostIP, p.HostPort, p.ContainerPort))
|
||||||
|
}
|
||||||
|
return strings.Join(str, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Kubernetes) inspect(id string) *v1.Pod {
|
||||||
|
p, err := k.clientset.CoreV1().Pods(k.namespace).Get(id, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
if _, ok := err.(*api.NoSuchContainer); !ok {
|
||||||
|
log.Errorf(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove containers by ID
|
||||||
|
func (k *Kubernetes) delByID(name string) {
|
||||||
|
k.lock.Lock()
|
||||||
|
delete(k.containers, name)
|
||||||
|
k.lock.Unlock()
|
||||||
|
log.Infof("removed dead container: %s", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Kubernetes) Get(name string) (c *container.Container, ok bool) {
|
||||||
|
k.lock.Lock()
|
||||||
|
c, ok = k.containers[name]
|
||||||
|
k.lock.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark all container IDs for refresh
|
||||||
|
func (k *Kubernetes) refreshAll() {
|
||||||
|
allPods, err := k.clientset.CoreV1().Pods(k.namespace).List(metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pod := range allPods.Items {
|
||||||
|
c := k.MustGet(pod.Name)
|
||||||
|
c.SetMeta("uid", string(pod.UID))
|
||||||
|
c.SetMeta("name", pod.Name)
|
||||||
|
if pod.Initializers != nil && pod.Initializers.Result != nil {
|
||||||
|
c.SetState(pod.Initializers.Result.Status)
|
||||||
|
} else {
|
||||||
|
c.SetState(string(pod.Status.Phase))
|
||||||
|
}
|
||||||
|
k.needsRefresh <- c.Id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Kubernetes) All() (containers container.Containers) {
|
||||||
|
k.lock.Lock()
|
||||||
|
for _, c := range k.containers {
|
||||||
|
containers = append(containers, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
containers.Sort()
|
||||||
|
containers.Filter()
|
||||||
|
k.lock.Unlock()
|
||||||
|
return containers
|
||||||
|
}
|
||||||
|
|
||||||
|
func homeDir() string {
|
||||||
|
if h := os.Getenv("HOME"); h != "" {
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
return os.Getenv("USERPROFILE") // windows
|
||||||
|
}
|
||||||
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)
|
||||||
|
}
|
||||||
65
connector/manager/docker.go
Normal file
65
connector/manager/docker.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dc *Docker) Pause() error {
|
||||||
|
if err := dc.client.PauseContainer(dc.id); err != nil {
|
||||||
|
return fmt.Errorf("cannot pause container: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dc *Docker) Unpause() error {
|
||||||
|
if err := dc.client.UnpauseContainer(dc.id); err != nil {
|
||||||
|
return fmt.Errorf("cannot unpause container: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dc *Docker) Restart() error {
|
||||||
|
if err := dc.client.RestartContainer(dc.id, 3); err != nil {
|
||||||
|
return fmt.Errorf("cannot restart container: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
64
connector/manager/kubernetes.go
Normal file
64
connector/manager/kubernetes.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package manager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"k8s.io/client-go/kubernetes"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Kubernetes struct {
|
||||||
|
id string
|
||||||
|
client *kubernetes.Clientset
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewKubernetes(client *kubernetes.Clientset, id string) *Kubernetes {
|
||||||
|
return &Kubernetes{
|
||||||
|
id: id,
|
||||||
|
client: client,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dc *Kubernetes) 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 *Kubernetes) Stop() error {
|
||||||
|
//if err := dc.client.StopContainer(dc.id, 3); err != nil {
|
||||||
|
// return fmt.Errorf("cannot stop container: %v", err)
|
||||||
|
//}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dc *Kubernetes) Remove() error {
|
||||||
|
//if err := dc.client.RemoveContainer(api.RemoveContainerOptions{ID: dc.id}); err != nil {
|
||||||
|
// return fmt.Errorf("cannot remove container: %v", err)
|
||||||
|
//}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dc *Kubernetes) Pause() error {
|
||||||
|
//if err := dc.client.PauseContainer(dc.id); err != nil {
|
||||||
|
// return fmt.Errorf("cannot pause container: %v", err)
|
||||||
|
//}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dc *Kubernetes) Unpause() error {
|
||||||
|
//if err := dc.client.UnpauseContainer(dc.id); err != nil {
|
||||||
|
// return fmt.Errorf("cannot unpause container: %v", err)
|
||||||
|
//}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dc *Kubernetes) Restart() error {
|
||||||
|
//if err := dc.client.RestartContainer(dc.id, 3); err != nil {
|
||||||
|
// return fmt.Errorf("cannot restart container: %v", err)
|
||||||
|
//}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
10
connector/manager/main.go
Normal file
10
connector/manager/main.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package manager
|
||||||
|
|
||||||
|
type Manager interface {
|
||||||
|
Start() error
|
||||||
|
Stop() error
|
||||||
|
Remove() error
|
||||||
|
Pause() error
|
||||||
|
Unpause() error
|
||||||
|
Restart() error
|
||||||
|
}
|
||||||
31
connector/manager/mock.go
Normal file
31
connector/manager/mock.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mock) Pause() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mock) Unpause() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mock) Restart() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
31
connector/manager/runc.go
Normal file
31
connector/manager/runc.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *Runc) Pause() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *Runc) Unpause() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *Runc) Restart() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -1,46 +1,56 @@
|
|||||||
// +build !release
|
// +build !release
|
||||||
|
|
||||||
package main
|
package connector
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bcicen/ctop/metrics"
|
"github.com/bcicen/ctop/connector/collector"
|
||||||
|
"github.com/bcicen/ctop/connector/manager"
|
||||||
|
"github.com/bcicen/ctop/container"
|
||||||
"github.com/jgautheron/codename-generator"
|
"github.com/jgautheron/codename-generator"
|
||||||
"github.com/nu7hatch/gouuid"
|
"github.com/nu7hatch/gouuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MockContainerSource struct {
|
func init() { enabled["mock"] = NewMock }
|
||||||
containers Containers
|
|
||||||
|
type Mock struct {
|
||||||
|
containers container.Containers
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMockContainerSource() *MockContainerSource {
|
func NewMock() Connector {
|
||||||
cs := &MockContainerSource{}
|
cs := &Mock{}
|
||||||
go cs.Init()
|
go cs.Init()
|
||||||
go cs.Loop()
|
go cs.Loop()
|
||||||
return cs
|
return cs
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create Mock containers
|
// Create Mock containers
|
||||||
func (cs *MockContainerSource) Init() {
|
func (cs *Mock) Init() {
|
||||||
total := 20
|
|
||||||
rand.Seed(int64(time.Now().Nanosecond()))
|
rand.Seed(int64(time.Now().Nanosecond()))
|
||||||
|
|
||||||
for i := 0; i < total; i++ {
|
for i := 0; i < 4; i++ {
|
||||||
//time.Sleep(1 * time.Second)
|
cs.makeContainer(3)
|
||||||
collector := metrics.NewMock()
|
}
|
||||||
c := NewContainer(makeID(), collector)
|
|
||||||
c.SetMeta("name", makeName())
|
for i := 0; i < 16; i++ {
|
||||||
c.SetState(makeState())
|
cs.makeContainer(1)
|
||||||
cs.containers = append(cs.containers, c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cs *MockContainerSource) Loop() {
|
func (cs *Mock) makeContainer(aggression int64) {
|
||||||
|
collector := collector.NewMock(aggression)
|
||||||
|
manager := manager.NewMock()
|
||||||
|
c := container.New(makeID(), collector, manager)
|
||||||
|
c.SetMeta("name", makeName())
|
||||||
|
c.SetState(makeState())
|
||||||
|
cs.containers = append(cs.containers, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *Mock) Loop() {
|
||||||
iter := 0
|
iter := 0
|
||||||
for {
|
for {
|
||||||
// Change state for random container
|
// Change state for random container
|
||||||
@@ -54,7 +64,7 @@ func (cs *MockContainerSource) Loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get a single container, by ID
|
// Get a single container, by ID
|
||||||
func (cs *MockContainerSource) Get(id string) (*Container, bool) {
|
func (cs *Mock) Get(id string) (*container.Container, bool) {
|
||||||
for _, c := range cs.containers {
|
for _, c := range cs.containers {
|
||||||
if c.Id == id {
|
if c.Id == id {
|
||||||
return c, true
|
return c, true
|
||||||
@@ -64,13 +74,14 @@ func (cs *MockContainerSource) Get(id string) (*Container, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Return array of all containers, sorted by field
|
// Return array of all containers, sorted by field
|
||||||
func (cs *MockContainerSource) All() Containers {
|
func (cs *Mock) All() container.Containers {
|
||||||
sort.Sort(cs.containers)
|
cs.containers.Sort()
|
||||||
|
cs.containers.Filter()
|
||||||
return cs.containers
|
return cs.containers
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove containers by ID
|
// Remove containers by ID
|
||||||
func (cs *MockContainerSource) delByID(id string) {
|
func (cs *Mock) delByID(id string) {
|
||||||
for n, c := range cs.containers {
|
for n, c := range cs.containers {
|
||||||
if c.Id == id {
|
if c.Id == id {
|
||||||
cs.del(n)
|
cs.del(n)
|
||||||
@@ -80,7 +91,7 @@ func (cs *MockContainerSource) delByID(id string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Remove one or more containers by index
|
// Remove one or more containers by index
|
||||||
func (cs *MockContainerSource) del(idx ...int) {
|
func (cs *Mock) del(idx ...int) {
|
||||||
for _, i := range idx {
|
for _, i := range idx {
|
||||||
cs.containers = append(cs.containers[:i], cs.containers[i+1:]...)
|
cs.containers = append(cs.containers[:i], cs.containers[i+1:]...)
|
||||||
}
|
}
|
||||||
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
75
container.go
75
container.go
@@ -1,75 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/bcicen/ctop/cwidgets"
|
|
||||||
"github.com/bcicen/ctop/cwidgets/compact"
|
|
||||||
"github.com/bcicen/ctop/metrics"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Metrics and metadata representing a container
|
|
||||||
type Container struct {
|
|
||||||
metrics.Metrics
|
|
||||||
Id string
|
|
||||||
Meta map[string]string
|
|
||||||
Widgets *compact.Compact
|
|
||||||
updater cwidgets.WidgetUpdater
|
|
||||||
collector metrics.Collector
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewContainer(id string, collector metrics.Collector) *Container {
|
|
||||||
widgets := compact.NewCompact(id)
|
|
||||||
return &Container{
|
|
||||||
Metrics: metrics.NewMetrics(),
|
|
||||||
Id: id,
|
|
||||||
Meta: make(map[string]string),
|
|
||||||
Widgets: widgets,
|
|
||||||
updater: widgets,
|
|
||||||
collector: collector,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Container) SetUpdater(u cwidgets.WidgetUpdater) {
|
|
||||||
c.updater = u
|
|
||||||
for k, v := range c.Meta {
|
|
||||||
c.updater.SetMeta(k, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Container) SetMeta(k, v string) {
|
|
||||||
c.Meta[k] = v
|
|
||||||
c.updater.SetMeta(k, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Container) GetMeta(k string) string {
|
|
||||||
if v, ok := c.Meta[k]; ok {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Container) SetState(s string) {
|
|
||||||
c.SetMeta("state", s)
|
|
||||||
// start collector, if needed
|
|
||||||
if s == "running" && !c.collector.Running() {
|
|
||||||
c.collector.Start()
|
|
||||||
c.Read(c.collector.Stream())
|
|
||||||
}
|
|
||||||
// stop collector, if needed
|
|
||||||
if s != "running" && c.collector.Running() {
|
|
||||||
c.collector.Stop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read metric stream, updating widgets
|
|
||||||
func (c *Container) Read(stream chan metrics.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.Widgets.Reset()
|
|
||||||
}()
|
|
||||||
log.Infof("reader started for container: %s", c.Id)
|
|
||||||
}
|
|
||||||
155
container/main.go
Normal file
155
container/main.go
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
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/logging"
|
||||||
|
"github.com/bcicen/ctop/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
log = logging.Init()
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
running = "running"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Metrics and metadata representing a container
|
||||||
|
type Container struct {
|
||||||
|
models.Metrics
|
||||||
|
Id string
|
||||||
|
Meta map[string]string
|
||||||
|
Widgets *compact.Compact
|
||||||
|
Display bool // display this container in compact view
|
||||||
|
updater cwidgets.WidgetUpdater
|
||||||
|
collector collector.Collector
|
||||||
|
manager manager.Manager
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(id string, collector collector.Collector, manager manager.Manager) *Container {
|
||||||
|
widgets := compact.NewCompact(id)
|
||||||
|
return &Container{
|
||||||
|
Metrics: models.NewMetrics(),
|
||||||
|
Id: id,
|
||||||
|
Meta: make(map[string]string),
|
||||||
|
Widgets: widgets,
|
||||||
|
updater: widgets,
|
||||||
|
collector: collector,
|
||||||
|
manager: manager,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Container) SetUpdater(u cwidgets.WidgetUpdater) {
|
||||||
|
c.updater = u
|
||||||
|
for k, v := range c.Meta {
|
||||||
|
c.updater.SetMeta(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Container) SetMeta(k, v string) {
|
||||||
|
c.Meta[k] = v
|
||||||
|
c.updater.SetMeta(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Container) GetMeta(k string) string {
|
||||||
|
if v, ok := c.Meta[k]; ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Container) SetState(s string) {
|
||||||
|
c.SetMeta("state", s)
|
||||||
|
// start collector, if needed
|
||||||
|
if s == running && !c.collector.Running() {
|
||||||
|
c.collector.Start()
|
||||||
|
c.Read(c.collector.Stream())
|
||||||
|
}
|
||||||
|
// stop collector, if needed
|
||||||
|
if s != running && c.collector.Running() {
|
||||||
|
c.collector.Stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 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 = 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Container) Pause() {
|
||||||
|
if c.Meta["state"] == running {
|
||||||
|
if err := c.manager.Pause(); err != nil {
|
||||||
|
log.Warningf("container %s: %v", c.Id, err)
|
||||||
|
log.StatusErr(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.SetState("paused")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Container) Unpause() {
|
||||||
|
if c.Meta["state"] == "paused" {
|
||||||
|
if err := c.manager.Unpause(); err != nil {
|
||||||
|
log.Warningf("container %s: %v", c.Id, err)
|
||||||
|
log.StatusErr(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.SetState(running)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Container) Restart() {
|
||||||
|
if c.Meta["state"] == running {
|
||||||
|
if err := c.manager.Restart(); err != nil {
|
||||||
|
log.Warningf("container %s: %v", c.Id, err)
|
||||||
|
log.StatusErr(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
package main
|
package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
|
||||||
"github.com/bcicen/ctop/config"
|
"github.com/bcicen/ctop/config"
|
||||||
)
|
)
|
||||||
@@ -53,6 +54,22 @@ var Sorters = map[string]sortMethod{
|
|||||||
}
|
}
|
||||||
return sum1 > sum2
|
return sum1 > sum2
|
||||||
},
|
},
|
||||||
|
"pids": func(c1, c2 *Container) bool {
|
||||||
|
// Use secondary sort method if equal values
|
||||||
|
if c1.Pids == c2.Pids {
|
||||||
|
return nameSorter(c1, c2)
|
||||||
|
}
|
||||||
|
return c1.Pids > c2.Pids
|
||||||
|
},
|
||||||
|
"io": func(c1, c2 *Container) bool {
|
||||||
|
sum1 := sumIO(c1)
|
||||||
|
sum2 := sumIO(c2)
|
||||||
|
// Use secondary sort method if equal values
|
||||||
|
if sum1 == sum2 {
|
||||||
|
return nameSorter(c1, c2)
|
||||||
|
}
|
||||||
|
return sum1 > sum2
|
||||||
|
},
|
||||||
"state": func(c1, c2 *Container) bool {
|
"state": func(c1, c2 *Container) bool {
|
||||||
// Use secondary sort method if equal values
|
// Use secondary sort method if equal values
|
||||||
c1state := c1.GetMeta("state")
|
c1state := c1.GetMeta("state")
|
||||||
@@ -73,6 +90,7 @@ func SortFields() (fields []string) {
|
|||||||
|
|
||||||
type Containers []*Container
|
type Containers []*Container
|
||||||
|
|
||||||
|
func (a Containers) Sort() { sort.Sort(a) }
|
||||||
func (a Containers) Len() int { return len(a) }
|
func (a Containers) Len() int { return len(a) }
|
||||||
func (a Containers) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
func (a Containers) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||||
func (a Containers) Less(i, j int) bool {
|
func (a Containers) Less(i, j int) bool {
|
||||||
@@ -83,23 +101,23 @@ func (a Containers) Less(i, j int) bool {
|
|||||||
return f(a[i], a[j])
|
return f(a[i], a[j])
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a Containers) Filter() (filtered []*Container) {
|
func (a Containers) Filter() {
|
||||||
filter := config.GetVal("filterStr")
|
filter := config.GetVal("filterStr")
|
||||||
re := regexp.MustCompile(fmt.Sprintf(".*%s", filter))
|
re := regexp.MustCompile(fmt.Sprintf(".*%s", filter))
|
||||||
|
|
||||||
for _, c := range a {
|
for _, c := range a {
|
||||||
|
c.Display = true
|
||||||
// Apply name filter
|
// Apply name filter
|
||||||
if re.FindAllString(c.GetMeta("name"), 1) == nil {
|
if re.FindAllString(c.GetMeta("name"), 1) == nil {
|
||||||
continue
|
c.Display = false
|
||||||
}
|
}
|
||||||
// Apply state filter
|
// Apply state filter
|
||||||
if !config.GetSwitchVal("allContainers") && c.GetMeta("state") != "running" {
|
if !config.GetSwitchVal("allContainers") && c.GetMeta("state") != "running" {
|
||||||
continue
|
c.Display = false
|
||||||
}
|
}
|
||||||
filtered = append(filtered, c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return filtered
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func sumNet(c *Container) int64 { return c.NetRx + c.NetTx }
|
func sumNet(c *Container) int64 { return c.NetRx + c.NetTx }
|
||||||
|
|
||||||
|
func sumIO(c *Container) int64 { return c.IOBytesRead + c.IOBytesWrite }
|
||||||
179
cursor.go
179
cursor.go
@@ -1,79 +1,192 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/bcicen/ctop/connector"
|
||||||
|
"github.com/bcicen/ctop/container"
|
||||||
ui "github.com/gizak/termui"
|
ui "github.com/gizak/termui"
|
||||||
)
|
)
|
||||||
|
|
||||||
type GridCursor struct {
|
type GridCursor struct {
|
||||||
selectedID string // id of currently selected container
|
selectedID string // id of currently selected container
|
||||||
containers Containers
|
filtered container.Containers
|
||||||
cSource ContainerSource
|
cSource connector.Connector
|
||||||
|
isScrolling bool // toggled when actively scrolling
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewGridCursor() *GridCursor {
|
func (gc *GridCursor) Len() int { return len(gc.filtered) }
|
||||||
return &GridCursor{
|
|
||||||
cSource: NewDockerContainerSource(),
|
func (gc *GridCursor) Selected() *container.Container {
|
||||||
|
idx := gc.Idx()
|
||||||
|
if idx < gc.Len() {
|
||||||
|
return gc.filtered[idx]
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gc *GridCursor) Len() int { return len(gc.containers) }
|
// Refresh containers from source
|
||||||
func (gc *GridCursor) Selected() *Container { return gc.containers[gc.Idx()] }
|
func (gc *GridCursor) RefreshContainers() (lenChanged bool) {
|
||||||
|
oldLen := gc.Len()
|
||||||
|
|
||||||
func (gc *GridCursor) RefreshContainers() {
|
// Containers filtered by display bool
|
||||||
gc.containers = gc.cSource.All().Filter()
|
gc.filtered = container.Containers{}
|
||||||
|
var cursorVisible bool
|
||||||
|
for _, c := range gc.cSource.All() {
|
||||||
|
if c.Display {
|
||||||
|
if c.Id == gc.selectedID {
|
||||||
|
cursorVisible = true
|
||||||
|
}
|
||||||
|
gc.filtered = append(gc.filtered, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if oldLen != gc.Len() {
|
||||||
|
lenChanged = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cursorVisible {
|
||||||
|
gc.Reset()
|
||||||
|
}
|
||||||
if gc.selectedID == "" {
|
if gc.selectedID == "" {
|
||||||
gc.Reset()
|
gc.Reset()
|
||||||
}
|
}
|
||||||
|
return lenChanged
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set an initial cursor position, if possible
|
// Set an initial cursor position, if possible
|
||||||
func (gc *GridCursor) Reset() {
|
func (gc *GridCursor) Reset() {
|
||||||
|
for _, c := range gc.cSource.All() {
|
||||||
|
c.Widgets.UnHighlight()
|
||||||
|
}
|
||||||
if gc.Len() > 0 {
|
if gc.Len() > 0 {
|
||||||
gc.selectedID = gc.containers[0].Id
|
gc.selectedID = gc.filtered[0].Id
|
||||||
gc.containers[0].Widgets.Name.Highlight()
|
gc.filtered[0].Widgets.Highlight()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return current cursor index
|
// Return current cursor index
|
||||||
func (gc *GridCursor) Idx() int {
|
func (gc *GridCursor) Idx() int {
|
||||||
for n, c := range gc.containers {
|
for n, c := range gc.filtered {
|
||||||
if c.Id == gc.selectedID {
|
if c.Id == gc.selectedID {
|
||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
gc.Reset()
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gc *GridCursor) Up() {
|
func (gc *GridCursor) ScrollPage() {
|
||||||
idx := gc.Idx()
|
// skip scroll if no need to page
|
||||||
// decrement if possible
|
if gc.Len() < cGrid.MaxRows() {
|
||||||
if idx <= 0 {
|
cGrid.Offset = 0
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
active := gc.containers[idx]
|
|
||||||
next := gc.containers[idx-1]
|
|
||||||
|
|
||||||
active.Widgets.Name.UnHighlight()
|
idx := gc.Idx()
|
||||||
|
|
||||||
|
// page down
|
||||||
|
if idx >= cGrid.Offset+cGrid.MaxRows() {
|
||||||
|
cGrid.Offset++
|
||||||
|
cGrid.Align()
|
||||||
|
}
|
||||||
|
// page up
|
||||||
|
if idx < cGrid.Offset {
|
||||||
|
cGrid.Offset--
|
||||||
|
cGrid.Align()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gc *GridCursor) Up() {
|
||||||
|
gc.isScrolling = true
|
||||||
|
defer func() { gc.isScrolling = false }()
|
||||||
|
|
||||||
|
idx := gc.Idx()
|
||||||
|
if idx <= 0 { // already at top
|
||||||
|
return
|
||||||
|
}
|
||||||
|
active := gc.filtered[idx]
|
||||||
|
next := gc.filtered[idx-1]
|
||||||
|
|
||||||
|
active.Widgets.UnHighlight()
|
||||||
gc.selectedID = next.Id
|
gc.selectedID = next.Id
|
||||||
next.Widgets.Name.Highlight()
|
next.Widgets.Highlight()
|
||||||
|
|
||||||
|
gc.ScrollPage()
|
||||||
ui.Render(cGrid)
|
ui.Render(cGrid)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gc *GridCursor) Down() {
|
func (gc *GridCursor) Down() {
|
||||||
idx := gc.Idx()
|
gc.isScrolling = true
|
||||||
// increment if possible
|
defer func() { gc.isScrolling = false }()
|
||||||
if idx >= (gc.Len() - 1) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if idx >= maxRows()-1 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
active := gc.containers[idx]
|
|
||||||
next := gc.containers[idx+1]
|
|
||||||
|
|
||||||
active.Widgets.Name.UnHighlight()
|
idx := gc.Idx()
|
||||||
|
if idx >= gc.Len()-1 { // already at bottom
|
||||||
|
return
|
||||||
|
}
|
||||||
|
active := gc.filtered[idx]
|
||||||
|
next := gc.filtered[idx+1]
|
||||||
|
|
||||||
|
active.Widgets.UnHighlight()
|
||||||
gc.selectedID = next.Id
|
gc.selectedID = next.Id
|
||||||
next.Widgets.Name.Highlight()
|
next.Widgets.Highlight()
|
||||||
|
|
||||||
|
gc.ScrollPage()
|
||||||
ui.Render(cGrid)
|
ui.Render(cGrid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (gc *GridCursor) PgUp() {
|
||||||
|
idx := gc.Idx()
|
||||||
|
if idx <= 0 { // already at top
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nextidx := int(math.Max(0.0, float64(idx-cGrid.MaxRows())))
|
||||||
|
if gc.pgCount() > 0 {
|
||||||
|
cGrid.Offset = int(math.Max(float64(cGrid.Offset-cGrid.MaxRows()),
|
||||||
|
float64(0)))
|
||||||
|
}
|
||||||
|
|
||||||
|
active := gc.filtered[idx]
|
||||||
|
next := gc.filtered[nextidx]
|
||||||
|
|
||||||
|
active.Widgets.UnHighlight()
|
||||||
|
gc.selectedID = next.Id
|
||||||
|
next.Widgets.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.UnHighlight()
|
||||||
|
gc.selectedID = next.Id
|
||||||
|
next.Widgets.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
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ func NewGaugeCol() *GaugeCol {
|
|||||||
g.Border = false
|
g.Border = false
|
||||||
g.Percent = 0
|
g.Percent = 0
|
||||||
g.PaddingBottom = 0
|
g.PaddingBottom = 0
|
||||||
g.BarColor = ui.ColorGreen
|
|
||||||
g.Label = "-"
|
g.Label = "-"
|
||||||
return &GaugeCol{g}
|
return &GaugeCol{g}
|
||||||
}
|
}
|
||||||
@@ -24,12 +23,22 @@ func (w *GaugeCol) Reset() {
|
|||||||
w.Percent = 0
|
w.Percent = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *GaugeCol) Highlight() {
|
||||||
|
w.Bg = ui.ThemeAttr("par.text.fg")
|
||||||
|
w.PercentColor = ui.ThemeAttr("par.text.hi")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *GaugeCol) UnHighlight() {
|
||||||
|
w.Bg = ui.ThemeAttr("par.text.bg")
|
||||||
|
w.PercentColor = ui.ThemeAttr("par.text.bg")
|
||||||
|
}
|
||||||
|
|
||||||
func colorScale(n int) ui.Attribute {
|
func colorScale(n int) ui.Attribute {
|
||||||
if n > 70 {
|
if n > 70 {
|
||||||
return ui.ColorRed
|
return ui.ThemeAttr("status.danger")
|
||||||
}
|
}
|
||||||
if n > 30 {
|
if n > 30 {
|
||||||
return ui.ColorYellow
|
return ui.ThemeAttr("status.warn")
|
||||||
}
|
}
|
||||||
return ui.ColorGreen
|
return ui.ThemeAttr("status.ok")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,51 +4,58 @@ import (
|
|||||||
ui "github.com/gizak/termui"
|
ui "github.com/gizak/termui"
|
||||||
)
|
)
|
||||||
|
|
||||||
var header = NewCompactHeader()
|
var header *CompactHeader
|
||||||
|
|
||||||
type CompactGrid struct {
|
type CompactGrid struct {
|
||||||
ui.GridBufferer
|
ui.GridBufferer
|
||||||
Rows []ui.GridBufferer
|
Rows []ui.GridBufferer
|
||||||
X, Y int
|
X, Y int
|
||||||
Width int
|
Width int
|
||||||
Height int
|
Height int
|
||||||
cursorID string
|
Offset int // starting row offset
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCompactGrid() *CompactGrid {
|
func NewCompactGrid() *CompactGrid {
|
||||||
|
header = NewCompactHeader() // init column header
|
||||||
return &CompactGrid{}
|
return &CompactGrid{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cg *CompactGrid) Align() {
|
func (cg *CompactGrid) Align() {
|
||||||
// update row y pos recursively
|
|
||||||
y := cg.Y
|
y := cg.Y
|
||||||
for _, r := range cg.Rows {
|
|
||||||
r.SetY(y)
|
if cg.Offset >= len(cg.Rows) || cg.Offset < 0 {
|
||||||
y += r.GetHeight()
|
cg.Offset = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// update row width recursively
|
// update row ypos, width recursively
|
||||||
for _, r := range cg.Rows {
|
for _, r := range cg.pageRows() {
|
||||||
|
r.SetY(y)
|
||||||
|
y += r.GetHeight()
|
||||||
r.SetWidth(cg.Width)
|
r.SetWidth(cg.Width)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cg *CompactGrid) Clear() { cg.Rows = []ui.GridBufferer{header} }
|
func (cg *CompactGrid) Clear() { cg.Rows = []ui.GridBufferer{} }
|
||||||
func (cg *CompactGrid) GetHeight() int { return len(cg.Rows) }
|
func (cg *CompactGrid) GetHeight() int { return len(cg.Rows) + header.Height }
|
||||||
func (cg *CompactGrid) SetX(x int) { cg.X = x }
|
func (cg *CompactGrid) SetX(x int) { cg.X = x }
|
||||||
func (cg *CompactGrid) SetY(y int) { cg.Y = y }
|
func (cg *CompactGrid) SetY(y int) { cg.Y = y }
|
||||||
func (cg *CompactGrid) SetWidth(w int) { cg.Width = w }
|
func (cg *CompactGrid) SetWidth(w int) { cg.Width = w }
|
||||||
|
func (cg *CompactGrid) MaxRows() int { return ui.TermHeight() - header.Height - cg.Y }
|
||||||
|
|
||||||
|
func (cg *CompactGrid) pageRows() (rows []ui.GridBufferer) {
|
||||||
|
rows = append(rows, header)
|
||||||
|
rows = append(rows, cg.Rows[cg.Offset:]...)
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
func (cg *CompactGrid) Buffer() ui.Buffer {
|
func (cg *CompactGrid) Buffer() ui.Buffer {
|
||||||
buf := ui.NewBuffer()
|
buf := ui.NewBuffer()
|
||||||
for _, r := range cg.Rows {
|
for _, r := range cg.pageRows() {
|
||||||
buf.Merge(r.Buffer())
|
buf.Merge(r.Buffer())
|
||||||
}
|
}
|
||||||
return buf
|
return buf
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cg *CompactGrid) AddRows(rows ...ui.GridBufferer) {
|
func (cg *CompactGrid) AddRows(rows ...ui.GridBufferer) {
|
||||||
for _, r := range rows {
|
cg.Rows = append(cg.Rows, rows...)
|
||||||
cg.Rows = append(cg.Rows, r)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ type CompactHeader struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewCompactHeader() *CompactHeader {
|
func NewCompactHeader() *CompactHeader {
|
||||||
fields := []string{"", "NAME", "CID", "CPU", "MEM", "NET RX/TX"}
|
fields := []string{"", "NAME", "CID", "CPU", "MEM", "NET RX/TX", "IO R/W", "PIDS"}
|
||||||
ch := &CompactHeader{}
|
ch := &CompactHeader{}
|
||||||
ch.Height = 2
|
ch.Height = 2
|
||||||
for _, f := range fields {
|
for _, f := range fields {
|
||||||
@@ -27,13 +27,13 @@ func (ch *CompactHeader) GetHeight() int {
|
|||||||
|
|
||||||
func (ch *CompactHeader) SetWidth(w int) {
|
func (ch *CompactHeader) SetWidth(w int) {
|
||||||
x := ch.X
|
x := ch.X
|
||||||
autoWidth := calcWidth(w, 5)
|
autoWidth := calcWidth(w)
|
||||||
for n, col := range ch.pars {
|
for n, col := range ch.pars {
|
||||||
// set status column to static width
|
// set column to static width
|
||||||
if n == 0 {
|
if colWidths[n] != 0 {
|
||||||
col.SetX(x)
|
col.SetX(x)
|
||||||
col.SetWidth(statusWidth)
|
col.SetWidth(colWidths[n])
|
||||||
x += statusWidth
|
x += colWidths[n]
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
col.SetX(x)
|
col.SetX(x)
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
package compact
|
package compact
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/bcicen/ctop/config"
|
||||||
"github.com/bcicen/ctop/logging"
|
"github.com/bcicen/ctop/logging"
|
||||||
"github.com/bcicen/ctop/metrics"
|
"github.com/bcicen/ctop/models"
|
||||||
ui "github.com/gizak/termui"
|
ui "github.com/gizak/termui"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -13,8 +14,11 @@ type Compact struct {
|
|||||||
Name *TextCol
|
Name *TextCol
|
||||||
Cid *TextCol
|
Cid *TextCol
|
||||||
Cpu *GaugeCol
|
Cpu *GaugeCol
|
||||||
Memory *GaugeCol
|
Mem *GaugeCol
|
||||||
Net *TextCol
|
Net *TextCol
|
||||||
|
IO *TextCol
|
||||||
|
Pids *TextCol
|
||||||
|
Bg *RowBg
|
||||||
X, Y int
|
X, Y int
|
||||||
Width int
|
Width int
|
||||||
Height int
|
Height int
|
||||||
@@ -30,8 +34,11 @@ func NewCompact(id string) *Compact {
|
|||||||
Name: NewTextCol("-"),
|
Name: NewTextCol("-"),
|
||||||
Cid: NewTextCol(id),
|
Cid: NewTextCol(id),
|
||||||
Cpu: NewGaugeCol(),
|
Cpu: NewGaugeCol(),
|
||||||
Memory: NewGaugeCol(),
|
Mem: NewGaugeCol(),
|
||||||
Net: NewTextCol("-"),
|
Net: NewTextCol("-"),
|
||||||
|
IO: NewTextCol("-"),
|
||||||
|
Pids: NewTextCol("-"),
|
||||||
|
Bg: NewRowBg(),
|
||||||
X: 1,
|
X: 1,
|
||||||
Height: 1,
|
Height: 1,
|
||||||
}
|
}
|
||||||
@@ -52,20 +59,26 @@ func (row *Compact) SetMeta(k, v string) {
|
|||||||
row.Name.Set(v)
|
row.Name.Set(v)
|
||||||
case "state":
|
case "state":
|
||||||
row.Status.Set(v)
|
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.SetCPU(m.CPUUtil)
|
||||||
row.SetNet(m.NetRx, m.NetTx)
|
row.SetNet(m.NetRx, m.NetTx)
|
||||||
row.SetMem(m.MemUsage, m.MemLimit, m.MemPercent)
|
row.SetMem(m.MemUsage, m.MemLimit, m.MemPercent)
|
||||||
|
row.SetIO(m.IOBytesRead, m.IOBytesWrite)
|
||||||
|
row.SetPids(m.Pids)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set gauges, counters to default unread values
|
// Set gauges, counters to default unread values
|
||||||
func (row *Compact) Reset() {
|
func (row *Compact) Reset() {
|
||||||
row.Cpu.Reset()
|
row.Cpu.Reset()
|
||||||
row.Memory.Reset()
|
row.Mem.Reset()
|
||||||
row.Net.Reset()
|
row.Net.Reset()
|
||||||
|
row.IO.Reset()
|
||||||
|
row.Pids.Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (row *Compact) GetHeight() int {
|
func (row *Compact) GetHeight() int {
|
||||||
@@ -80,6 +93,8 @@ func (row *Compact) SetY(y int) {
|
|||||||
if y == row.Y {
|
if y == row.Y {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
row.Bg.Y = y
|
||||||
for _, col := range row.all() {
|
for _, col := range row.all() {
|
||||||
col.SetY(y)
|
col.SetY(y)
|
||||||
}
|
}
|
||||||
@@ -91,13 +106,16 @@ func (row *Compact) SetWidth(width int) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
x := row.X
|
x := row.X
|
||||||
autoWidth := calcWidth(width, 5)
|
|
||||||
|
row.Bg.SetX(x + colWidths[0] + 1)
|
||||||
|
row.Bg.SetWidth(width)
|
||||||
|
|
||||||
|
autoWidth := calcWidth(width)
|
||||||
for n, col := range row.all() {
|
for n, col := range row.all() {
|
||||||
// set status column to static width
|
if colWidths[n] != 0 {
|
||||||
if n == 0 {
|
|
||||||
col.SetX(x)
|
col.SetX(x)
|
||||||
col.SetWidth(statusWidth)
|
col.SetWidth(colWidths[n])
|
||||||
x += statusWidth
|
x += colWidths[n]
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
col.SetX(x)
|
col.SetX(x)
|
||||||
@@ -110,13 +128,15 @@ func (row *Compact) SetWidth(width int) {
|
|||||||
func (row *Compact) Buffer() ui.Buffer {
|
func (row *Compact) Buffer() ui.Buffer {
|
||||||
buf := ui.NewBuffer()
|
buf := ui.NewBuffer()
|
||||||
|
|
||||||
|
buf.Merge(row.Bg.Buffer())
|
||||||
buf.Merge(row.Status.Buffer())
|
buf.Merge(row.Status.Buffer())
|
||||||
buf.Merge(row.Name.Buffer())
|
buf.Merge(row.Name.Buffer())
|
||||||
buf.Merge(row.Cid.Buffer())
|
buf.Merge(row.Cid.Buffer())
|
||||||
buf.Merge(row.Cpu.Buffer())
|
buf.Merge(row.Cpu.Buffer())
|
||||||
buf.Merge(row.Memory.Buffer())
|
buf.Merge(row.Mem.Buffer())
|
||||||
buf.Merge(row.Net.Buffer())
|
buf.Merge(row.Net.Buffer())
|
||||||
|
buf.Merge(row.IO.Buffer())
|
||||||
|
buf.Merge(row.Pids.Buffer())
|
||||||
return buf
|
return buf
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,7 +146,50 @@ func (row *Compact) all() []ui.GridBufferer {
|
|||||||
row.Name,
|
row.Name,
|
||||||
row.Cid,
|
row.Cid,
|
||||||
row.Cpu,
|
row.Cpu,
|
||||||
row.Memory,
|
row.Mem,
|
||||||
row.Net,
|
row.Net,
|
||||||
|
row.IO,
|
||||||
|
row.Pids,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (row *Compact) Highlight() {
|
||||||
|
row.Name.Highlight()
|
||||||
|
if config.GetSwitchVal("fullRowCursor") {
|
||||||
|
row.Bg.Highlight()
|
||||||
|
row.Cid.Highlight()
|
||||||
|
row.Cpu.Highlight()
|
||||||
|
row.Mem.Highlight()
|
||||||
|
row.Net.Highlight()
|
||||||
|
row.IO.Highlight()
|
||||||
|
row.Pids.Highlight()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (row *Compact) UnHighlight() {
|
||||||
|
row.Name.UnHighlight()
|
||||||
|
if config.GetSwitchVal("fullRowCursor") {
|
||||||
|
row.Bg.UnHighlight()
|
||||||
|
row.Cid.UnHighlight()
|
||||||
|
row.Cpu.UnHighlight()
|
||||||
|
row.Mem.UnHighlight()
|
||||||
|
row.Net.UnHighlight()
|
||||||
|
row.IO.UnHighlight()
|
||||||
|
row.Pids.UnHighlight()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type RowBg struct {
|
||||||
|
*ui.Par
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRowBg() *RowBg {
|
||||||
|
bg := ui.NewPar("")
|
||||||
|
bg.Height = 1
|
||||||
|
bg.Border = false
|
||||||
|
bg.Bg = ui.ThemeAttr("par.text.bg")
|
||||||
|
return &RowBg{bg}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *RowBg) Highlight() { w.Bg = ui.ThemeAttr("par.text.fg") }
|
||||||
|
func (w *RowBg) UnHighlight() { w.Bg = ui.ThemeAttr("par.text.bg") }
|
||||||
|
|||||||
@@ -13,23 +13,36 @@ func (row *Compact) SetNet(rx int64, tx int64) {
|
|||||||
row.Net.Set(label)
|
row.Net.Set(label)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (row *Compact) SetIO(read int64, write int64) {
|
||||||
|
label := fmt.Sprintf("%s / %s", cwidgets.ByteFormat(read), cwidgets.ByteFormat(write))
|
||||||
|
row.IO.Set(label)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (row *Compact) SetPids(val int) {
|
||||||
|
label := strconv.Itoa(val)
|
||||||
|
row.Pids.Set(label)
|
||||||
|
}
|
||||||
|
|
||||||
func (row *Compact) SetCPU(val int) {
|
func (row *Compact) SetCPU(val int) {
|
||||||
row.Cpu.BarColor = colorScale(val)
|
row.Cpu.BarColor = colorScale(val)
|
||||||
row.Cpu.Label = fmt.Sprintf("%s%%", strconv.Itoa(val))
|
row.Cpu.Label = fmt.Sprintf("%s%%", strconv.Itoa(val))
|
||||||
if val < 5 {
|
if val < 5 {
|
||||||
val = 5
|
val = 5
|
||||||
row.Cpu.BarColor = ui.ColorBlack
|
row.Cpu.BarColor = ui.ThemeAttr("gauge.bar.bg")
|
||||||
|
}
|
||||||
|
if val > 100 {
|
||||||
|
val = 100
|
||||||
}
|
}
|
||||||
row.Cpu.Percent = val
|
row.Cpu.Percent = val
|
||||||
}
|
}
|
||||||
|
|
||||||
func (row *Compact) SetMem(val int64, limit int64, percent 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 {
|
if percent < 5 {
|
||||||
percent = 5
|
percent = 5
|
||||||
row.Memory.BarColor = ui.ColorBlack
|
row.Mem.BarColor = ui.ColorBlack
|
||||||
} else {
|
} else {
|
||||||
row.Memory.BarColor = ui.ColorGreen
|
row.Mem.BarColor = ui.ThemeAttr("gauge.bar.bg")
|
||||||
}
|
}
|
||||||
row.Memory.Percent = percent
|
row.Mem.Percent = percent
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,42 @@
|
|||||||
package compact
|
package compact
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
|
|
||||||
ui "github.com/gizak/termui"
|
ui "github.com/gizak/termui"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
mark = string('\u25C9')
|
mark = string('\u25C9')
|
||||||
vBar = string('\u25AE')
|
healthMark = string('\u207A')
|
||||||
statusWidth = 3
|
vBar = string('\u25AE') + string('\u25AE')
|
||||||
)
|
)
|
||||||
|
|
||||||
// Status indicator
|
// Status indicator
|
||||||
type Status struct {
|
type Status struct {
|
||||||
*ui.Par
|
*ui.Block
|
||||||
|
status []ui.Cell
|
||||||
|
health []ui.Cell
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewStatus() *Status {
|
func NewStatus() *Status {
|
||||||
p := ui.NewPar(mark)
|
s := &Status{Block: ui.NewBlock()}
|
||||||
p.Border = false
|
s.Height = 1
|
||||||
p.Height = 1
|
s.Border = false
|
||||||
p.Width = statusWidth
|
s.Set("")
|
||||||
return &Status{p}
|
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) {
|
func (s *Status) Set(val string) {
|
||||||
@@ -32,13 +46,38 @@ func (s *Status) Set(val string) {
|
|||||||
|
|
||||||
switch val {
|
switch val {
|
||||||
case "running":
|
case "running":
|
||||||
color = ui.ColorGreen
|
color = ui.ThemeAttr("status.ok")
|
||||||
case "exited":
|
case "exited":
|
||||||
color = ui.ColorRed
|
color = ui.ThemeAttr("status.danger")
|
||||||
case "paused":
|
case "paused":
|
||||||
text = fmt.Sprintf("%s%s", vBar, vBar)
|
text = vBar
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Text = text
|
var cells []ui.Cell
|
||||||
s.TextFgColor = color
|
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", "Succeeded":
|
||||||
|
color = ui.ThemeAttr("status.ok")
|
||||||
|
case "unhealthy", "Failed", "Unknown":
|
||||||
|
color = ui.ThemeAttr("status.danger")
|
||||||
|
case "starting", "Pending", "Running":
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,11 +17,13 @@ func NewTextCol(s string) *TextCol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *TextCol) Highlight() {
|
func (w *TextCol) Highlight() {
|
||||||
w.TextFgColor = ui.ThemeAttr("par.text.bg")
|
w.Bg = ui.ThemeAttr("par.text.fg")
|
||||||
|
w.TextFgColor = ui.ThemeAttr("par.text.hi")
|
||||||
w.TextBgColor = ui.ThemeAttr("par.text.fg")
|
w.TextBgColor = ui.ThemeAttr("par.text.fg")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *TextCol) UnHighlight() {
|
func (w *TextCol) UnHighlight() {
|
||||||
|
w.Bg = ui.ThemeAttr("par.text.bg")
|
||||||
w.TextFgColor = ui.ThemeAttr("par.text.fg")
|
w.TextFgColor = ui.ThemeAttr("par.text.fg")
|
||||||
w.TextBgColor = ui.ThemeAttr("par.text.bg")
|
w.TextBgColor = ui.ThemeAttr("par.text.bg")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,15 +4,35 @@ package compact
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
ui "github.com/gizak/termui"
|
ui "github.com/gizak/termui"
|
||||||
)
|
)
|
||||||
|
|
||||||
const colSpacing = 1
|
const colSpacing = 1
|
||||||
|
|
||||||
// Calculate per-column width, given total width and number of items
|
// per-column width. 0 == auto width
|
||||||
func calcWidth(width, items int) int {
|
var colWidths = []int{
|
||||||
spacing := colSpacing * items
|
3, // status
|
||||||
return (width - statusWidth - spacing) / items
|
0, // name
|
||||||
|
0, // cid
|
||||||
|
0, // cpu
|
||||||
|
0, // memory
|
||||||
|
0, // net
|
||||||
|
0, // io
|
||||||
|
4, // pids
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate per-column width, given total width
|
||||||
|
func calcWidth(width int) int {
|
||||||
|
spacing := colSpacing * len(colWidths)
|
||||||
|
var staticCols int
|
||||||
|
for _, w := range colWidths {
|
||||||
|
width -= w
|
||||||
|
if w == 0 {
|
||||||
|
staticCols++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (width - spacing) / staticCols
|
||||||
}
|
}
|
||||||
|
|
||||||
func centerParText(p *ui.Par) {
|
func centerParText(p *ui.Par) {
|
||||||
|
|||||||
@@ -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.ColorWhite
|
|
||||||
p.Seperator = 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 (
|
import (
|
||||||
"github.com/bcicen/ctop/logging"
|
"github.com/bcicen/ctop/logging"
|
||||||
"github.com/bcicen/ctop/metrics"
|
"github.com/bcicen/ctop/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
var log = logging.Init()
|
var log = logging.Init()
|
||||||
|
|
||||||
type WidgetUpdater interface {
|
type WidgetUpdater interface {
|
||||||
SetMeta(string, string)
|
SetMeta(string, string)
|
||||||
SetMetrics(metrics.Metrics)
|
SetMetrics(models.Metrics)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package expanded
|
package single
|
||||||
|
|
||||||
import (
|
import (
|
||||||
ui "github.com/gizak/termui"
|
ui "github.com/gizak/termui"
|
||||||
@@ -17,8 +17,6 @@ func NewCpu() *Cpu {
|
|||||||
cpu.Width = colWidth[0]
|
cpu.Width = colWidth[0]
|
||||||
cpu.X = 0
|
cpu.X = 0
|
||||||
cpu.DataLabels = cpu.hist.Labels
|
cpu.DataLabels = cpu.hist.Labels
|
||||||
cpu.AxesColor = ui.ColorDefault
|
|
||||||
cpu.LineColor = ui.ColorGreen
|
|
||||||
|
|
||||||
// hack to force the default minY scale to 0
|
// hack to force the default minY scale to 0
|
||||||
tmpData := []float64{20}
|
tmpData := []float64{20}
|
||||||
36
cwidgets/single/env.go
Normal file
36
cwidgets/single/env.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package single
|
||||||
|
|
||||||
|
import (
|
||||||
|
ui "github.com/gizak/termui"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var envPattern = regexp.MustCompile(`(?P<KEY>[^=]+)=(?P<VALUJE>.*)`)
|
||||||
|
|
||||||
|
type Env struct {
|
||||||
|
*ui.Table
|
||||||
|
data map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEnv() *Env {
|
||||||
|
p := ui.NewTable()
|
||||||
|
p.Height = 4
|
||||||
|
p.Width = colWidth[0]
|
||||||
|
p.FgColor = ui.ThemeAttr("par.text.fg")
|
||||||
|
p.Separator = false
|
||||||
|
i := &Env{p, make(map[string]string)}
|
||||||
|
i.BorderLabel = "Env"
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Env) Set(k, v string) {
|
||||||
|
match := envPattern.FindStringSubmatch(v)
|
||||||
|
key := match[1]
|
||||||
|
value := match[2]
|
||||||
|
w.data[key] = value
|
||||||
|
|
||||||
|
w.Rows = [][]string{}
|
||||||
|
w.Rows = append(w.Rows, mkInfoRows(key, value)...)
|
||||||
|
|
||||||
|
w.Height = len(w.Rows) + 2
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package expanded
|
package single
|
||||||
|
|
||||||
type IntHist struct {
|
type IntHist struct {
|
||||||
Val int // most current data point
|
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", "IPs", "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 separate row
|
||||||
|
if len(lines) > 1 {
|
||||||
|
for _, line := range lines[1:] {
|
||||||
|
if line != "" {
|
||||||
|
rows = append(rows, []string{"", line})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows
|
||||||
|
}
|
||||||
51
cwidgets/single/io.go
Normal file
51
cwidgets/single/io.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package single
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/bcicen/ctop/cwidgets"
|
||||||
|
ui "github.com/gizak/termui"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IO struct {
|
||||||
|
*ui.Sparklines
|
||||||
|
readHist *DiffHist
|
||||||
|
writeHist *DiffHist
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIO() *IO {
|
||||||
|
io := &IO{ui.NewSparklines(), NewDiffHist(60), NewDiffHist(60)}
|
||||||
|
io.BorderLabel = "IO"
|
||||||
|
io.Height = 6
|
||||||
|
io.Width = colWidth[0]
|
||||||
|
io.X = 0
|
||||||
|
io.Y = 24
|
||||||
|
|
||||||
|
read := ui.NewSparkline()
|
||||||
|
read.Title = "READ"
|
||||||
|
read.Height = 1
|
||||||
|
read.Data = io.readHist.Data
|
||||||
|
read.LineColor = ui.ColorGreen
|
||||||
|
|
||||||
|
write := ui.NewSparkline()
|
||||||
|
write.Title = "WRITE"
|
||||||
|
write.Height = 1
|
||||||
|
write.Data = io.writeHist.Data
|
||||||
|
write.LineColor = ui.ColorYellow
|
||||||
|
|
||||||
|
io.Lines = []ui.Sparkline{read, write}
|
||||||
|
return io
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *IO) Update(read int64, write int64) {
|
||||||
|
var rate string
|
||||||
|
|
||||||
|
w.readHist.Append(int(read))
|
||||||
|
rate = strings.ToLower(cwidgets.ByteFormatInt(w.readHist.Val))
|
||||||
|
w.Lines[0].Title = fmt.Sprintf("read [%s/s]", rate)
|
||||||
|
|
||||||
|
w.writeHist.Append(int(write))
|
||||||
|
rate = strings.ToLower(cwidgets.ByteFormatInt(w.writeHist.Val))
|
||||||
|
w.Lines[1].Title = fmt.Sprintf("write [%s/s]", rate)
|
||||||
|
}
|
||||||
83
cwidgets/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 (
|
import (
|
||||||
"github.com/bcicen/ctop/logging"
|
"github.com/bcicen/ctop/logging"
|
||||||
"github.com/bcicen/ctop/metrics"
|
"github.com/bcicen/ctop/models"
|
||||||
ui "github.com/gizak/termui"
|
ui "github.com/gizak/termui"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -12,47 +12,87 @@ var (
|
|||||||
colWidth = [2]int{65, 0} // left,right column width
|
colWidth = [2]int{65, 0} // left,right column width
|
||||||
)
|
)
|
||||||
|
|
||||||
type Expanded struct {
|
type Single struct {
|
||||||
Info *Info
|
Info *Info
|
||||||
Net *Net
|
Net *Net
|
||||||
Cpu *Cpu
|
Cpu *Cpu
|
||||||
Mem *Mem
|
Mem *Mem
|
||||||
|
IO *IO
|
||||||
|
Env *Env
|
||||||
|
X, Y int
|
||||||
Width int
|
Width int
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewExpanded(id string) *Expanded {
|
func NewSingle(id string) *Single {
|
||||||
if len(id) > 12 {
|
if len(id) > 12 {
|
||||||
id = id[:12]
|
id = id[:12]
|
||||||
}
|
}
|
||||||
return &Expanded{
|
return &Single{
|
||||||
Info: NewInfo(id),
|
Info: NewInfo(id),
|
||||||
Net: NewNet(),
|
Net: NewNet(),
|
||||||
Cpu: NewCpu(),
|
Cpu: NewCpu(),
|
||||||
Mem: NewMem(),
|
Mem: NewMem(),
|
||||||
|
IO: NewIO(),
|
||||||
|
Env: NewEnv(),
|
||||||
Width: ui.TermWidth(),
|
Width: ui.TermWidth(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Expanded) SetWidth(w int) {
|
func (e *Single) Up() {
|
||||||
e.Width = w
|
if e.Y < 0 {
|
||||||
|
e.Y++
|
||||||
|
e.Align()
|
||||||
|
ui.Render(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Expanded) SetMeta(k, v string) {
|
func (e *Single) Down() {
|
||||||
e.Info.Set(k, v)
|
if e.Y > (ui.TermHeight() - e.GetHeight()) {
|
||||||
|
e.Y--
|
||||||
|
e.Align()
|
||||||
|
ui.Render(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Expanded) SetMetrics(m metrics.Metrics) {
|
func (e *Single) SetWidth(w int) { e.Width = w }
|
||||||
|
func (e *Single) SetMeta(k, v string) {
|
||||||
|
if k == "[ENV-VAR]" {
|
||||||
|
e.Env.Set(k, v)
|
||||||
|
} else {
|
||||||
|
e.Info.Set(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Single) SetMetrics(m models.Metrics) {
|
||||||
e.Cpu.Update(m.CPUUtil)
|
e.Cpu.Update(m.CPUUtil)
|
||||||
e.Net.Update(m.NetRx, m.NetTx)
|
e.Net.Update(m.NetRx, m.NetTx)
|
||||||
e.Mem.Update(int(m.MemUsage), int(m.MemLimit))
|
e.Mem.Update(int(m.MemUsage), int(m.MemLimit))
|
||||||
|
e.IO.Update(m.IOBytesRead, m.IOBytesWrite)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Expanded) Align() {
|
// Return total column height
|
||||||
y := 0
|
func (e *Single) GetHeight() (h int) {
|
||||||
|
h += e.Info.Height
|
||||||
|
h += e.Net.Height
|
||||||
|
h += e.Cpu.Height
|
||||||
|
h += e.Mem.Height
|
||||||
|
h += e.IO.Height
|
||||||
|
h += e.Env.Height
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Single) Align() {
|
||||||
|
// reset offset if needed
|
||||||
|
if e.GetHeight() <= ui.TermHeight() {
|
||||||
|
e.Y = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
y := e.Y
|
||||||
for _, i := range e.all() {
|
for _, i := range e.all() {
|
||||||
i.SetY(y)
|
i.SetY(y)
|
||||||
y += i.GetHeight()
|
y += i.GetHeight()
|
||||||
}
|
}
|
||||||
|
|
||||||
if e.Width > colWidth[0] {
|
if e.Width > colWidth[0] {
|
||||||
colWidth[1] = e.Width - (colWidth[0] + 1)
|
colWidth[1] = e.Width - (colWidth[0] + 1)
|
||||||
}
|
}
|
||||||
@@ -60,10 +100,7 @@ func (e *Expanded) Align() {
|
|||||||
log.Debugf("align: width=%v left-col=%v right-col=%v", e.Width, colWidth[0], colWidth[1])
|
log.Debugf("align: width=%v left-col=%v right-col=%v", e.Width, colWidth[0], colWidth[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
func calcWidth(w int) {
|
func (e *Single) Buffer() ui.Buffer {
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Expanded) Buffer() ui.Buffer {
|
|
||||||
buf := ui.NewBuffer()
|
buf := ui.NewBuffer()
|
||||||
if e.Width < (colWidth[0] + colWidth[1]) {
|
if e.Width < (colWidth[0] + colWidth[1]) {
|
||||||
ui.Clear()
|
ui.Clear()
|
||||||
@@ -74,15 +111,19 @@ func (e *Expanded) Buffer() ui.Buffer {
|
|||||||
buf.Merge(e.Cpu.Buffer())
|
buf.Merge(e.Cpu.Buffer())
|
||||||
buf.Merge(e.Mem.Buffer())
|
buf.Merge(e.Mem.Buffer())
|
||||||
buf.Merge(e.Net.Buffer())
|
buf.Merge(e.Net.Buffer())
|
||||||
|
buf.Merge(e.IO.Buffer())
|
||||||
|
buf.Merge(e.Env.Buffer())
|
||||||
return buf
|
return buf
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Expanded) all() []ui.GridBufferer {
|
func (e *Single) all() []ui.GridBufferer {
|
||||||
return []ui.GridBufferer{
|
return []ui.GridBufferer{
|
||||||
e.Info,
|
e.Info,
|
||||||
e.Cpu,
|
e.Cpu,
|
||||||
e.Mem,
|
e.Mem,
|
||||||
e.Net,
|
e.Net,
|
||||||
|
e.IO,
|
||||||
|
e.Env,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package expanded
|
package single
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -57,7 +57,6 @@ func newMemLabel() *ui.Par {
|
|||||||
p.Border = false
|
p.Border = false
|
||||||
p.Height = 1
|
p.Height = 1
|
||||||
p.Width = 20
|
p.Width = 20
|
||||||
p.TextFgColor = ui.ColorDefault
|
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,9 +66,6 @@ func newMemChart() *ui.MBarChart {
|
|||||||
mbar.Border = false
|
mbar.Border = false
|
||||||
mbar.BarGap = 1
|
mbar.BarGap = 1
|
||||||
mbar.BarWidth = 6
|
mbar.BarWidth = 6
|
||||||
mbar.TextColor = ui.ColorDefault
|
|
||||||
|
|
||||||
mbar.BarColor[0] = ui.ColorGreen
|
|
||||||
|
|
||||||
mbar.BarColor[1] = ui.ColorBlack
|
mbar.BarColor[1] = ui.ColorBlack
|
||||||
mbar.NumColor[1] = ui.ColorBlack
|
mbar.NumColor[1] = ui.ColorBlack
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package expanded
|
package single
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -26,14 +26,12 @@ func NewNet() *Net {
|
|||||||
rx.Title = "RX"
|
rx.Title = "RX"
|
||||||
rx.Height = 1
|
rx.Height = 1
|
||||||
rx.Data = net.rxHist.Data
|
rx.Data = net.rxHist.Data
|
||||||
rx.TitleColor = ui.ColorDefault
|
|
||||||
rx.LineColor = ui.ColorGreen
|
rx.LineColor = ui.ColorGreen
|
||||||
|
|
||||||
tx := ui.NewSparkline()
|
tx := ui.NewSparkline()
|
||||||
tx.Title = "TX"
|
tx.Title = "TX"
|
||||||
tx.Height = 1
|
tx.Height = 1
|
||||||
tx.Data = net.txHist.Data
|
tx.Data = net.txHist.Data
|
||||||
tx.TitleColor = ui.ColorDefault
|
|
||||||
tx.LineColor = ui.ColorYellow
|
tx.LineColor = ui.ColorYellow
|
||||||
|
|
||||||
net.Lines = []ui.Sparkline{rx, tx}
|
net.Lines = []ui.Sparkline{rx, tx}
|
||||||
@@ -9,6 +9,7 @@ const (
|
|||||||
kb = 1024
|
kb = 1024
|
||||||
mb = kb * 1024
|
mb = kb * 1024
|
||||||
gb = mb * 1024
|
gb = mb * 1024
|
||||||
|
tb = gb * 1024
|
||||||
)
|
)
|
||||||
|
|
||||||
// convenience method
|
// convenience method
|
||||||
@@ -28,8 +29,12 @@ func ByteFormat(n int64) string {
|
|||||||
n = n / mb
|
n = n / mb
|
||||||
return fmt.Sprintf("%sM", strconv.FormatInt(n, 10))
|
return fmt.Sprintf("%sM", strconv.FormatInt(n, 10))
|
||||||
}
|
}
|
||||||
nf := float64(n) / gb
|
if n < tb {
|
||||||
return fmt.Sprintf("%sG", unpadFloat(nf))
|
nf := float64(n) / gb
|
||||||
|
return fmt.Sprintf("%sG", unpadFloat(nf))
|
||||||
|
}
|
||||||
|
nf := float64(n) / tb
|
||||||
|
return fmt.Sprintf("%sT", unpadFloat(nf))
|
||||||
}
|
}
|
||||||
|
|
||||||
func unpadFloat(f float64) string {
|
func unpadFloat(f float64) string {
|
||||||
|
|||||||
22
debug.go
22
debug.go
@@ -3,10 +3,14 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/bcicen/ctop/container"
|
||||||
ui "github.com/gizak/termui"
|
ui "github.com/gizak/termui"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var mstats = &runtime.MemStats{}
|
||||||
|
|
||||||
func logEvent(e ui.Event) {
|
func logEvent(e ui.Event) {
|
||||||
var s string
|
var s string
|
||||||
s += fmt.Sprintf("Type=%s", quote(e.Type))
|
s += fmt.Sprintf("Type=%s", quote(e.Type))
|
||||||
@@ -18,8 +22,24 @@ func logEvent(e ui.Event) {
|
|||||||
log.Debugf("new event: %s", s)
|
log.Debugf("new event: %s", s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runtimeStats() {
|
||||||
|
var msg string
|
||||||
|
msg += fmt.Sprintf("cgo calls=%v", runtime.NumCgoCall())
|
||||||
|
msg += fmt.Sprintf(" routines=%v", runtime.NumGoroutine())
|
||||||
|
runtime.ReadMemStats(mstats)
|
||||||
|
msg += fmt.Sprintf(" numgc=%v", mstats.NumGC)
|
||||||
|
msg += fmt.Sprintf(" alloc=%v", mstats.Alloc)
|
||||||
|
log.Debugf("runtime: %v", msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runtimeStack() {
|
||||||
|
buf := make([]byte, 32768)
|
||||||
|
buf = buf[:runtime.Stack(buf, true)]
|
||||||
|
log.Infof(fmt.Sprintf("stack:\n%v", string(buf)))
|
||||||
|
}
|
||||||
|
|
||||||
// log container, metrics, and widget state
|
// log container, metrics, and widget state
|
||||||
func dumpContainer(c *Container) {
|
func dumpContainer(c *container.Container) {
|
||||||
msg := fmt.Sprintf("logging state for container: %s\n", c.Id)
|
msg := fmt.Sprintf("logging state for container: %s\n", c.Id)
|
||||||
for k, v := range c.Meta {
|
for k, v := range c.Meta {
|
||||||
msg += fmt.Sprintf("Meta.%s = %s\n", k, v)
|
msg += fmt.Sprintf("Meta.%s = %s\n", k, v)
|
||||||
|
|||||||
145
dockersource.go
145
dockersource.go
@@ -1,145 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/bcicen/ctop/config"
|
|
||||||
"github.com/bcicen/ctop/metrics"
|
|
||||||
"github.com/fsouza/go-dockerclient"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ContainerSource interface {
|
|
||||||
All() Containers
|
|
||||||
Get(string) (*Container, bool)
|
|
||||||
}
|
|
||||||
|
|
||||||
type DockerContainerSource struct {
|
|
||||||
client *docker.Client
|
|
||||||
containers map[string]*Container
|
|
||||||
needsRefresh chan string // container IDs requiring refresh
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewDockerContainerSource() *DockerContainerSource {
|
|
||||||
// init docker client
|
|
||||||
client, err := docker.NewClient(config.GetVal("dockerHost"))
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
cm := &DockerContainerSource{
|
|
||||||
client: client,
|
|
||||||
containers: make(map[string]*Container),
|
|
||||||
needsRefresh: make(chan string, 60),
|
|
||||||
}
|
|
||||||
cm.refreshAll()
|
|
||||||
go cm.Loop()
|
|
||||||
go cm.watchEvents()
|
|
||||||
return cm
|
|
||||||
}
|
|
||||||
|
|
||||||
// Docker events watcher
|
|
||||||
func (cm *DockerContainerSource) watchEvents() {
|
|
||||||
log.Info("docker event listener starting")
|
|
||||||
events := make(chan *docker.APIEvents)
|
|
||||||
cm.client.AddEventListener(events)
|
|
||||||
|
|
||||||
for e := range events {
|
|
||||||
if e.Type != "container" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
switch e.Action {
|
|
||||||
case "start", "die", "pause", "unpause":
|
|
||||||
log.Debugf("handling docker event: action=%s id=%s", e.Action, e.ID)
|
|
||||||
cm.needsRefresh <- e.ID
|
|
||||||
case "destroy":
|
|
||||||
log.Debugf("handling docker event: action=%s id=%s", e.Action, e.ID)
|
|
||||||
cm.delByID(e.ID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cm *DockerContainerSource) refresh(c *Container) {
|
|
||||||
insp := cm.inspect(c.Id)
|
|
||||||
// remove container if no longer exists
|
|
||||||
if insp == nil {
|
|
||||||
cm.delByID(c.Id)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.SetMeta("name", shortName(insp.Name))
|
|
||||||
c.SetMeta("image", insp.Config.Image)
|
|
||||||
c.SetMeta("created", insp.Created.Format("Mon Jan 2 15:04:05 2006"))
|
|
||||||
c.SetState(insp.State.Status)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cm *DockerContainerSource) inspect(id string) *docker.Container {
|
|
||||||
c, err := cm.client.InspectContainer(id)
|
|
||||||
if err != nil {
|
|
||||||
if _, ok := err.(*docker.NoSuchContainer); ok == false {
|
|
||||||
log.Errorf(err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark all container IDs for refresh
|
|
||||||
func (cm *DockerContainerSource) refreshAll() {
|
|
||||||
opts := docker.ListContainersOptions{All: true}
|
|
||||||
allContainers, err := cm.client.ListContainers(opts)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, i := range allContainers {
|
|
||||||
c := cm.MustGet(i.ID)
|
|
||||||
c.SetMeta("name", shortName(i.Names[0]))
|
|
||||||
c.SetState(i.State)
|
|
||||||
cm.needsRefresh <- c.Id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cm *DockerContainerSource) Loop() {
|
|
||||||
for id := range cm.needsRefresh {
|
|
||||||
c := cm.MustGet(id)
|
|
||||||
cm.refresh(c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get a single container, creating one anew if not existing
|
|
||||||
func (cm *DockerContainerSource) MustGet(id string) *Container {
|
|
||||||
c, ok := cm.Get(id)
|
|
||||||
// append container struct for new containers
|
|
||||||
if !ok {
|
|
||||||
// create collector
|
|
||||||
collector := metrics.NewDocker(cm.client, id)
|
|
||||||
// create container
|
|
||||||
c = NewContainer(id, collector)
|
|
||||||
cm.containers[id] = c
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get a single container, by ID
|
|
||||||
func (cm *DockerContainerSource) Get(id string) (*Container, bool) {
|
|
||||||
c, ok := cm.containers[id]
|
|
||||||
return c, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove containers by ID
|
|
||||||
func (cm *DockerContainerSource) delByID(id string) {
|
|
||||||
delete(cm.containers, id)
|
|
||||||
log.Infof("removed dead container: %s", id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return array of all containers, sorted by field
|
|
||||||
func (cm *DockerContainerSource) All() (containers Containers) {
|
|
||||||
for _, c := range cm.containers {
|
|
||||||
containers = append(containers, c)
|
|
||||||
}
|
|
||||||
sort.Sort(containers)
|
|
||||||
return containers
|
|
||||||
}
|
|
||||||
|
|
||||||
// use primary container name
|
|
||||||
func shortName(name string) string {
|
|
||||||
return strings.Replace(name, "/", "", 1)
|
|
||||||
}
|
|
||||||
61
go.mod
Normal file
61
go.mod
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
module github.com/bcicen/ctop
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/Azure/go-ansiterm v0.0.0-20160622173216-fa152c58bc15 // indirect
|
||||||
|
github.com/BurntSushi/toml v0.3.0
|
||||||
|
github.com/Microsoft/go-winio v0.3.8 // indirect
|
||||||
|
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
|
||||||
|
github.com/Sirupsen/logrus v0.0.0-20150423025312-26709e271410 // indirect
|
||||||
|
github.com/c9s/goprocinfo v0.0.0-20170609001544-b34328d6e0cd
|
||||||
|
github.com/coreos/go-systemd v0.0.0-20151104194251-b4a58d95188d // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/docker/docker v0.0.0-20170502054910-90d35abf7b35 // indirect
|
||||||
|
github.com/docker/go-connections v0.0.0-20170301234100-a2afab980204 // indirect
|
||||||
|
github.com/docker/go-units v0.3.2 // indirect
|
||||||
|
github.com/fsouza/go-dockerclient v0.0.0-20170307141636-318513eb1ab2
|
||||||
|
github.com/ghodss/yaml v1.0.0 // indirect
|
||||||
|
github.com/gizak/termui v2.3.0+incompatible
|
||||||
|
github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55 // indirect
|
||||||
|
github.com/gogo/protobuf v1.1.1 // indirect
|
||||||
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
|
||||||
|
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c // indirect
|
||||||
|
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf // indirect
|
||||||
|
github.com/googleapis/gnostic v0.2.0 // indirect
|
||||||
|
github.com/gregjones/httpcache v0.0.0-20181110185634-c63ab54fda8f // indirect
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.0.0-20170211013415-3573b8b52aa7 // indirect
|
||||||
|
github.com/imdario/mergo v0.3.6 // indirect
|
||||||
|
github.com/jgautheron/codename-generator v0.0.0-20150829203204-16d037c7cc3c
|
||||||
|
github.com/json-iterator/go v1.1.5 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.0-20170201023540-14207d285c6c // indirect
|
||||||
|
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.1 // indirect
|
||||||
|
github.com/nsf/termbox-go v0.0.0-20180303152453-e2050e41c884
|
||||||
|
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d
|
||||||
|
github.com/op/go-logging v0.0.0-20160211212156-b2cb9fa56473
|
||||||
|
github.com/opencontainers/runc v0.1.1
|
||||||
|
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/seccomp/libseccomp-golang v0.0.0-20150813023252-1b506fc7c24e // indirect
|
||||||
|
github.com/spf13/pflag v1.0.3 // indirect
|
||||||
|
github.com/stretchr/testify v1.2.2 // indirect
|
||||||
|
github.com/syndtr/gocapability v0.0.0-20150716010906-2c00daeb6c3b // indirect
|
||||||
|
github.com/vishvananda/netlink v0.0.0-20150820014904-1e2e08e8a2dc // indirect
|
||||||
|
github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc // indirect
|
||||||
|
golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85 // indirect
|
||||||
|
golang.org/x/oauth2 v0.0.0-20181128211412-28207608b838 // indirect
|
||||||
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35 // indirect
|
||||||
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c // indirect
|
||||||
|
google.golang.org/appengine v1.3.0 // indirect
|
||||||
|
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.2.2 // indirect
|
||||||
|
k8s.io/api v0.0.0-20181130031204-d04500c8c3dd
|
||||||
|
k8s.io/apimachinery v0.0.0-20181201231028-18a5ff3097b4
|
||||||
|
k8s.io/client-go v9.0.0+incompatible
|
||||||
|
k8s.io/klog v0.1.0 // indirect
|
||||||
|
k8s.io/metrics v0.0.0-20181121073115-d8618695b08f
|
||||||
|
sigs.k8s.io/yaml v1.1.0 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
replace github.com/gizak/termui => github.com/bcicen/termui v0.0.0-20180326052246-4eb80249d3f5
|
||||||
160
grid.go
160
grid.go
@@ -2,15 +2,11 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/bcicen/ctop/config"
|
"github.com/bcicen/ctop/config"
|
||||||
"github.com/bcicen/ctop/cwidgets/expanded"
|
"github.com/bcicen/ctop/cwidgets/single"
|
||||||
ui "github.com/gizak/termui"
|
ui "github.com/gizak/termui"
|
||||||
)
|
)
|
||||||
|
|
||||||
func maxRows() int {
|
func RedrawRows(clr bool) {
|
||||||
return ui.TermHeight() - 2 - cGrid.Y
|
|
||||||
}
|
|
||||||
|
|
||||||
func RedrawRows() {
|
|
||||||
// reinit body rows
|
// reinit body rows
|
||||||
cGrid.Clear()
|
cGrid.Clear()
|
||||||
|
|
||||||
@@ -21,85 +17,112 @@ func RedrawRows() {
|
|||||||
header.SetFilter(config.GetVal("filterStr"))
|
header.SetFilter(config.GetVal("filterStr"))
|
||||||
y += header.Height()
|
y += header.Height()
|
||||||
}
|
}
|
||||||
|
|
||||||
cGrid.SetY(y)
|
cGrid.SetY(y)
|
||||||
|
|
||||||
var cursorVisible bool
|
for _, c := range cursor.filtered {
|
||||||
max := maxRows()
|
|
||||||
for n, c := range cursor.containers {
|
|
||||||
if n >= max {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
cGrid.AddRows(c.Widgets)
|
cGrid.AddRows(c.Widgets)
|
||||||
if c.Id == cursor.selectedID {
|
|
||||||
cursorVisible = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !cursorVisible {
|
if clr {
|
||||||
cursor.Reset()
|
ui.Clear()
|
||||||
|
log.Debugf("screen cleared")
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.Clear()
|
|
||||||
if config.GetSwitchVal("enableHeader") {
|
if config.GetSwitchVal("enableHeader") {
|
||||||
header.Render()
|
ui.Render(header)
|
||||||
}
|
}
|
||||||
cGrid.Align()
|
cGrid.Align()
|
||||||
ui.Render(cGrid)
|
ui.Render(cGrid)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExpandView(c *Container) {
|
func SingleView() MenuFn {
|
||||||
|
c := cursor.Selected()
|
||||||
|
if c == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
ui.Clear()
|
ui.Clear()
|
||||||
ui.DefaultEvtStream.ResetHandlers()
|
ui.DefaultEvtStream.ResetHandlers()
|
||||||
defer ui.DefaultEvtStream.ResetHandlers()
|
defer ui.DefaultEvtStream.ResetHandlers()
|
||||||
|
|
||||||
ex := expanded.NewExpanded(c.Id)
|
ex := single.NewSingle(c.Id)
|
||||||
c.SetUpdater(ex)
|
c.SetUpdater(ex)
|
||||||
|
|
||||||
ex.Align()
|
ex.Align()
|
||||||
ui.Render(ex)
|
ui.Render(ex)
|
||||||
ui.Handle("/timer/1s", func(ui.Event) {
|
|
||||||
ui.Render(ex)
|
HandleKeys("up", ex.Up)
|
||||||
})
|
HandleKeys("down", ex.Down)
|
||||||
|
ui.Handle("/sys/kbd/", func(ui.Event) { ui.StopLoop() })
|
||||||
|
|
||||||
|
ui.Handle("/timer/1s", func(ui.Event) { ui.Render(ex) })
|
||||||
ui.Handle("/sys/wnd/resize", func(e ui.Event) {
|
ui.Handle("/sys/wnd/resize", func(e ui.Event) {
|
||||||
ex.SetWidth(ui.TermWidth())
|
ex.SetWidth(ui.TermWidth())
|
||||||
ex.Align()
|
ex.Align()
|
||||||
log.Infof("resize: width=%v max-rows=%v", ex.Width, maxRows())
|
log.Infof("resize: width=%v max-rows=%v", ex.Width, cGrid.MaxRows())
|
||||||
})
|
})
|
||||||
ui.Handle("/sys/kbd/", func(ui.Event) {
|
|
||||||
ui.StopLoop()
|
|
||||||
})
|
|
||||||
ui.Loop()
|
|
||||||
|
|
||||||
|
ui.Loop()
|
||||||
c.SetUpdater(c.Widgets)
|
c.SetUpdater(c.Widgets)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func RefreshDisplay() {
|
||||||
|
// skip display refresh during scroll
|
||||||
|
if !cursor.isScrolling {
|
||||||
|
needsClear := cursor.RefreshContainers()
|
||||||
|
RedrawRows(needsClear)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Display() bool {
|
func Display() bool {
|
||||||
var menu func()
|
var menu MenuFn
|
||||||
var expand bool
|
|
||||||
|
|
||||||
cGrid.SetWidth(ui.TermWidth())
|
cGrid.SetWidth(ui.TermWidth())
|
||||||
ui.DefaultEvtStream.Hook(logEvent)
|
ui.DefaultEvtStream.Hook(logEvent)
|
||||||
|
|
||||||
// initial draw
|
// initial draw
|
||||||
header.Align()
|
header.Align()
|
||||||
|
status.Align()
|
||||||
cursor.RefreshContainers()
|
cursor.RefreshContainers()
|
||||||
RedrawRows()
|
RedrawRows(true)
|
||||||
|
|
||||||
ui.Handle("/sys/kbd/<up>", func(ui.Event) {
|
HandleKeys("up", cursor.Up)
|
||||||
cursor.Up()
|
HandleKeys("down", cursor.Down)
|
||||||
})
|
|
||||||
ui.Handle("/sys/kbd/<down>", func(ui.Event) {
|
HandleKeys("pgup", cursor.PgUp)
|
||||||
cursor.Down()
|
HandleKeys("pgdown", cursor.PgDown)
|
||||||
})
|
|
||||||
ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
|
HandleKeys("exit", ui.StopLoop)
|
||||||
expand = true
|
HandleKeys("help", func() {
|
||||||
|
menu = HelpMenu
|
||||||
ui.StopLoop()
|
ui.StopLoop()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
|
||||||
|
menu = ContainerMenu
|
||||||
|
ui.StopLoop()
|
||||||
|
})
|
||||||
|
ui.Handle("/sys/kbd/<left>", func(ui.Event) {
|
||||||
|
menu = LogMenu
|
||||||
|
ui.StopLoop()
|
||||||
|
})
|
||||||
|
ui.Handle("/sys/kbd/<right>", func(ui.Event) {
|
||||||
|
menu = SingleView
|
||||||
|
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) {
|
ui.Handle("/sys/kbd/a", func(ui.Event) {
|
||||||
config.Toggle("allContainers")
|
config.Toggle("allContainers")
|
||||||
cursor.RefreshContainers()
|
RefreshDisplay()
|
||||||
RedrawRows()
|
|
||||||
})
|
})
|
||||||
ui.Handle("/sys/kbd/D", func(ui.Event) {
|
ui.Handle("/sys/kbd/D", func(ui.Event) {
|
||||||
dumpContainer(cursor.Selected())
|
dumpContainer(cursor.Selected())
|
||||||
@@ -108,16 +131,9 @@ func Display() bool {
|
|||||||
menu = FilterMenu
|
menu = FilterMenu
|
||||||
ui.StopLoop()
|
ui.StopLoop()
|
||||||
})
|
})
|
||||||
ui.Handle("/sys/kbd/h", func(ui.Event) {
|
|
||||||
menu = HelpMenu
|
|
||||||
ui.StopLoop()
|
|
||||||
})
|
|
||||||
ui.Handle("/sys/kbd/H", func(ui.Event) {
|
ui.Handle("/sys/kbd/H", func(ui.Event) {
|
||||||
config.Toggle("enableHeader")
|
config.Toggle("enableHeader")
|
||||||
RedrawRows()
|
RedrawRows(true)
|
||||||
})
|
|
||||||
ui.Handle("/sys/kbd/q", func(ui.Event) {
|
|
||||||
ui.StopLoop()
|
|
||||||
})
|
})
|
||||||
ui.Handle("/sys/kbd/r", func(e ui.Event) {
|
ui.Handle("/sys/kbd/r", func(e ui.Event) {
|
||||||
config.Toggle("sortReversed")
|
config.Toggle("sortReversed")
|
||||||
@@ -126,27 +142,51 @@ func Display() bool {
|
|||||||
menu = SortMenu
|
menu = SortMenu
|
||||||
ui.StopLoop()
|
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) {
|
ui.Handle("/timer/1s", func(e ui.Event) {
|
||||||
cursor.RefreshContainers()
|
if log.StatusQueued() {
|
||||||
RedrawRows()
|
ui.StopLoop()
|
||||||
|
}
|
||||||
|
RefreshDisplay()
|
||||||
})
|
})
|
||||||
|
|
||||||
ui.Handle("/sys/wnd/resize", func(e ui.Event) {
|
ui.Handle("/sys/wnd/resize", func(e ui.Event) {
|
||||||
header.Align()
|
header.Align()
|
||||||
|
status.Align()
|
||||||
|
cursor.ScrollPage()
|
||||||
cGrid.SetWidth(ui.TermWidth())
|
cGrid.SetWidth(ui.TermWidth())
|
||||||
log.Infof("resize: width=%v max-rows=%v", cGrid.Width, maxRows())
|
log.Infof("resize: width=%v max-rows=%v", cGrid.Width, cGrid.MaxRows())
|
||||||
RedrawRows()
|
RedrawRows(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
ui.Loop()
|
ui.Loop()
|
||||||
|
|
||||||
|
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 {
|
if menu != nil {
|
||||||
menu()
|
for menu != nil {
|
||||||
return false
|
menu = menu()
|
||||||
}
|
}
|
||||||
if expand {
|
|
||||||
ExpandView(cursor.Selected())
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
84
install.sh
Executable file
84
install.sh
Executable file
@@ -0,0 +1,84 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# a simple install script for ctop
|
||||||
|
|
||||||
|
KERNEL=$(uname -s)
|
||||||
|
|
||||||
|
function output() { echo -e "\033[32mctop-install\033[0m $@"; }
|
||||||
|
|
||||||
|
function command_exists() {
|
||||||
|
command -v "$@" > /dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
for req in curl wget; do
|
||||||
|
command_exists $req || {
|
||||||
|
output "missing required $req binary"
|
||||||
|
req_failed=1
|
||||||
|
}
|
||||||
|
done
|
||||||
|
[ "$req_failed" == 1 ] && exit 1
|
||||||
|
|
||||||
|
sh_c='sh -c'
|
||||||
|
if [ "$CURRENT_USER" != 'root' ]; then
|
||||||
|
if command_exists sudo; then
|
||||||
|
sh_c='sudo -E sh -c'
|
||||||
|
elif command_exists su; then
|
||||||
|
sh_c='su -c'
|
||||||
|
else
|
||||||
|
output "Error: this installer needs the ability to run commands as root. We are unable to find either "sudo" or "su" available to make this happen."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
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-*
|
||||||
|
$sh_c "mv ctop-* /usr/local/bin/ctop"
|
||||||
|
|
||||||
|
output "done!"
|
||||||
41
keys.go
Normal file
41
keys.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
ui "github.com/gizak/termui"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Common action keybindings
|
||||||
|
var keyMap = map[string][]string{
|
||||||
|
"up": []string{
|
||||||
|
"/sys/kbd/<up>",
|
||||||
|
"/sys/kbd/k",
|
||||||
|
},
|
||||||
|
"down": []string{
|
||||||
|
"/sys/kbd/<down>",
|
||||||
|
"/sys/kbd/j",
|
||||||
|
},
|
||||||
|
"pgup": []string{
|
||||||
|
"/sys/kbd/<previous>",
|
||||||
|
"/sys/kbd/C-<up>",
|
||||||
|
},
|
||||||
|
"pgdown": []string{
|
||||||
|
"/sys/kbd/<next>",
|
||||||
|
"/sys/kbd/C-<down>",
|
||||||
|
},
|
||||||
|
"exit": []string{
|
||||||
|
"/sys/kbd/q",
|
||||||
|
"/sys/kbd/C-c",
|
||||||
|
"/sys/kbd/<escape>",
|
||||||
|
},
|
||||||
|
"help": []string{
|
||||||
|
"/sys/kbd/h",
|
||||||
|
"/sys/kbd/?",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply a common handler function to all given keys
|
||||||
|
func HandleKeys(i string, f func()) {
|
||||||
|
for _, k := range keyMap[i] {
|
||||||
|
ui.Handle(k, func(ui.Event) { f() })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
package logging
|
package logging
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/op/go-logging"
|
"github.com/op/go-logging"
|
||||||
@@ -13,17 +15,42 @@ const (
|
|||||||
var (
|
var (
|
||||||
Log *CTopLogger
|
Log *CTopLogger
|
||||||
exited bool
|
exited bool
|
||||||
level = logging.INFO
|
level = logging.INFO // default level
|
||||||
format = logging.MustStringFormatter(
|
format = logging.MustStringFormatter(
|
||||||
`%{color}%{time:15:04:05.000} ▶ %{level:.4s} %{id:03x}%{color:reset} %{message}`,
|
`%{color}%{time:15:04:05.000} ▶ %{level:.4s} %{id:03x}%{color:reset} %{message}`,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type statusMsg struct {
|
||||||
|
Text string
|
||||||
|
IsError bool
|
||||||
|
}
|
||||||
|
|
||||||
type CTopLogger struct {
|
type CTopLogger struct {
|
||||||
*logging.Logger
|
*logging.Logger
|
||||||
backend *logging.MemoryBackend
|
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 {
|
func Init() *CTopLogger {
|
||||||
if Log == nil {
|
if Log == nil {
|
||||||
logging.SetFormatter(format) // setup default formatter
|
logging.SetFormatter(format) // setup default formatter
|
||||||
@@ -31,6 +58,12 @@ func Init() *CTopLogger {
|
|||||||
Log = &CTopLogger{
|
Log = &CTopLogger{
|
||||||
logging.MustGetLogger("ctop"),
|
logging.MustGetLogger("ctop"),
|
||||||
logging.NewMemoryBackend(size),
|
logging.NewMemoryBackend(size),
|
||||||
|
[]statusMsg{},
|
||||||
|
}
|
||||||
|
|
||||||
|
if debugMode() {
|
||||||
|
level = logging.DEBUG
|
||||||
|
StartServer()
|
||||||
}
|
}
|
||||||
|
|
||||||
backendLvl := logging.AddModuleLevel(Log.backend)
|
backendLvl := logging.AddModuleLevel(Log.backend)
|
||||||
@@ -71,3 +104,6 @@ func (log *CTopLogger) Exit() {
|
|||||||
exited = true
|
exited = true
|
||||||
StopServer()
|
StopServer()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func debugMode() bool { return os.Getenv("CTOP_DEBUG") == "1" }
|
||||||
|
func debugModeTCP() bool { return os.Getenv("CTOP_DEBUG_TCP") == "1" }
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ package logging
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
path = "/tmp/ctop.sock"
|
socketPath = "./ctop.sock"
|
||||||
|
socketAddr = "0.0.0.0:9000"
|
||||||
)
|
)
|
||||||
|
|
||||||
var server struct {
|
var server struct {
|
||||||
@@ -16,7 +18,13 @@ var server struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getListener() net.Listener {
|
func getListener() net.Listener {
|
||||||
ln, err := net.Listen("unix", path)
|
var ln net.Listener
|
||||||
|
var err error
|
||||||
|
if debugModeTCP() {
|
||||||
|
ln, err = net.Listen("tcp", socketAddr)
|
||||||
|
} else {
|
||||||
|
ln, err = net.Listen("unix", socketPath)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -49,13 +57,13 @@ func StopServer() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handler(conn net.Conn) {
|
func handler(wc io.WriteCloser) {
|
||||||
server.wg.Add(1)
|
server.wg.Add(1)
|
||||||
defer server.wg.Done()
|
defer server.wg.Done()
|
||||||
defer conn.Close()
|
defer wc.Close()
|
||||||
for msg := range Log.tail() {
|
for msg := range Log.tail() {
|
||||||
msg = fmt.Sprintf("%s\n", msg)
|
msg = fmt.Sprintf("%s\n", msg)
|
||||||
conn.Write([]byte(msg))
|
wc.Write([]byte(msg))
|
||||||
}
|
}
|
||||||
conn.Write([]byte("bye\n"))
|
wc.Write([]byte("bye\n"))
|
||||||
}
|
}
|
||||||
|
|||||||
134
main.go
134
main.go
@@ -1,100 +1,156 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/bcicen/ctop/config"
|
"github.com/bcicen/ctop/config"
|
||||||
|
"github.com/bcicen/ctop/connector"
|
||||||
|
"github.com/bcicen/ctop/container"
|
||||||
"github.com/bcicen/ctop/cwidgets/compact"
|
"github.com/bcicen/ctop/cwidgets/compact"
|
||||||
"github.com/bcicen/ctop/logging"
|
"github.com/bcicen/ctop/logging"
|
||||||
"github.com/bcicen/ctop/widgets"
|
"github.com/bcicen/ctop/widgets"
|
||||||
ui "github.com/gizak/termui"
|
ui "github.com/gizak/termui"
|
||||||
|
tm "github.com/nsf/termbox-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
build = "none"
|
build = "none"
|
||||||
version = "dev-build"
|
version = "dev-build"
|
||||||
|
goVersion = runtime.Version()
|
||||||
|
|
||||||
log *logging.CTopLogger
|
log *logging.CTopLogger
|
||||||
cursor *GridCursor
|
cursor *GridCursor
|
||||||
cGrid *compact.CompactGrid
|
cGrid *compact.CompactGrid
|
||||||
header *widgets.CTopHeader
|
header *widgets.CTopHeader
|
||||||
|
status *widgets.StatusLine
|
||||||
|
|
||||||
|
versionStr = fmt.Sprintf("ctop version %v, build %v %v", version, build, goVersion)
|
||||||
|
|
||||||
|
fs = flag.NewFlagSet("ctop", flag.ExitOnError)
|
||||||
|
versionFlag = fs.Bool("version", false, "output version information and exit")
|
||||||
|
helpFlag = fs.Bool("h", false, "display this help dialog")
|
||||||
|
filterFlag = fs.String("f", "", "filter containers")
|
||||||
|
activeOnlyFlag = fs.Bool("a", false, "show active containers only")
|
||||||
|
sortFieldFlag = fs.String("s", "", "select container sort field")
|
||||||
|
reverseSortFlag = fs.Bool("r", false, "reverse container sort order")
|
||||||
|
invertFlag = fs.Bool("i", false, "invert default colors")
|
||||||
|
scaleCpu = fs.Bool("scale-cpu", false, "show cpu as % of system total")
|
||||||
|
connectorFlag = fs.String("connector", "docker", "container connector to use")
|
||||||
|
namespaceFlag = fs.String("n", "default", "Kubernetes namespace for monitoring")
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
readArgs()
|
|
||||||
defer panicExit()
|
defer panicExit()
|
||||||
|
|
||||||
// init ui
|
fs.Parse(os.Args[1:])
|
||||||
if err := ui.Init(); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
defer ui.Close()
|
|
||||||
|
|
||||||
// init global config
|
if *versionFlag {
|
||||||
config.Init()
|
fmt.Println(versionStr)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *helpFlag {
|
||||||
|
printHelp()
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
// init logger
|
// init logger
|
||||||
log = logging.Init()
|
log = logging.Init()
|
||||||
if config.GetSwitchVal("loggingEnabled") {
|
|
||||||
logging.StartServer()
|
// init global config and read config file if exists
|
||||||
|
config.Init()
|
||||||
|
config.Read()
|
||||||
|
|
||||||
|
// override default config values with command line flags
|
||||||
|
if *filterFlag != "" {
|
||||||
|
config.Update("filterStr", *filterFlag)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if *activeOnlyFlag {
|
||||||
|
config.Toggle("allContainers")
|
||||||
|
}
|
||||||
|
|
||||||
|
if *sortFieldFlag != "" {
|
||||||
|
validSort(*sortFieldFlag)
|
||||||
|
config.Update("sortField", *sortFieldFlag)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *reverseSortFlag {
|
||||||
|
config.Toggle("sortReversed")
|
||||||
|
}
|
||||||
|
|
||||||
|
if *scaleCpu {
|
||||||
|
config.Toggle("scaleCpu")
|
||||||
|
}
|
||||||
|
|
||||||
|
// init ui
|
||||||
|
if *invertFlag {
|
||||||
|
InvertColorMap()
|
||||||
|
}
|
||||||
|
config.Update("namespace", *namespaceFlag)
|
||||||
|
ui.ColorMap = ColorMap // override default colormap
|
||||||
|
if err := ui.Init(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
tm.SetInputMode(tm.InputAlt)
|
||||||
|
|
||||||
|
defer Shutdown()
|
||||||
// init grid, cursor, header
|
// init grid, cursor, header
|
||||||
cursor = NewGridCursor()
|
conn, err := connector.ByName(*connectorFlag)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
cursor = &GridCursor{cSource: conn}
|
||||||
cGrid = compact.NewCompactGrid()
|
cGrid = compact.NewCompactGrid()
|
||||||
header = widgets.NewCTopHeader()
|
header = widgets.NewCTopHeader()
|
||||||
|
status = widgets.NewStatusLine()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
exit := Display()
|
exit := Display()
|
||||||
if exit {
|
if exit {
|
||||||
log.Notice("shutting down")
|
|
||||||
log.Exit()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func readArgs() {
|
func Shutdown() {
|
||||||
if len(os.Args) < 2 {
|
log.Notice("shutting down")
|
||||||
return
|
log.Exit()
|
||||||
|
if tm.IsInit {
|
||||||
|
ui.Close()
|
||||||
}
|
}
|
||||||
for _, arg := range os.Args[1:] {
|
}
|
||||||
switch arg {
|
|
||||||
case "-v", "version":
|
// ensure a given sort field is valid
|
||||||
printVersion()
|
func validSort(s string) {
|
||||||
os.Exit(0)
|
if _, ok := container.Sorters[s]; !ok {
|
||||||
case "-h", "help":
|
fmt.Printf("invalid sort field: %s\n", s)
|
||||||
printHelp()
|
os.Exit(1)
|
||||||
os.Exit(0)
|
|
||||||
default:
|
|
||||||
fmt.Printf("invalid option or argument: \"%s\"\n", arg)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func panicExit() {
|
func panicExit() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
ui.Clear()
|
Shutdown()
|
||||||
fmt.Printf("panic: %s\n", r)
|
fmt.Printf("error: %s\n", r)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var helpMsg = `cTop - container metric viewer
|
var helpMsg = `ctop - interactive container viewer
|
||||||
|
|
||||||
usage: ctop [options]
|
usage: ctop [options]
|
||||||
|
|
||||||
options:
|
options:
|
||||||
-h display this help dialog
|
|
||||||
-v output version information and exit
|
|
||||||
`
|
`
|
||||||
|
|
||||||
func printHelp() {
|
func printHelp() {
|
||||||
fmt.Println(helpMsg)
|
fmt.Println(helpMsg)
|
||||||
}
|
fs.PrintDefaults()
|
||||||
|
fmt.Printf("\navailable connectors: ")
|
||||||
func printVersion() {
|
fmt.Println(strings.Join(connector.Enabled(), ", "))
|
||||||
fmt.Printf("cTop version %v, build %v\n", version, build)
|
|
||||||
}
|
}
|
||||||
|
|||||||
251
menus.go
251
menus.go
@@ -1,48 +1,61 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/bcicen/ctop/config"
|
"github.com/bcicen/ctop/config"
|
||||||
|
"github.com/bcicen/ctop/container"
|
||||||
"github.com/bcicen/ctop/widgets"
|
"github.com/bcicen/ctop/widgets"
|
||||||
"github.com/bcicen/ctop/widgets/menu"
|
"github.com/bcicen/ctop/widgets/menu"
|
||||||
ui "github.com/gizak/termui"
|
ui "github.com/gizak/termui"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// MenuFn executes a menu window, returning the next menu or nil
|
||||||
|
type MenuFn func() MenuFn
|
||||||
|
|
||||||
var helpDialog = []menu.Item{
|
var helpDialog = []menu.Item{
|
||||||
menu.Item{"[a] - toggle display of all containers", ""},
|
{"<enter> - open container menu", ""},
|
||||||
menu.Item{"[f] - filter displayed containers", ""},
|
{"", ""},
|
||||||
menu.Item{"[h] - open this help dialog", ""},
|
{"[a] - toggle display of all containers", ""},
|
||||||
menu.Item{"[H] - toggle cTop header", ""},
|
{"[f] - filter displayed containers", ""},
|
||||||
menu.Item{"[s] - select container sort field", ""},
|
{"[h] - open this help dialog", ""},
|
||||||
menu.Item{"[r] - reverse container sort order", ""},
|
{"[H] - toggle ctop header", ""},
|
||||||
menu.Item{"[q] - exit ctop", ""},
|
{"[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.Clear()
|
||||||
ui.DefaultEvtStream.ResetHandlers()
|
ui.DefaultEvtStream.ResetHandlers()
|
||||||
defer ui.DefaultEvtStream.ResetHandlers()
|
defer ui.DefaultEvtStream.ResetHandlers()
|
||||||
|
|
||||||
m := menu.NewMenu()
|
m := menu.NewMenu()
|
||||||
m.TextFgColor = ui.ColorWhite
|
|
||||||
m.BorderLabel = "Help"
|
m.BorderLabel = "Help"
|
||||||
m.BorderFg = ui.ColorCyan
|
|
||||||
m.AddItems(helpDialog...)
|
m.AddItems(helpDialog...)
|
||||||
ui.Render(m)
|
ui.Handle("/sys/wnd/resize", func(e ui.Event) {
|
||||||
|
ui.Clear()
|
||||||
|
ui.Render(m)
|
||||||
|
})
|
||||||
ui.Handle("/sys/kbd/", func(ui.Event) {
|
ui.Handle("/sys/kbd/", func(ui.Event) {
|
||||||
ui.StopLoop()
|
ui.StopLoop()
|
||||||
})
|
})
|
||||||
ui.Loop()
|
ui.Loop()
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func FilterMenu() {
|
func FilterMenu() MenuFn {
|
||||||
ui.DefaultEvtStream.ResetHandlers()
|
ui.DefaultEvtStream.ResetHandlers()
|
||||||
defer ui.DefaultEvtStream.ResetHandlers()
|
defer ui.DefaultEvtStream.ResetHandlers()
|
||||||
|
|
||||||
i := widgets.NewInput()
|
i := widgets.NewInput()
|
||||||
i.TextFgColor = ui.ColorWhite
|
|
||||||
i.BorderLabel = "Filter"
|
i.BorderLabel = "Filter"
|
||||||
i.BorderFg = ui.ColorCyan
|
|
||||||
i.SetY(ui.TermHeight() - i.Height)
|
i.SetY(ui.TermHeight() - i.Height)
|
||||||
|
i.Data = config.GetVal("filterStr")
|
||||||
ui.Render(i)
|
ui.Render(i)
|
||||||
|
|
||||||
// refresh container rows on input
|
// refresh container rows on input
|
||||||
@@ -50,21 +63,25 @@ func FilterMenu() {
|
|||||||
go func() {
|
go func() {
|
||||||
for s := range stream {
|
for s := range stream {
|
||||||
config.Update("filterStr", s)
|
config.Update("filterStr", s)
|
||||||
cursor.RefreshContainers()
|
RefreshDisplay()
|
||||||
RedrawRows()
|
|
||||||
ui.Render(i)
|
ui.Render(i)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
i.InputHandlers()
|
i.InputHandlers()
|
||||||
|
ui.Handle("/sys/kbd/<escape>", func(ui.Event) {
|
||||||
|
config.Update("filterStr", "")
|
||||||
|
ui.StopLoop()
|
||||||
|
})
|
||||||
ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
|
ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
|
||||||
config.Update("filterStr", i.Data)
|
config.Update("filterStr", i.Data)
|
||||||
ui.StopLoop()
|
ui.StopLoop()
|
||||||
})
|
})
|
||||||
ui.Loop()
|
ui.Loop()
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func SortMenu() {
|
func SortMenu() MenuFn {
|
||||||
ui.Clear()
|
ui.Clear()
|
||||||
ui.DefaultEvtStream.ResetHandlers()
|
ui.DefaultEvtStream.ResetHandlers()
|
||||||
defer ui.DefaultEvtStream.ResetHandlers()
|
defer ui.DefaultEvtStream.ResetHandlers()
|
||||||
@@ -72,22 +89,212 @@ func SortMenu() {
|
|||||||
m := menu.NewMenu()
|
m := menu.NewMenu()
|
||||||
m.Selectable = true
|
m.Selectable = true
|
||||||
m.SortItems = true
|
m.SortItems = true
|
||||||
m.TextFgColor = ui.ColorWhite
|
|
||||||
m.BorderLabel = "Sort Field"
|
m.BorderLabel = "Sort Field"
|
||||||
m.BorderFg = ui.ColorCyan
|
|
||||||
|
|
||||||
for _, field := range SortFields() {
|
for _, field := range container.SortFields() {
|
||||||
m.AddItems(menu.Item{field, ""})
|
m.AddItems(menu.Item{field, ""})
|
||||||
}
|
}
|
||||||
|
|
||||||
// set cursor position to current sort field
|
// set cursor position to current sort field
|
||||||
m.SetCursor(config.GetVal("sortField"))
|
m.SetCursor(config.GetVal("sortField"))
|
||||||
|
|
||||||
ui.Render(m)
|
HandleKeys("up", m.Up)
|
||||||
m.NavigationHandlers()
|
HandleKeys("down", m.Down)
|
||||||
|
HandleKeys("exit", ui.StopLoop)
|
||||||
|
|
||||||
ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
|
ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
|
||||||
config.Update("sortField", m.SelectedItem().Val)
|
config.Update("sortField", m.SelectedItem().Val)
|
||||||
ui.StopLoop()
|
ui.StopLoop()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ui.Render(m)
|
||||||
ui.Loop()
|
ui.Loop()
|
||||||
|
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"})
|
||||||
|
items = append(items, menu.Item{Val: "pause", Label: "pause"})
|
||||||
|
items = append(items, menu.Item{Val: "restart", Label: "restart"})
|
||||||
|
}
|
||||||
|
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"})
|
||||||
|
}
|
||||||
|
if c.Meta["state"] == "paused" {
|
||||||
|
items = append(items, menu.Item{Val: "unpause", Label: "unpause"})
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
case "pause":
|
||||||
|
nextMenu = Confirm(confirmTxt("pause", c.GetMeta("name")), c.Pause)
|
||||||
|
case "unpause":
|
||||||
|
nextMenu = Confirm(confirmTxt("unpause", c.GetMeta("name")), c.Unpause)
|
||||||
|
case "restart":
|
||||||
|
nextMenu = Confirm(confirmTxt("restart", c.GetMeta("name")), c.Restart)
|
||||||
|
}
|
||||||
|
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 = fmt.Sprintf("Logs [%s]", c.GetMeta("name"))
|
||||||
|
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,39 +0,0 @@
|
|||||||
package metrics
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math"
|
|
||||||
|
|
||||||
"github.com/bcicen/ctop/logging"
|
|
||||||
)
|
|
||||||
|
|
||||||
var log = logging.Init()
|
|
||||||
|
|
||||||
type Metrics struct {
|
|
||||||
CPUUtil int
|
|
||||||
NetTx int64
|
|
||||||
NetRx int64
|
|
||||||
MemLimit int64
|
|
||||||
MemPercent int
|
|
||||||
MemUsage int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMetrics() Metrics {
|
|
||||||
return Metrics{
|
|
||||||
CPUUtil: -1,
|
|
||||||
NetTx: -1,
|
|
||||||
NetRx: -1,
|
|
||||||
MemUsage: -1,
|
|
||||||
MemPercent: -1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Collector interface {
|
|
||||||
Stream() chan Metrics
|
|
||||||
Running() bool
|
|
||||||
Start()
|
|
||||||
Stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
func round(num float64) int {
|
|
||||||
return int(num + math.Copysign(0.5, num))
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
// +build !release
|
|
||||||
|
|
||||||
package metrics
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math/rand"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Mock collector
|
|
||||||
type Mock struct {
|
|
||||||
Metrics
|
|
||||||
stream chan Metrics
|
|
||||||
done bool
|
|
||||||
running bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMock() *Mock {
|
|
||||||
c := &Mock{
|
|
||||||
Metrics: Metrics{},
|
|
||||||
}
|
|
||||||
c.MemLimit = 2147483648
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Mock) Running() bool {
|
|
||||||
return c.running
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Mock) Start() {
|
|
||||||
c.done = false
|
|
||||||
c.stream = make(chan Metrics)
|
|
||||||
go c.run()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Mock) Stop() {
|
|
||||||
c.done = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Mock) Stream() chan Metrics {
|
|
||||||
return c.stream
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Mock) run() {
|
|
||||||
c.running = true
|
|
||||||
rand.Seed(int64(time.Now().Nanosecond()))
|
|
||||||
defer close(c.stream)
|
|
||||||
|
|
||||||
for {
|
|
||||||
c.CPUUtil += rand.Intn(2)
|
|
||||||
if c.CPUUtil > 100 {
|
|
||||||
c.CPUUtil = 0
|
|
||||||
}
|
|
||||||
c.NetTx += rand.Int63n(600)
|
|
||||||
c.NetRx += rand.Int63n(600)
|
|
||||||
c.MemUsage += rand.Int63n(c.MemLimit / 32)
|
|
||||||
if c.MemUsage > c.MemLimit {
|
|
||||||
c.MemUsage = 0
|
|
||||||
}
|
|
||||||
c.MemPercent = round((float64(c.MemUsage) / float64(c.MemLimit)) * 100)
|
|
||||||
c.stream <- c.Metrics
|
|
||||||
if c.done {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.running = false
|
|
||||||
}
|
|
||||||
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,16 +17,20 @@ type CTopHeader struct {
|
|||||||
func NewCTopHeader() *CTopHeader {
|
func NewCTopHeader() *CTopHeader {
|
||||||
return &CTopHeader{
|
return &CTopHeader{
|
||||||
Time: headerPar(2, timeStr()),
|
Time: headerPar(2, timeStr()),
|
||||||
Count: headerPar(27, "-"),
|
Count: headerPar(24, "-"),
|
||||||
Filter: headerPar(47, ""),
|
Filter: headerPar(40, ""),
|
||||||
bg: headerBg(),
|
bg: headerBg(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CTopHeader) Render() {
|
func (c *CTopHeader) Buffer() ui.Buffer {
|
||||||
|
buf := ui.NewBuffer()
|
||||||
c.Time.Text = timeStr()
|
c.Time.Text = timeStr()
|
||||||
ui.Render(c.bg)
|
buf.Merge(c.bg.Buffer())
|
||||||
ui.Render(c.Time, c.Count, c.Filter)
|
buf.Merge(c.Time.Buffer())
|
||||||
|
buf.Merge(c.Count.Buffer())
|
||||||
|
buf.Merge(c.Filter.Buffer())
|
||||||
|
return buf
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CTopHeader) Align() {
|
func (c *CTopHeader) Align() {
|
||||||
@@ -41,7 +45,7 @@ func headerBgBordered() *ui.Par {
|
|||||||
bg := ui.NewPar("")
|
bg := ui.NewPar("")
|
||||||
bg.X = 1
|
bg.X = 1
|
||||||
bg.Height = 3
|
bg.Height = 3
|
||||||
bg.Bg = ui.ColorWhite
|
bg.Bg = ui.ThemeAttr("header.bg")
|
||||||
return bg
|
return bg
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +54,7 @@ func headerBg() *ui.Par {
|
|||||||
bg.X = 1
|
bg.X = 1
|
||||||
bg.Height = 1
|
bg.Height = 1
|
||||||
bg.Border = false
|
bg.Border = false
|
||||||
bg.Bg = ui.ColorWhite
|
bg.Bg = ui.ThemeAttr("header.bg")
|
||||||
return bg
|
return bg
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,7 +72,7 @@ func (c *CTopHeader) SetFilter(val string) {
|
|||||||
|
|
||||||
func timeStr() string {
|
func timeStr() string {
|
||||||
ts := time.Now().Local().Format("15:04:05 MST")
|
ts := time.Now().Local().Format("15:04:05 MST")
|
||||||
return fmt.Sprintf("cTop - %s", ts)
|
return fmt.Sprintf("ctop - %s", ts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func headerPar(x int, s string) *ui.Par {
|
func headerPar(x int, s string) *ui.Par {
|
||||||
@@ -77,8 +81,8 @@ func headerPar(x int, s string) *ui.Par {
|
|||||||
p.Border = false
|
p.Border = false
|
||||||
p.Height = 1
|
p.Height = 1
|
||||||
p.Width = 20
|
p.Width = 20
|
||||||
p.TextFgColor = ui.ColorDefault
|
p.Bg = ui.ThemeAttr("header.bg")
|
||||||
p.TextBgColor = ui.ColorWhite
|
p.TextFgColor = ui.ThemeAttr("header.fg")
|
||||||
p.Bg = ui.ColorWhite
|
p.TextBgColor = ui.ThemeAttr("header.bg")
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
input_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_."
|
input_chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_."
|
||||||
)
|
)
|
||||||
|
|
||||||
type Padding [2]int // x,y padding
|
type Padding [2]int // x,y padding
|
||||||
@@ -28,10 +28,12 @@ func NewInput() *Input {
|
|||||||
Block: *ui.NewBlock(),
|
Block: *ui.NewBlock(),
|
||||||
Label: "input",
|
Label: "input",
|
||||||
MaxLen: 20,
|
MaxLen: 20,
|
||||||
TextFgColor: ui.ThemeAttr("par.text.fg"),
|
TextFgColor: ui.ThemeAttr("menu.text.fg"),
|
||||||
TextBgColor: ui.ThemeAttr("par.text.bg"),
|
TextBgColor: ui.ThemeAttr("menu.text.bg"),
|
||||||
padding: Padding{4, 2},
|
padding: Padding{4, 2},
|
||||||
}
|
}
|
||||||
|
i.BorderFg = ui.ThemeAttr("menu.border.fg")
|
||||||
|
i.BorderLabelFg = ui.ThemeAttr("menu.label.fg")
|
||||||
i.calcSize()
|
i.calcSize()
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
@@ -75,7 +77,7 @@ func (i *Input) KeyPress(e ui.Event) {
|
|||||||
if len(i.Data) >= i.MaxLen {
|
if len(i.Data) >= i.MaxLen {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if strings.Index(input_chars, ch) > -1 {
|
if strings.Contains(input_chars, ch) {
|
||||||
i.Data += ch
|
i.Data += ch
|
||||||
i.stream <- i.Data
|
i.stream <- i.Data
|
||||||
ui.Render(i)
|
ui.Render(i)
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ type Padding [2]int // x,y padding
|
|||||||
|
|
||||||
type Menu struct {
|
type Menu struct {
|
||||||
ui.Block
|
ui.Block
|
||||||
SortItems bool // enable automatic sorting of menu items
|
SortItems bool // enable automatic sorting of menu items
|
||||||
|
SubText string // optional text to display before items
|
||||||
TextFgColor ui.Attribute
|
TextFgColor ui.Attribute
|
||||||
TextBgColor ui.Attribute
|
TextBgColor ui.Attribute
|
||||||
Selectable bool
|
Selectable bool
|
||||||
@@ -22,11 +23,13 @@ type Menu struct {
|
|||||||
func NewMenu() *Menu {
|
func NewMenu() *Menu {
|
||||||
m := &Menu{
|
m := &Menu{
|
||||||
Block: *ui.NewBlock(),
|
Block: *ui.NewBlock(),
|
||||||
TextFgColor: ui.ThemeAttr("par.text.fg"),
|
TextFgColor: ui.ThemeAttr("menu.text.fg"),
|
||||||
TextBgColor: ui.ThemeAttr("par.text.bg"),
|
TextBgColor: ui.ThemeAttr("menu.text.bg"),
|
||||||
cursorPos: 0,
|
cursorPos: 0,
|
||||||
padding: Padding{4, 2},
|
padding: Padding{4, 2},
|
||||||
}
|
}
|
||||||
|
m.BorderFg = ui.ThemeAttr("menu.border.fg")
|
||||||
|
m.BorderLabelFg = ui.ThemeAttr("menu.label.fg")
|
||||||
m.X = 1
|
m.X = 1
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
@@ -80,13 +83,23 @@ func (m *Menu) Buffer() ui.Buffer {
|
|||||||
var cell ui.Cell
|
var cell ui.Cell
|
||||||
buf := m.Block.Buffer()
|
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 {
|
for n, item := range m.items {
|
||||||
x := m.X + m.padding[0]
|
x := m.X + m.padding[0]
|
||||||
y := m.Y + m.padding[1]
|
|
||||||
for _, ch := range item.Text() {
|
for _, ch := range item.Text() {
|
||||||
// invert bg/fg colors on currently selected row
|
// invert bg/fg colors on currently selected row
|
||||||
if m.Selectable && n == m.cursorPos {
|
if m.Selectable && n == m.cursorPos {
|
||||||
cell = ui.Cell{Ch: ch, Fg: m.TextBgColor, Bg: m.TextFgColor}
|
cell = ui.Cell{Ch: ch, Fg: ui.ColorBlack, Bg: m.TextFgColor}
|
||||||
} else {
|
} else {
|
||||||
cell = ui.Cell{Ch: ch, Fg: m.TextFgColor, Bg: m.TextBgColor}
|
cell = ui.Cell{Ch: ch, Fg: m.TextFgColor, Bg: m.TextBgColor}
|
||||||
}
|
}
|
||||||
@@ -98,39 +111,40 @@ func (m *Menu) Buffer() ui.Buffer {
|
|||||||
return buf
|
return buf
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Menu) Up(ui.Event) {
|
func (m *Menu) Up() {
|
||||||
if m.cursorPos > 0 {
|
if m.cursorPos > 0 {
|
||||||
m.cursorPos--
|
m.cursorPos--
|
||||||
ui.Render(m)
|
ui.Render(m)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Menu) Down(ui.Event) {
|
func (m *Menu) Down() {
|
||||||
if m.cursorPos < (len(m.items) - 1) {
|
if m.cursorPos < (len(m.items) - 1) {
|
||||||
m.cursorPos++
|
m.cursorPos++
|
||||||
ui.Render(m)
|
ui.Render(m)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup some default handlers for menu navigation
|
|
||||||
func (m *Menu) NavigationHandlers() {
|
|
||||||
ui.Handle("/sys/kbd/<up>", m.Up)
|
|
||||||
ui.Handle("/sys/kbd/<down>", m.Down)
|
|
||||||
ui.Handle("/sys/kbd/q", func(ui.Event) { ui.StopLoop() })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set width and height based on menu items
|
// Set width and height based on menu items
|
||||||
func (m *Menu) calcSize() {
|
func (m *Menu) calcSize() {
|
||||||
m.Width = 7 // minimum width
|
m.Width = 7 // minimum width
|
||||||
|
|
||||||
items := m.items
|
var height int
|
||||||
for _, i := range m.items {
|
for _, i := range m.items {
|
||||||
s := i.Text()
|
s := i.Text()
|
||||||
if len(s) > m.Width {
|
if len(s) > m.Width {
|
||||||
m.Width = len(s)
|
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.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