2020-08-19 04:38:42 +00:00
|
|
|
package docker
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"encoding/json"
|
|
|
|
"io"
|
|
|
|
"math"
|
2021-10-03 19:59:03 +00:00
|
|
|
"time"
|
2021-08-02 21:07:00 +00:00
|
|
|
|
|
|
|
"emperror.dev/errors"
|
|
|
|
"github.com/docker/docker/api/types"
|
|
|
|
|
|
|
|
"github.com/pterodactyl/wings/environment"
|
2020-08-19 04:38:42 +00:00
|
|
|
)
|
|
|
|
|
2021-10-03 19:59:03 +00:00
|
|
|
// Uptime returns the current uptime of the container in milliseconds. If the
|
|
|
|
// container is not currently running this will return 0.
|
|
|
|
func (e *Environment) Uptime(ctx context.Context) (int64, error) {
|
|
|
|
ins, err := e.client.ContainerInspect(ctx, e.Id)
|
|
|
|
if err != nil {
|
|
|
|
return 0, errors.Wrap(err, "environment: could not inspect container")
|
|
|
|
}
|
|
|
|
if !ins.State.Running {
|
|
|
|
return 0, nil
|
|
|
|
}
|
|
|
|
started, err := time.Parse(time.RFC3339, ins.State.StartedAt)
|
|
|
|
if err != nil {
|
|
|
|
return 0, errors.Wrap(err, "environment: failed to parse container start time")
|
|
|
|
}
|
|
|
|
return time.Since(started).Milliseconds(), nil
|
|
|
|
}
|
|
|
|
|
2020-08-19 04:38:42 +00:00
|
|
|
// Attach to the instance and then automatically emit an event whenever the resource usage for the
|
|
|
|
// server process changes.
|
|
|
|
func (e *Environment) pollResources(ctx context.Context) error {
|
2020-11-07 05:53:00 +00:00
|
|
|
if e.st.Load() == environment.ProcessOfflineState {
|
2020-09-11 03:05:01 +00:00
|
|
|
return errors.New("cannot enable resource polling on a stopped server")
|
2020-08-19 04:38:42 +00:00
|
|
|
}
|
|
|
|
|
2021-01-07 04:36:29 +00:00
|
|
|
e.log().Info("starting resource polling for container")
|
|
|
|
defer e.log().Debug("stopped resource polling for container")
|
2020-11-11 05:21:20 +00:00
|
|
|
|
2021-01-07 04:36:29 +00:00
|
|
|
stats, err := e.client.ContainerStats(ctx, e.Id, true)
|
2020-08-19 04:38:42 +00:00
|
|
|
if err != nil {
|
2020-11-28 23:57:10 +00:00
|
|
|
return err
|
2020-08-19 04:38:42 +00:00
|
|
|
}
|
2020-09-07 20:04:56 +00:00
|
|
|
defer stats.Body.Close()
|
2020-08-19 04:38:42 +00:00
|
|
|
|
2021-10-03 19:59:03 +00:00
|
|
|
uptime, err := e.Uptime(ctx)
|
|
|
|
if err != nil {
|
|
|
|
e.log().WithField("error", err).Warn("failed to calculate container uptime")
|
|
|
|
}
|
|
|
|
|
2020-08-19 04:38:42 +00:00
|
|
|
dec := json.NewDecoder(stats.Body)
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
2020-11-28 23:57:10 +00:00
|
|
|
return ctx.Err()
|
2020-08-19 04:38:42 +00:00
|
|
|
default:
|
2021-01-07 04:47:44 +00:00
|
|
|
var v types.StatsJSON
|
2020-08-19 04:38:42 +00:00
|
|
|
if err := dec.Decode(&v); err != nil {
|
2021-01-07 04:36:29 +00:00
|
|
|
if err != io.EOF && !errors.Is(err, context.Canceled) {
|
|
|
|
e.log().WithField("error", err).Warn("error while processing Docker stats output for container")
|
2020-09-11 03:05:01 +00:00
|
|
|
} else {
|
2021-01-07 04:36:29 +00:00
|
|
|
e.log().Debug("io.EOF encountered during stats decode, stopping polling...")
|
2020-08-19 04:38:42 +00:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Disable collection if the server is in an offline state and this process is still running.
|
2020-11-07 05:53:00 +00:00
|
|
|
if e.st.Load() == environment.ProcessOfflineState {
|
2021-01-07 04:36:29 +00:00
|
|
|
e.log().Debug("process in offline state while resource polling is still active; stopping poll")
|
2020-08-19 04:38:42 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-10-03 19:59:03 +00:00
|
|
|
if !v.PreRead.IsZero() {
|
|
|
|
uptime = uptime + v.Read.Sub(v.PreRead).Milliseconds()
|
|
|
|
}
|
|
|
|
|
2020-12-17 06:03:35 +00:00
|
|
|
st := environment.Stats{
|
2021-10-03 19:59:03 +00:00
|
|
|
Uptime: uptime,
|
2020-08-19 04:38:42 +00:00
|
|
|
Memory: calculateDockerMemory(v.MemoryStats),
|
|
|
|
MemoryLimit: v.MemoryStats.Limit,
|
2021-01-07 04:47:44 +00:00
|
|
|
CpuAbsolute: calculateDockerAbsoluteCpu(v.PreCPUStats, v.CPUStats),
|
|
|
|
Network: environment.NetworkStats{},
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, nw := range v.Networks {
|
|
|
|
st.Network.RxBytes += nw.RxBytes
|
|
|
|
st.Network.TxBytes += nw.TxBytes
|
2020-08-19 04:38:42 +00:00
|
|
|
}
|
|
|
|
|
2020-09-04 04:19:06 +00:00
|
|
|
if b, err := json.Marshal(st); err != nil {
|
2021-01-07 04:36:29 +00:00
|
|
|
e.log().WithField("error", err).Warn("error while marshaling stats object for environment")
|
2020-09-04 04:19:06 +00:00
|
|
|
} else {
|
|
|
|
e.Events().Publish(environment.ResourceEvent, string(b))
|
|
|
|
}
|
2020-08-19 04:38:42 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// The "docker stats" CLI call does not return the same value as the types.MemoryStats.Usage
|
|
|
|
// value which can be rather confusing to people trying to compare panel usage to
|
|
|
|
// their stats output.
|
|
|
|
//
|
2021-08-02 21:07:00 +00:00
|
|
|
// This math is from their CLI repository in order to show the same values to avoid people
|
|
|
|
// bothering me about it. It should also reflect a slightly more correct memory value anyways.
|
2020-08-19 04:38:42 +00:00
|
|
|
//
|
|
|
|
// @see https://github.com/docker/cli/blob/96e1d1d6/cli/command/container/stats_helpers.go#L227-L249
|
|
|
|
func calculateDockerMemory(stats types.MemoryStats) uint64 {
|
|
|
|
if v, ok := stats.Stats["total_inactive_file"]; ok && v < stats.Usage {
|
|
|
|
return stats.Usage - v
|
|
|
|
}
|
|
|
|
|
|
|
|
if v := stats.Stats["inactive_file"]; v < stats.Usage {
|
|
|
|
return stats.Usage - v
|
|
|
|
}
|
|
|
|
|
|
|
|
return stats.Usage
|
|
|
|
}
|
|
|
|
|
|
|
|
// Calculates the absolute CPU usage used by the server process on the system, not constrained
|
|
|
|
// by the defined CPU limits on the container.
|
|
|
|
//
|
|
|
|
// @see https://github.com/docker/cli/blob/aa097cf1aa19099da70930460250797c8920b709/cli/command/container/stats_helpers.go#L166
|
2021-01-07 04:47:44 +00:00
|
|
|
func calculateDockerAbsoluteCpu(pStats types.CPUStats, stats types.CPUStats) float64 {
|
2020-08-19 04:38:42 +00:00
|
|
|
// Calculate the change in CPU usage between the current and previous reading.
|
|
|
|
cpuDelta := float64(stats.CPUUsage.TotalUsage) - float64(pStats.CPUUsage.TotalUsage)
|
|
|
|
|
|
|
|
// Calculate the change for the entire system's CPU usage between current and previous reading.
|
|
|
|
systemDelta := float64(stats.SystemUsage) - float64(pStats.SystemUsage)
|
|
|
|
|
|
|
|
// Calculate the total number of CPU cores being used.
|
|
|
|
cpus := float64(stats.OnlineCPUs)
|
|
|
|
if cpus == 0.0 {
|
|
|
|
cpus = float64(len(stats.CPUUsage.PercpuUsage))
|
|
|
|
}
|
|
|
|
|
|
|
|
percent := 0.0
|
|
|
|
if systemDelta > 0.0 && cpuDelta > 0.0 {
|
|
|
|
percent = (cpuDelta / systemDelta) * cpus * 100.0
|
|
|
|
}
|
|
|
|
|
|
|
|
return math.Round(percent*1000) / 1000
|
|
|
|
}
|