diff --git a/services/docker.go b/services/docker.go index 223de90..803793a 100644 --- a/services/docker.go +++ b/services/docker.go @@ -1,6 +1,7 @@ package services import ( + "io" "log" "strings" @@ -28,6 +29,12 @@ func init() { } +func GetContainerStats(id string) (io.ReadCloser, error) { + stats, err := c.ContainerStats(context.Background(), id, true) + + return stats.Body, err +} + func GetContainerInfo(id string) (types.ContainerJSON, error) { return c.ContainerInspect(context.Background(), id) } diff --git a/services/instance.go b/services/instance.go index fa4cd7c..4097832 100644 --- a/services/instance.go +++ b/services/instance.go @@ -2,6 +2,8 @@ package services import ( "context" + "encoding/json" + "fmt" "io" "log" "os" @@ -10,17 +12,19 @@ import ( "golang.org/x/text/encoding" "github.com/docker/docker/api/types" + units "github.com/docker/go-units" ) var rw sync.Mutex type Instance struct { - session *Session `json:"-"` - Name string `json:"name"` - Hostname string `json:"hostname"` - IP string `json:"ip"` - conn *types.HijackedResponse `json:"-"` - ctx context.Context `json:"-"` + session *Session `json:"-"` + Name string `json:"name"` + Hostname string `json:"hostname"` + IP string `json:"ip"` + conn *types.HijackedResponse `json:"-"` + ctx context.Context `json:"-"` + statsReader io.ReadCloser `json:"-"` } func (i *Instance) IsConnected() bool { @@ -70,6 +74,9 @@ func NewInstance(session *Session) (*Instance, error) { wsServer.BroadcastTo(session.Id, "new instance", instance.Name, instance.IP, instance.Hostname) + // Start collecting stats + go instance.CollectStats() + return instance, nil } @@ -82,6 +89,67 @@ func (s *sessionWriter) Write(p []byte) (n int, err error) { return len(p), nil } +func (o *Instance) CollectStats() { + reader, err := GetContainerStats(o.Hostname) + if err != nil { + log.Println("Error while trying to collect instance stats", err) + return + } + o.statsReader = reader + dec := json.NewDecoder(o.statsReader) + var ( + mem = 0.0 + memLimit = 0.0 + memPercent = 0.0 + v *types.StatsJSON + memFormatted = "" + + cpuPercent = 0.0 + previousCPU uint64 + previousSystem uint64 + cpuFormatted = "" + ) + for { + e := dec.Decode(&v) + if e != nil { + break + } + + // Memory + if v.MemoryStats.Limit != 0 { + memPercent = float64(v.MemoryStats.Usage) / float64(v.MemoryStats.Limit) * 100.0 + } + mem = float64(v.MemoryStats.Usage) + memLimit = float64(v.MemoryStats.Limit) + + memFormatted = fmt.Sprintf("%.2f%% (%s / %s)", memPercent, units.BytesSize(mem), units.BytesSize(memLimit)) + + // cpu + previousCPU = v.PreCPUStats.CPUUsage.TotalUsage + previousSystem = v.PreCPUStats.SystemUsage + cpuPercent = calculateCPUPercentUnix(previousCPU, previousSystem, v) + cpuFormatted = fmt.Sprintf("%.2f%%", cpuPercent) + + wsServer.BroadcastTo(o.session.Id, "instance stats", o.Name, memFormatted, cpuFormatted) + } + +} + +func calculateCPUPercentUnix(previousCPU, previousSystem uint64, v *types.StatsJSON) float64 { + var ( + cpuPercent = 0.0 + // calculate the change for the cpu usage of the container in between readings + cpuDelta = float64(v.CPUStats.CPUUsage.TotalUsage) - float64(previousCPU) + // calculate the change for the entire system between readings + systemDelta = float64(v.CPUStats.SystemUsage) - float64(previousSystem) + ) + + if systemDelta > 0.0 && cpuDelta > 0.0 { + cpuPercent = (cpuDelta / systemDelta) * float64(len(v.CPUStats.CPUUsage.PercpuUsage)) * 100.0 + } + return cpuPercent +} + func (i *Instance) ResizeTerminal(cols, rows uint) error { return ResizeConnection(i.Name, cols, rows) } @@ -111,6 +179,11 @@ func GetInstance(session *Session, name string) *Instance { return session.Instances[name] } func DeleteInstance(session *Session, instance *Instance) error { + // stop collecting stats + if instance.statsReader != nil { + instance.statsReader.Close() + } + //TODO: Use redis delete(session.Instances, instance.Name) err := DeleteContainer(instance.Name) diff --git a/services/session.go b/services/session.go index 6ac4c55..841fb6d 100644 --- a/services/session.go +++ b/services/session.go @@ -144,6 +144,13 @@ func LoadSessionsFromDisk() error { for _, s := range sessions { timeLeft := s.ExpiresAt.Sub(time.Now()) CloseSessionAfter(s, timeLeft) + + // start collecting stats for every instance + for _, i := range s.Instances { + // wire the session back to the instance + i.session = s + go i.CollectStats() + } } } file.Close() diff --git a/www/assets/app.js b/www/assets/app.js index 5f2300d..477608b 100644 --- a/www/assets/app.js +++ b/www/assets/app.js @@ -132,6 +132,12 @@ $scope.connected = true; }); + socket.on('instance stats', function(name, mem, cpu) { + $scope.idx[name].mem = mem; + $scope.idx[name].cpu = cpu; + $scope.$apply(); + }); + $scope.socket = socket; var i = response.data; diff --git a/www/assets/style.css b/www/assets/style.css index 11a12aa..1e3a50f 100644 --- a/www/assets/style.css +++ b/www/assets/style.css @@ -46,3 +46,10 @@ md-card-content.terminal { .disconnected { background-color: #FDF4B6; } +md-input-container { + margin-bottom: 0; +} +md-input-container .md-errors-spacer { + height: 0; + min-height: 0; +} diff --git a/www/index.html b/www/index.html index 5af9f25..c24270d 100644 --- a/www/index.html +++ b/www/index.html @@ -2,6 +2,7 @@ Docker Playground + @@ -64,7 +65,20 @@ {{instance.name}} - {{instance.ip}} + + + + +
+ + + + + + + + +