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 } // BuildCachePruneResult reports the outcome of a build-cache prune. type BuildCachePruneResult struct { CachesDeleted int `json:"caches_deleted"` // number of cache records removed SpaceReclaimed int64 `json:"space_reclaimed"` // bytes reclaimed } // PruneBuildCache deletes unused Docker build-cache records and returns the // number of records removed and bytes reclaimed. Docker's build-cache API is // prune-by-filter only — there is no surgical per-record eviction — so this // is the daemon-wide "prune unused" operation. // // When all is false (the default), only build cache not currently in use is // removed, so an app's next rebuild still hits its warm cache. When all is // true, every build-cache record is removed regardless of use, forcing a cold // rebuild for every app. func (c *Client) PruneBuildCache(ctx context.Context, all bool) (BuildCachePruneResult, error) { res, err := c.api.BuildCachePrune(ctx, client.BuildCachePruneOptions{All: all}) if err != nil { return BuildCachePruneResult{}, fmt.Errorf("prune build cache: %w", err) } return BuildCachePruneResult{ CachesDeleted: len(res.Report.CachesDeleted), SpaceReclaimed: int64(res.Report.SpaceReclaimed), }, nil }