diff --git a/beszel/internal/agent/docker.go b/beszel/internal/agent/docker.go index 84f732d..a50b50c 100644 --- a/beszel/internal/agent/docker.go +++ b/beszel/internal/agent/docker.go @@ -14,9 +14,13 @@ import ( "sync" "time" + "regexp" + "github.com/blang/semver" ) +var headerRegexp = regexp.MustCompile(`\ADocker/.+\s\((.+)\)\z`) + type dockerManager struct { client *http.Client // Client to query Docker API wg sync.WaitGroup // WaitGroup to wait for all goroutines to finish @@ -167,22 +171,31 @@ func (dm *dockerManager) updateContainerStats(ctr *container.ApiInfo) error { return err } - // check if container has valid data, otherwise may be in restart loop (#103) - if res.MemoryStats.Usage == 0 { - return fmt.Errorf("%s - no memory stats - see https://github.com/henrygd/beszel/issues/144", name) + var usedMemory uint64 + var cpuPct float64 + + if getDockerOS(resp.Header.Get("Server")) == "windows" { + + usedMemory = res.MemoryStats.PrivateWorkingSet + cpuPct = calculateCPUPercentWindows(res, stats.PrevCpu[0], stats.PrevRead) + stats.PrevRead = res.Read + + } else { + // check if container has valid data, otherwise may be in restart loop (#103) + if res.MemoryStats.Usage == 0 { + return fmt.Errorf("%s - no memory stats - see https://github.com/henrygd/beszel/issues/144", name) + } + memCache := res.MemoryStats.Stats.InactiveFile + if memCache == 0 { + memCache = res.MemoryStats.Stats.Cache + } + usedMemory = res.MemoryStats.Usage - memCache + + cpuDelta := res.CPUStats.CPUUsage.TotalUsage - stats.PrevCpu[0] + systemDelta := res.CPUStats.SystemUsage - stats.PrevCpu[1] + cpuPct = float64(cpuDelta) / float64(systemDelta) * 100 } - // memory (https://docs.docker.com/reference/cli/docker/container/stats/) - memCache := res.MemoryStats.Stats.InactiveFile - if memCache == 0 { - memCache = res.MemoryStats.Stats.Cache - } - usedMemory := res.MemoryStats.Usage - memCache - - // cpu - cpuDelta := res.CPUStats.CPUUsage.TotalUsage - stats.PrevCpu[0] - systemDelta := res.CPUStats.SystemUsage - stats.PrevCpu[1] - cpuPct := float64(cpuDelta) / float64(systemDelta) * 100 if cpuPct > 100 { return fmt.Errorf("%s cpu pct greater than 100: %+v", name, cpuPct) } @@ -324,3 +337,31 @@ func getDockerHost() string { } return scheme + socks[0] } + +// getDockerOS returns the operating system based on the server header from the daemon. +// from: https://github.com/docker/cli/blob/master/vendor/github.com/docker/docker/client/utils.go#L34 +func getDockerOS(serverHeader string) string { + var osType string + matches := headerRegexp.FindStringSubmatch(serverHeader) + if len(matches) > 0 { + osType = matches[1] + } + return osType +} + +// from: https://github.com/docker/cli/blob/master/cli/command/container/stats_helpers.go#L185 +func calculateCPUPercentWindows(v container.ApiStats, prevCPUSsage uint64, prevRead time.Time) float64 { + // Max number of 100ns intervals between the previous time read and now + possIntervals := uint64(v.Read.Sub(prevRead).Nanoseconds()) + possIntervals /= 100 // Convert to number of 100ns intervals + possIntervals *= uint64(v.NumProcs) // Multiple by the number of processors + + // Intervals used + intervalsUsed := v.CPUStats.CPUUsage.TotalUsage - prevCPUSsage + + // Percentage avoiding divide-by-zero + if possIntervals > 0 { + return float64(intervalsUsed) / float64(possIntervals) * 100.0 + } + return 0.00 +} diff --git a/beszel/internal/entities/container/container.go b/beszel/internal/entities/container/container.go index 294f2bb..7c85614 100644 --- a/beszel/internal/entities/container/container.go +++ b/beszel/internal/entities/container/container.go @@ -28,7 +28,7 @@ type ApiInfo struct { // Docker container resources from /containers/{id}/stats type ApiStats struct { // Common stats - // Read time.Time `json:"read"` + Read time.Time `json:"read"` // PreRead time.Time `json:"preread"` // Linux specific stats, not populated on Windows. @@ -36,7 +36,7 @@ type ApiStats struct { // BlkioStats BlkioStats `json:"blkio_stats,omitempty"` // Windows specific stats, not populated on Linux. - // NumProcs uint32 `json:"num_procs"` + NumProcs uint32 `json:"num_procs"` // StorageStats StorageStats `json:"storage_stats,omitempty"` // Networks request version >=1.21 Networks map[string]NetworkStats @@ -101,7 +101,7 @@ type MemoryStats struct { // // peak committed bytes // CommitPeak uint64 `json:"commitpeakbytes,omitempty"` // // private working set - // PrivateWorkingSet uint64 `json:"privateworkingset,omitempty"` + PrivateWorkingSet uint64 `json:"privateworkingset,omitempty"` } type MemoryStatsStats struct { @@ -131,4 +131,5 @@ type Stats struct { NetworkRecv float64 `json:"nr"` PrevCpu [2]uint64 `json:"-"` PrevNet prevNetStats `json:"-"` + PrevRead time.Time `json:"-"` }