From 57c0ddd71db5af67367e67a57c58a2a8ec6c5e62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20=C5=A0trauch?= Date: Wed, 15 Jul 2020 23:32:28 +0200 Subject: [PATCH] UI enhancements, stats gathering done --- api.http | 9 +++++ apps/types.go | 14 +++++--- docker/docker.go | 40 +++++++++++++++------ docker/main.go | 1 - docker/stats.go | 19 ++++++++++ docker/tools.go | 16 +++++++-- docker/types.go | 24 +++++++++---- main.go | 22 +----------- ui/index.html | 91 +++++++++++++++++++++++++++++++++++++++++++----- 9 files changed, 181 insertions(+), 55 deletions(-) delete mode 100644 docker/main.go create mode 100644 docker/stats.go diff --git a/api.http b/api.http index 1ecb124..2000617 100644 --- a/api.http +++ b/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 diff --git a/apps/types.go b/apps/types.go index 0c31a73..7e9c79f 100644 --- a/apps/types.go +++ b/apps/types.go @@ -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 diff --git a/docker/docker.go b/docker/docker.go index ebe5d80..d09348f 100644 --- a/docker/docker.go +++ b/docker/docker.go @@ -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 diff --git a/docker/main.go b/docker/main.go deleted file mode 100644 index 1cdc3ff..0000000 --- a/docker/main.go +++ /dev/null @@ -1 +0,0 @@ -package docker diff --git a/docker/stats.go b/docker/stats.go new file mode 100644 index 0000000..7e9a073 --- /dev/null +++ b/docker/stats.go @@ -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"` +} diff --git a/docker/tools.go b/docker/tools.go index fe3fdff..5bf0e0b 100644 --- a/docker/tools.go +++ b/docker/tools.go @@ -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 } diff --git a/docker/types.go b/docker/types.go index 65ea4dc..9408728 100644 --- a/docker/types.go +++ b/docker/types.go @@ -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 diff --git a/main.go b/main.go index a488778..cfcea56 100644 --- a/main.go +++ b/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) }) diff --git a/ui/index.html b/ui/index.html index 10ea451..bcfe0ec 100644 --- a/ui/index.html +++ b/ui/index.html @@ -13,6 +13,20 @@ + + @@ -32,6 +46,10 @@

Applications

+
+ Response: {( api_response )} +
+ @@ -50,18 +68,18 @@ - + @@ -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) + }, + }, })
{( app.name )} {( app.state.state )}{( app.image )}{( app.image.replace("docker.io/", "") )} - / {( app.cpu )} % - / {( app.memory )} MB - GB - - - - - - - + + + + +