UI enhancements, stats gathering done
This commit is contained in:
parent
c728f4f2af
commit
57c0ddd71d
9 changed files with 181 additions and 55 deletions
9
api.http
9
api.http
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
package docker
|
19
docker/stats.go
Normal file
19
docker/stats.go
Normal 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"`
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
22
main.go
|
@ -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)
|
||||
})
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
Loading…
Reference in a new issue