package docker import ( "context" "fmt" "log/slog" "time" "github.com/moby/moby/client" "golang.org/x/sync/errgroup" ) // SystemStats is a host-level snapshot combining daemon capacity // (NCPU, memory total) with container counts and disk usage broken down // by category. Workload CPU/memory utilization is aggregated from // per-container samples by the stats collector, not here. type SystemStats struct { Timestamp time.Time `json:"timestamp"` // Capacity from Docker daemon. NCPU int `json:"ncpu"` MemoryTotal int64 `json:"memory_total"` // Container/image counts. Containers int `json:"containers"` Running int `json:"running"` Paused int `json:"paused"` Stopped int `json:"stopped"` Images int `json:"images"` // Disk usage by category (bytes). DiskImagesBytes int64 `json:"disk_images_bytes"` DiskContainersBytes int64 `json:"disk_containers_bytes"` DiskVolumesBytes int64 `json:"disk_volumes_bytes"` DiskBuildCacheBytes int64 `json:"disk_build_cache_bytes"` // Reclaimable disk space by category (bytes). DiskImagesReclaimable int64 `json:"disk_images_reclaimable"` DiskContainersReclaimable int64 `json:"disk_containers_reclaimable"` DiskVolumesReclaimable int64 `json:"disk_volumes_reclaimable"` DiskBuildCacheReclaimable int64 `json:"disk_build_cache_reclaimable"` // DiskTotalBytes is the sum of the category totals. DiskTotalBytes int64 `json:"disk_total_bytes"` } // GetSystemStats returns a one-shot host-level snapshot. Info and DiskUsage // are issued in parallel because DiskUsage walks every layer/volume and is // often the slowest call on a busy host (1-3 s); Info typically completes in // ~10 ms. Disk usage failures do not fail the whole call — the result // degrades gracefully with zero disk fields and a warning log. func (c *Client) GetSystemStats(ctx context.Context) (SystemStats, error) { stats := SystemStats{Timestamp: time.Now().UTC()} g, gctx := errgroup.WithContext(ctx) g.Go(func() error { info, err := c.Info(gctx) if err != nil { return fmt.Errorf("system stats info: %w", err) } stats.NCPU = info.NCPU stats.MemoryTotal = info.MemoryTotal stats.Containers = info.Containers stats.Running = info.Running stats.Paused = info.Paused stats.Stopped = info.Stopped stats.Images = info.Images return nil }) var du *client.DiskUsageResult g.Go(func() error { usage, err := c.api.DiskUsage(gctx, client.DiskUsageOptions{ Containers: true, Images: true, Volumes: true, BuildCache: true, }) if err != nil { // Disk usage is best-effort; swallow but log so the dashboard // shows zeroed disk fields rather than failing entirely. slog.Warn("system stats: disk usage failed", "error", err) return nil } du = &usage return nil }) if err := g.Wait(); err != nil { return SystemStats{}, err } if du != nil { stats.DiskImagesBytes = du.Images.TotalSize stats.DiskContainersBytes = du.Containers.TotalSize stats.DiskVolumesBytes = du.Volumes.TotalSize stats.DiskBuildCacheBytes = du.BuildCache.TotalSize stats.DiskImagesReclaimable = du.Images.Reclaimable stats.DiskContainersReclaimable = du.Containers.Reclaimable stats.DiskVolumesReclaimable = du.Volumes.Reclaimable stats.DiskBuildCacheReclaimable = du.BuildCache.Reclaimable stats.DiskTotalBytes = stats.DiskImagesBytes + stats.DiskContainersBytes + stats.DiskVolumesBytes + stats.DiskBuildCacheBytes } return stats, nil }