From e0e5be152cab0e8910463858f2db7d7a288d8ff4 Mon Sep 17 00:00:00 2001 From: Amir Alavi Date: Sun, 31 May 2026 21:59:09 -0400 Subject: [PATCH 1/2] containerd: overlay gVisor (runsc) per-container stats from runsc events cAdvisor reports zero CPU/memory/network/pids for gVisor (runsc) containers. The container's processes run inside the sandbox -- a single host process in a single host cgroup -- so the per-container host cgroup cAdvisor reads is empty and libcontainer's GetStats returns zeros. (For the per-container cgroup directory to exist at all on cgroup v2 + systemd, gVisor google/gvisor#13070 is required; before it, runsc containers were not even discoverable.) gVisor's sentry holds the real per-container accounting and exposes it via `runsc events --stats ` -- the same source containerd's CRI stats path already consumes for the kubelet /stats/summary endpoint (which is why `kubectl top` and metrics-server are correct for runsc pods while /metrics/cadvisor is not). When the containerd handler detects a runsc-managed container (runtime name prefixed io.containerd.runsc), it now overlays the values from `runsc events --stats` on top of the libcontainer base, honoring includedMetrics. The overlay is best-effort: any runsc failure leaves the base stats untouched so a scrape never fails. CPU (user/system/total), memory (usage/max/cache, with working set approximated as usage-cache), pids (ProcessCount), and per-interface network counters are mapped; other fields are left to the libcontainer base pending a stabilized gVisor stats contract. Two flags configure the source: --runsc (binary, default "runsc") and --runsc_root (containerd's runsc state dir, default /run/containerd/runsc/k8s.io). Refs: google/gvisor#13067, google/gvisor#13070 --- container/containerd/gvisor.go | 205 ++++++++++++++++++++++++++++++++ container/containerd/handler.go | 12 ++ 2 files changed, 217 insertions(+) create mode 100644 container/containerd/gvisor.go diff --git a/container/containerd/gvisor.go b/container/containerd/gvisor.go new file mode 100644 index 0000000000..8defa79ce7 --- /dev/null +++ b/container/containerd/gvisor.go @@ -0,0 +1,205 @@ +// Copyright 2026 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build linux + +// gVisor (runsc) stats overlay for the containerd handler. +// +// A gVisor container's processes run inside the sandbox (a single host +// process), so the per-container host cgroup that cAdvisor reads is empty and +// libcontainer reports zero CPU/memory/network/pids. gVisor's sentry holds the +// real per-container accounting and exposes it via `runsc events --stats`, +// which is the same source containerd's CRI stats path consumes for the +// kubelet `/stats/summary` endpoint. +// +// When the containerd handler detects a runsc-managed container, it overlays +// the values from `runsc events --stats` on top of the (zero) libcontainer +// stats so that /metrics/cadvisor reports real numbers, matching runc. +// +// See gVisor google/gvisor#13067 (problem) and #13070 (host-side compat dirs +// that make these containers discoverable by cAdvisor in the first place). +package containerd + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "os/exec" + "strings" + "time" + + "k8s.io/klog/v2" + + "github.com/google/cadvisor/container" + info "github.com/google/cadvisor/info/v1" +) + +var ( + argRunscBinary = flag.String("runsc", "runsc", "path to the runsc binary, used to collect per-container stats for gVisor containers") + argRunscRoot = flag.String("runsc_root", "/run/containerd/runsc/k8s.io", "runsc root directory (containerd's runsc state dir for the configured namespace), used to collect per-container stats for gVisor containers") +) + +// gvisorRuntimePrefix matches the containerd runtime names used by the runsc +// shim, e.g. "io.containerd.runsc.v1". +const gvisorRuntimePrefix = "io.containerd.runsc" + +// gvisorStatsTimeout bounds a single `runsc events --stats` invocation. +const gvisorStatsTimeout = 5 * time.Second + +// isGVisorRuntime reports whether the containerd runtime name belongs to the +// runsc (gVisor) shim. +func isGVisorRuntime(runtimeName string) bool { + return strings.HasPrefix(runtimeName, gvisorRuntimePrefix) +} + +// gvisorEvent mirrors the JSON emitted by `runsc events --stats ` (gVisor's +// runsc/boot.Event). Only the fields consumed here are declared. +type gvisorEvent struct { + Data gvisorEventData `json:"data"` +} + +type gvisorEventData struct { + CPU gvisorCPU `json:"cpu"` + Memory gvisorMemory `json:"memory"` + Pids gvisorPids `json:"pids"` + NetworkInterfaces []gvisorNetworkInterface `json:"network_interfaces"` +} + +type gvisorCPU struct { + Usage gvisorCPUUsage `json:"usage"` +} + +type gvisorCPUUsage struct { + // Nanoseconds. + Kernel uint64 `json:"kernel"` + User uint64 `json:"user"` + Total uint64 `json:"total"` +} + +type gvisorMemory struct { + Cache uint64 `json:"cache"` + Usage gvisorMemoryEntry `json:"usage"` +} + +type gvisorMemoryEntry struct { + Usage uint64 `json:"usage"` + Max uint64 `json:"max"` +} + +type gvisorPids struct { + Current uint64 `json:"current"` +} + +// gvisorNetworkInterface mirrors runsc/boot.NetworkInterface, which has no JSON +// tags -- so the wire keys are the exported Go field names. +type gvisorNetworkInterface struct { + Name string + RxBytes uint64 + RxPackets uint64 + RxErrors uint64 + RxDropped uint64 + TxBytes uint64 + TxPackets uint64 + TxErrors uint64 + TxDropped uint64 +} + +// gvisorContainerStats runs `runsc --root events --stats ` once and +// parses its JSON output. +func gvisorContainerStats(ctx context.Context, id string) (*gvisorEvent, error) { + ctx, cancel := context.WithTimeout(ctx, gvisorStatsTimeout) + defer cancel() + + cmd := exec.CommandContext(ctx, *argRunscBinary, "--root", *argRunscRoot, "events", "--stats", id) + out, err := cmd.Output() + if err != nil { + return nil, fmt.Errorf("running %q events --stats %s (root %q): %w", *argRunscBinary, id, *argRunscRoot, err) + } + var ev gvisorEvent + if err := json.Unmarshal(out, &ev); err != nil { + return nil, fmt.Errorf("parsing runsc events output for %s: %w", id, err) + } + return &ev, nil +} + +// applyGVisorStats overwrites the cgroup-derived (zero) values in stats with +// the per-container values gVisor reports, honoring includedMetrics. Only the +// metrics gVisor reliably provides are overlaid; the rest are left as the +// libcontainer base so consumers that read them degrade gracefully. +func applyGVisorStats(stats *info.ContainerStats, ev *gvisorEvent, includedMetrics container.MetricSet) { + d := ev.Data + + if includedMetrics.Has(container.CpuUsageMetrics) { + stats.Cpu.Usage.Total = d.CPU.Usage.Total + stats.Cpu.Usage.User = d.CPU.Usage.User + stats.Cpu.Usage.System = d.CPU.Usage.Kernel + } + + if includedMetrics.Has(container.MemoryUsageMetrics) { + stats.Memory.Usage = d.Memory.Usage.Usage + stats.Memory.MaxUsage = d.Memory.Usage.Max + stats.Memory.Cache = d.Memory.Cache + // gVisor does not break out inactive_file, so approximate the + // working set as usage minus page cache (clamped at >= 0). + if d.Memory.Usage.Usage > d.Memory.Cache { + stats.Memory.WorkingSet = d.Memory.Usage.Usage - d.Memory.Cache + } else { + stats.Memory.WorkingSet = d.Memory.Usage.Usage + } + } + + if includedMetrics.Has(container.ProcessMetrics) { + stats.Processes.ProcessCount = d.Pids.Current + } + + if includedMetrics.Has(container.NetworkUsageMetrics) && len(d.NetworkInterfaces) > 0 { + ifaces := make([]info.InterfaceStats, 0, len(d.NetworkInterfaces)) + for _, ni := range d.NetworkInterfaces { + ifaces = append(ifaces, info.InterfaceStats{ + Name: ni.Name, + RxBytes: ni.RxBytes, + RxPackets: ni.RxPackets, + RxErrors: ni.RxErrors, + RxDropped: ni.RxDropped, + TxBytes: ni.TxBytes, + TxPackets: ni.TxPackets, + TxErrors: ni.TxErrors, + TxDropped: ni.TxDropped, + }) + } + stats.Network.Interfaces = ifaces + // cAdvisor's inline InterfaceStats reports the primary interface; + // prefer the first non-loopback one. + stats.Network.InterfaceStats = ifaces[0] + for _, is := range ifaces { + if is.Name != "lo" { + stats.Network.InterfaceStats = is + break + } + } + } +} + +// overlayGVisorStats fetches gVisor's per-container stats and overlays them on +// the libcontainer base. It is best-effort: on any error the base (zeroed) +// stats are left untouched so a transient runsc failure never fails a scrape. +func (h *containerdContainerHandler) overlayGVisorStats(stats *info.ContainerStats) { + ev, err := gvisorContainerStats(context.Background(), h.reference.Id) + if err != nil { + klog.V(4).Infof("containerd: could not get gVisor stats for %q, leaving cgroup-derived values: %v", h.reference.Name, err) + return + } + applyGVisorStats(stats, ev, h.includedMetrics) +} diff --git a/container/containerd/handler.go b/container/containerd/handler.go index e91e894dc6..917084e60b 100644 --- a/container/containerd/handler.go +++ b/container/containerd/handler.go @@ -55,6 +55,10 @@ type containerdContainerHandler struct { libcontainerHandler *containerlibcontainer.Handler client ContainerdClient + // isGVisor is true when this container is managed by the runsc (gVisor) + // shim. Its processes run inside the sandbox, so the host cgroup is empty + // and stats are instead overlaid from `runsc events --stats`. + isGVisor bool } var _ container.ContainerHandler = &containerdContainerHandler{} @@ -149,6 +153,7 @@ func newContainerdContainerHandler( reference: containerReference, libcontainerHandler: libcontainerHandler, client: client, + isGVisor: isGVisorRuntime(cntr.Runtime.Name), } if !cntr.CreatedAt.IsZero() && !cntr.CreatedAt.Before(time.Unix(0, 0)) { handler.creationTime = cntr.CreatedAt @@ -217,6 +222,13 @@ func (h *containerdContainerHandler) GetStats() (*info.ContainerStats, error) { return stats, err } + // gVisor containers run inside the sandbox, so the host cgroup is empty + // and the stats above are zero. Overlay the real per-container values + // reported by the sandbox via `runsc events --stats`. + if h.isGVisor { + h.overlayGVisorStats(stats) + } + // Get filesystem stats. err = h.getFsStats(stats) return stats, err From 720126fb7dcebfbf5269663b7556a8575d7f256a Mon Sep 17 00:00:00 2001 From: Amir Alavi Date: Mon, 1 Jun 2026 11:19:41 -0400 Subject: [PATCH 2/2] containerd: generalize sandboxed-runtime stats behind a provider interface Make the per-container stats overlay runtime-agnostic instead of gVisor-specific. The containerd handler now talks to a sandboxStatsProvider abstraction, selected by containerd runtime name via a small registry of sandboxed runtimes (runtimes whose processes run outside the host cgroup, so the host cgroup cAdvisor reads is empty). runsc is the first provider, sourcing CPU/memory/pids/network from `runsc events --stats`. Adding another sandboxed runtime (Kata, Firecracker, ...) is a registry entry plus a provider, with no handler changes. A generic provider backed by containerd's task Metrics API -- which every shim implements, returning the standard cgroup metrics proto -- could cover any sandboxed runtime for CPU/memory/pids; runsc keeps a dedicated provider because it also reports per-interface network stats the cgroup metrics proto does not carry. No functional change for runsc; runc and other host-cgroup runtimes have no provider and keep the standard libcontainer path. --- container/containerd/handler.go | 24 +++-- container/containerd/{gvisor.go => runsc.go} | 104 ++++++++----------- container/containerd/sandbox.go | 84 +++++++++++++++ 3 files changed, 141 insertions(+), 71 deletions(-) rename container/containerd/{gvisor.go => runsc.go} (56%) create mode 100644 container/containerd/sandbox.go diff --git a/container/containerd/handler.go b/container/containerd/handler.go index 917084e60b..f8f92247fc 100644 --- a/container/containerd/handler.go +++ b/container/containerd/handler.go @@ -28,6 +28,7 @@ import ( "github.com/containerd/errdefs" "github.com/opencontainers/cgroups" specs "github.com/opencontainers/runtime-spec/specs-go" + "k8s.io/klog/v2" "github.com/google/cadvisor/container" "github.com/google/cadvisor/container/common" @@ -55,10 +56,10 @@ type containerdContainerHandler struct { libcontainerHandler *containerlibcontainer.Handler client ContainerdClient - // isGVisor is true when this container is managed by the runsc (gVisor) - // shim. Its processes run inside the sandbox, so the host cgroup is empty - // and stats are instead overlaid from `runsc events --stats`. - isGVisor bool + // sandbox is non-nil when this container is managed by a sandboxed runtime + // (e.g. gVisor/runsc) whose processes run outside the host cgroup, so the + // host cgroup is empty and stats are instead overlaid from the runtime. + sandbox sandboxStatsProvider } var _ container.ContainerHandler = &containerdContainerHandler{} @@ -153,7 +154,7 @@ func newContainerdContainerHandler( reference: containerReference, libcontainerHandler: libcontainerHandler, client: client, - isGVisor: isGVisorRuntime(cntr.Runtime.Name), + sandbox: sandboxProviderFor(cntr.Runtime.Name), } if !cntr.CreatedAt.IsZero() && !cntr.CreatedAt.Before(time.Unix(0, 0)) { handler.creationTime = cntr.CreatedAt @@ -222,11 +223,14 @@ func (h *containerdContainerHandler) GetStats() (*info.ContainerStats, error) { return stats, err } - // gVisor containers run inside the sandbox, so the host cgroup is empty - // and the stats above are zero. Overlay the real per-container values - // reported by the sandbox via `runsc events --stats`. - if h.isGVisor { - h.overlayGVisorStats(stats) + // Sandboxed runtimes (e.g. gVisor) run the workload outside the host + // cgroup, so the stats above are zero. Overlay the real per-container + // values reported by the runtime. Best-effort: any failure leaves the + // base stats untouched so a transient error never fails a scrape. + if h.sandbox != nil { + if err := h.sandbox.overlay(context.Background(), h.reference.Id, stats, h.includedMetrics); err != nil { + klog.V(4).Infof("containerd: %s stats unavailable for %q, keeping cgroup-derived values: %v", h.sandbox.name(), h.reference.Name, err) + } } // Get filesystem stats. diff --git a/container/containerd/gvisor.go b/container/containerd/runsc.go similarity index 56% rename from container/containerd/gvisor.go rename to container/containerd/runsc.go index 8defa79ce7..71f3bc1cae 100644 --- a/container/containerd/gvisor.go +++ b/container/containerd/runsc.go @@ -14,18 +14,12 @@ //go:build linux -// gVisor (runsc) stats overlay for the containerd handler. -// -// A gVisor container's processes run inside the sandbox (a single host -// process), so the per-container host cgroup that cAdvisor reads is empty and -// libcontainer reports zero CPU/memory/network/pids. gVisor's sentry holds the -// real per-container accounting and exposes it via `runsc events --stats`, -// which is the same source containerd's CRI stats path consumes for the -// kubelet `/stats/summary` endpoint. -// -// When the containerd handler detects a runsc-managed container, it overlays -// the values from `runsc events --stats` on top of the (zero) libcontainer -// stats so that /metrics/cadvisor reports real numbers, matching runc. +// runscStatsProvider is the sandboxStatsProvider for gVisor (runsc). It sources +// per-container stats from `runsc events --stats `, the same interface +// containerd's CRI stats path uses for the kubelet /stats/summary endpoint. +// Unlike the generic cgroup metrics proto, runsc events also reports +// per-interface network counters from the sandbox's netstack (which never +// appear in the host /proc or cgroup), so runsc gets a dedicated provider. // // See gVisor google/gvisor#13067 (problem) and #13070 (host-side compat dirs // that make these containers discoverable by cAdvisor in the first place). @@ -37,11 +31,8 @@ import ( "flag" "fmt" "os/exec" - "strings" "time" - "k8s.io/klog/v2" - "github.com/google/cadvisor/container" info "github.com/google/cadvisor/info/v1" ) @@ -51,60 +42,63 @@ var ( argRunscRoot = flag.String("runsc_root", "/run/containerd/runsc/k8s.io", "runsc root directory (containerd's runsc state dir for the configured namespace), used to collect per-container stats for gVisor containers") ) -// gvisorRuntimePrefix matches the containerd runtime names used by the runsc -// shim, e.g. "io.containerd.runsc.v1". -const gvisorRuntimePrefix = "io.containerd.runsc" +// runscStatsTimeout bounds a single `runsc events --stats` invocation. +const runscStatsTimeout = 5 * time.Second -// gvisorStatsTimeout bounds a single `runsc events --stats` invocation. -const gvisorStatsTimeout = 5 * time.Second +type runscStatsProvider struct{} -// isGVisorRuntime reports whether the containerd runtime name belongs to the -// runsc (gVisor) shim. -func isGVisorRuntime(runtimeName string) bool { - return strings.HasPrefix(runtimeName, gvisorRuntimePrefix) +func (runscStatsProvider) name() string { return "runsc" } + +func (runscStatsProvider) overlay(ctx context.Context, id string, stats *info.ContainerStats, includedMetrics container.MetricSet) error { + ev, err := runscContainerStats(ctx, id) + if err != nil { + return err + } + applyRunscStats(stats, ev, includedMetrics) + return nil } -// gvisorEvent mirrors the JSON emitted by `runsc events --stats ` (gVisor's +// runscEvent mirrors the JSON emitted by `runsc events --stats ` (gVisor's // runsc/boot.Event). Only the fields consumed here are declared. -type gvisorEvent struct { - Data gvisorEventData `json:"data"` +type runscEvent struct { + Data runscEventData `json:"data"` } -type gvisorEventData struct { - CPU gvisorCPU `json:"cpu"` - Memory gvisorMemory `json:"memory"` - Pids gvisorPids `json:"pids"` - NetworkInterfaces []gvisorNetworkInterface `json:"network_interfaces"` +type runscEventData struct { + CPU runscCPU `json:"cpu"` + Memory runscMemory `json:"memory"` + Pids runscPids `json:"pids"` + NetworkInterfaces []runscNetworkInterface `json:"network_interfaces"` } -type gvisorCPU struct { - Usage gvisorCPUUsage `json:"usage"` +type runscCPU struct { + Usage runscCPUUsage `json:"usage"` } -type gvisorCPUUsage struct { +type runscCPUUsage struct { // Nanoseconds. Kernel uint64 `json:"kernel"` User uint64 `json:"user"` Total uint64 `json:"total"` } -type gvisorMemory struct { - Cache uint64 `json:"cache"` - Usage gvisorMemoryEntry `json:"usage"` +type runscMemory struct { + Cache uint64 `json:"cache"` + Usage runscMemoryEntry `json:"usage"` } -type gvisorMemoryEntry struct { +type runscMemoryEntry struct { Usage uint64 `json:"usage"` Max uint64 `json:"max"` } -type gvisorPids struct { +type runscPids struct { Current uint64 `json:"current"` } -// gvisorNetworkInterface mirrors runsc/boot.NetworkInterface, which has no JSON +// runscNetworkInterface mirrors runsc/boot.NetworkInterface, which has no JSON // tags -- so the wire keys are the exported Go field names. -type gvisorNetworkInterface struct { +type runscNetworkInterface struct { Name string RxBytes uint64 RxPackets uint64 @@ -116,10 +110,10 @@ type gvisorNetworkInterface struct { TxDropped uint64 } -// gvisorContainerStats runs `runsc --root events --stats ` once and +// runscContainerStats runs `runsc --root events --stats ` once and // parses its JSON output. -func gvisorContainerStats(ctx context.Context, id string) (*gvisorEvent, error) { - ctx, cancel := context.WithTimeout(ctx, gvisorStatsTimeout) +func runscContainerStats(ctx context.Context, id string) (*runscEvent, error) { + ctx, cancel := context.WithTimeout(ctx, runscStatsTimeout) defer cancel() cmd := exec.CommandContext(ctx, *argRunscBinary, "--root", *argRunscRoot, "events", "--stats", id) @@ -127,18 +121,18 @@ func gvisorContainerStats(ctx context.Context, id string) (*gvisorEvent, error) if err != nil { return nil, fmt.Errorf("running %q events --stats %s (root %q): %w", *argRunscBinary, id, *argRunscRoot, err) } - var ev gvisorEvent + var ev runscEvent if err := json.Unmarshal(out, &ev); err != nil { return nil, fmt.Errorf("parsing runsc events output for %s: %w", id, err) } return &ev, nil } -// applyGVisorStats overwrites the cgroup-derived (zero) values in stats with -// the per-container values gVisor reports, honoring includedMetrics. Only the +// applyRunscStats overwrites the cgroup-derived (zero) values in stats with the +// per-container values gVisor reports, honoring includedMetrics. Only the // metrics gVisor reliably provides are overlaid; the rest are left as the // libcontainer base so consumers that read them degrade gracefully. -func applyGVisorStats(stats *info.ContainerStats, ev *gvisorEvent, includedMetrics container.MetricSet) { +func applyRunscStats(stats *info.ContainerStats, ev *runscEvent, includedMetrics container.MetricSet) { d := ev.Data if includedMetrics.Has(container.CpuUsageMetrics) { @@ -191,15 +185,3 @@ func applyGVisorStats(stats *info.ContainerStats, ev *gvisorEvent, includedMetri } } } - -// overlayGVisorStats fetches gVisor's per-container stats and overlays them on -// the libcontainer base. It is best-effort: on any error the base (zeroed) -// stats are left untouched so a transient runsc failure never fails a scrape. -func (h *containerdContainerHandler) overlayGVisorStats(stats *info.ContainerStats) { - ev, err := gvisorContainerStats(context.Background(), h.reference.Id) - if err != nil { - klog.V(4).Infof("containerd: could not get gVisor stats for %q, leaving cgroup-derived values: %v", h.reference.Name, err) - return - } - applyGVisorStats(stats, ev, h.includedMetrics) -} diff --git a/container/containerd/sandbox.go b/container/containerd/sandbox.go new file mode 100644 index 0000000000..e51129923a --- /dev/null +++ b/container/containerd/sandbox.go @@ -0,0 +1,84 @@ +// Copyright 2026 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build linux + +// Per-container stats for sandboxed runtimes. +// +// Some container runtimes run the workload's processes inside a sandbox -- a +// user-space kernel (gVisor/runsc) or a VM (Kata, Firecracker) -- rather than +// directly on the host. For those, the per-container host cgroup that cAdvisor +// reads is empty, so libcontainer's GetStats returns zero CPU/memory/network/ +// pids. The runtime, however, holds the real per-container accounting and +// exposes it out of band (the same source containerd's CRI stats path consumes +// for the kubelet /stats/summary endpoint). +// +// A sandboxStatsProvider knows how to fetch those values for one such runtime +// and overlay them onto the (zero) libcontainer base. Detection is by the +// containerd runtime name; runc and other host-cgroup runtimes have no provider +// and keep the standard libcontainer path unchanged. +package containerd + +import ( + "context" + "strings" + + "github.com/google/cadvisor/container" + info "github.com/google/cadvisor/info/v1" +) + +// sandboxStatsProvider supplies per-container stats for a sandboxed runtime +// whose processes do not live in the host cgroup. +type sandboxStatsProvider interface { + // name identifies the provider, for logging. + name() string + + // overlay fetches per-container stats for id and overlays them onto stats, + // honoring includedMetrics. It should leave fields it cannot populate as + // the libcontainer base. A returned error signals the caller to keep the + // base stats (best-effort: a transient failure must not fail a scrape). + overlay(ctx context.Context, id string, stats *info.ContainerStats, includedMetrics container.MetricSet) error +} + +// sandboxRuntime binds a containerd runtime-name prefix to its stats provider. +type sandboxRuntime struct { + // prefix is matched against containerd's container Runtime.Name, e.g. + // "io.containerd.runsc" matches "io.containerd.runsc.v1". + prefix string + provider sandboxStatsProvider +} + +// sandboxRuntimes is the registry of runtimes whose host cgroup is empty and +// whose stats must come from the runtime instead. To support a new runtime +// (e.g. Kata or Firecracker), add an entry here with a provider -- the handler +// wiring is runtime-agnostic. A generic provider backed by containerd's task +// Metrics API (which every shim implements, returning the standard cgroup +// metrics proto) could serve any sandboxed runtime for CPU/memory/pids; runsc +// uses a dedicated provider because it also reports per-interface network +// stats, which the cgroup metrics proto does not carry. +var sandboxRuntimes = []sandboxRuntime{ + {prefix: "io.containerd.runsc", provider: runscStatsProvider{}}, +} + +// sandboxProviderFor returns the stats provider for a containerd runtime name, +// or nil if the runtime's processes live in the host cgroup (e.g. runc) and the +// standard libcontainer cgroup read is authoritative. +func sandboxProviderFor(runtimeName string) sandboxStatsProvider { + for _, r := range sandboxRuntimes { + if strings.HasPrefix(runtimeName, r.prefix) { + return r.provider + } + } + return nil +}