Compare commits

..

89 Commits
v0.5 ... v0.6.1

Author SHA1 Message Date
Bradley Cicenas
a2011b8bc7 v0.6.1 2017-06-29 14:02:52 +00:00
Bradley Cicenas
40fd9e935a combine image build steps 2017-06-29 14:02:52 +00:00
Bradley Cicenas
b88c143914 add offset sanity check to CompactGrid Align() 2017-07-07 15:43:03 +03:00
Bradley Cicenas
0a05007c4e skip offset updates in page scroll if no pages 2017-07-07 15:38:02 +03:00
Bradley Cicenas
c47ba3f804 add pgCount() method to GridCursor 2017-07-07 15:28:26 +03:00
Bradley Cicenas
79a3f361a7 add container log struct to models, collectors 2017-07-04 12:32:25 +00:00
Bradley Cicenas
65399a37e5 add log panel to expanded widgets 2017-06-29 14:02:52 +00:00
Bradley Cicenas
25a3fcf731 add runtimestats, stack logging to debug 2017-06-28 09:12:24 -03:00
Bradley Cicenas
17e2c2df8e add LogCollector interface, docker, mock log collectors 2017-06-27 14:18:17 -03:00
Bradley Cicenas
240345d527 add StreamLogs() to collector interface 2017-06-26 15:35:57 +00:00
Bradley Cicenas
2d284d9277 rename metrics subpackage 2017-06-26 15:35:57 +00:00
Bradley Cicenas
bfa5c5944f rename 2017-06-26 15:35:57 +00:00
Bradley Cicenas
e1051cd40f use underscore-prefixed build dir in makefile 2017-06-24 08:04:56 -03:00
Bradley Cicenas
13029cc7fe add go runtime to version output 2017-06-19 12:23:39 +00:00
Bradley Cicenas
58d9e4e194 reverse host and container port in metadata 2017-06-18 17:17:56 -03:00
Bradley Cicenas
4de7036e2f fix release url 2017-06-14 15:04:47 -03:00
Bradley Cicenas
617b1b2863 omit runc connector from darwin build 2017-06-14 10:11:40 -03:00
bradley
e59a360b60 Create connectors.md 2017-06-14 09:32:48 -03:00
bradley
91cd53a878 change option header sizing 2017-06-14 09:32:26 -03:00
Bradley Cicenas
c6f2c7b617 add badges back 2017-06-14 09:30:03 -03:00
Bradley Cicenas
568bfb2513 v0.6 doc updates 2017-06-14 09:17:44 -03:00
Bradley Cicenas
66ff8ad7ec update potentially conflicting variable name 2017-06-13 17:35:00 -03:00
Bradley Cicenas
400d9471b6 add pidcount,io to mock collector 2017-06-13 17:25:58 -03:00
Bradley Cicenas
288380ca8d remove arbitrary branch deploy commands 2017-06-12 11:25:02 -03:00
Bradley Cicenas
429d5b9101 v0.6.0 2017-06-12 11:16:03 -03:00
Bradley Cicenas
1be452d7c0 refactor collectors into subpackage 2017-06-12 11:12:03 -03:00
Bradley Cicenas
671c944272 disable timed display refresh while scrolling 2017-06-12 10:52:45 -03:00
Bradley Cicenas
a48a9031cc move container sort to struct method 2017-06-12 10:40:52 -03:00
Bradley Cicenas
aff6943d07 add runc connector doc 2017-06-10 12:25:18 -03:00
Bradley Cicenas
f56ff96b88 Revert "remove build-time container in make image"
This reverts commit b49e174483.
2017-06-10 12:15:42 -03:00
Bradley Cicenas
b5361c2a28 panic on missing runc root 2017-06-10 11:44:12 -03:00
Bradley Cicenas
e71b6cacce prevent unlock until container sort complete 2017-06-10 11:09:21 -03:00
Bradley Cicenas
8a1297d3c5 add created meta field to expanded view 2017-06-10 10:46:54 -03:00
Bradley Cicenas
bdea7d5853 remove containers from connector map on destroyed state 2017-06-10 10:46:16 -03:00
Bradley Cicenas
389dee0f3c add percent helper method to metrics 2017-06-10 10:00:54 -03:00
Bradley Cicenas
b49e174483 remove build-time container in make image 2017-06-10 09:37:36 -03:00
Bradley Cicenas
53b612ab07 add additional logging messages 2017-06-10 09:36:34 -03:00
Bradley Cicenas
446708e456 add default runc root path 2017-06-09 18:35:28 -03:00
Bradley Cicenas
1233ff0ead add arbitrary branch image pushes to circle ci 2017-06-09 18:18:48 -03:00
Bradley Cicenas
af3f1e2a85 update glide deps 2017-06-09 18:11:59 -03:00
Bradley Cicenas
4dbc5653ff update build ldflags to permit multiple runc dep versions 2017-06-09 15:18:11 -03:00
Bradley Cicenas
e8d9f3327c runc connector optimizations 2017-06-09 14:56:39 -03:00
Bradley Cicenas
d372043a17 add --connector switch, validation 2017-06-09 14:35:29 -03:00
Bradley Cicenas
eeac65da8c add sys proc methods to metrics 2017-06-09 14:15:12 -03:00
Bradley Cicenas
c1780ae30a add byte formatting for tb 2017-06-09 14:14:27 -03:00
Bradley Cicenas
fb39d69fa7 add runc metric collector 2017-06-09 13:07:25 -03:00
Bradley Cicenas
6392d63ff8 prevent panic messages from being hidden due to ui.Init() race condition 2017-06-08 17:19:34 -03:00
Bradley Cicenas
b009a260a4 initial runc connector implementation 2017-06-08 15:33:34 -03:00
Bradley Cicenas
44379cd9fd rename connectors 2017-06-08 12:01:08 -03:00
Bradley Cicenas
b85ca680f0 restructure container,connectors in subpackage 2017-06-08 11:51:02 -03:00
Bradley Cicenas
8fb7a8988f include <escape> in exit keygroup 2017-06-08 09:47:30 -03:00
Bradley Cicenas
6d097c2085 add quickstart section to debug doc 2017-06-08 09:33:07 -03:00
Bradley Cicenas
f9d68d688d add tcp logging section to debug doc 2017-05-31 10:55:31 -04:00
Bradley Cicenas
bc08b85191 add option to log debug messages to unix or tcp socket 2017-05-31 10:45:48 -04:00
Bradley Cicenas
f3d26e038d use current directory as default logging socket path 2017-05-31 10:21:32 -04:00
Bradley Cicenas
b4e1fbf290 add release, homebrew badges to README 2017-05-28 17:03:19 -04:00
Bradley Cicenas
58d5fba945 update port formatting for multi-line display 2017-05-15 11:54:35 +01:00
Bradley Cicenas
c76036a6f2 allow multi-line metadata in expanded view 2017-05-15 11:37:27 +01:00
bradley
6a8848d1e2 Merge pull request #65 from kenan-rhoton/ports
Add Ports information to the expanded view
2017-05-15 11:35:20 +01:00
Kenan Rhoton
02d1050130 Revert "Added Ports information to the expanded view"
This reverts commit b2165b6a29.
2017-05-15 06:53:15 +02:00
Kenan Rhoton
ccb44c964c Moved port Info to be fetched from the standard inspect call to avoid superfluous calls. Also moved the information to the info section instead of a whole new section in the expanded view 2017-05-15 06:52:38 +02:00
Kenan Rhoton
b2165b6a29 Added Ports information to the expanded view 2017-05-11 07:38:12 +02:00
bradley
d81d10ec27 Merge pull request #69 from vielmetti/patch-1
WORK IN PROGRESS Add arm64 build
2017-04-10 10:15:51 +08:00
Edward Vielmetti
9529c04680 Update Makefile
Foolish typo; several lines changed, to make future additions more obvious.
2017-04-07 17:43:18 -04:00
Edward Vielmetti
6a89c9af38 WORK IN PROGRESS Add arm64 build
This change would close #68 and would add an arm64 (ARMv8) build.
2017-04-07 02:10:37 -04:00
Bradley Cicenas
06a29fc912 fix circleci build
circleci apparently does not allow containers/volumes to be removed
2017-04-06 20:58:54 +08:00
Bradley Cicenas
2cba7253fc use create container in makefile 2017-04-03 20:51:05 +08:00
Bradley Cicenas
47d60fe51b gofmt main package 2017-03-28 13:57:30 +10:00
bradley
28f16c9a17 Merge pull request #62 from yashpatel5400/yash
Added page up/down features
2017-03-27 10:09:44 +10:00
yashpatel5400
6560768e08 Added page up/down features 2017-03-23 22:31:08 -04:00
Bradley Cicenas
084c0c4ec8 add global Shutdown() method for exit cleanup 2017-03-23 16:48:25 +10:00
Bradley Cicenas
5db90f31dc v0.5.1 2017-03-21 10:35:24 +10:00
Bradley Cicenas
82677d52ef add build section to docs 2017-03-20 08:39:57 +10:00
Bradley Cicenas
2b2338805b update circleci to build image from source 2017-03-19 16:14:56 +10:00
Bradley Cicenas
60213f1551 add debug section to docs 2017-03-19 15:10:03 +10:00
Bradley Cicenas
8aa932b29f Toggle debug mode via env var
remove logging param from global config, allowing logging server and
level to be configured inside logging subpackage from CTOP_DEBUG env var
2017-03-19 15:10:03 +10:00
Bradley Cicenas
35cc8d095d include Makefile instructions for building image from source 2017-03-19 15:10:03 +10:00
bradley
30530bc2a1 Merge pull request #52 from InTheCloudDan/patch-1
change glide to github repo, url is expired.
2017-03-19 10:50:33 +10:00
Dan O'Brien
2c282923c0 change glide to github repo, url is expired. 2017-03-18 20:39:53 -04:00
bradley
d0d39749de Merge pull request #47 from thomasleveil/patch-1
README: optimize install instructions
2017-03-18 10:47:19 +10:00
Bradley Cicenas
26b88a9790 add Makefile 2017-03-18 10:38:03 +10:00
bradley
a135a67c06 Merge pull request #49 from firecat53/patch-2
Add minimal Docker image build instructions
2017-03-17 16:57:29 +10:00
Scott Hansen
19b212f45d Add minimal Docker image build instructions
Update README to include instructions for building from source a minimal Docker image with only ctop.
2017-03-16 11:53:45 -07:00
Thomas LÉVEIL
34987df010 README: optimize install instructions 2017-03-15 20:22:08 +01:00
bradley
e2bc4d0a08 fix newline 2017-03-15 22:54:23 +10:00
bradley
4ac1348fbb Merge pull request #43 from mieciu/patch-1
Update README.md
2017-03-15 20:21:38 +10:00
mieciu
66d78a7d74 Update README.md 2017-03-15 11:14:00 +01:00
Bradley Cicenas
e62a8881a2 add brew install steps 2017-03-15 19:45:41 +10:00
Bradley Cicenas
a5b2e7b074 update aur link 2017-03-15 12:49:42 +10:00
44 changed files with 1272 additions and 214 deletions

View File

@@ -1,10 +1,16 @@
FROM quay.io/vektorcloud/glibc:latest
FROM quay.io/vektorcloud/go:1.8
ARG CTOP_VERSION=0.5
ENV CTOP_URL https://github.com/bcicen/ctop/releases/download/v${CTOP_VERSION}/ctop-${CTOP_VERSION}-linux-amd64
RUN apk add --no-cache make
RUN echo $CTOP_URL && \
wget -q $CTOP_URL -O /ctop && \
chmod +x /ctop
COPY glide.* /go/src/github.com/bcicen/ctop/
WORKDIR /go/src/github.com/bcicen/ctop/
RUN glide install
COPY . /go/src/github.com/bcicen/ctop
RUN make build && \
mkdir -p /go/bin && \
mv -v ctop /go/bin/
FROM scratch
COPY --from=0 /go/bin/ctop /ctop
ENTRYPOINT ["/ctop"]

34
Makefile Normal file
View File

@@ -0,0 +1,34 @@
NAME=ctop
VERSION=$(shell cat VERSION)
BUILD=$(shell git rev-parse --short HEAD)
EXT_LD_FLAGS="-Wl,--allow-multiple-definition"
LD_FLAGS="-w -X main.version=$(VERSION) -X main.build=$(BUILD) -extldflags=$(EXT_LD_FLAGS)"
clean:
rm -rf _build/ _release/
build:
glide install
CGO_ENABLED=0 go build -tags release -ldflags $(LD_FLAGS) -o ctop
build-dev:
go build -ldflags "-w -X main.version=$(VERSION)-dev -X main.build=$(BUILD) -extldflags=$(EXT_LD_FLAGS)"
build-all:
mkdir -p build
GOOS=darwin GOARCH=amd64 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-darwin-amd64
GOOS=linux GOARCH=amd64 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-linux-amd64
GOOS=linux GOARCH=arm go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-linux-arm
GOOS=linux GOARCH=arm64 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-linux-arm64
image:
docker build -t ctop -f Dockerfile .
release:
mkdir _release
go get github.com/progrium/gh-release/...
cp _build/* _release
gh-release create bcicen/$(NAME) $(VERSION) \
$(shell git rev-parse --abbrev-ref HEAD) $(VERSION)
.PHONY: build

View File

@@ -1,6 +1,9 @@
<p align="center"><img width="200px" src="/_docs/img/logo.png" alt="ctop"/></p>
#
![release][release] ![homebrew][homebrew]
Top-like interface for container metrics
`ctop` provides a concise and condensed overview of real-time metrics for multiple containers:
@@ -8,7 +11,7 @@ Top-like interface for container metrics
as well as an [expanded view][expanded_view] for inspecting a specific container.
`ctop` currently comes with built-in support for Docker; connectors for other container and cluster systems are planned for future releases.
`ctop` comes with built-in support for Docker and runC; connectors for other container and cluster systems are planned for future releases.
## Install
@@ -17,44 +20,39 @@ Fetch the [latest release](https://github.com/bcicen/ctop/releases) for your pla
#### Linux
```bash
wget https://github.com/bcicen/ctop/releases/download/v0.5/ctop-0.5-linux-amd64 -O ctop
sudo mv ctop /usr/local/bin/
sudo wget https://github.com/bcicen/ctop/releases/download/v0.6.0/ctop-0.6.0-linux-amd64 -O /usr/local/bin/ctop
sudo chmod +x /usr/local/bin/ctop
```
#### OS X
```bash
curl -Lo ctop https://github.com/bcicen/ctop/releases/download/v0.5/ctop-0.5-darwin-amd64
sudo mv ctop /usr/local/bin/
brew install ctop
```
or
```bash
sudo curl -Lo /usr/local/bin/ctop https://github.com/bcicen/ctop/releases/download/v0.6.0/ctop-0.6.0-darwin-amd64
sudo chmod +x /usr/local/bin/ctop
```
or run via Docker:
#### Docker
```bash
docker run -ti --name ctop --rm -v /var/run/docker.sock:/var/run/docker.sock quay.io/vektorlab/ctop:latest
docker run --rm -ti \
--name=ctop \
-v /var/run/docker.sock:/var/run/docker.sock \
quay.io/vektorlab/ctop:latest
```
`ctop` is also available for Arch in the [AUR](https://aur.archlinux.org/packages/ctop/)
`ctop` is also available for Arch in the [AUR](https://aur.archlinux.org/packages/ctop-bin/)
## Building
To build `ctop` from source, ensure you have a recent version of [glide](http://glide.sh/) installed and run:
```bash
git clone https://github.com/bcicen/ctop.git $GOPATH/src/github.com/bcicen/ctop && \
cd $GOPATH/src/github.com/bcicen/ctop && \
glide install && \
go build
```
Build steps can be found [here][build].
## Usage
`ctop` requires no arguments and will configure itself using the `DOCKER_HOST` environment variable
```bash
export DOCKER_HOST=tcp://127.0.0.1:4243
ctop
```
`ctop` requires no arguments and uses Docker host variables by default. See [connectors][connectors] for further configuration options.
### Options
@@ -80,4 +78,8 @@ s | Select container sort field
r | Reverse container sort order
q | Quit ctop
[build]: _docs/build.md
[connectors]: _docs/connectors.md
[expanded_view]: _docs/expanded.md
[release]: https://img.shields.io/github/release/bcicen/ctop.svg "ctop"
[homebrew]: https://img.shields.io/homebrew/v/ctop.svg "ctop"

View File

@@ -1 +1 @@
0.5
0.6.1

20
_docs/build.md Normal file
View File

@@ -0,0 +1,20 @@
# Build
To build `ctop` from source, ensure you have a recent version of [glide](https://github.com/Masterminds/glide) installed and run:
```bash
go get github.com/bcicen/ctop && \
cd $GOPATH/src/github.com/bcicen/ctop && \
make build
```
To build a minimal Docker image containing only `ctop`:
```bash
make image
```
Now you can run your local image:
```bash
docker run -ti --name ctop --rm -v /var/run/docker.sock:/var/run/docker.sock ctop
```

26
_docs/connectors.md Normal file
View File

@@ -0,0 +1,26 @@
# connectors
`ctop` comes with the below native connectors, enabled via the `--connector` option.
Default connector behavior can be changed by setting the relevant environment variables.
## Docker
Default connector, configurable via standard [Docker commandline varaibles](https://docs.docker.com/engine/reference/commandline/cli/#environment-variables)
#### Options
Var | Description
--- | ---
DOCKER_HOST | Daemon socket to connect to (default: `unix://var/run/docker.sock`)
## RunC
Using this connector requires full privileges to the local runC root dir (default: `/run/runc`)
#### Options
Var | Description
--- | ---
RUNC_ROOT | path to runc root (default: `/run/runc`)
RUNC_SYSTEMD_CGROUP | if set, enable systemd cgroups

56
_docs/debug.md Normal file
View 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`)

View File

@@ -7,7 +7,7 @@ machine:
dependencies:
override:
- docker info
- docker build --build-arg CTOP_VERSION=$(cat VERSION) -t ctop .
- make image
test:
override:

View File

@@ -18,7 +18,6 @@ func Init() {
GlobalParams = append(GlobalParams, p)
log.Infof("loaded config param: %s: %s", quote(p.Key), quote(p.Val))
}
for _, s := range switches {
GlobalSwitches = append(GlobalSwitches, s)
log.Infof("loaded config switch: %s: %t", quote(s.Key), s.Val)

View File

@@ -17,11 +17,6 @@ var switches = []*Switch{
Val: true,
Label: "Enable Status Header",
},
&Switch{
Key: "loggingEnabled",
Val: false,
Label: "Enable Logging Server",
},
}
type Switch struct {

View File

@@ -1,16 +1,17 @@
package metrics
package collector
import (
"github.com/bcicen/ctop/models"
api "github.com/fsouza/go-dockerclient"
)
// Docker collector
type Docker struct {
Metrics
models.Metrics
id string
client *api.Client
running bool
stream chan Metrics
stream chan models.Metrics
done chan bool
lastCpu float64
lastSysCpu float64
@@ -18,7 +19,7 @@ type Docker struct {
func NewDocker(client *api.Client, id string) *Docker {
return &Docker{
Metrics: Metrics{},
Metrics: models.Metrics{},
id: id,
client: client,
}
@@ -26,7 +27,7 @@ func NewDocker(client *api.Client, id string) *Docker {
func (c *Docker) Start() {
c.done = make(chan bool)
c.stream = make(chan Metrics)
c.stream = make(chan models.Metrics)
stats := make(chan *api.Stats)
go func() {
@@ -60,10 +61,14 @@ func (c *Docker) Running() bool {
return c.running
}
func (c *Docker) Stream() chan Metrics {
func (c *Docker) Stream() chan models.Metrics {
return c.stream
}
func (c *Docker) Logs() LogCollector {
return &DockerLogs{c.id, c.client, make(chan bool)}
}
// Stop collector
func (c *Docker) Stop() {
c.done <- true
@@ -86,7 +91,7 @@ func (c *Docker) ReadCPU(stats *api.Stats) {
func (c *Docker) ReadMem(stats *api.Stats) {
c.MemUsage = int64(stats.MemoryStats.Usage)
c.MemLimit = int64(stats.MemoryStats.Limit)
c.MemPercent = round((float64(c.MemUsage) / float64(c.MemLimit)) * 100)
c.MemPercent = percent(float64(c.MemUsage), float64(c.MemLimit))
}
func (c *Docker) ReadNet(stats *api.Stats) {

View File

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

View 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)
}

View File

@@ -1,16 +1,18 @@
// +build !release
package metrics
package collector
import (
"math/rand"
"time"
"github.com/bcicen/ctop/models"
)
// Mock collector
type Mock struct {
Metrics
stream chan Metrics
models.Metrics
stream chan models.Metrics
done bool
running bool
aggression int64
@@ -18,7 +20,7 @@ type Mock struct {
func NewMock(a int64) *Mock {
c := &Mock{
Metrics: Metrics{},
Metrics: models.Metrics{},
aggression: a,
}
c.MemLimit = 2147483648
@@ -31,7 +33,7 @@ func (c *Mock) Running() bool {
func (c *Mock) Start() {
c.done = false
c.stream = make(chan Metrics)
c.stream = make(chan models.Metrics)
go c.run()
}
@@ -39,15 +41,24 @@ func (c *Mock) Stop() {
c.done = true
}
func (c *Mock) Stream() chan Metrics {
func (c *Mock) Stream() chan models.Metrics {
return c.stream
}
func (c *Mock) Logs() LogCollector {
return &MockLogs{make(chan bool)}
}
func (c *Mock) run() {
c.running = true
rand.Seed(int64(time.Now().Nanosecond()))
defer close(c.stream)
// set to random static value, once
c.Pids = rand.Intn(12)
c.IOBytesRead = rand.Int63n(8098) * c.aggression
c.IOBytesWrite = rand.Int63n(8098) * c.aggression
for {
c.CPUUtil += rand.Intn(2) * int(c.aggression)
if c.CPUUtil >= 100 {
@@ -60,7 +71,7 @@ func (c *Mock) run() {
if c.MemUsage > c.MemLimit {
c.MemUsage = 0
}
c.MemPercent = round((float64(c.MemUsage) / float64(c.MemLimit)) * 100)
c.MemPercent = percent(float64(c.MemUsage), float64(c.MemLimit))
c.stream <- c.Metrics
if c.done {
break

View File

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

View File

@@ -0,0 +1,44 @@
// +build !darwin
package collector
import (
linuxproc "github.com/c9s/goprocinfo/linux"
"github.com/opencontainers/runc/libcontainer/system"
)
var sysMemTotal = getSysMemTotal()
var clockTicksPerSecond = uint64(system.GetClockTicks())
const nanoSecondsPerSecond = 1e9
func getSysMemTotal() int64 {
stat, err := linuxproc.ReadMemInfo("/proc/meminfo")
if err != nil {
log.Errorf("error reading system stats: %s", err)
return 0
}
return int64(stat.MemTotal * 1024)
}
// return cumulative system cpu usage in nanoseconds
func getSysCPUUsage() uint64 {
stat, err := linuxproc.ReadStat("/proc/stat")
if err != nil {
log.Errorf("error reading system stats: %s", err)
return 0
}
sum := stat.CPUStatAll.User +
stat.CPUStatAll.Nice +
stat.CPUStatAll.System +
stat.CPUStatAll.Idle +
stat.CPUStatAll.IOWait +
stat.CPUStatAll.IRQ +
stat.CPUStatAll.SoftIRQ +
stat.CPUStatAll.Steal +
stat.CPUStatAll.Guest +
stat.CPUStatAll.GuestNice
return (sum * nanoSecondsPerSecond) / clockTicksPerSecond
}

128
connector/collector/runc.go Normal file
View File

@@ -0,0 +1,128 @@
// +build !darwin
package collector
import (
"time"
"github.com/bcicen/ctop/models"
"github.com/opencontainers/runc/libcontainer"
"github.com/opencontainers/runc/libcontainer/cgroups"
)
// Runc collector
type Runc struct {
models.Metrics
id string
libc libcontainer.Container
stream chan models.Metrics
done bool
running bool
interval int // collection interval, in seconds
lastCpu float64
lastSysCpu float64
}
func NewRunc(libc libcontainer.Container) *Runc {
c := &Runc{
Metrics: models.Metrics{},
id: libc.ID(),
libc: libc,
interval: 1,
}
return c
}
func (c *Runc) Running() bool {
return c.running
}
func (c *Runc) Start() {
c.done = false
c.stream = make(chan models.Metrics)
go c.run()
}
func (c *Runc) Stop() {
c.done = true
}
func (c *Runc) Stream() chan models.Metrics {
return c.stream
}
func (c *Runc) Logs() LogCollector {
return nil
}
func (c *Runc) run() {
c.running = true
defer close(c.stream)
log.Debugf("collector started for container: %s", c.id)
for {
stats, err := c.libc.Stats()
if err != nil {
log.Errorf("failed to collect stats for container %s:\n%s", c.id, err)
break
}
c.ReadCPU(stats.CgroupStats)
c.ReadMem(stats.CgroupStats)
c.ReadNet(stats.Interfaces)
c.stream <- c.Metrics
if c.done {
break
}
time.Sleep(1 * time.Second)
}
c.running = false
}
func (c *Runc) ReadCPU(stats *cgroups.Stats) {
u := stats.CpuStats.CpuUsage
ncpus := float64(len(u.PercpuUsage))
total := float64(u.TotalUsage)
system := float64(getSysCPUUsage())
cpudiff := total - c.lastCpu
syscpudiff := system - c.lastSysCpu
c.CPUUtil = round((cpudiff / syscpudiff * 100) * ncpus)
c.lastCpu = total
c.lastSysCpu = system
c.Pids = int(stats.PidsStats.Current)
}
func (c *Runc) ReadMem(stats *cgroups.Stats) {
c.MemUsage = int64(stats.MemoryStats.Usage.Usage)
c.MemLimit = int64(stats.MemoryStats.Usage.Limit)
if c.MemLimit > sysMemTotal && sysMemTotal > 0 {
c.MemLimit = sysMemTotal
}
c.MemPercent = percent(float64(c.MemUsage), float64(c.MemLimit))
}
func (c *Runc) ReadNet(interfaces []*libcontainer.NetworkInterface) {
var rx, tx int64
for _, network := range interfaces {
rx += int64(network.RxBytes)
tx += int64(network.TxBytes)
}
c.NetRx, c.NetTx = rx, tx
}
func (c *Runc) ReadIO(stats *cgroups.Stats) {
var read, write int64
for _, blk := range stats.BlkioStats.IoServiceBytesRecursive {
if blk.Op == "Read" {
read = int64(blk.Value)
}
if blk.Op == "Write" {
write = int64(blk.Value)
}
}
c.IOBytesRead, c.IOBytesWrite = read, write
}

View File

@@ -1,35 +1,31 @@
package main
package connector
import (
"sort"
"fmt"
"strings"
"sync"
"github.com/bcicen/ctop/metrics"
"github.com/fsouza/go-dockerclient"
"github.com/bcicen/ctop/connector/collector"
"github.com/bcicen/ctop/container"
api "github.com/fsouza/go-dockerclient"
)
type ContainerSource interface {
All() Containers
Get(string) (*Container, bool)
}
type DockerContainerSource struct {
client *docker.Client
containers map[string]*Container
type Docker struct {
client *api.Client
containers map[string]*container.Container
needsRefresh chan string // container IDs requiring refresh
lock sync.RWMutex
}
func NewDockerContainerSource() *DockerContainerSource {
func NewDocker() Connector {
// init docker client
client, err := docker.NewClientFromEnv()
client, err := api.NewClientFromEnv()
if err != nil {
panic(err)
}
cm := &DockerContainerSource{
cm := &Docker{
client: client,
containers: make(map[string]*Container),
containers: make(map[string]*container.Container),
needsRefresh: make(chan string, 60),
lock: sync.RWMutex{},
}
@@ -40,9 +36,9 @@ func NewDockerContainerSource() *DockerContainerSource {
}
// Docker events watcher
func (cm *DockerContainerSource) watchEvents() {
func (cm *Docker) watchEvents() {
log.Info("docker event listener starting")
events := make(chan *docker.APIEvents)
events := make(chan *api.APIEvents)
cm.client.AddEventListener(events)
for e := range events {
@@ -60,7 +56,25 @@ func (cm *DockerContainerSource) watchEvents() {
}
}
func (cm *DockerContainerSource) refresh(c *Container) {
func portsFormat(ports map[api.Port][]api.PortBinding) string {
var exposed []string
var published []string
for k, v := range ports {
if len(v) == 0 {
exposed = append(exposed, string(k))
continue
}
for _, binding := range v {
s := fmt.Sprintf("%s:%s -> %s", binding.HostIP, binding.HostPort, k)
published = append(published, s)
}
}
return strings.Join(append(exposed, published...), "\n")
}
func (cm *Docker) refresh(c *container.Container) {
insp := cm.inspect(c.Id)
// remove container if no longer exists
if insp == nil {
@@ -69,14 +83,15 @@ func (cm *DockerContainerSource) refresh(c *Container) {
}
c.SetMeta("name", shortName(insp.Name))
c.SetMeta("image", insp.Config.Image)
c.SetMeta("ports", portsFormat(insp.NetworkSettings.Ports))
c.SetMeta("created", insp.Created.Format("Mon Jan 2 15:04:05 2006"))
c.SetState(insp.State.Status)
}
func (cm *DockerContainerSource) inspect(id string) *docker.Container {
func (cm *Docker) inspect(id string) *api.Container {
c, err := cm.client.InspectContainer(id)
if err != nil {
if _, ok := err.(*docker.NoSuchContainer); ok == false {
if _, ok := err.(*api.NoSuchContainer); ok == false {
log.Errorf(err.Error())
}
}
@@ -84,8 +99,8 @@ func (cm *DockerContainerSource) inspect(id string) *docker.Container {
}
// Mark all container IDs for refresh
func (cm *DockerContainerSource) refreshAll() {
opts := docker.ListContainersOptions{All: true}
func (cm *Docker) refreshAll() {
opts := api.ListContainersOptions{All: true}
allContainers, err := cm.client.ListContainers(opts)
if err != nil {
panic(err)
@@ -99,7 +114,7 @@ func (cm *DockerContainerSource) refreshAll() {
}
}
func (cm *DockerContainerSource) Loop() {
func (cm *Docker) Loop() {
for id := range cm.needsRefresh {
c := cm.MustGet(id)
cm.refresh(c)
@@ -107,14 +122,14 @@ func (cm *DockerContainerSource) Loop() {
}
// Get a single container, creating one anew if not existing
func (cm *DockerContainerSource) MustGet(id string) *Container {
func (cm *Docker) MustGet(id string) *container.Container {
c, ok := cm.Get(id)
// append container struct for new containers
if !ok {
// create collector
collector := metrics.NewDocker(cm.client, id)
collector := collector.NewDocker(cm.client, id)
// create container
c = NewContainer(id, collector)
c = container.New(id, collector)
cm.lock.Lock()
cm.containers[id] = c
cm.lock.Unlock()
@@ -123,7 +138,7 @@ func (cm *DockerContainerSource) MustGet(id string) *Container {
}
// Get a single container, by ID
func (cm *DockerContainerSource) Get(id string) (*Container, bool) {
func (cm *Docker) Get(id string) (*container.Container, bool) {
cm.lock.Lock()
c, ok := cm.containers[id]
cm.lock.Unlock()
@@ -131,7 +146,7 @@ func (cm *DockerContainerSource) Get(id string) (*Container, bool) {
}
// Remove containers by ID
func (cm *DockerContainerSource) delByID(id string) {
func (cm *Docker) delByID(id string) {
cm.lock.Lock()
delete(cm.containers, id)
cm.lock.Unlock()
@@ -139,14 +154,14 @@ func (cm *DockerContainerSource) delByID(id string) {
}
// Return array of all containers, sorted by field
func (cm *DockerContainerSource) All() (containers Containers) {
func (cm *Docker) All() (containers container.Containers) {
cm.lock.Lock()
for _, c := range cm.containers {
containers = append(containers, c)
}
cm.lock.Unlock()
sort.Sort(containers)
containers.Sort()
containers.Filter()
cm.lock.Unlock()
return containers
}

View File

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

View File

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

26
connector/main.go Normal file
View File

@@ -0,0 +1,26 @@
package connector
import (
"fmt"
"github.com/bcicen/ctop/container"
"github.com/bcicen/ctop/logging"
)
var log = logging.Init()
func ByName(s string) (Connector, error) {
if _, ok := enabled[s]; !ok {
msg := fmt.Sprintf("invalid connector type \"%s\"\nconnector must be one of:", s)
for k, _ := range enabled {
msg += fmt.Sprintf("\n %s", k)
}
return nil, fmt.Errorf(msg)
}
return enabled[s](), nil
}
type Connector interface {
All() container.Containers
Get(string) (*container.Container, bool)
}

View File

@@ -1,31 +1,31 @@
// +build !release
package main
package connector
import (
"math/rand"
"sort"
"strings"
"time"
"github.com/bcicen/ctop/metrics"
"github.com/bcicen/ctop/connector/collector"
"github.com/bcicen/ctop/container"
"github.com/jgautheron/codename-generator"
"github.com/nu7hatch/gouuid"
)
type MockContainerSource struct {
containers Containers
type Mock struct {
containers container.Containers
}
func NewMockContainerSource() *MockContainerSource {
cs := &MockContainerSource{}
func NewMock() *Mock {
cs := &Mock{}
go cs.Init()
go cs.Loop()
return cs
}
// Create Mock containers
func (cs *MockContainerSource) Init() {
func (cs *Mock) Init() {
rand.Seed(int64(time.Now().Nanosecond()))
for i := 0; i < 4; i++ {
@@ -38,15 +38,15 @@ func (cs *MockContainerSource) Init() {
}
func (cs *MockContainerSource) makeContainer(aggression int64) {
collector := metrics.NewMock(aggression)
c := NewContainer(makeID(), collector)
func (cs *Mock) makeContainer(aggression int64) {
collector := collector.NewMock(aggression)
c := container.New(makeID(), collector)
c.SetMeta("name", makeName())
c.SetState(makeState())
cs.containers = append(cs.containers, c)
}
func (cs *MockContainerSource) Loop() {
func (cs *Mock) Loop() {
iter := 0
for {
// Change state for random container
@@ -60,7 +60,7 @@ func (cs *MockContainerSource) Loop() {
}
// Get a single container, by ID
func (cs *MockContainerSource) Get(id string) (*Container, bool) {
func (cs *Mock) Get(id string) (*container.Container, bool) {
for _, c := range cs.containers {
if c.Id == id {
return c, true
@@ -70,14 +70,14 @@ func (cs *MockContainerSource) Get(id string) (*Container, bool) {
}
// Return array of all containers, sorted by field
func (cs *MockContainerSource) All() Containers {
sort.Sort(cs.containers)
func (cs *Mock) All() container.Containers {
cs.containers.Sort()
cs.containers.Filter()
return cs.containers
}
// Remove containers by ID
func (cs *MockContainerSource) delByID(id string) {
func (cs *Mock) delByID(id string) {
for n, c := range cs.containers {
if c.Id == id {
cs.del(n)
@@ -87,7 +87,7 @@ func (cs *MockContainerSource) delByID(id string) {
}
// Remove one or more containers by index
func (cs *MockContainerSource) del(idx ...int) {
func (cs *Mock) del(idx ...int) {
for _, i := range idx {
cs.containers = append(cs.containers[:i], cs.containers[i+1:]...)
}

243
connector/runc.go Normal file
View File

@@ -0,0 +1,243 @@
// +build !darwin
package connector
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sync"
"time"
"github.com/bcicen/ctop/connector/collector"
"github.com/bcicen/ctop/container"
"github.com/opencontainers/runc/libcontainer"
"github.com/opencontainers/runc/libcontainer/cgroups/systemd"
)
type RuncOpts struct {
root string // runc root path
systemdCgroups bool // use systemd cgroups
}
func NewRuncOpts() (RuncOpts, error) {
var opts RuncOpts
// read runc root path
root := os.Getenv("RUNC_ROOT")
if root == "" {
root = "/run/runc"
}
abs, err := filepath.Abs(root)
if err != nil {
return opts, err
}
opts.root = abs
// ensure runc root path is readable
_, err = ioutil.ReadDir(opts.root)
if err != nil {
return opts, err
}
if os.Getenv("RUNC_SYSTEMD_CGROUP") == "1" {
opts.systemdCgroups = true
}
return opts, nil
}
type Runc struct {
opts RuncOpts
factory libcontainer.Factory
containers map[string]*container.Container
libContainers map[string]libcontainer.Container
needsRefresh chan string // container IDs requiring refresh
lock sync.RWMutex
}
func NewRunc() Connector {
opts, err := NewRuncOpts()
runcFailOnErr(err)
factory, err := getFactory(opts)
runcFailOnErr(err)
cm := &Runc{
opts: opts,
factory: factory,
containers: make(map[string]*container.Container),
libContainers: make(map[string]libcontainer.Container),
needsRefresh: make(chan string, 60),
lock: sync.RWMutex{},
}
go func() {
for {
cm.refreshAll()
time.Sleep(5 * time.Second)
}
}()
go cm.Loop()
return cm
}
func (cm *Runc) GetLibc(id string) libcontainer.Container {
// return previously loaded container
libc, ok := cm.libContainers[id]
if ok {
return libc
}
// load container
libc, err := cm.factory.Load(id)
if err != nil {
// remove container if no longer exists
if lerr, ok := err.(libcontainer.Error); ok && lerr.Code() == libcontainer.ContainerNotExists {
cm.delByID(id)
} else {
log.Warningf("failed to read container: %s\n", err)
}
return nil
}
return libc
}
// update a ctop container from libcontainer
func (cm *Runc) refresh(id string) {
libc := cm.GetLibc(id)
if libc == nil {
return
}
c := cm.MustGet(id)
// remove container if entered destroyed state on last refresh
// this gives adequate time for the collector to be shut down
if c.GetMeta("state") == "destroyed" {
cm.delByID(id)
return
}
status, err := libc.Status()
if err != nil {
log.Warningf("failed to read status for container: %s\n", err)
} else {
c.SetState(status.String())
}
state, err := libc.State()
if err != nil {
log.Warningf("failed to read state for container: %s\n", err)
} else {
c.SetMeta("created", state.BaseState.Created.Format("Mon Jan 2 15:04:05 2006"))
}
conf := libc.Config()
c.SetMeta("rootfs", conf.Rootfs)
}
// Read runc root, creating any new containers
func (cm *Runc) refreshAll() {
list, err := ioutil.ReadDir(cm.opts.root)
runcFailOnErr(err)
for _, i := range list {
if i.IsDir() {
name := i.Name()
// attempt to load
libc := cm.GetLibc(name)
if libc == nil {
continue
}
_ = cm.MustGet(i.Name()) // ensure container exists
}
}
// queue all existing containers for refresh
for id, _ := range cm.containers {
cm.needsRefresh <- id
}
log.Debugf("queued %d containers for refresh", len(cm.containers))
}
func (cm *Runc) Loop() {
for id := range cm.needsRefresh {
cm.refresh(id)
}
}
// Get a single ctop container in the map matching libc container, creating one anew if not existing
func (cm *Runc) MustGet(id string) *container.Container {
c, ok := cm.Get(id)
if !ok {
libc := cm.GetLibc(id)
// create collector
collector := collector.NewRunc(libc)
// create container
c = container.New(id, collector)
name := libc.ID()
// set initial metadata
if len(name) > 12 {
name = name[0:12]
}
c.SetMeta("name", name)
// add to map
cm.lock.Lock()
cm.containers[id] = c
cm.libContainers[id] = libc
cm.lock.Unlock()
log.Debugf("saw new container: %s", id)
}
return c
}
// Get a single container, by ID
func (cm *Runc) Get(id string) (*container.Container, bool) {
cm.lock.Lock()
defer cm.lock.Unlock()
c, ok := cm.containers[id]
return c, ok
}
// Remove containers by ID
func (cm *Runc) delByID(id string) {
cm.lock.Lock()
delete(cm.containers, id)
delete(cm.libContainers, id)
cm.lock.Unlock()
log.Infof("removed dead container: %s", id)
}
// Return array of all containers, sorted by field
func (cm *Runc) All() (containers container.Containers) {
cm.lock.Lock()
for _, c := range cm.containers {
containers = append(containers, c)
}
containers.Sort()
containers.Filter()
cm.lock.Unlock()
return containers
}
func getFactory(opts RuncOpts) (libcontainer.Factory, error) {
cgroupManager := libcontainer.Cgroupfs
if opts.systemdCgroups {
if systemd.UseSystemd() {
cgroupManager = libcontainer.SystemdCgroups
} else {
return nil, fmt.Errorf("systemd cgroup enabled, but systemd support for managing cgroups is not available")
}
}
return libcontainer.New(opts.root, cgroupManager)
}
func runcFailOnErr(err error) {
if err != nil {
panic(fmt.Errorf("fatal runc error: %s", err))
}
}

View File

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

View File

@@ -1,8 +1,9 @@
package main
package container
import (
"fmt"
"regexp"
"sort"
"github.com/bcicen/ctop/config"
)
@@ -89,6 +90,7 @@ func SortFields() (fields []string) {
type Containers []*Container
func (a Containers) Sort() { sort.Sort(a) }
func (a Containers) Len() int { return len(a) }
func (a Containers) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a Containers) Less(i, j int) bool {
@@ -104,14 +106,14 @@ func (a Containers) Filter() {
re := regexp.MustCompile(fmt.Sprintf(".*%s", filter))
for _, c := range a {
c.display = true
c.Display = true
// Apply name filter
if re.FindAllString(c.GetMeta("name"), 1) == nil {
c.display = false
c.Display = false
}
// Apply state filter
if !config.GetSwitchVal("allContainers") && c.GetMeta("state") != "running" {
c.display = false
c.Display = false
}
}
}

View File

@@ -1,24 +1,23 @@
package main
import (
"math"
"github.com/bcicen/ctop/connector"
"github.com/bcicen/ctop/container"
ui "github.com/gizak/termui"
)
type GridCursor struct {
selectedID string // id of currently selected container
filtered Containers
cSource ContainerSource
}
func NewGridCursor() *GridCursor {
return &GridCursor{
cSource: NewDockerContainerSource(),
}
selectedID string // id of currently selected container
filtered container.Containers
cSource connector.Connector
isScrolling bool // toggled when actively scrolling
}
func (gc *GridCursor) Len() int { return len(gc.filtered) }
func (gc *GridCursor) Selected() *Container {
func (gc *GridCursor) Selected() *container.Container {
idx := gc.Idx()
if idx < gc.Len() {
return gc.filtered[idx]
@@ -31,10 +30,10 @@ func (gc *GridCursor) RefreshContainers() (lenChanged bool) {
oldLen := gc.Len()
// Containers filtered by display bool
gc.filtered = Containers{}
gc.filtered = container.Containers{}
var cursorVisible bool
for _, c := range gc.cSource.All() {
if c.display {
if c.Display {
if c.Id == gc.selectedID {
cursorVisible = true
}
@@ -100,6 +99,9 @@ func (gc *GridCursor) ScrollPage() {
}
func (gc *GridCursor) Up() {
gc.isScrolling = true
defer func() { gc.isScrolling = false }()
idx := gc.Idx()
if idx <= 0 { // already at top
return
@@ -116,6 +118,9 @@ func (gc *GridCursor) Up() {
}
func (gc *GridCursor) Down() {
gc.isScrolling = true
defer func() { gc.isScrolling = false }()
idx := gc.Idx()
if idx >= gc.Len()-1 { // already at bottom
return
@@ -130,3 +135,58 @@ func (gc *GridCursor) Down() {
gc.ScrollPage()
ui.Render(cGrid)
}
func (gc *GridCursor) PgUp() {
idx := gc.Idx()
if idx <= 0 { // already at top
return
}
nextidx := int(math.Max(0.0, float64(idx-cGrid.MaxRows())))
if gc.pgCount() > 0 {
cGrid.Offset = int(math.Max(float64(cGrid.Offset-cGrid.MaxRows()),
float64(0)))
}
active := gc.filtered[idx]
next := gc.filtered[nextidx]
active.Widgets.Name.UnHighlight()
gc.selectedID = next.Id
next.Widgets.Name.Highlight()
cGrid.Align()
ui.Render(cGrid)
}
func (gc *GridCursor) PgDown() {
idx := gc.Idx()
if idx >= gc.Len()-1 { // already at bottom
return
}
nextidx := int(math.Min(float64(gc.Len()-1), float64(idx+cGrid.MaxRows())))
if gc.pgCount() > 0 {
cGrid.Offset = int(math.Min(float64(cGrid.Offset+cGrid.MaxRows()),
float64(gc.Len()-cGrid.MaxRows())))
}
active := gc.filtered[idx]
next := gc.filtered[nextidx]
active.Widgets.Name.UnHighlight()
gc.selectedID = next.Id
next.Widgets.Name.Highlight()
cGrid.Align()
ui.Render(cGrid)
}
// number of pages at current row count and term height
func (gc *GridCursor) pgCount() int {
pages := gc.Len() / cGrid.MaxRows()
if gc.Len()%cGrid.MaxRows() > 0 {
pages++
}
return pages
}

View File

@@ -22,9 +22,14 @@ func NewCompactGrid() *CompactGrid {
func (cg *CompactGrid) Align() {
y := cg.Y
if cg.Offset >= len(cg.Rows) {
cg.Offset = 0
}
if cg.Offset < 0 {
cg.Offset = 0
}
// update row ypos, width recursively
for _, r := range cg.pageRows() {
r.SetY(y)

View File

@@ -2,7 +2,7 @@ package compact
import (
"github.com/bcicen/ctop/logging"
"github.com/bcicen/ctop/metrics"
"github.com/bcicen/ctop/models"
ui "github.com/gizak/termui"
)
@@ -59,7 +59,7 @@ func (row *Compact) SetMeta(k, v string) {
}
}
func (row *Compact) SetMetrics(m metrics.Metrics) {
func (row *Compact) SetMetrics(m models.Metrics) {
row.SetCPU(m.CPUUtil)
row.SetNet(m.NetRx, m.NetTx)
row.SetMem(m.MemUsage, m.MemLimit, m.MemPercent)

View File

@@ -1,10 +1,12 @@
package expanded
import (
"strings"
ui "github.com/gizak/termui"
)
var displayInfo = []string{"id", "name", "image", "state"}
var displayInfo = []string{"id", "name", "image", "ports", "state", "created"}
type Info struct {
*ui.Table
@@ -24,12 +26,33 @@ func NewInfo(id string) *Info {
func (w *Info) Set(k, v string) {
w.data[k] = v
// rebuild rows
w.Rows = [][]string{}
for _, k := range displayInfo {
if v, ok := w.data[k]; ok {
w.Rows = append(w.Rows, []string{k, v})
w.Rows = append(w.Rows, mkInfoRows(k, v)...)
}
}
w.Height = len(w.Rows) + 2
}
// Build row(s) from a key and value string
func mkInfoRows(k, v string) (rows [][]string) {
lines := strings.Split(v, "\n")
// initial row with field name
rows = append(rows, []string{k, lines[0]})
// append any additional lines in seperate row
if len(lines) > 1 {
for _, line := range lines[1:] {
if line != "" {
rows = append(rows, []string{"", line})
}
}
}
return rows
}

83
cwidgets/expanded/logs.go Normal file
View File

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

View File

@@ -2,7 +2,7 @@ package expanded
import (
"github.com/bcicen/ctop/logging"
"github.com/bcicen/ctop/metrics"
"github.com/bcicen/ctop/models"
ui "github.com/gizak/termui"
)
@@ -55,7 +55,7 @@ func (e *Expanded) Down() {
func (e *Expanded) SetWidth(w int) { e.Width = w }
func (e *Expanded) SetMeta(k, v string) { e.Info.Set(k, v) }
func (e *Expanded) SetMetrics(m metrics.Metrics) {
func (e *Expanded) SetMetrics(m models.Metrics) {
e.Cpu.Update(m.CPUUtil)
e.Net.Update(m.NetRx, m.NetTx)
e.Mem.Update(int(m.MemUsage), int(m.MemLimit))

View File

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

View File

@@ -9,6 +9,7 @@ const (
kb = 1024
mb = kb * 1024
gb = mb * 1024
tb = gb * 1024
)
// convenience method
@@ -28,8 +29,12 @@ func ByteFormat(n int64) string {
n = n / mb
return fmt.Sprintf("%sM", strconv.FormatInt(n, 10))
}
nf := float64(n) / gb
return fmt.Sprintf("%sG", unpadFloat(nf))
if n < tb {
nf := float64(n) / gb
return fmt.Sprintf("%sG", unpadFloat(nf))
}
nf := float64(n) / tb
return fmt.Sprintf("%sT", unpadFloat(nf))
}
func unpadFloat(f float64) string {

View File

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

54
glide.lock generated
View File

@@ -1,12 +1,22 @@
hash: c13011881e895378f374b68596a59a0ea6def372b4f5239b2d8aa342eaa46a4b
updated: 2017-03-12T09:53:35.212073637+07:00
hash: 0d550b01b3a1c4751a8f5c3fba0c43f62252055e231712729628e514bb494da8
updated: 2017-06-09T18:11:10.930196504-03:00
imports:
- name: github.com/Azure/go-ansiterm
version: fa152c58bc15761d0200cb75fe958b89a9d4888e
subpackages:
- winterm
- name: github.com/c9s/goprocinfo
version: b34328d6e0cd139894ea7347d2624ccf31fa3c58
subpackages:
- linux
- name: github.com/coreos/go-systemd
version: b4a58d95188dd092ae20072bac14cece0e67c388
subpackages:
- activation
- dbus
- util
- name: github.com/docker/docker
version: ce07fb6b0f1b8765b92022e45f96bd4349812e06
version: 90d35abf7b3535c1c319c872900fbd76374e521c
subpackages:
- api/types
- api/types/blkiodev
@@ -27,9 +37,11 @@ imports:
- pkg/jsonlog
- pkg/jsonmessage
- pkg/longpath
- pkg/mount
- pkg/pools
- pkg/promise
- pkg/stdcopy
- pkg/symlink
- pkg/system
- pkg/term
- pkg/term/windows
@@ -45,6 +57,12 @@ imports:
version: ea10e6ccee219e572ffad0ac1909f1a17f6db7d6
repo: https://github.com/bcicen/termui
vcs: git
- name: github.com/godbus/dbus
version: c7fdd8b5cd55e87b4e1f4e372cdb1db61dd6c66f
- name: github.com/golang/protobuf
version: f7137ae6b19afbfd61a94b746fda3b3fe0491874
subpackages:
- proto
- name: github.com/hashicorp/go-cleanhttp
version: 3573b8b52aa7b37b9358d966a898feb387f62437
- name: github.com/jgautheron/codename-generator
@@ -63,15 +81,41 @@ imports:
version: 91bae1bb5fa9ee504905ecbe7043fa30e92feaa3
- name: github.com/nu7hatch/gouuid
version: 179d4d0c4d8d407a32af483c2354df1d2c91e6c3
- name: github.com/Nvveen/Gotty
version: cd527374f1e5bff4938207604a14f2e38a9cf512
- name: github.com/op/go-logging
version: b2cb9fa56473e98db8caba80237377e83fe44db5
- name: github.com/opencontainers/runc
version: 31980a53ae7887b2c8f8715d13c3eb486c27b6cf
version: baf6536d6259209c3edfa2b22237af82942d3dfa
subpackages:
- libcontainer
- libcontainer/apparmor
- libcontainer/cgroups
- libcontainer/cgroups/fs
- libcontainer/cgroups/systemd
- libcontainer/configs
- libcontainer/configs/validate
- libcontainer/criurpc
- libcontainer/keys
- libcontainer/label
- libcontainer/seccomp
- libcontainer/selinux
- libcontainer/stacktrace
- libcontainer/system
- libcontainer/user
- libcontainer/utils
- name: github.com/seccomp/libseccomp-golang
version: 1b506fc7c24eec5a3693cdcbed40d9c226cfc6a1
- name: github.com/Sirupsen/logrus
version: 1deb2db2a6fff8a35532079061b903c3a25eed52
version: 26709e2714106fb8ad40b773b711ebce25b78914
- name: github.com/syndtr/gocapability
version: 2c00daeb6c3b45114c80ac44119e7b8801fdd852
subpackages:
- capability
- name: github.com/vishvananda/netlink
version: 1e2e08e8a2dcdacaae3f14ac44c5cfa31361f270
subpackages:
- nl
- name: golang.org/x/net
version: a6577fac2d73be281a500b310739095313165611
subpackages:

View File

@@ -1,11 +1,18 @@
package: github.com/bcicen/ctop
import:
- package: github.com/c9s/goprocinfo/linux
- package: github.com/docker/docker
version: ^17.5.0-ce-rc3
- package: github.com/opencontainers/runc
version: 0.1.1
- package: github.com/fsouza/go-dockerclient
version: 318513eb1ab27495afbc67f671ba1080513d8aa0
- package: github.com/gizak/termui
version: barchart-numfmt
repo: https://github.com/bcicen/termui
vcs: git
- package: github.com/jgautheron/codename-generator
- package: github.com/nu7hatch/gouuid
- package: github.com/c9s/goprocinfo/linux
- package: github.com/op/go-logging
version: ^1.0.0

14
grid.go
View File

@@ -2,6 +2,7 @@ package main
import (
"github.com/bcicen/ctop/config"
"github.com/bcicen/ctop/container"
"github.com/bcicen/ctop/cwidgets/expanded"
ui "github.com/gizak/termui"
)
@@ -34,7 +35,7 @@ func RedrawRows(clr bool) {
ui.Render(cGrid)
}
func ExpandView(c *Container) {
func ExpandView(c *container.Container) {
ui.Clear()
ui.DefaultEvtStream.ResetHandlers()
defer ui.DefaultEvtStream.ResetHandlers()
@@ -61,8 +62,11 @@ func ExpandView(c *Container) {
}
func RefreshDisplay() {
needsClear := cursor.RefreshContainers()
RedrawRows(needsClear)
// skip display refresh during scroll
if !cursor.isScrolling {
needsClear := cursor.RefreshContainers()
RedrawRows(needsClear)
}
}
func Display() bool {
@@ -79,6 +83,10 @@ func Display() bool {
HandleKeys("up", cursor.Up)
HandleKeys("down", cursor.Down)
HandleKeys("pgup", cursor.PgUp)
HandleKeys("pgdown", cursor.PgDown)
HandleKeys("exit", ui.StopLoop)
HandleKeys("help", func() {
menu = HelpMenu

View File

@@ -14,9 +14,18 @@ var keyMap = map[string][]string{
"/sys/kbd/<down>",
"/sys/kbd/j",
},
"pgup": []string{
"/sys/kbd/<previous>",
"/sys/kbd/C-<up>",
},
"pgdown": []string{
"/sys/kbd/<next>",
"/sys/kbd/C-<down>",
},
"exit": []string{
"/sys/kbd/q",
"/sys/kbd/C-c",
"/sys/kbd/<escape>",
},
"help": []string{
"/sys/kbd/h",

View File

@@ -1,6 +1,7 @@
package logging
import (
"os"
"time"
"github.com/op/go-logging"
@@ -13,7 +14,7 @@ const (
var (
Log *CTopLogger
exited bool
level = logging.INFO
level = logging.INFO // default level
format = logging.MustStringFormatter(
`%{color}%{time:15:04:05.000} ▶ %{level:.4s} %{id:03x}%{color:reset} %{message}`,
)
@@ -33,6 +34,11 @@ func Init() *CTopLogger {
logging.NewMemoryBackend(size),
}
if debugMode() {
level = logging.DEBUG
StartServer()
}
backendLvl := logging.AddModuleLevel(Log.backend)
backendLvl.SetLevel(level, "")
@@ -71,3 +77,6 @@ func (log *CTopLogger) Exit() {
exited = true
StopServer()
}
func debugMode() bool { return os.Getenv("CTOP_DEBUG") == "1" }
func debugModeTCP() bool { return os.Getenv("CTOP_DEBUG_TCP") == "1" }

View File

@@ -7,7 +7,8 @@ import (
)
const (
path = "/tmp/ctop.sock"
socketPath = "./ctop.sock"
socketAddr = "0.0.0.0:9000"
)
var server struct {
@@ -16,7 +17,13 @@ var server struct {
}
func getListener() net.Listener {
ln, err := net.Listen("unix", path)
var ln net.Listener
var err error
if debugModeTCP() {
ln, err = net.Listen("tcp", socketAddr)
} else {
ln, err = net.Listen("unix", socketPath)
}
if err != nil {
panic(err)
}

57
main.go
View File

@@ -4,30 +4,34 @@ import (
"flag"
"fmt"
"os"
"runtime"
"github.com/bcicen/ctop/config"
"github.com/bcicen/ctop/connector"
"github.com/bcicen/ctop/container"
"github.com/bcicen/ctop/cwidgets/compact"
"github.com/bcicen/ctop/logging"
"github.com/bcicen/ctop/widgets"
ui "github.com/gizak/termui"
tm "github.com/nsf/termbox-go"
)
var (
build = "none"
version = "dev-build"
build = "none"
version = "dev-build"
goVersion = runtime.Version()
log *logging.CTopLogger
cursor *GridCursor
cGrid *compact.CompactGrid
header *widgets.CTopHeader
versionStr = fmt.Sprintf("ctop version %v, build %v %v", version, build, goVersion)
)
func main() {
defer panicExit()
// init global config
config.Init()
// parse command line arguments
var versionFlag = flag.Bool("v", false, "output version information and exit")
var helpFlag = flag.Bool("h", false, "display this help dialog")
@@ -36,10 +40,11 @@ func main() {
var sortFieldFlag = flag.String("s", "", "select container sort field")
var reverseSortFlag = flag.Bool("r", false, "reverse container sort order")
var invertFlag = flag.Bool("i", false, "invert default colors")
var connectorFlag = flag.String("connector", "docker", "container connector to use")
flag.Parse()
if *versionFlag {
printVersion()
fmt.Println(versionStr)
os.Exit(0)
}
@@ -48,6 +53,12 @@ func main() {
os.Exit(0)
}
// init logger
log = logging.Init()
// init global config
config.Init()
// override default config values with command line flags
if *filterFlag != "" {
config.Update("filterStr", *filterFlag)
@@ -66,12 +77,6 @@ func main() {
config.Toggle("sortReversed")
}
// init logger
log = logging.Init()
if config.GetSwitchVal("loggingEnabled") {
logging.StartServer()
}
// init ui
if *invertFlag {
InvertColorMap()
@@ -80,26 +85,36 @@ func main() {
if err := ui.Init(); err != nil {
panic(err)
}
defer ui.Close()
defer Shutdown()
// init grid, cursor, header
cursor = NewGridCursor()
conn, err := connector.ByName(*connectorFlag)
if err != nil {
panic(err)
}
cursor = &GridCursor{cSource: conn}
cGrid = compact.NewCompactGrid()
header = widgets.NewCTopHeader()
for {
exit := Display()
if exit {
log.Notice("shutting down")
log.Exit()
return
}
}
}
func Shutdown() {
log.Notice("shutting down")
log.Exit()
if tm.IsInit {
ui.Close()
}
}
// ensure a given sort field is valid
func validSort(s string) {
if _, ok := Sorters[s]; !ok {
if _, ok := container.Sorters[s]; !ok {
fmt.Printf("invalid sort field: %s\n", s)
os.Exit(1)
}
@@ -107,8 +122,8 @@ func validSort(s string) {
func panicExit() {
if r := recover(); r != nil {
ui.Clear()
fmt.Printf("panic: %s\n", r)
Shutdown()
fmt.Printf("error: %s\n", r)
os.Exit(1)
}
}
@@ -124,7 +139,3 @@ func printHelp() {
fmt.Println(helpMsg)
flag.PrintDefaults()
}
func printVersion() {
fmt.Printf("ctop version %v, build %v\n", version, build)
}

View File

@@ -2,6 +2,7 @@ package main
import (
"github.com/bcicen/ctop/config"
"github.com/bcicen/ctop/container"
"github.com/bcicen/ctop/widgets"
"github.com/bcicen/ctop/widgets/menu"
ui "github.com/gizak/termui"
@@ -74,7 +75,7 @@ func SortMenu() {
m.SortItems = true
m.BorderLabel = "Sort Field"
for _, field := range SortFields() {
for _, field := range container.SortFields() {
m.AddItems(menu.Item{field, ""})
}

View File

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

33
models/main.go Normal file
View 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,
}
}