diff --git a/CHANGELOG.md b/CHANGELOG.md index 22e33fb..ae93449 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **`edgelet system status` resource breakdown:** `agentCpuPercent`, `agentMemoryMiB`, optional embedded `runtime*` fields, and `edgeletTotal*` stack totals on `GET /v1/system/status`. +### Changed + +- **Agent resource metrics:** `cpuUsage` / `memoryUsage` now report edgelet stack totals using process RSS and smoothed CPU (control plane + embedded containerd child when present). Controller `PUT status` keys are unchanged; reported values are more accurate (especially memory). External `docker` / `podman` engines remain agent-only totals. ## [1.0.0-rc.6] — June 2026 diff --git a/docs/cli/output-schemas.md b/docs/cli/output-schemas.md index 4ff15f7..2656025 100644 --- a/docs/cli/output-schemas.md +++ b/docs/cli/output-schemas.md @@ -9,15 +9,23 @@ Route: `GET /v1/system/status` | Field | Type | Description | |-------|------|-------------| | `connectionToController` | string | Controller connectivity summary | -| `cpuUsage` | number | Host CPU usage snapshot | +| `agentCpuPercent` | number | Control-plane CPU percent | +| `agentMemoryMiB` | number | Control-plane RSS in MiB | +| `runtimeCpuPercent` | number | Embedded containerd child CPU (embedded engine only) | +| `runtimeMemoryMiB` | number | Embedded containerd child RSS in MiB | +| `runtimeAvailable` | boolean | Embedded runtime process present | +| `runtimeDegraded` | boolean | Embedded engine configured but runtime child missing | +| `edgeletTotalCpuPercent` | number | Edgelet stack CPU total | +| `edgeletTotalMemoryMiB` | number | Edgelet stack memory total in MiB | +| `cpuUsage` | number | Edgelet stack CPU total (controller alias) | | `edgeletDaemon` | string | Daemon lifecycle state (e.g. `running`) | -| `memoryUsage` | string | Optional memory summary | +| `memoryUsage` | number | Edgelet stack memory total in MiB (controller alias) | | `diskUsage` | string | Optional disk summary | | `runningMicroservices` | number | Optional running MS count | | `systemAvailableDisk` | string | Optional free disk | | `systemAvailableMemory` | string | Optional free memory | | `systemTime` | string | Optional agent time | -| `systemTotalCpu` | string | Optional total CPU | +| `systemTotalCpu` | string | Host CPU usage percent | | `availableNetworkInterfaces` | string | Comma-separated interfaces | | `availableRuntimes` | string | Comma-separated runtime handlers | diff --git a/docs/edgelet/modules/resourceconsumption.md b/docs/edgelet/modules/resourceconsumption.md index f54cf23..fb3c2cc 100644 --- a/docs/edgelet/modules/resourceconsumption.md +++ b/docs/edgelet/modules/resourceconsumption.md @@ -1,23 +1,36 @@ # Resource Consumption Manager -The resource consumption manager samples **host CPU, memory, and disk** usage against configured limits and publishes results to StatusReporter. It also participates in limit enforcement signaling for the agent. +The resource consumption manager samples **edgelet stack and host** usage against configured limits and publishes results to StatusReporter. It also participates in limit enforcement signaling for the agent. **Code:** `internal/resourceconsumption/` ## Purpose -- Poll system metrics via gopsutil (CPU, mem, disk) -- Compare against configured limits (bytes / CPU percentage) +- Sample control-plane RSS/CPU via gopsutil (`process.MemoryInfo`, `process.Percent`) +- When `containerEngine=edgelet` on an embedded build, include the `--edgelet-containerd-child` data-plane process(es) +- Compare stack totals against configured limits (bytes / CPU percentage) - Update `ResourceConsumptionManagerStatus` on StatusReporter - Collect initial sample immediately on start (no wait for first tick) +## Metrics + +| Field | Meaning | +|-------|---------| +| `agentCpuPercent` / `agentMemoryMiB` | Control-plane `edgelet daemon` process | +| `runtimeCpuPercent` / `runtimeMemoryMiB` | Embedded containerd child (embedded engine only) | +| `cpuUsage` / `memoryUsage` | **Edgelet stack total** (agent + runtime when available) — also sent to Pot in `PUT status` | +| `systemTotalCpu` / `systemAvailableMemory` | Whole host | + +External `docker` / `podman` engines report agent-only stack totals (no runtime child tracking). + ## Dependencies | Depends on | Reason | |------------|--------| | `statusreporter` | Publish usage metrics | | `config` | Limits and poll frequency | -| `gopsutil` | Host metrics | +| `gopsutil` | Process and host metrics | +| `pkg/containerd` | Discover embedded containerd child PIDs on Linux | | Used by | Reason | |---------|--------| @@ -54,7 +67,7 @@ Exact YAML names match `config.yaml` profiles — see default config in `interna | StatusReporter index | `0` (`utils.ResourceConsumptionManager`) | | First slot in `modulesStatus[]` | Resource Consumption | -Status fields include `memoryUsage`, `cpuUsage`, `diskUsage` (human-readable in logs on start). +Status fields include stack breakdown plus legacy `memoryUsage`, `cpuUsage`, `diskUsage`. ## External APIs @@ -64,18 +77,23 @@ Exposed indirectly via `GET /v1/system/status` resource section and Controller s - Log module: `"Resource Consumption Manager"` - Initial and periodic debug logs with MiB/GiB formatting +- Warn when multiple embedded containerd child PIDs are detected ## Failure modes | Symptom | Typical cause | |---------|----------------| | Zero usage reported | gopsutil error on platform | -| Limit warnings | Usage exceeded configured thresholds | +| `runtimeDegraded=true` | Embedded engine configured but containerd child not running | +| Limit warnings | Stack usage exceeded configured thresholds | ## Code map | File | Role | |------|------| | `manager.go` | Sampling loop, limit comparison, status updates | +| `stats.go` | Parallel process/host sampling and CPU smoothing | +| `runtime_linux.go` | Embedded runtime PID discovery | +| `host_cpu_linux.go` | Linux host CPU fallback | Related: [statusreporter.md](statusreporter.md), [supervisor.md](supervisor.md). diff --git a/internal/cli/output/edgeletapi_human.go b/internal/cli/output/edgeletapi_human.go index e179be7..c67b120 100644 --- a/internal/cli/output/edgeletapi_human.go +++ b/internal/cli/output/edgeletapi_human.go @@ -10,6 +10,14 @@ import ( var statusOutputOrder = []string{ "connectionToController", + "agentCpuPercent", + "agentMemoryMiB", + "runtimeCpuPercent", + "runtimeMemoryMiB", + "runtimeAvailable", + "runtimeDegraded", + "edgeletTotalCpuPercent", + "edgeletTotalMemoryMiB", "cpuUsage", "diskUsage", "edgeletDaemon", diff --git a/internal/edgeletapi/handlers/report_key_normalization.go b/internal/edgeletapi/handlers/report_key_normalization.go index 7229048..5f99c08 100644 --- a/internal/edgeletapi/handlers/report_key_normalization.go +++ b/internal/edgeletapi/handlers/report_key_normalization.go @@ -29,5 +29,18 @@ func normalizeReportKey(raw string) string { runes[0] = unicode.ToUpper(runes[0]) camel += string(runes) } - return camel + return fixStatusReportKeyAcronyms(camel) +} + +func fixStatusReportKeyAcronyms(key string) string { + switch key { + case "agentMemoryMib": + return "agentMemoryMiB" + case "runtimeMemoryMib": + return "runtimeMemoryMiB" + case "edgeletTotalMemoryMib": + return "edgeletTotalMemoryMiB" + default: + return key + } } diff --git a/internal/edgeletapi/handlers/report_key_normalization_test.go b/internal/edgeletapi/handlers/report_key_normalization_test.go index 0a5eec3..8d37686 100644 --- a/internal/edgeletapi/handlers/report_key_normalization_test.go +++ b/internal/edgeletapi/handlers/report_key_normalization_test.go @@ -6,6 +6,8 @@ func TestNormalizeReportKey_CamelCase(t *testing.T) { tests := map[string]string{ "connection-to-controller": "connectionToController", "cpu usage": "cpuUsage", + "agent cpu percent": "agentCpuPercent", + "edgelet total memory mib": "edgeletTotalMemoryMiB", "edgelet daemon": "edgeletDaemon", "gps-coordinates(lat,lon)": "gpsCoordinates", "developer's-mode": "developerMode", diff --git a/internal/models/resource_consumption_manager_status.go b/internal/models/resource_consumption_manager_status.go index dad5e3c..8369faf 100644 --- a/internal/models/resource_consumption_manager_status.go +++ b/internal/models/resource_consumption_manager_status.go @@ -1,22 +1,36 @@ package models -// ResourceConsumptionManagerStatus represents the Resource Consumption Manager status +// ResourceConsumptionManagerStatus represents the Resource Consumption Manager status. +// CPUUsage and MemoryUsage are edgelet stack totals (control plane + embedded runtime when available). type ResourceConsumptionManagerStatus struct { - MemoryUsage float64 `json:"memoryUsage" yaml:"memoryUsage"` // Memory usage in MiB + AgentCPUPercent float64 `json:"agentCpuPercent" yaml:"agentCpuPercent"` + AgentMemoryMiB float64 `json:"agentMemoryMiB" yaml:"agentMemoryMiB"` + RuntimeCPUPercent float64 `json:"runtimeCpuPercent" yaml:"runtimeCpuPercent"` + RuntimeMemoryMiB float64 `json:"runtimeMemoryMiB" yaml:"runtimeMemoryMiB"` + RuntimeAvailable bool `json:"runtimeAvailable" yaml:"runtimeAvailable"` + RuntimeDegraded bool `json:"runtimeDegraded" yaml:"runtimeDegraded"` + RuntimeTracked bool `json:"runtimeTracked" yaml:"runtimeTracked"` + RuntimePIDCount int `json:"runtimePidCount" yaml:"runtimePidCount"` + EdgeletTotalCPUPercent float64 `json:"edgeletTotalCpuPercent" yaml:"edgeletTotalCpuPercent"` + EdgeletTotalMemoryMiB float64 `json:"edgeletTotalMemoryMiB" yaml:"edgeletTotalMemoryMiB"` + + MemoryUsage float64 `json:"memoryUsage" yaml:"memoryUsage"` // Edgelet stack memory in MiB (alias of EdgeletTotalMemoryMiB) DiskUsage float64 `json:"diskUsage" yaml:"diskUsage"` // Disk usage in GiB - CPUUsage float64 `json:"cpuUsage" yaml:"cpuUsage"` // CPU usage percentage (0-100) + CPUUsage float64 `json:"cpuUsage" yaml:"cpuUsage"` // Edgelet stack CPU percent (alias of EdgeletTotalCPUPercent) MemoryViolation bool `json:"memoryViolation" yaml:"memoryViolation"` // Whether memory limit is violated DiskViolation bool `json:"diskViolation" yaml:"diskViolation"` // Whether disk limit is violated CPUViolation bool `json:"cpuViolation" yaml:"cpuViolation"` // Whether CPU limit is violated AvailableMemory int64 `json:"availableMemory" yaml:"availableMemory"` // System available memory in bytes - TotalCPU float64 `json:"totalCpu" yaml:"totalCpu"` // Total system CPU usage percentage + TotalCPU float64 `json:"totalCpu" yaml:"totalCpu"` // Host CPU usage percentage AvailableDisk int64 `json:"availableDisk" yaml:"availableDisk"` // System available disk space in bytes TotalDiskSpace int64 `json:"totalDiskSpace" yaml:"totalDiskSpace"` // Total system disk space in bytes } // NewResourceConsumptionManagerStatus creates a new ResourceConsumptionManagerStatus func NewResourceConsumptionManagerStatus() *ResourceConsumptionManagerStatus { - return &ResourceConsumptionManagerStatus{} + return &ResourceConsumptionManagerStatus{ + RuntimeAvailable: true, + } } // SetMemoryUsage sets the memory usage and returns the status for chaining diff --git a/internal/resourceconsumption/host_cpu_linux.go b/internal/resourceconsumption/host_cpu_linux.go new file mode 100644 index 0000000..f63d5e0 --- /dev/null +++ b/internal/resourceconsumption/host_cpu_linux.go @@ -0,0 +1,53 @@ +//go:build linux + +package resourceconsumption + +import ( + "os" + "strconv" + "strings" + + "github.com/eclipse-iofog/edgelet/internal/utils/logging" +) + +func (rcm *Manager) getTotalCPULinux() float64 { + data, err := os.ReadFile("/proc/stat") + if err != nil { + logging.LogError(moduleName, "Error reading /proc/stat", err) + return 0.0 + } + + lines := strings.Split(string(data), "\n") + if len(lines) == 0 { + return 0.0 + } + + firstLine := strings.TrimSpace(lines[0]) + if !strings.HasPrefix(firstLine, "cpu ") { + return 0.0 + } + + parts := strings.Fields(firstLine) + if len(parts) < 8 { + return 0.0 + } + + user, _ := strconv.ParseInt(parts[1], 10, 64) + nice, _ := strconv.ParseInt(parts[2], 10, 64) + system, _ := strconv.ParseInt(parts[3], 10, 64) + idle, _ := strconv.ParseInt(parts[4], 10, 64) + iowait, _ := strconv.ParseInt(parts[5], 10, 64) + irq, _ := strconv.ParseInt(parts[6], 10, 64) + softirq, _ := strconv.ParseInt(parts[7], 10, 64) + steal := int64(0) + if len(parts) >= 9 { + steal, _ = strconv.ParseInt(parts[8], 10, 64) + } + + totalTime := user + nice + system + idle + iowait + irq + softirq + steal + idleTime := idle + iowait + if totalTime <= 0 { + return 0.0 + } + return float64(totalTime-idleTime) / float64(totalTime) * 100.0 +} diff --git a/internal/resourceconsumption/host_cpu_stub.go b/internal/resourceconsumption/host_cpu_stub.go new file mode 100644 index 0000000..7601ee9 --- /dev/null +++ b/internal/resourceconsumption/host_cpu_stub.go @@ -0,0 +1,7 @@ +//go:build !linux + +package resourceconsumption + +func (rcm *Manager) getTotalCPULinux() float64 { + return 0 +} diff --git a/internal/resourceconsumption/manager.go b/internal/resourceconsumption/manager.go index 800e404..d3d629b 100644 --- a/internal/resourceconsumption/manager.go +++ b/internal/resourceconsumption/manager.go @@ -5,21 +5,19 @@ import ( "fmt" "os" "path/filepath" - "runtime" - "strconv" "strings" "sync" "time" + "github.com/eclipse-iofog/edgelet/internal/buildmeta" "github.com/eclipse-iofog/edgelet/internal/config" + "github.com/eclipse-iofog/edgelet/internal/constants" "github.com/eclipse-iofog/edgelet/internal/models" "github.com/eclipse-iofog/edgelet/internal/statusreporter" "github.com/eclipse-iofog/edgelet/internal/utils" "github.com/eclipse-iofog/edgelet/internal/utils/logging" - "github.com/shirou/gopsutil/v4/cpu" "github.com/shirou/gopsutil/v4/disk" "github.com/shirou/gopsutil/v4/mem" - "github.com/shirou/gopsutil/v4/process" ) const ( @@ -35,6 +33,13 @@ type Manager struct { wg sync.WaitGroup mu sync.RWMutex + processReader processMetricsReader + hostCPUReader hostCPUReader + runtimePIDReader runtimePIDReader + + cpuHistoryMu sync.Mutex + cpuHistory map[string][]float64 + // Limits (in bytes for memory/disk, percentage for CPU) diskLimit int64 cpuLimit float64 @@ -50,7 +55,9 @@ var ( func GetInstance() *Manager { once.Do(func() { instance = &Manager{ - config: config.GetInstance(), + config: config.GetInstance(), + processReader: gopsutilProcessReader{}, + cpuHistory: make(map[string][]float64), } }) return instance @@ -65,11 +72,8 @@ func (rcm *Manager) Start() error { logging.LogDebug(moduleName, fmt.Sprintf("Resource limits set: Disk=%.2f GiB, Memory=%.2f MiB, CPU=%.2f%%", float64(rcm.diskLimit)/1_000_000_000, float64(rcm.memoryLimit)/1_000_000, rcm.cpuLimit)) - // Create context for cancellation rcm.ctx, rcm.cancel = context.WithCancel(context.Background()) - // Collect initial usage data immediately (before starting periodic worker) - // This ensures status is available right away logging.LogDebug(moduleName, "Collecting initial resource usage data") rcm.collectUsageData() logging.LogDebug(moduleName, fmt.Sprintf("Initial resource usage: Memory=%.2f MiB, CPU=%.2f%%, Disk=%.2f GiB", @@ -77,7 +81,6 @@ func (rcm *Manager) Start() error { rcm.statusReporter.GetResourceConsumptionManagerStatus().CPUUsage, rcm.statusReporter.GetResourceConsumptionManagerStatus().DiskUsage)) - // Start background worker rcm.wg.Add(1) go rcm.runUsageDataWorker() @@ -85,44 +88,69 @@ func (rcm *Manager) Start() error { return nil } -// collectUsageData collects resource usage data and updates status -// Extracted from getUsageDataWorker for immediate collection on startup func (rcm *Manager) collectUsageData() { logging.LogDebug(moduleName, "Get usage data") - memoryUsage := rcm.getMemoryUsage() - cpuUsage := rcm.getCPUUsage() + trackRuntime := rcm.shouldTrackEmbeddedRuntime() + runtimePIDs := []int(nil) + if trackRuntime { + runtimePIDs = rcm.embeddedRuntimePIDs() + if len(runtimePIDs) > 1 { + logging.LogWarn(moduleName, fmt.Sprintf("Multiple embedded containerd child processes detected: count=%d", len(runtimePIDs))) + } + } + + sample := rcm.sampleEdgeletUsage(trackRuntime, runtimePIDs) - // Calculate disk usage (directory may not exist yet, which is OK) - diskUsage := rcm.directorySize(rcm.config.DiskDirectory) + agentCPU := rcm.smoothCPU("agent", sample.agentCPU) + runtimeCPU := 0.0 + if trackRuntime && sample.runtimeAvailable { + runtimeCPU = rcm.smoothCPU("runtime", sample.runtimeCPU) + } + totalCPU := rcm.smoothCPU("total", agentCPU+runtimeCPU) - logging.LogDebug(moduleName, fmt.Sprintf("Disk usage: diskDirectory=%d bytes", diskUsage)) + agentMemoryMiB := bytesToMiB(sample.agentRSS) + runtimeMemoryMiB := bytesToMiB(sample.runtimeRSS) + totalMemoryMiB := agentMemoryMiB + runtimeMemoryMiB + totalMemoryBytes := sample.agentRSS + sample.runtimeRSS + diskUsage := rcm.directorySize(rcm.config.DiskDirectory) availableMemory := rcm.getSystemAvailableMemory() - totalCPU := rcm.getTotalCPU() availableDisk := rcm.getAvailableDisk() totalDiskSpace := rcm.getTotalDiskSpace() - logging.LogDebug(moduleName, fmt.Sprintf("System resources: availableMemory=%d bytes, availableDisk=%d bytes, totalDiskSpace=%d bytes, totalCpu=%.2f%%", - availableMemory, availableDisk, totalDiskSpace, totalCPU)) + runtimeAvailable := !trackRuntime || sample.runtimeAvailable + runtimeDegraded := trackRuntime && !sample.runtimeAvailable + + logging.LogDebug(moduleName, fmt.Sprintf( + "Edgelet usage: agentCPU=%.2f runtimeCPU=%.2f totalCPU=%.2f agentMem=%.2f MiB runtimeMem=%.2f MiB hostCPU=%.2f runtimeAvailable=%v", + agentCPU, runtimeCPU, totalCPU, agentMemoryMiB, runtimeMemoryMiB, sample.hostCPU, runtimeAvailable, + )) - // Update status atomically (fixes race condition) rcm.statusReporter.UpdateResourceConsumptionManagerStatus(func(status *models.ResourceConsumptionManagerStatus) { - status.MemoryUsage = float64(memoryUsage) / 1_000_000 // bytes to MiB - status.CPUUsage = cpuUsage - status.DiskUsage = float64(diskUsage) / 1_000_000_000 // bytes to GiB - status.MemoryViolation = memoryUsage > rcm.memoryLimit + status.AgentCPUPercent = agentCPU + status.AgentMemoryMiB = agentMemoryMiB + status.RuntimeCPUPercent = runtimeCPU + status.RuntimeMemoryMiB = runtimeMemoryMiB + status.RuntimeAvailable = runtimeAvailable + status.RuntimeDegraded = runtimeDegraded + status.RuntimeTracked = trackRuntime + status.RuntimePIDCount = sample.runtimePIDCount + status.EdgeletTotalCPUPercent = totalCPU + status.EdgeletTotalMemoryMiB = totalMemoryMiB + + status.CPUUsage = totalCPU + status.MemoryUsage = totalMemoryMiB + status.DiskUsage = float64(diskUsage) / 1_000_000_000 + status.MemoryViolation = totalMemoryBytes > rcm.memoryLimit status.DiskViolation = diskUsage > rcm.diskLimit - status.CPUViolation = cpuUsage > rcm.cpuLimit + status.CPUViolation = totalCPU > rcm.cpuLimit status.AvailableMemory = availableMemory status.AvailableDisk = availableDisk status.TotalDiskSpace = totalDiskSpace - status.TotalCPU = totalCPU + status.TotalCPU = sample.hostCPU }) - logging.LogDebug(moduleName, fmt.Sprintf("Updated status: MemoryUsage=%.2f MiB, CPUUsage=%.2f%%, DiskUsage=%.2f GiB", - float64(memoryUsage)/1_000_000, cpuUsage, float64(diskUsage)/1_000_000_000)) - logging.LogDebug(moduleName, "Finished Get usage data") } @@ -134,7 +162,6 @@ func (rcm *Manager) Stop() error { rcm.cancel() } - // Wait for all workers to finish rcm.wg.Wait() logging.LogDebug(moduleName, "Resource Consumption Manager stopped") @@ -156,13 +183,22 @@ func (rcm *Manager) InstanceConfigUpdated() { rcm.mu.Lock() defer rcm.mu.Unlock() - // Convert limits from config (GiB/MiB) to bytes - rcm.diskLimit = int64(rcm.config.DiskLimit * 1_000_000_000) // GiB to bytes - rcm.memoryLimit = int64(rcm.config.MemoryLimit * 1_000_000) // MiB to bytes - rcm.cpuLimit = rcm.config.CPULimit // Percentage + rcm.diskLimit = int64(rcm.config.DiskLimit * 1_000_000_000) + rcm.memoryLimit = int64(rcm.config.MemoryLimit * 1_000_000) + rcm.cpuLimit = rcm.config.CPULimit +} + +func (rcm *Manager) shouldTrackEmbeddedRuntime() bool { + cfg := rcm.config + if cfg == nil { + return false + } + if !buildmeta.HasEmbeddedEngine() { + return false + } + return strings.EqualFold(strings.TrimSpace(cfg.ContainerEngine), constants.EngineEdgelet) } -// runUsageDataWorker periodically computes resource usage and updates status func (rcm *Manager) runUsageDataWorker() { defer rcm.wg.Done() @@ -180,190 +216,43 @@ func (rcm *Manager) runUsageDataWorker() { } } -// getMemoryUsage gets the memory usage of the ioFog process in bytes -func (rcm *Manager) getMemoryUsage() int64 { - logging.LogDebug(moduleName, "Start get memory usage") - - var m runtime.MemStats - runtime.ReadMemStats(&m) - // Go equivalent: Sys (total memory obtained from OS) - HeapIdle (free heap memory) - // But more accurate: Alloc (allocated heap) + (Sys - HeapSys) (non-heap memory) - memoryUsage := int64(m.Alloc) // #nosec G115 -- Alloc is heap bytes; practical values fit in int64 - - logging.LogDebug(moduleName, fmt.Sprintf("Finished get memory usage: %d bytes (Alloc=%d, Sys=%d, HeapSys=%d, HeapIdle=%d)", - memoryUsage, m.Alloc, m.Sys, m.HeapSys, m.HeapIdle)) - return memoryUsage -} - -// getCPUUsage gets the CPU usage percentage of the ioFog process -func (rcm *Manager) getCPUUsage() float64 { - logging.LogDebug(moduleName, "Start get cpu usage") - - // Get current process - proc, err := process.NewProcess(int32(os.Getpid())) // #nosec G115 -- PID fits in int32 on all supported platforms - if err != nil { - logging.LogError(moduleName, "Error getting current process", err) - return 0.0 - } - - // Get CPU percentage for this process - cpuPercent, err := proc.Percent(time.Second) - if err != nil { - logging.LogError(moduleName, "Error getting CPU usage", err) - return 0.0 - } - - logging.LogDebug(moduleName, fmt.Sprintf("Finished get cpu usage: %.2f", cpuPercent)) - return cpuPercent -} - -// getSystemAvailableMemory gets the system available memory in bytes func (rcm *Manager) getSystemAvailableMemory() int64 { - logging.LogDebug(moduleName, "Start get system available memory") - vmStat, err := mem.VirtualMemory() if err != nil { logging.LogError(moduleName, "Error getting system available memory", err) return 0 } - - availableMemory := int64(vmStat.Available) // #nosec G115 -- system available memory is below int64 max in practice - logging.LogDebug(moduleName, fmt.Sprintf("Finished get system available memory: %d", availableMemory)) - return availableMemory + return int64(vmStat.Available) // #nosec G115 -- system available memory is below int64 max in practice } -// getTotalCPU gets the total system CPU usage percentage -// Uses gopsutil cpu.Percent() for cross-platform support (Linux, Windows, macOS, etc.) -// This is the recommended method that works on all platforms -func (rcm *Manager) getTotalCPU() float64 { - logging.LogDebug(moduleName, "Start get total cpu") - - // Use gopsutil cpu.Percent() - the recommended cross-platform method - // When interval > 0, it blocks for that duration and measures CPU usage - // percpu=false returns overall CPU usage (single value) - percentages, err := cpu.Percent(1*time.Second, false) - if err != nil { - logging.LogError(moduleName, "Error getting total CPU usage", err) - // Fallback to Linux-specific method if on Linux - if runtime.GOOS == "linux" { - logging.LogDebug(moduleName, "Falling back to Linux /proc/stat method") - return rcm.getTotalCPULinux() - } - return 0.0 - } - - if len(percentages) == 0 { - logging.LogWarn(moduleName, "No CPU percentage returned from gopsutil") - // Fallback to Linux-specific method if on Linux - if runtime.GOOS == "linux" { - return rcm.getTotalCPULinux() - } - return 0.0 - } - - totalCPU := percentages[0] - logging.LogDebug(moduleName, fmt.Sprintf("Finished get total cpu: %.2f%%", totalCPU)) - return totalCPU -} - -// getTotalCPULinux reads /proc/stat to calculate total CPU usage -func (rcm *Manager) getTotalCPULinux() float64 { - // Read /proc/stat - statFile := "/proc/stat" - data, err := os.ReadFile(statFile) - if err != nil { - logging.LogError(moduleName, "Error reading /proc/stat", err) - return 0.0 - } - - lines := strings.Split(string(data), "\n") - if len(lines) == 0 { - return 0.0 - } - - // Parse first line (cpu line) - firstLine := strings.TrimSpace(lines[0]) - if !strings.HasPrefix(firstLine, "cpu ") { - return 0.0 - } - - parts := strings.Fields(firstLine) - if len(parts) < 8 { - return 0.0 - } - - // Parse CPU times - user, _ := strconv.ParseInt(parts[1], 10, 64) - nice, _ := strconv.ParseInt(parts[2], 10, 64) - system, _ := strconv.ParseInt(parts[3], 10, 64) - idle, _ := strconv.ParseInt(parts[4], 10, 64) - iowait, _ := strconv.ParseInt(parts[5], 10, 64) - irq, _ := strconv.ParseInt(parts[6], 10, 64) - softirq, _ := strconv.ParseInt(parts[7], 10, 64) - steal := int64(0) - if len(parts) >= 9 { - steal, _ = strconv.ParseInt(parts[8], 10, 64) - } - - totalTime := user + nice + system + idle + iowait + irq + softirq + steal - idleTime := idle + iowait - - if totalTime > 0 { - cpuUsage := float64(totalTime-idleTime) / float64(totalTime) * 100.0 - logging.LogDebug(moduleName, fmt.Sprintf("Finished get total cpu: %.2f", cpuUsage)) - return cpuUsage - } - - return 0.0 -} - -// getAvailableDisk gets the available disk space in bytes func (rcm *Manager) getAvailableDisk() int64 { - logging.LogDebug(moduleName, "Start get available disk") - cfg := rcm.config usage, err := disk.Usage(cfg.DiskDirectory) if err != nil { logging.LogError(moduleName, "Error getting available disk", err) return 0 } - - availableDisk := int64(usage.Free) // #nosec G115 -- disk size is below int64 max in practice - logging.LogDebug(moduleName, fmt.Sprintf("Finished get available disk: %d", availableDisk)) - return availableDisk + return int64(usage.Free) // #nosec G115 -- disk size is below int64 max in practice } -// getTotalDiskSpace gets the total disk space in bytes func (rcm *Manager) getTotalDiskSpace() int64 { - logging.LogDebug(moduleName, "Start get total disk space") - cfg := rcm.config usage, err := disk.Usage(cfg.DiskDirectory) if err != nil { logging.LogError(moduleName, "Error getting total disk space", err) return 0 } - - totalDiskSpace := int64(usage.Total) // #nosec G115 -- disk size is below int64 max in practice - logging.LogDebug(moduleName, fmt.Sprintf("Finished get total disk space: %d", totalDiskSpace)) - return totalDiskSpace + return int64(usage.Total) // #nosec G115 -- disk size is below int64 max in practice } -// directorySize computes the size of a directory in bytes func (rcm *Manager) directorySize(path string) int64 { - logging.LogDebug(moduleName, fmt.Sprintf("Inside get directory size: %s", path)) - - // Check if directory exists if _, err := os.Stat(path); os.IsNotExist(err) { - logging.LogDebug(moduleName, fmt.Sprintf("Directory does not exist: %s, returning 0", path)) return 0 } var size int64 err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error { if err != nil { - // Log but continue on error (e.g., permission denied) - logging.LogDebug(moduleName, fmt.Sprintf("Error accessing file in %s: %v", path, err)) return nil } if !info.IsDir() { @@ -371,12 +260,9 @@ func (rcm *Manager) directorySize(path string) int64 { } return nil }) - if err != nil { logging.LogError(moduleName, fmt.Sprintf("Error calculating directory size for %s", path), err) return 0 } - - logging.LogDebug(moduleName, fmt.Sprintf("Finished directory size: %d bytes for %s", size, path)) return size } diff --git a/internal/resourceconsumption/manager_test.go b/internal/resourceconsumption/manager_test.go new file mode 100644 index 0000000..9c11dd6 --- /dev/null +++ b/internal/resourceconsumption/manager_test.go @@ -0,0 +1,208 @@ +package resourceconsumption + +import ( + "context" + "testing" + + "github.com/eclipse-iofog/edgelet/internal/buildmeta" + "github.com/eclipse-iofog/edgelet/internal/config" + "github.com/eclipse-iofog/edgelet/internal/constants" + "github.com/eclipse-iofog/edgelet/internal/models" + "github.com/eclipse-iofog/edgelet/internal/statusreporter" +) + +type fakeProcessReader struct { + cpuByPID map[int32]float64 + rssByPID map[int32]int64 +} + +func (f fakeProcessReader) processCPUPercent(_ context.Context, pid int32) float64 { + return f.cpuByPID[pid] +} + +func (f fakeProcessReader) processRSSBytes(pid int32) int64 { + return f.rssByPID[pid] +} + +func TestCollectUsageData_EmbeddedStackTotals(t *testing.T) { + t.Cleanup(resetResourceConsumptionTestState()) + + embedded := true + buildmeta.SetHasEmbeddedEngineForTest(&embedded) + t.Cleanup(func() { buildmeta.SetHasEmbeddedEngineForTest(nil) }) + + cfg := config.GetInstance() + cfg.ContainerEngine = constants.EngineEdgelet + cfg.MemoryLimit = 4096 + cfg.CPULimit = 80 + + agentPID := int32(1000) + runtimePID := int32(2000) + + rcm := &Manager{ + config: cfg, + processReader: fakeProcessReader{ + cpuByPID: map[int32]float64{agentPID: 3, runtimePID: 2}, + rssByPID: map[int32]int64{ + agentPID: 80 * 1024 * 1024, + runtimePID: 70 * 1024 * 1024, + }, + }, + hostCPUReader: func(context.Context) float64 { return 1.25 }, + runtimePIDReader: func() []int { return []int{int(runtimePID)} }, + cpuHistory: make(map[string][]float64), + } + rcm.ctx = context.Background() + rcm.statusReporter = statusreporter.GetInstance() + rcm.InstanceConfigUpdated() + + oldGetpid := getAgentPID + getAgentPID = func() int32 { return agentPID } + t.Cleanup(func() { getAgentPID = oldGetpid }) + + rcm.collectUsageData() + + status := statusreporter.GetInstance().GetResourceConsumptionManagerStatus() + if status.AgentCPUPercent != 3 { + t.Fatalf("agent cpu: got %.2f want 3", status.AgentCPUPercent) + } + if status.RuntimeCPUPercent != 2 { + t.Fatalf("runtime cpu: got %.2f want 2", status.RuntimeCPUPercent) + } + if status.CPUUsage != 5 { + t.Fatalf("total cpu: got %.2f want 5", status.CPUUsage) + } + if status.AgentMemoryMiB < 79.9 || status.AgentMemoryMiB > 80.1 { + t.Fatalf("agent memory: got %.2f want ~80", status.AgentMemoryMiB) + } + if status.RuntimeMemoryMiB < 69.9 || status.RuntimeMemoryMiB > 70.1 { + t.Fatalf("runtime memory: got %.2f want ~70", status.RuntimeMemoryMiB) + } + if status.MemoryUsage < 149.9 || status.MemoryUsage > 150.1 { + t.Fatalf("total memory: got %.2f want ~150", status.MemoryUsage) + } + if !status.RuntimeAvailable || status.RuntimeDegraded { + t.Fatalf("runtime availability: available=%v degraded=%v", status.RuntimeAvailable, status.RuntimeDegraded) + } + if status.TotalCPU != 1.25 { + t.Fatalf("host cpu: got %.2f want 1.25", status.TotalCPU) + } +} + +func TestCollectUsageData_ExternalEngineAgentOnly(t *testing.T) { + t.Cleanup(resetResourceConsumptionTestState()) + + embedded := true + buildmeta.SetHasEmbeddedEngineForTest(&embedded) + t.Cleanup(func() { buildmeta.SetHasEmbeddedEngineForTest(nil) }) + + cfg := config.GetInstance() + cfg.ContainerEngine = constants.EngineDocker + cfg.MemoryLimit = 4096 + cfg.CPULimit = 80 + + agentPID := int32(1000) + + rcm := &Manager{ + config: cfg, + processReader: fakeProcessReader{ + cpuByPID: map[int32]float64{agentPID: 4}, + rssByPID: map[int32]int64{agentPID: 50 * 1024 * 1024}, + }, + hostCPUReader: func(context.Context) float64 { return 0.5 }, + runtimePIDReader: func() []int { + t.Fatal("runtime PID lookup should not run for external engine") + return nil + }, + cpuHistory: make(map[string][]float64), + } + rcm.ctx = context.Background() + rcm.statusReporter = statusreporter.GetInstance() + rcm.InstanceConfigUpdated() + + oldGetpid := getAgentPID + getAgentPID = func() int32 { return agentPID } + t.Cleanup(func() { getAgentPID = oldGetpid }) + + rcm.collectUsageData() + + status := statusreporter.GetInstance().GetResourceConsumptionManagerStatus() + if status.RuntimeTracked { + t.Fatal("expected runtime tracking disabled for docker engine") + } + if status.CPUUsage != 4 || status.MemoryUsage < 49.9 || status.MemoryUsage > 50.1 { + t.Fatalf("unexpected totals: cpu=%.2f mem=%.2f", status.CPUUsage, status.MemoryUsage) + } + if status.RuntimeCPUPercent != 0 || status.RuntimeMemoryMiB != 0 { + t.Fatalf("expected zero runtime metrics, got cpu=%.2f mem=%.2f", status.RuntimeCPUPercent, status.RuntimeMemoryMiB) + } +} + +func TestCollectUsageData_EmbeddedRuntimeMissingDegraded(t *testing.T) { + t.Cleanup(resetResourceConsumptionTestState()) + + embedded := true + buildmeta.SetHasEmbeddedEngineForTest(&embedded) + t.Cleanup(func() { buildmeta.SetHasEmbeddedEngineForTest(nil) }) + + cfg := config.GetInstance() + cfg.ContainerEngine = constants.EngineEdgelet + + agentPID := int32(1000) + rcm := &Manager{ + config: cfg, + processReader: fakeProcessReader{ + cpuByPID: map[int32]float64{agentPID: 2}, + rssByPID: map[int32]int64{agentPID: 40 * 1024 * 1024}, + }, + hostCPUReader: func(context.Context) float64 { return 0.2 }, + runtimePIDReader: func() []int { return nil }, + cpuHistory: make(map[string][]float64), + } + rcm.ctx = context.Background() + rcm.statusReporter = statusreporter.GetInstance() + rcm.InstanceConfigUpdated() + + oldGetpid := getAgentPID + getAgentPID = func() int32 { return agentPID } + t.Cleanup(func() { getAgentPID = oldGetpid }) + + rcm.collectUsageData() + + status := statusreporter.GetInstance().GetResourceConsumptionManagerStatus() + if !status.RuntimeDegraded || status.RuntimeAvailable { + t.Fatalf("expected degraded missing runtime, got available=%v degraded=%v", status.RuntimeAvailable, status.RuntimeDegraded) + } + if status.CPUUsage != 2 { + t.Fatalf("total cpu should equal agent only, got %.2f", status.CPUUsage) + } +} + +func TestSmoothCPURollingAverage(t *testing.T) { + rcm := &Manager{cpuHistory: make(map[string][]float64)} + + first := rcm.smoothCPU("total", 3) + second := rcm.smoothCPU("total", 9) + third := rcm.smoothCPU("total", 6) + fourth := rcm.smoothCPU("total", 0) + + if first != 3 { + t.Fatalf("first average: got %.2f want 3", first) + } + if second != 6 { + t.Fatalf("second average: got %.2f want 6", second) + } + if third != 6 { + t.Fatalf("third average: got %.2f want 6", third) + } + if fourth != 5 { + t.Fatalf("fourth average: got %.2f want 5", fourth) + } +} + +func resetResourceConsumptionTestState() func() { + statusreporter.GetInstance().UpdateResourceConsumptionManagerStatus(func(status *models.ResourceConsumptionManagerStatus) { + *status = *models.NewResourceConsumptionManagerStatus() + }) + return func() {} +} diff --git a/internal/resourceconsumption/runtime_linux.go b/internal/resourceconsumption/runtime_linux.go new file mode 100644 index 0000000..7a03df8 --- /dev/null +++ b/internal/resourceconsumption/runtime_linux.go @@ -0,0 +1,18 @@ +//go:build linux + +package resourceconsumption + +import ( + "github.com/eclipse-iofog/edgelet/pkg/containerd" +) + +func (rcm *Manager) embeddedRuntimePIDs() []int { + if rcm.runtimePIDReader != nil { + return rcm.runtimePIDReader() + } + pids, err := containerd.FindEmbeddedContainerdChildPIDs() + if err != nil || len(pids) == 0 { + return nil + } + return pids +} diff --git a/internal/resourceconsumption/runtime_stub.go b/internal/resourceconsumption/runtime_stub.go new file mode 100644 index 0000000..3b83c4d --- /dev/null +++ b/internal/resourceconsumption/runtime_stub.go @@ -0,0 +1,10 @@ +//go:build !linux + +package resourceconsumption + +func (rcm *Manager) embeddedRuntimePIDs() []int { + if rcm.runtimePIDReader != nil { + return rcm.runtimePIDReader() + } + return nil +} diff --git a/internal/resourceconsumption/stats.go b/internal/resourceconsumption/stats.go new file mode 100644 index 0000000..915f7cd --- /dev/null +++ b/internal/resourceconsumption/stats.go @@ -0,0 +1,166 @@ +package resourceconsumption + +import ( + "context" + "os" + "runtime" + "sync" + "time" + + "github.com/shirou/gopsutil/v4/cpu" + "github.com/shirou/gopsutil/v4/process" +) + +const ( + cpuSampleInterval = time.Second + cpuSmoothingSize = 3 + collectTimeout = 5 * time.Second +) + +type hostCPUReader func(ctx context.Context) float64 +type runtimePIDReader func() []int + +var getAgentPID = func() int32 { + return int32(os.Getpid()) // #nosec G115 -- PID fits in int32 on supported platforms +} + +type processMetricsReader interface { + processCPUPercent(ctx context.Context, pid int32) float64 + processRSSBytes(pid int32) int64 +} + +type gopsutilProcessReader struct{} + +func (gopsutilProcessReader) processCPUPercent(ctx context.Context, pid int32) float64 { + proc, err := process.NewProcessWithContext(ctx, pid) + if err != nil { + return 0 + } + value, err := proc.PercentWithContext(ctx, cpuSampleInterval) + if err != nil { + return 0 + } + return value +} + +func (gopsutilProcessReader) processRSSBytes(pid int32) int64 { + proc, err := process.NewProcess(pid) + if err != nil { + return 0 + } + memInfo, err := proc.MemoryInfo() + if err != nil { + return 0 + } + return int64(memInfo.RSS) // #nosec G115 -- RSS is below int64 max in practice +} + +type edgeletUsageSample struct { + agentCPU float64 + runtimeCPU float64 + hostCPU float64 + agentRSS int64 + runtimeRSS int64 + runtimeAvailable bool + runtimePIDCount int +} + +func (rcm *Manager) sampleEdgeletUsage(trackRuntime bool, runtimePIDs []int) edgeletUsageSample { + baseCtx := rcm.ctx + if baseCtx == nil { + baseCtx = context.Background() + } + ctx, cancel := context.WithTimeout(baseCtx, collectTimeout) + defer cancel() + + reader := rcm.processReader + if reader == nil { + reader = gopsutilProcessReader{} + } + + var ( + wg sync.WaitGroup + mu sync.Mutex + sample edgeletUsageSample + ) + + wg.Add(1) + go func() { + defer wg.Done() + agentPID := getAgentPID() + agentCPU := reader.processCPUPercent(ctx, agentPID) + agentRSS := reader.processRSSBytes(agentPID) + mu.Lock() + sample.agentCPU = agentCPU + sample.agentRSS = agentRSS + mu.Unlock() + }() + + wg.Add(1) + go func() { + defer wg.Done() + hostCPU := rcm.sampleHostCPU(ctx) + mu.Lock() + sample.hostCPU = hostCPU + mu.Unlock() + }() + + if trackRuntime { + sample.runtimePIDCount = len(runtimePIDs) + sample.runtimeAvailable = len(runtimePIDs) > 0 + for _, pid := range runtimePIDs { + pid := pid + wg.Add(1) + go func() { + defer wg.Done() + runtimeCPU := reader.processCPUPercent(ctx, int32(pid)) // #nosec G115 -- proc PIDs fit in int32 + runtimeRSS := reader.processRSSBytes(int32(pid)) // #nosec G115 -- proc PIDs fit in int32 + mu.Lock() + sample.runtimeCPU += runtimeCPU + sample.runtimeRSS += runtimeRSS + mu.Unlock() + }() + } + } else { + sample.runtimeAvailable = true + } + + wg.Wait() + return sample +} + +func (rcm *Manager) sampleHostCPU(ctx context.Context) float64 { + if rcm.hostCPUReader != nil { + return rcm.hostCPUReader(ctx) + } + percentages, err := cpu.PercentWithContext(ctx, cpuSampleInterval, false) + if err != nil || len(percentages) == 0 { + if runtime.GOOS == "linux" { + return rcm.getTotalCPULinux() + } + return 0 + } + return percentages[0] +} + +func bytesToMiB(bytes int64) float64 { + return float64(bytes) / (1024 * 1024) +} + +func (rcm *Manager) smoothCPU(key string, value float64) float64 { + rcm.cpuHistoryMu.Lock() + defer rcm.cpuHistoryMu.Unlock() + + history := rcm.cpuHistory[key] + history = append(history, value) + if len(history) > cpuSmoothingSize { + history = history[len(history)-cpuSmoothingSize:] + } + rcm.cpuHistory[key] = history + + sum := 0.0 + for _, item := range history { + sum += item + } + return sum / float64(len(history)) +} diff --git a/internal/statusreporter/reporter.go b/internal/statusreporter/reporter.go index df19bd1..cd13222 100644 --- a/internal/statusreporter/reporter.go +++ b/internal/statusreporter/reporter.go @@ -144,16 +144,18 @@ func (sr *StatusReporter) GetStatusReport() string { defer sr.mu.RUnlock() var result string - diskUsage := sr.resourceConsumptionManagerStatus.DiskUsage - availableDisk := float64(sr.resourceConsumptionManagerStatus.AvailableDisk) / 1024.0 / 1024.0 - availableMemory := float64(sr.resourceConsumptionManagerStatus.AvailableMemory) / 1024.0 / 1024.0 - totalCPU := sr.resourceConsumptionManagerStatus.TotalCPU - memoryUsage := sr.resourceConsumptionManagerStatus.MemoryUsage - cpuUsage := sr.resourceConsumptionManagerStatus.CPUUsage - - // Debug logging to trace status values - logging.LogDebug(moduleName, fmt.Sprintf("Status values: MemoryUsage=%.2f MiB, CPUUsage=%.2f%%, DiskUsage=%.2f GiB, AvailableMemory=%.2f MB, AvailableDisk=%.2f MB, TotalCPU=%.2f%%", - memoryUsage, cpuUsage, diskUsage, availableMemory, availableDisk, totalCPU)) + rcs := sr.resourceConsumptionManagerStatus + diskUsage := rcs.DiskUsage + availableDisk := float64(rcs.AvailableDisk) / 1024.0 / 1024.0 + availableMemory := float64(rcs.AvailableMemory) / 1024.0 / 1024.0 + hostCPU := rcs.TotalCPU + memoryUsage := rcs.MemoryUsage + cpuUsage := rcs.CPUUsage + + logging.LogDebug(moduleName, fmt.Sprintf( + "Status values: agentCPU=%.2f runtimeCPU=%.2f totalCPU=%.2f agentMem=%.2f MiB runtimeMem=%.2f MiB totalMem=%.2f MiB hostCPU=%.2f%%", + rcs.AgentCPUPercent, rcs.RuntimeCPUPercent, cpuUsage, rcs.AgentMemoryMiB, rcs.RuntimeMemoryMiB, memoryUsage, hostCPU, + )) // Get connection status var connectionStatus string @@ -189,6 +191,16 @@ func (sr *StatusReporter) GetStatusReport() string { daemonStatus = "RUNNING" } result += fmt.Sprintf("Edgelet daemon : %s\n", daemonStatus) + result += fmt.Sprintf("Agent CPU percent : about %.2f %%\n", rcs.AgentCPUPercent) + result += fmt.Sprintf("Agent memory MiB : about %.2f\n", rcs.AgentMemoryMiB) + if rcs.RuntimeTracked { + result += fmt.Sprintf("Runtime CPU percent : about %.2f %%\n", rcs.RuntimeCPUPercent) + result += fmt.Sprintf("Runtime memory MiB : about %.2f\n", rcs.RuntimeMemoryMiB) + result += fmt.Sprintf("Runtime available : %t\n", rcs.RuntimeAvailable) + if rcs.RuntimeDegraded { + result += "Runtime degraded : true\n" + } + } result += fmt.Sprintf("Memory Usage : about %.2f MiB\n", memoryUsage) if diskUsage < 1 { result += fmt.Sprintf("Disk Usage : about %.2f MiB\n", diskUsage*1024) @@ -201,7 +213,7 @@ func (sr *StatusReporter) GetStatusReport() string { result += fmt.Sprintf("System Time : %s\n", dateFormat) // Calculate total disk for percentage - totalDisk := float64(sr.resourceConsumptionManagerStatus.TotalDiskSpace) / 1024.0 / 1024.0 + totalDisk := float64(rcs.TotalDiskSpace) / 1024.0 / 1024.0 diskPercent := 0.0 if totalDisk > 0 { diskPercent = (availableDisk / totalDisk) * 100.0 @@ -209,7 +221,9 @@ func (sr *StatusReporter) GetStatusReport() string { result += fmt.Sprintf("System Available Disk : %.2f MB (%.2f %%)\n", availableDisk, diskPercent) result += fmt.Sprintf("System Available Memory : %.2f MB\n", availableMemory) - result += fmt.Sprintf("System Total CPU : %.2f %%\n", totalCPU) + result += fmt.Sprintf("System Total CPU : %.2f %%\n", hostCPU) + result += fmt.Sprintf("Edgelet total CPU percent : %.2f\n", rcs.EdgeletTotalCPUPercent) + result += fmt.Sprintf("Edgelet total memory MiB : %.2f\n", rcs.EdgeletTotalMemoryMiB) availableInterfaces := getAvailableNetworkInterfaces() availableInterfacesLine := "none" if len(availableInterfaces) > 0 { diff --git a/pkg/containerd/pids_linux.go b/pkg/containerd/pids_linux.go new file mode 100644 index 0000000..b3245d4 --- /dev/null +++ b/pkg/containerd/pids_linux.go @@ -0,0 +1,8 @@ +//go:build linux + +package containerd + +// FindEmbeddedContainerdChildPIDs returns PIDs whose cmdline includes --edgelet-containerd-child. +func FindEmbeddedContainerdChildPIDs() ([]int, error) { + return findContainerdChildPIDs() +} diff --git a/pkg/containerd/pids_stub.go b/pkg/containerd/pids_stub.go new file mode 100644 index 0000000..1ac0053 --- /dev/null +++ b/pkg/containerd/pids_stub.go @@ -0,0 +1,8 @@ +//go:build !linux + +package containerd + +// FindEmbeddedContainerdChildPIDs is unsupported off Linux. +func FindEmbeddedContainerdChildPIDs() ([]int, error) { + return nil, nil +}