UI enhancements, stats gathering done

This commit is contained in:
Adam Štrauch 2020-07-15 23:32:28 +02:00
parent c728f4f2af
commit 57c0ddd71d
Signed by: cx
GPG Key ID: 018304FFA8988F8D
9 changed files with 181 additions and 55 deletions

View File

@ -31,6 +31,7 @@ Content-type: application/json
###
# Stop
PUT http://localhost:1323/v1/apps/test_1234/stop
Content-type: application/json
@ -58,6 +59,14 @@ Content-type: application/json
GET http://localhost:1323/v1/apps/test_1234/stats
Content-type: application/json
###
# Get
GET http://localhost:1323/v1/apps/test_1234
Content-type: application/json
###
# List of all apps

View File

@ -25,9 +25,11 @@ type Label struct {
// AppState contains info about runnint application, it's not saved in the database
type AppState struct {
State string `json:"state"`
CPUUsage int `json:"cpu_usage"`
MemoryUsage int `json:"memory_usage"`
State string `json:"state"`
CPUUsage float64 `json:"cpu_usage"` // in percents
MemoryUsage int `json:"memory_usage"` // in MB
DiskUsageBytes int `json:"disk_usage_bytes"`
DiskUsageinodes int `json:"disk_usage_inodes"`
}
// App keeps info about hosted application
@ -42,7 +44,11 @@ type App struct {
Memory int `json:"memory"` // Limit in MB
// Labels []Label `json:"labels"` // username:cx or user_id:1
State AppState `json:"state" gorm:"-"`
State string `json:"state"`
CPUUsage float64 `json:"cpu_usage"` // in percents
MemoryUsage int `json:"memory_usage"` // in MB
DiskUsageBytes int `json:"disk_usage_bytes"`
DiskUsageinodes int `json:"disk_usage_inodes"`
}
// Validate do basic checks of the struct values

View File

@ -2,6 +2,7 @@ package docker
import (
"context"
"encoding/json"
"errors"
"io"
"io/ioutil"
@ -18,6 +19,9 @@ import (
"github.com/docker/go-connections/nat"
)
// Stats delay in seconds
const statsDelay = 5
// Docker timeout
const dockerTimeout = 10
@ -118,20 +122,34 @@ func (d *Driver) Stats(name string) (float64, int, error) {
return 0.0, 0, err
}
stats, err := cli.ContainerStats(context.TODO(), containerID, false)
if err != nil {
return 0.0, 0, nil
rows := make([]ContainerStats, 2)
for idx := range rows {
stats, err := cli.ContainerStats(context.TODO(), containerID, false)
if err != nil {
return 0.0, 0, err
}
data, err := ioutil.ReadAll(stats.Body)
if err != nil {
return 0.0, 0, err
}
// It returns one JSON:
// {"read":"2020-07-11T20:42:31.486726241Z","preread":"2020-07-11T20:42:30.484048602Z","pids_stats":{"current":7},"blkio_stats":{"io_service_bytes_recursive":[{"major":253,"minor":0,"op":"Read","value":0},{"major":253,"minor":0,"op":"Write","value":20480},{"major":253,"minor":0,"op":"Sync","value":12288},{"major":253,"minor":0,"op":"Async","value":8192},{"major":253,"minor":0,"op":"Discard","value":0},{"major":253,"minor":0,"op":"Total","value":20480}],"io_serviced_recursive":[{"major":253,"minor":0,"op":"Read","value":0},{"major":253,"minor":0,"op":"Write","value":5},{"major":253,"minor":0,"op":"Sync","value":3},{"major":253,"minor":0,"op":"Async","value":2},{"major":253,"minor":0,"op":"Discard","value":0},{"major":253,"minor":0,"op":"Total","value":5}],"io_queue_recursive":[],"io_service_time_recursive":[],"io_wait_time_recursive":[],"io_merged_recursive":[],"io_time_recursive":[],"sectors_recursive":[]},"num_procs":0,"storage_stats":{},"cpu_stats":{"cpu_usage":{"total_usage":758392753,"percpu_usage":[302688474,0,11507116,124238500,222136766,5656446,3009320,0,19406386,1397028,6201423,62151294,0,0,0,0],"usage_in_kernelmode":100000000,"usage_in_usermode":640000000},"system_cpu_usage":119385810000000,"online_cpus":12,"throttling_data":{"periods":21,"throttled_periods":1,"throttled_time":2995938}},"precpu_stats":{"cpu_usage":{"total_usage":758282347,"percpu_usage":[302688474,0,11507116,124238500,222026360,5656446,3009320,0,19406386,1397028,6201423,62151294,0,0,0,0],"usage_in_kernelmode":100000000,"usage_in_usermode":640000000},"system_cpu_usage":119373720000000,"online_cpus":12,"throttling_data":{"periods":21,"throttled_periods":1,"throttled_time":2995938}},"memory_stats":{"usage":21626880,"max_usage":22630400,"stats":{"active_anon":15949824,"active_file":0,"cache":0,"dirty":0,"hierarchical_memory_limit":144179200,"hierarchical_memsw_limit":288358400,"inactive_anon":0,"inactive_file":0,"mapped_file":0,"pgfault":13167,"pgmajfault":0,"pgpgin":7293,"pgpgout":3406,"rss":15900672,"rss_huge":0,"total_active_anon":15949824,"total_active_file":0,"total_cache":0,"total_dirty":0,"total_inactive_anon":0,"total_inactive_file":0,"total_mapped_file":0,"total_pgfault":13167,"total_pgmajfault":0,"total_pgpgin":7293,"total_pgpgout":3406,"total_rss":15900672,"total_rss_huge":0,"total_unevictable":0,"total_writeback":0,"unevictable":0,"writeback":0},"limit":144179200},"name":"/test_1234","id":"576878d645efecc8e5e2a57b88351f7b5c551e3fc72dc8473fd965d10dfddbec","networks":{"eth0":{"rx_bytes":6150,"rx_packets":37,"rx_errors":0,"rx_dropped":0,"tx_bytes":0,"tx_packets":0,"tx_errors":0,"tx_dropped":0}}}
err = json.Unmarshal(data, &rows[idx])
if err != nil {
return 0.0, 0, err
}
if idx == 0 {
time.Sleep(statsDelay * time.Second)
}
}
data, err := ioutil.ReadAll(stats.Body)
if err != nil {
return 0.0, 0, err
}
// It returns one JSON:
// {"read":"2020-07-11T20:42:31.486726241Z","preread":"2020-07-11T20:42:30.484048602Z","pids_stats":{"current":7},"blkio_stats":{"io_service_bytes_recursive":[{"major":253,"minor":0,"op":"Read","value":0},{"major":253,"minor":0,"op":"Write","value":20480},{"major":253,"minor":0,"op":"Sync","value":12288},{"major":253,"minor":0,"op":"Async","value":8192},{"major":253,"minor":0,"op":"Discard","value":0},{"major":253,"minor":0,"op":"Total","value":20480}],"io_serviced_recursive":[{"major":253,"minor":0,"op":"Read","value":0},{"major":253,"minor":0,"op":"Write","value":5},{"major":253,"minor":0,"op":"Sync","value":3},{"major":253,"minor":0,"op":"Async","value":2},{"major":253,"minor":0,"op":"Discard","value":0},{"major":253,"minor":0,"op":"Total","value":5}],"io_queue_recursive":[],"io_service_time_recursive":[],"io_wait_time_recursive":[],"io_merged_recursive":[],"io_time_recursive":[],"sectors_recursive":[]},"num_procs":0,"storage_stats":{},"cpu_stats":{"cpu_usage":{"total_usage":758392753,"percpu_usage":[302688474,0,11507116,124238500,222136766,5656446,3009320,0,19406386,1397028,6201423,62151294,0,0,0,0],"usage_in_kernelmode":100000000,"usage_in_usermode":640000000},"system_cpu_usage":119385810000000,"online_cpus":12,"throttling_data":{"periods":21,"throttled_periods":1,"throttled_time":2995938}},"precpu_stats":{"cpu_usage":{"total_usage":758282347,"percpu_usage":[302688474,0,11507116,124238500,222026360,5656446,3009320,0,19406386,1397028,6201423,62151294,0,0,0,0],"usage_in_kernelmode":100000000,"usage_in_usermode":640000000},"system_cpu_usage":119373720000000,"online_cpus":12,"throttling_data":{"periods":21,"throttled_periods":1,"throttled_time":2995938}},"memory_stats":{"usage":21626880,"max_usage":22630400,"stats":{"active_anon":15949824,"active_file":0,"cache":0,"dirty":0,"hierarchical_memory_limit":144179200,"hierarchical_memsw_limit":288358400,"inactive_anon":0,"inactive_file":0,"mapped_file":0,"pgfault":13167,"pgmajfault":0,"pgpgin":7293,"pgpgout":3406,"rss":15900672,"rss_huge":0,"total_active_anon":15949824,"total_active_file":0,"total_cache":0,"total_dirty":0,"total_inactive_anon":0,"total_inactive_file":0,"total_mapped_file":0,"total_pgfault":13167,"total_pgmajfault":0,"total_pgpgin":7293,"total_pgpgout":3406,"total_rss":15900672,"total_rss_huge":0,"total_unevictable":0,"total_writeback":0,"unevictable":0,"writeback":0},"limit":144179200},"name":"/test_1234","id":"576878d645efecc8e5e2a57b88351f7b5c551e3fc72dc8473fd965d10dfddbec","networks":{"eth0":{"rx_bytes":6150,"rx_packets":37,"rx_errors":0,"rx_dropped":0,"tx_bytes":0,"tx_packets":0,"tx_errors":0,"tx_dropped":0}}}
log.Println(string(data))
cpuUsage := (float64(rows[1].CPU.Usage.Total) - float64(rows[0].CPU.Usage.Total)) / statsDelay / 10000000
return 0.0, 0, nil
return cpuUsage, rows[1].Memory.Usage, nil
}
// Remove removes container represented by containerID

View File

@ -1 +0,0 @@
package docker

19
docker/stats.go Normal file
View File

@ -0,0 +1,19 @@
package docker
// ContainerStats contains fields returned in docker stats function stream
type ContainerStats struct {
Pids struct {
Current int `json:"current"`
} `json:"pids_stats"`
CPU struct {
Usage struct {
Total int `json:"total_usage"`
} `json:"cpu_usage"`
} `json:"cpu_stats"`
Memory struct {
Usage int `json:"usage"`
MaxUsage int `json:"max_usage"`
Limit int `json:"limit"`
} `json:"memory_stats"`
ID string `json:"id"`
}

View File

@ -2,6 +2,7 @@ package docker
import (
"bytes"
"log"
"os"
"os/exec"
"path/filepath"
@ -15,14 +16,19 @@ func du(path string) (int, int, error) {
// Occupied space
var out bytes.Buffer
command := exec.Command("/usr/bin/du -m -s " + path)
var errOut bytes.Buffer
command := exec.Command("/usr/bin/du", "-m", "-s", path)
command.Stdout = &out
command.Stderr = &errOut
err := command.Run()
if err != nil {
log.Println(errOut.String())
log.Println("/usr/bin/du -m -s " + path)
return space, inodes, err
}
fields := strings.Fields(strings.TrimSpace(out.String()))
out.Reset()
errOut.Reset()
if len(fields) == 2 {
space, err = strconv.Atoi(fields[0])
@ -32,17 +38,21 @@ func du(path string) (int, int, error) {
}
// Occupied inodes
command = exec.Command("/usr/bin/du --inodes -s " + path)
command = exec.Command("/usr/bin/du", "--inodes", "-s", path)
command.Stdout = &out
command.Stderr = &errOut
err = command.Run()
if err != nil {
log.Println(errOut.String())
log.Println("/usr/bin/du --inodes -s " + path)
return space, inodes, err
}
fields = strings.Fields(strings.TrimSpace(out.String()))
out.Reset()
errOut.Reset()
if len(fields) == 2 {
space, err = strconv.Atoi(fields[0])
inodes, err = strconv.Atoi(fields[0])
if err != nil {
return inodes, inodes, err
}

View File

@ -23,21 +23,31 @@ func (c *Container) volumeHostPath() string {
}
// GetApp app object with populated state fields
func (c *Container) GetApp() (*apps.App, error) {
func (c *Container) GetApp() (*apps.AppState, error) {
status, err := c.Status()
if err != nil {
return nil, err
}
state := apps.AppState{
State: status,
CPUUsage: 0,
MemoryUsage: 0,
cpu, memory, err := c.ResourceUsage()
if err != nil {
return nil, err
}
c.App.State = state
bytes, inodes, err := c.DiskUsage()
if err != nil {
return nil, err
}
return c.App, nil
state := apps.AppState{
State: status,
CPUUsage: cpu,
MemoryUsage: memory,
DiskUsageBytes: bytes,
DiskUsageinodes: inodes,
}
return &state, nil
}
// Status returns state of the container

22
main.go
View File

@ -41,19 +41,7 @@ func main() {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
populatedApps := []*apps.App{}
for _, app := range *applications {
container := docker.Container{
App: &app,
}
populatedApp, err := container.GetApp()
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
populatedApps = append(populatedApps, populatedApp)
}
return c.JSON(http.StatusOK, populatedApps)
return c.JSON(http.StatusOK, applications)
})
// Returns one app
@ -65,14 +53,6 @@ func main() {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
container := docker.Container{
App: app,
}
_, err = container.GetApp()
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
return c.JSON(http.StatusOK, app)
})

View File

@ -13,6 +13,20 @@
<!-- Bootstrap core CSS -->
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
font-size: 12px;
}
.success {
color: green;
}
.error {
color: red;
}
</style>
</head>
<body>
@ -32,6 +46,10 @@
<div class="col-lg-12">
<h1 class="mt-5">Applications</h1>
<div v-if="api_response">
<strong>Response:</strong> <span v-bind:class="{ success: isSuccess, error: hasError }">{( api_response )}</span>
</div>
<table class="table table-striped">
<thead>
<tr>
@ -50,18 +68,18 @@
<tr v-for="app in apps">
<td><strong>{( app.name )}</strong></td>
<td>{( app.state.state )}</td>
<td>{( app.image )}</td>
<td>{( app.image.replace("docker.io/", "") )}</td>
<td>- / {( app.cpu )} %</td>
<td>- / {( app.memory )} MB</td>
<td>- GB</td>
<td>-</td>
<td>-</td>
<td>
<button class="btn btn-warning btn-sm">Rebuild</button>
<button class="btn btn-success btn-sm">Start</button>
<button class="btn btn-warning btn-sm">Stop</button>
<button class="btn btn-warning btn-sm">Destroy</button>
<button class="btn btn-danger btn-sm">Delete</button>
<button class="btn btn-success btn-sm" v-on:click="start(app.name)" v-if="['stopped'].includes(app.state.state)">Start</button>
<button class="btn btn-warning btn-sm" v-on:click="stop(app.name)" v-if="['running'].includes(app.state.state)">Stop</button>
<button class="btn btn-warning btn-sm" v-on:click="restart(app.name)" v-if="['running'].includes(app.state.state)">Restart</button>
<button class="btn btn-warning btn-sm" v-on:click="rebuild(app.name)">Rebuild</button>
<button class="btn btn-danger btn-sm" v-on:click="remove(app.name)">Delete</button>
</td>
</tr>
</tbody>
@ -80,11 +98,68 @@
delimiters: ['{(', ')}'],
data: {
apps: [],
api_status_code: 0,
api_response: "",
},
created() {
fetch('/v1/apps').then(response => response.json())
.then(data => this.apps = data);
}
.then(data => this.apps = data);
},
methods: {
isSuccess: () => {
return this.api_status_code >= 200 && this.api_status_code <= 299
},
hasError: () => {
return this.api_status_code >= 400 && this.api_status_code < 505
},
start: (id) => {
app.api_response = "working"
fetch('/v1/apps/'+id+'/start', {method: 'PUT'})
.then(response => {
app.api_status_code = response.status
return response.json()
})
.then(data => app.api_response = data)
},
stop: (id) => {
app.api_response = "working"
fetch('/v1/apps/'+id+'/stop', {method: 'PUT'})
.then(response => {
app.api_status_code = response.status
return response.json()
})
.then(data => app.api_response = data)
},
restart: (id) => {
app.api_response = "working"
fetch('/v1/apps/'+id+'/restart', {method: 'PUT'})
.then(response => {
app.api_status_code = response.status
return response.json()
})
.then(data => app.api_response = data)
},
rebuild: (id) => {
app.api_response = "working"
fetch('/v1/apps/'+id+'/rebuild', {method: 'PUT'})
.then(response => {
app.api_status_code = response.status
return response.json()
})
.then(data => {
app.api_response = data
})
},
remove: (id) => {
app.api_response = "working"
fetch('/v1/apps/'+id, {method: 'DELETE'})
.then(response => {
response.json()
app.api_status_code = response.status
})
.then(data => app.api_response = data)
},
},
})
</script>