package docker import ( "context" "encoding/json" "fmt" "strings" "time" "github.com/moby/moby/api/types/container" "github.com/moby/moby/client" ) // ContainerStats holds computed CPU, memory, network, and block I/O // usage for a container. Network and block I/O values are cumulative // byte counters — compute rates by differencing two samples. type ContainerStats struct { Timestamp time.Time `json:"timestamp"` CPUPercent float64 `json:"cpu_percent"` MemoryUsage int64 `json:"memory_usage"` MemoryLimit int64 `json:"memory_limit"` MemoryPercent float64 `json:"memory_percent"` NetworkRxBytes int64 `json:"network_rx_bytes"` NetworkTxBytes int64 `json:"network_tx_bytes"` BlockReadBytes int64 `json:"block_read_bytes"` BlockWriteBytes int64 `json:"block_write_bytes"` } // GetContainerStats retrieves a one-shot stats snapshot for the given container // and computes CPU, memory, network, and block I/O metrics. func (c *Client) GetContainerStats(ctx context.Context, containerID string) (ContainerStats, error) { result, err := c.api.ContainerStats(ctx, containerID, client.ContainerStatsOptions{ Stream: false, IncludePreviousSample: true, }) if err != nil { return ContainerStats{}, fmt.Errorf("get container stats %s: %w", containerID, err) } defer result.Body.Close() var stats container.StatsResponse if err := json.NewDecoder(result.Body).Decode(&stats); err != nil { return ContainerStats{}, fmt.Errorf("decode container stats %s: %w", containerID, err) } cpuPercent := calculateCPUPercent(stats) memUsage := int64(stats.MemoryStats.Usage) memLimit := int64(stats.MemoryStats.Limit) var memPercent float64 if memLimit > 0 { memPercent = float64(memUsage) / float64(memLimit) * 100.0 } rxBytes, txBytes := sumNetworkBytes(stats.Networks) readBytes, writeBytes := sumBlockIOBytes(stats.BlkioStats.IoServiceBytesRecursive) ts := stats.Read if ts.IsZero() { ts = time.Now().UTC() } return ContainerStats{ Timestamp: ts, CPUPercent: cpuPercent, MemoryUsage: memUsage, MemoryLimit: memLimit, MemoryPercent: memPercent, NetworkRxBytes: rxBytes, NetworkTxBytes: txBytes, BlockReadBytes: readBytes, BlockWriteBytes: writeBytes, }, nil } // sumNetworkBytes aggregates rx/tx byte counters across all network interfaces // for a single container. Missing Networks map (disabled networking) yields zeros. func sumNetworkBytes(nets map[string]container.NetworkStats) (rx, tx int64) { for _, n := range nets { rx += int64(n.RxBytes) tx += int64(n.TxBytes) } return rx, tx } // sumBlockIOBytes totals read/write bytes across all block devices from the // cgroup io_service_bytes_recursive entries. The "Op" field is "read"/"write" // on cgroup v2 and "Read"/"Write" on cgroup v1 — match case-insensitively so // either runtime works. func sumBlockIOBytes(entries []container.BlkioStatEntry) (read, write int64) { for _, e := range entries { switch { case strings.EqualFold(e.Op, "read"): read += int64(e.Value) case strings.EqualFold(e.Op, "write"): write += int64(e.Value) } } return read, write } // calculateCPUPercent computes CPU usage percentage from a stats response // using the delta between current and previous CPU readings. func calculateCPUPercent(stats container.StatsResponse) float64 { cpuDelta := float64(stats.CPUStats.CPUUsage.TotalUsage) - float64(stats.PreCPUStats.CPUUsage.TotalUsage) systemDelta := float64(stats.CPUStats.SystemUsage) - float64(stats.PreCPUStats.SystemUsage) if systemDelta <= 0 || cpuDelta < 0 { return 0.0 } onlineCPUs := float64(stats.CPUStats.OnlineCPUs) if onlineCPUs == 0 { onlineCPUs = 1 } return (cpuDelta / systemDelta) * onlineCPUs * 100.0 }