From 3dbf4bf0ae1e777ff4c1247fae5a9b9d44d8a16f Mon Sep 17 00:00:00 2001 From: John Oliver <1615532+johnoliver@users.noreply.github.com> Date: Mon, 19 Jan 2026 14:14:52 +0000 Subject: [PATCH 1/3] Add cgroupv2 support. Add support for modifying where cgroups is mounted to. Fix bug with reading of the body of profile upload responses, early closing of the response prevented the body from being read --- .../diagnostics/DiagnosticEngineFactory.java | 4 +- ...OptimizerApplicationInsightFactoryJfr.java | 49 ++++++- .../CodeOptimizerDiagnosticEngineJfr.java | 26 ++-- .../libos/kernel/CGroupDataReader.java | 2 + .../libos/kernel/CGroupUsageDataReader.java | 2 + .../linux/cgroups/CGroupCpuSystemReader.java | 9 +- .../linux/cgroups/CGroupCpuUsageReader.java | 9 +- .../os/linux/cgroups/CGroupCpuUserReader.java | 9 +- .../os/linux/cgroups/CGroupStatReader.java | 9 +- .../os/linux/cgroups/CGroupValueReader.java | 9 +- .../linux/cgroups/LinuxCGroupDataReader.java | 64 ++++++---- .../cgroups/LinuxCGroupUsageDataReader.java | 30 ++++- .../os/linux/cgroupsv2/CGroupv2CpuReader.java | 52 ++++++++ .../cgroupsv2/LinuxCGroupV2DataReader.java | 120 ++++++++++++++++++ .../LinuxCGroupV2UsageDataReader.java | 63 +++++++++ .../libos/os/nop/NoOpCGroupDataReader.java | 5 + .../os/nop/NoOpCGroupUsageDataReader.java | 5 + .../jfr/CodeOptimizerDiagnosticsJfrInit.java | 30 +++-- .../diagnostics/jfr/SystemStatsProvider.java | 60 ++++++--- .../internal/configuration/Configuration.java | 1 + .../PerformanceMonitoringService.java | 3 +- .../service/ServiceProfilerClient.java | 28 ++-- .../MockDiagnosticEngineFactory.java | 2 +- 23 files changed, 488 insertions(+), 103 deletions(-) create mode 100644 agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/linux/cgroupsv2/CGroupv2CpuReader.java create mode 100644 agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/linux/cgroupsv2/LinuxCGroupV2DataReader.java create mode 100644 agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/linux/cgroupsv2/LinuxCGroupV2UsageDataReader.java diff --git a/agent/agent-profiler/agent-diagnostics-api/src/main/java/com/microsoft/applicationinsights/diagnostics/DiagnosticEngineFactory.java b/agent/agent-profiler/agent-diagnostics-api/src/main/java/com/microsoft/applicationinsights/diagnostics/DiagnosticEngineFactory.java index 22468d866c7..cd9402ab2a0 100644 --- a/agent/agent-profiler/agent-diagnostics-api/src/main/java/com/microsoft/applicationinsights/diagnostics/DiagnosticEngineFactory.java +++ b/agent/agent-profiler/agent-diagnostics-api/src/main/java/com/microsoft/applicationinsights/diagnostics/DiagnosticEngineFactory.java @@ -4,6 +4,7 @@ package com.microsoft.applicationinsights.diagnostics; import java.util.concurrent.ScheduledExecutorService; +import javax.annotation.Nullable; /** * Factory to be invoked to create a DiagnosticEngine. This factory will be service loaded by the @@ -11,5 +12,6 @@ * this interface. */ public interface DiagnosticEngineFactory { - DiagnosticEngine create(ScheduledExecutorService executorService); + DiagnosticEngine create( + ScheduledExecutorService executorService, @Nullable String cgroupBasePath); } diff --git a/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/appinsights/CodeOptimizerApplicationInsightFactoryJfr.java b/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/appinsights/CodeOptimizerApplicationInsightFactoryJfr.java index ac7533a2703..b237b9ef862 100644 --- a/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/appinsights/CodeOptimizerApplicationInsightFactoryJfr.java +++ b/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/appinsights/CodeOptimizerApplicationInsightFactoryJfr.java @@ -6,13 +6,58 @@ import com.google.auto.service.AutoService; import com.microsoft.applicationinsights.diagnostics.DiagnosticEngine; import com.microsoft.applicationinsights.diagnostics.DiagnosticEngineFactory; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.concurrent.ScheduledExecutorService; +import javax.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** Factory for Code Optimizer diagnostics to be service loaded */ @AutoService(DiagnosticEngineFactory.class) public class CodeOptimizerApplicationInsightFactoryJfr implements DiagnosticEngineFactory { + + private static final Path FILE_SYSTEM_ROOT = + Paths.get(System.getProperty("applicationinsights.profiler.filesystemRoot", "/")); + private static final Path CGROUP_DIR = Paths.get("./sys/fs/cgroup"); + + private static final Logger logger = + LoggerFactory.getLogger(CodeOptimizerApplicationInsightFactoryJfr.class); + @Override - public DiagnosticEngine create(ScheduledExecutorService executorService) { - return new CodeOptimizerDiagnosticEngineJfr(executorService); + public DiagnosticEngine create( + ScheduledExecutorService executorService, @Nullable String cgroupBasePath) { + Path cgroupPath = getCgroupPath(cgroupBasePath); + return new CodeOptimizerDiagnosticEngineJfr(executorService, cgroupPath); + } + + @SuppressFBWarnings( + value = "SECPTI", // Potential Path Traversal + justification = + "The constructed file path cannot be controlled by an end user of the instrumented application") + @Nullable + private static Path getCgroupPath(@Nullable String cgroupBasePath) { + Path cgroupPath = null; + if (cgroupBasePath != null) { + cgroupPath = Paths.get(cgroupBasePath); + + if (!Files.exists(cgroupPath)) { + logger.warn("Configured Cgroup path {} does not exist, setting to default", cgroupBasePath); + cgroupPath = null; + } + } + + if (cgroupPath == null) { + cgroupPath = FILE_SYSTEM_ROOT.resolve(CGROUP_DIR); + + if (!Files.exists(cgroupPath)) { + logger.warn("Expected default Cgroup path {} does not exist", cgroupBasePath); + cgroupPath = null; + } + } + + return cgroupPath; } } diff --git a/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/appinsights/CodeOptimizerDiagnosticEngineJfr.java b/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/appinsights/CodeOptimizerDiagnosticEngineJfr.java index 8334272398a..de01d5d6901 100644 --- a/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/appinsights/CodeOptimizerDiagnosticEngineJfr.java +++ b/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/appinsights/CodeOptimizerDiagnosticEngineJfr.java @@ -14,6 +14,7 @@ import com.microsoft.applicationinsights.diagnostics.jfr.SystemStatsProvider; import java.io.IOException; import java.io.StringWriter; +import java.nio.file.Path; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; @@ -33,10 +34,13 @@ public class CodeOptimizerDiagnosticEngineJfr implements DiagnosticEngine { public static final long TIME_BEFORE_END_OF_PROFILE_TO_EMIT_EVENT = 10L; private final ScheduledExecutorService executorService; private final Semaphore semaphore = new Semaphore(1, false); + private final Path cgroupBasePath; private int thisPid; - public CodeOptimizerDiagnosticEngineJfr(ScheduledExecutorService executorService) { + public CodeOptimizerDiagnosticEngineJfr( + ScheduledExecutorService executorService, Path cgroupBasePath) { this.executorService = executorService; + this.cgroupBasePath = cgroupBasePath; } @Override @@ -49,14 +53,14 @@ public void init(int thisPid) { this.thisPid = thisPid; logger.debug("Initialising Code Optimizer Diagnostic Engine"); - CodeOptimizerDiagnosticsJfrInit.initFeature(thisPid); + CodeOptimizerDiagnosticsJfrInit.initFeature(thisPid, cgroupBasePath); logger.debug("Code Optimizer Diagnostic Engine Initialised"); } - private static void startDiagnosticCycle(int thisPid) { + private static void startDiagnosticCycle(int thisPid, Path cgroupBasePath) { logger.debug("Starting Code Optimizer Diagnostic Cycle"); - CodeOptimizerDiagnosticsJfrInit.initFeature(thisPid); - CodeOptimizerDiagnosticsJfrInit.start(thisPid); + CodeOptimizerDiagnosticsJfrInit.initFeature(thisPid, cgroupBasePath); + CodeOptimizerDiagnosticsJfrInit.start(thisPid, cgroupBasePath); } private static void endDiagnosticCycle() { @@ -70,13 +74,13 @@ public Future> performDiagnosis(AlertBreach alert) { new CompletableFuture<>(); try { if (semaphore.tryAcquire(SEMAPHORE_TIMEOUT_IN_SEC, TimeUnit.SECONDS)) { - emitInfo(alert); + emitInfo(alert, cgroupBasePath); long profileDurationInSec = alert.getAlertConfiguration().getProfileDurationSeconds(); long end = profileDurationInSec - TIME_BEFORE_END_OF_PROFILE_TO_EMIT_EVENT; - startDiagnosticCycle(thisPid); + startDiagnosticCycle(thisPid, cgroupBasePath); scheduleEmittingAlertBreachEvent(alert, end); @@ -101,7 +105,7 @@ private void scheduleShutdown( executorService.schedule( () -> { try { - emitInfo(alert); + emitInfo(alert, cgroupBasePath); // We do not return a result atm diagnosisResultCompletableFuture.complete(null); @@ -123,7 +127,7 @@ private void scheduleEmittingAlertBreachEvent(AlertBreach alert, long end) { executorService.schedule( () -> { try { - emitInfo(alert); + emitInfo(alert, cgroupBasePath); } catch (RuntimeException e) { logger.error("Failed to emit breach", e); } @@ -132,10 +136,10 @@ private void scheduleEmittingAlertBreachEvent(AlertBreach alert, long end) { TimeUnit.SECONDS); } - private static void emitInfo(AlertBreach alert) { + private static void emitInfo(AlertBreach alert, Path cgroupBasePath) { logger.debug("Emitting Code Optimizer Diagnostic Event"); emitAlertBreachJfrEvent(alert); - CodeOptimizerDiagnosticsJfrInit.emitCGroupData(); + CodeOptimizerDiagnosticsJfrInit.emitCGroupData(cgroupBasePath); emitMachineStats(); } diff --git a/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/kernel/CGroupDataReader.java b/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/kernel/CGroupDataReader.java index 212190bb792..5af595f2382 100644 --- a/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/kernel/CGroupDataReader.java +++ b/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/kernel/CGroupDataReader.java @@ -19,4 +19,6 @@ public interface CGroupDataReader { long getCpuLimit() throws OperatingSystemInteractionException; long getCpuPeriod() throws OperatingSystemInteractionException; + + boolean isAvailable(); } diff --git a/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/kernel/CGroupUsageDataReader.java b/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/kernel/CGroupUsageDataReader.java index f13463ac2a5..3a4217423c6 100644 --- a/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/kernel/CGroupUsageDataReader.java +++ b/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/kernel/CGroupUsageDataReader.java @@ -13,4 +13,6 @@ public interface CGroupUsageDataReader extends TwoStepUpdatable, Closeable { @Nullable List getTelemetry(); + + boolean isAvailable(); } diff --git a/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/linux/cgroups/CGroupCpuSystemReader.java b/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/linux/cgroups/CGroupCpuSystemReader.java index ecb14207ce6..6ea67dbcf23 100644 --- a/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/linux/cgroups/CGroupCpuSystemReader.java +++ b/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/linux/cgroups/CGroupCpuSystemReader.java @@ -3,11 +3,12 @@ package com.microsoft.applicationinsights.diagnostics.collection.libos.os.linux.cgroups; -@SuppressWarnings( - "checkstyle:AbbreviationAsWordInName") // CGroup is the standard abbreviation for Control Group +import java.nio.file.Path; + +@SuppressWarnings("checkstyle:AbbreviationAsWordInName") public class CGroupCpuSystemReader extends CGroupValueReader { // total system CPU time (in nanoseconds) consumed by all tasks in this cgroup - public CGroupCpuSystemReader() { - super("/sys/fs/cgroup/cpu,cpuacct/cpuacct.usage_sys"); + public CGroupCpuSystemReader(Path cgroupPath) { + super(cgroupPath.resolve("./cpuacct.usage_sys")); } } diff --git a/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/linux/cgroups/CGroupCpuUsageReader.java b/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/linux/cgroups/CGroupCpuUsageReader.java index f642e04872d..55770fd50cb 100644 --- a/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/linux/cgroups/CGroupCpuUsageReader.java +++ b/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/linux/cgroups/CGroupCpuUsageReader.java @@ -3,11 +3,12 @@ package com.microsoft.applicationinsights.diagnostics.collection.libos.os.linux.cgroups; -@SuppressWarnings( - "checkstyle:AbbreviationAsWordInName") // CGroup is the standard abbreviation for Control Group +import java.nio.file.Path; + +@SuppressWarnings("checkstyle:AbbreviationAsWordInName") public class CGroupCpuUsageReader extends CGroupValueReader { // total CPU usage (in nanoseconds) consumed by all tasks in this cgroup - public CGroupCpuUsageReader() { - super("/sys/fs/cgroup/cpu,cpuacct/cpuacct.usage"); + public CGroupCpuUsageReader(Path cgroupPath) { + super(cgroupPath.resolve("./cpuacct.usage")); } } diff --git a/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/linux/cgroups/CGroupCpuUserReader.java b/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/linux/cgroups/CGroupCpuUserReader.java index c797dcdeefe..fd151073313 100644 --- a/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/linux/cgroups/CGroupCpuUserReader.java +++ b/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/linux/cgroups/CGroupCpuUserReader.java @@ -3,11 +3,12 @@ package com.microsoft.applicationinsights.diagnostics.collection.libos.os.linux.cgroups; -@SuppressWarnings( - "checkstyle:AbbreviationAsWordInName") // CGroup is the standard abbreviation for Control Group +import java.nio.file.Path; + +@SuppressWarnings("checkstyle:AbbreviationAsWordInName") public class CGroupCpuUserReader extends CGroupValueReader { // total user CPU time (in nanoseconds) consumed by all tasks in this cgroup - public CGroupCpuUserReader() { - super("/sys/fs/cgroup/cpu,cpuacct/cpuacct.usage_user"); + public CGroupCpuUserReader(Path cgroupPath) { + super(cgroupPath.resolve("./cpuacct.usage_user")); } } diff --git a/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/linux/cgroups/CGroupStatReader.java b/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/linux/cgroups/CGroupStatReader.java index 9bd25f426ba..dfec08eb909 100644 --- a/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/linux/cgroups/CGroupStatReader.java +++ b/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/linux/cgroups/CGroupStatReader.java @@ -5,16 +5,15 @@ import com.microsoft.applicationinsights.diagnostics.collection.libos.BigIncrementalCounter; import com.microsoft.applicationinsights.diagnostics.collection.libos.os.linux.TwoStepProcReader; -import java.io.File; +import java.nio.file.Path; -@SuppressWarnings( - "checkstyle:AbbreviationAsWordInName") // CGroup is the standard abbreviation for Control Group +@SuppressWarnings("checkstyle:AbbreviationAsWordInName") public class CGroupStatReader extends TwoStepProcReader { private final BigIncrementalCounter user = new BigIncrementalCounter(); private final BigIncrementalCounter system = new BigIncrementalCounter(); - public CGroupStatReader() { - super(new File("/sys/fs/cgroup/cpu,cpuacct/cpuacct.stat"), true); + public CGroupStatReader(Path cgroupPath) { + super(cgroupPath.resolve("./cpuacct.stat").toFile(), true); } @Override diff --git a/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/linux/cgroups/CGroupValueReader.java b/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/linux/cgroups/CGroupValueReader.java index 4b076f9b3b6..bb1c23455ad 100644 --- a/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/linux/cgroups/CGroupValueReader.java +++ b/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/linux/cgroups/CGroupValueReader.java @@ -5,15 +5,14 @@ import com.microsoft.applicationinsights.diagnostics.collection.libos.BigIncrementalCounter; import com.microsoft.applicationinsights.diagnostics.collection.libos.os.linux.TwoStepProcReader; -import java.io.File; +import java.nio.file.Path; -@SuppressWarnings( - "checkstyle:AbbreviationAsWordInName") // CGroup is the standard abbreviation for Control Group +@SuppressWarnings("checkstyle:AbbreviationAsWordInName") public abstract class CGroupValueReader extends TwoStepProcReader { private final BigIncrementalCounter usage = new BigIncrementalCounter(); - public CGroupValueReader(String fileName) { - super(new File(fileName), true); + public CGroupValueReader(Path file) { + super(file.toFile(), true); } @Override diff --git a/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/linux/cgroups/LinuxCGroupDataReader.java b/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/linux/cgroups/LinuxCGroupDataReader.java index 0f9782bc13f..dbef2cf80b4 100644 --- a/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/linux/cgroups/LinuxCGroupDataReader.java +++ b/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/linux/cgroups/LinuxCGroupDataReader.java @@ -5,60 +5,80 @@ import com.microsoft.applicationinsights.diagnostics.collection.libos.OperatingSystemInteractionException; import com.microsoft.applicationinsights.diagnostics.collection.libos.kernel.CGroupDataReader; -import java.io.File; import java.nio.charset.Charset; import java.nio.file.Files; +import java.nio.file.Path; import java.util.List; -@SuppressWarnings( - "checkstyle:AbbreviationAsWordInName") // CGroup is the standard abbreviation for Control Group +@SuppressWarnings("checkstyle:AbbreviationAsWordInName") public class LinuxCGroupDataReader implements CGroupDataReader { - private static final String CGROUP_DIR = "/sys/fs/cgroup"; - private static final String K_MEM_LIMIT_FILE = CGROUP_DIR + "/memory/memory.kmem.limit_in_bytes"; - private static final String MEM_LIMIT_FILE = CGROUP_DIR + "/memory/memory.limit_in_bytes"; - private static final String MEM_SOFT_LIMIT_FILE = - CGROUP_DIR + "/memory/memory.soft_limit_in_bytes"; - private static final String CPU_LIMIT_FILE = CGROUP_DIR + "/cpu,cpuacct/cpu.cfs_quota_us"; - private static final String CPU_PERIOD_FILE = CGROUP_DIR + "/cpu,cpuacct/cpu.cfs_period_us"; + private static final String K_MEM_LIMIT_FILE = "./memory/memory.kmem.limit_in_bytes"; + private static final String MEM_LIMIT_FILE = "./memory/memory.limit_in_bytes"; + private static final String MEM_SOFT_LIMIT_FILE = "./memory/memory.soft_limit_in_bytes"; + private static final String CPU_LIMIT_FILE = "./cpu,cpuacct/cpu.cfs_quota_us"; + private static final String CPU_PERIOD_FILE = "./cpu,cpuacct/cpu.cfs_period_us"; + + private final Path kmemLimitFile; + private final Path memLimitFile; + private final Path memSoftLimitFile; + private final Path cpuLimitFile; + private final Path cpuPeriodFile; + + public LinuxCGroupDataReader(Path cgroupRoot) { + kmemLimitFile = cgroupRoot.resolve(K_MEM_LIMIT_FILE); + memLimitFile = cgroupRoot.resolve(MEM_LIMIT_FILE); + memSoftLimitFile = cgroupRoot.resolve(MEM_SOFT_LIMIT_FILE); + cpuLimitFile = cgroupRoot.resolve(CPU_LIMIT_FILE); + cpuPeriodFile = cgroupRoot.resolve(CPU_PERIOD_FILE); + } @Override public long getKmemLimit() throws OperatingSystemInteractionException { - return readLong(K_MEM_LIMIT_FILE); + return readLong(kmemLimitFile); } @Override public long getMemoryLimit() throws OperatingSystemInteractionException { - return readLong(MEM_LIMIT_FILE); + return readLong(memLimitFile); } @Override public long getMemorySoftLimit() throws OperatingSystemInteractionException { - return readLong(MEM_SOFT_LIMIT_FILE); + return readLong(memSoftLimitFile); } @Override public long getCpuLimit() throws OperatingSystemInteractionException { - return readLong(CPU_LIMIT_FILE); + return readLong(cpuLimitFile); } @Override public long getCpuPeriod() throws OperatingSystemInteractionException { - return readLong(CPU_PERIOD_FILE); + return readLong(cpuPeriodFile); + } + + @Override + public boolean isAvailable() { + return Files.exists(cpuLimitFile) + || Files.exists(memLimitFile) + || Files.exists(memSoftLimitFile) + || Files.exists(kmemLimitFile) + || Files.exists(cpuPeriodFile); } - private static long readLong(String fileName) throws OperatingSystemInteractionException { + private static long readLong(Path file) throws OperatingSystemInteractionException { try { - File file = new File(fileName); - if (!file.exists() || !file.isFile()) { - throw new OperatingSystemInteractionException("File does not exist: " + fileName); + if (!Files.exists(file) || !Files.isRegularFile(file)) { + throw new OperatingSystemInteractionException("File does not exist: " + file.getFileName()); } - List lines = Files.readAllLines(file.toPath(), Charset.defaultCharset()); - if (lines.size() > 0) { + List lines = Files.readAllLines(file, Charset.defaultCharset()); + if (!lines.isEmpty()) { return Long.parseLong(lines.get(0)); } else { - throw new OperatingSystemInteractionException("Unable to read value from: " + fileName); + throw new OperatingSystemInteractionException( + "Unable to read value from: " + file.getFileName()); } } catch (Exception e) { throw new OperatingSystemInteractionException(e); diff --git a/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/linux/cgroups/LinuxCGroupUsageDataReader.java b/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/linux/cgroups/LinuxCGroupUsageDataReader.java index ae643aac343..fe511144dbd 100644 --- a/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/linux/cgroups/LinuxCGroupUsageDataReader.java +++ b/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/linux/cgroups/LinuxCGroupUsageDataReader.java @@ -5,21 +5,34 @@ import com.microsoft.applicationinsights.diagnostics.collection.libos.kernel.CGroupUsageDataReader; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; -@SuppressWarnings( - "checkstyle:AbbreviationAsWordInName") // CGroup is the standard abbreviation for Control Group +@SuppressWarnings("checkstyle:AbbreviationAsWordInName") public class LinuxCGroupUsageDataReader implements CGroupUsageDataReader { + private static final Path CGROUP_CPU_PATH = Paths.get("./cpu,cpuacct/"); - private final CGroupCpuUsageReader cgroupCpuUsageReader = new CGroupCpuUsageReader(); + private final CGroupCpuUsageReader cgroupCpuUsageReader; - private final CGroupCpuUserReader cgroupCpuUserReader = new CGroupCpuUserReader(); + private final CGroupCpuUserReader cgroupCpuUserReader; - private final CGroupCpuSystemReader cgroupCpuSystemReader = new CGroupCpuSystemReader(); + private final CGroupCpuSystemReader cgroupCpuSystemReader; - private final CGroupStatReader cgroupStatReader = new CGroupStatReader(); + private final CGroupStatReader cgroupStatReader; + + private final Path cgroupDirectory; + + public LinuxCGroupUsageDataReader(Path cgroupDirectory) { + this.cgroupDirectory = cgroupDirectory.resolve(CGROUP_CPU_PATH); + cgroupCpuUsageReader = new CGroupCpuUsageReader(this.cgroupDirectory); + cgroupCpuUserReader = new CGroupCpuUserReader(this.cgroupDirectory); + cgroupCpuSystemReader = new CGroupCpuSystemReader(this.cgroupDirectory); + cgroupStatReader = new CGroupStatReader(this.cgroupDirectory); + } @Override public void poll() { @@ -56,6 +69,11 @@ public List getTelemetry() { .collect(Collectors.toList()); } + @Override + public boolean isAvailable() { + return Files.exists(cgroupDirectory); + } + @Override public void close() throws IOException { cgroupCpuUsageReader.close(); diff --git a/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/linux/cgroupsv2/CGroupv2CpuReader.java b/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/linux/cgroupsv2/CGroupv2CpuReader.java new file mode 100644 index 00000000000..95163743346 --- /dev/null +++ b/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/linux/cgroupsv2/CGroupv2CpuReader.java @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.applicationinsights.diagnostics.collection.libos.os.linux.cgroupsv2; + +import com.microsoft.applicationinsights.diagnostics.collection.libos.BigIncrementalCounter; +import com.microsoft.applicationinsights.diagnostics.collection.libos.os.linux.TwoStepProcReader; +import java.nio.file.Path; + +@SuppressWarnings("checkstyle:AbbreviationAsWordInName") +public class CGroupv2CpuReader extends TwoStepProcReader { + + private static final String CPU_USAGE_PROPERTY = "usage_usec"; + private static final String CPU_SYSTEM_PROPERTY = "system_usec"; + private static final String CPU_USER_PROPERTY = "user_usec"; + + private final BigIncrementalCounter cpuUsage = new BigIncrementalCounter(); + private final BigIncrementalCounter cpuSystem = new BigIncrementalCounter(); + private final BigIncrementalCounter cpuUser = new BigIncrementalCounter(); + + // total CPU usage (in microseconds) consumed by all tasks in this cgroup + public CGroupv2CpuReader(Path cgroupDir) { + super(cgroupDir.resolve("./cpu.stat").toFile()); + } + + @Override + protected void parseLine(String line) { + String[] tokens = line.split(" "); + + if (tokens.length == 2) { + if (CPU_USAGE_PROPERTY.equals(tokens[0])) { + cpuUsage.newValue(Long.parseLong(tokens[1])); + } else if (CPU_SYSTEM_PROPERTY.equals(tokens[0])) { + cpuSystem.newValue(Long.parseLong(tokens[1])); + } else if (CPU_USER_PROPERTY.equals(tokens[0])) { + cpuUser.newValue(Long.parseLong(tokens[1])); + } + } + } + + public BigIncrementalCounter getCpuUsage() { + return cpuUsage; + } + + public BigIncrementalCounter getCpuSystem() { + return cpuSystem; + } + + public BigIncrementalCounter getCpuUser() { + return cpuUser; + } +} diff --git a/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/linux/cgroupsv2/LinuxCGroupV2DataReader.java b/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/linux/cgroupsv2/LinuxCGroupV2DataReader.java new file mode 100644 index 00000000000..b199ba7c014 --- /dev/null +++ b/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/linux/cgroupsv2/LinuxCGroupV2DataReader.java @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.applicationinsights.diagnostics.collection.libos.os.linux.cgroupsv2; + +import com.microsoft.applicationinsights.diagnostics.collection.libos.OperatingSystemInteractionException; +import com.microsoft.applicationinsights.diagnostics.collection.libos.kernel.CGroupDataReader; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +@SuppressWarnings("checkstyle:AbbreviationAsWordInName") +public class LinuxCGroupV2DataReader implements CGroupDataReader { + private static final Path MEM_MAX_FILE = Paths.get("./memory.max"); + private static final Path MEM_HIGH_FILE = Paths.get("./memory.high"); + private static final Path CPU_MAX_FILE = Paths.get("./cpu.max"); + + private final Path memMaxFile; + private final Path memHighFile; + private final Path cpuMaxFile; + + public LinuxCGroupV2DataReader(Path cgroupDir) { + memMaxFile = cgroupDir.resolve(MEM_MAX_FILE); + memHighFile = cgroupDir.resolve(MEM_HIGH_FILE); + cpuMaxFile = cgroupDir.resolve(CPU_MAX_FILE); + } + + @Override + public long getKmemLimit() { + // In cgroup v2, kernel memory accounting is not separately exposed + // Return a large value to indicate no specific limit + return Long.MAX_VALUE; + } + + @Override + public long getMemoryLimit() { + return parseMemoryValue(memMaxFile); + } + + @Override + public long getMemorySoftLimit() { + return parseMemoryValue(memHighFile); + } + + @Override + public long getCpuLimit() { + return parseCpuQuota(); + } + + @Override + public long getCpuPeriod() { + return parseCpuPeriod(); + } + + @Override + public boolean isAvailable() { + return Files.exists(memMaxFile) || Files.exists(memHighFile) || Files.exists(cpuMaxFile); + } + + private static long parseMemoryValue(Path file) { + try { + String content = readFileContent(file); + if ("max".equalsIgnoreCase(content.trim())) { + return Long.MAX_VALUE; + } + return Long.parseLong(content.trim()); + } catch (Exception e) { + return Long.MAX_VALUE; + } + } + + private long parseCpuQuota() { + try { + String content = readFileContent(cpuMaxFile); + String[] parts = content.trim().split("\\s+"); + if (parts.length >= 1) { + if ("max".equalsIgnoreCase(parts[0])) { + return -1; // No quota defined + } + return Long.parseLong(parts[0]); + } + } catch (Exception ignored) { + return -1; // No quota defined + } + return -1; // No quota defined + } + + private long parseCpuPeriod() { + try { + String content = readFileContent(cpuMaxFile); + String[] parts = content.trim().split("\\s+"); + if (parts.length >= 2) { + return Long.parseLong(parts[1]); + } + } catch (Exception ignored) { + return -1; // No quota defined + } + return -1; // No period defined + } + + private static String readFileContent(Path file) throws OperatingSystemInteractionException { + try { + if (!Files.exists(file) || !Files.isRegularFile(file)) { + throw new OperatingSystemInteractionException( + "File does not exist: " + file.getFileName().toString()); + } + + List lines = Files.readAllLines(file, Charset.defaultCharset()); + if (lines.isEmpty()) { + throw new OperatingSystemInteractionException( + "Empty file: " + file.getFileName().toString()); + } + return lines.get(0); + } catch (Exception e) { + throw new OperatingSystemInteractionException(e); + } + } +} diff --git a/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/linux/cgroupsv2/LinuxCGroupV2UsageDataReader.java b/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/linux/cgroupsv2/LinuxCGroupV2UsageDataReader.java new file mode 100644 index 00000000000..73b72b9c73c --- /dev/null +++ b/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/linux/cgroupsv2/LinuxCGroupV2UsageDataReader.java @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.applicationinsights.diagnostics.collection.libos.os.linux.cgroupsv2; + +import com.microsoft.applicationinsights.diagnostics.collection.libos.kernel.CGroupUsageDataReader; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@SuppressWarnings("checkstyle:AbbreviationAsWordInName") +public class LinuxCGroupV2UsageDataReader implements CGroupUsageDataReader { + + private final CGroupv2CpuReader cgroupV2CpuReader; + private final Path cgroupDir; + + public LinuxCGroupV2UsageDataReader(Path cgroupDir) { + this.cgroupDir = cgroupDir; + cgroupV2CpuReader = new CGroupv2CpuReader(cgroupDir); + } + + @Override + public void poll() { + cgroupV2CpuReader.poll(); + } + + @Override + public void update() { + cgroupV2CpuReader.update(); + } + + @Override + public List getTelemetry() { + return Stream.of( + cgroupV2CpuReader.getCpuUsage().getIncrement(), + cgroupV2CpuReader.getCpuUser().getIncrement(), + cgroupV2CpuReader.getCpuSystem().getIncrement(), + cgroupV2CpuReader.getCpuUser().getIncrement(), + cgroupV2CpuReader.getCpuSystem().getIncrement()) + .map( + value -> { + if (value == null) { + return -1.0d; + } else { + return value.doubleValue(); + } + }) + .collect(Collectors.toList()); + } + + @Override + public boolean isAvailable() { + return Files.exists(cgroupDir.resolve("./cgroup.controllers")); + } + + @Override + public void close() throws IOException { + cgroupV2CpuReader.close(); + } +} diff --git a/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/nop/NoOpCGroupDataReader.java b/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/nop/NoOpCGroupDataReader.java index e0304030e46..97561e0d682 100644 --- a/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/nop/NoOpCGroupDataReader.java +++ b/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/nop/NoOpCGroupDataReader.java @@ -35,4 +35,9 @@ public long getCpuLimit() { public long getCpuPeriod() { return -1; } + + @Override + public boolean isAvailable() { + return true; + } } diff --git a/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/nop/NoOpCGroupUsageDataReader.java b/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/nop/NoOpCGroupUsageDataReader.java index 794971337fe..9b1c90ff0e5 100644 --- a/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/nop/NoOpCGroupUsageDataReader.java +++ b/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/collection/libos/os/nop/NoOpCGroupUsageDataReader.java @@ -25,4 +25,9 @@ public void update() {} @Override public void close() throws IOException {} + + @Override + public boolean isAvailable() { + return true; + } } diff --git a/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/jfr/CodeOptimizerDiagnosticsJfrInit.java b/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/jfr/CodeOptimizerDiagnosticsJfrInit.java index d5007db4b29..55dbd815331 100644 --- a/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/jfr/CodeOptimizerDiagnosticsJfrInit.java +++ b/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/jfr/CodeOptimizerDiagnosticsJfrInit.java @@ -7,6 +7,7 @@ import com.microsoft.applicationinsights.diagnostics.collection.libos.OperatingSystemInteractionException; import com.microsoft.applicationinsights.diagnostics.collection.libos.os.OperatingSystemDetector; import java.io.IOException; +import java.nio.file.Path; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -26,8 +27,9 @@ public class CodeOptimizerDiagnosticsJfrInit { private static final AtomicInteger exceptionLogCount = new AtomicInteger(0); private static final AtomicInteger telemetryFailureLogCount = new AtomicInteger(0); - private static final Runnable readCGroupData = CodeOptimizerDiagnosticsJfrInit::emitCGroupData; private static final AtomicReference telemetryEmitter = new AtomicReference<>(null); + private static final AtomicReference cgroupTelemetryEmitter = + new AtomicReference<>(null); private CodeOptimizerDiagnosticsJfrInit() {} @@ -70,9 +72,9 @@ private static void logFailure(String logLine, @Nullable Exception e, AtomicInte @SuppressWarnings( "checkstyle:AbbreviationAsWordInName") // CGroup is the standard abbreviation for Control // Group - public static void emitCGroupData() { + public static void emitCGroupData(Path cgroupBasePath) { try { - CGroupData cgroupData = SystemStatsProvider.getCGroupData(); + CGroupData cgroupData = SystemStatsProvider.getCGroupData(cgroupBasePath); if (cgroupData != null) { cgroupData.commit(); @@ -86,22 +88,23 @@ public static boolean isOsSupported() { return OperatingSystemDetector.getOperatingSystem().supportsDiagnostics(); } - public static void initFeature(int thisPid) { + public static void initFeature(int thisPid, Path cgroupBasePath) { if (!isOsSupported()) { return; } // eagerly get stats to warm it up - SystemStatsProvider.init(thisPid); + SystemStatsProvider.init(thisPid, cgroupBasePath); } - public static void start(int thisPidSupplier) { + public static void start(int thisPidSupplier, Path cgroupBasePath) { if (!isOsSupported()) { return; } if (running.compareAndSet(false, true)) { - SystemStatsReader statsReader = SystemStatsProvider.getStatsReader(thisPidSupplier); + SystemStatsReader statsReader = + SystemStatsProvider.getStatsReader(thisPidSupplier, cgroupBasePath); Runnable emitter = emitTelemetry(statsReader); if (telemetryEmitter.compareAndSet(null, emitter)) { FlightRecorder.addPeriodicEvent(Telemetry.class, emitter); @@ -112,9 +115,12 @@ public static void start(int thisPidSupplier) { logger.error("Failed to init stats reader", e); } } - FlightRecorder.addPeriodicEvent(CGroupData.class, readCGroupData); - readCGroupData.run(); + if (cgroupTelemetryEmitter.compareAndSet(null, () -> emitCGroupData(cgroupBasePath))) { + FlightRecorder.addPeriodicEvent(CGroupData.class, cgroupTelemetryEmitter.get()); + } + + cgroupTelemetryEmitter.get().run(); } } @@ -128,7 +134,11 @@ public static void stop() { FlightRecorder.removePeriodicEvent(telemetryEmitter.get()); telemetryEmitter.set(null); } - FlightRecorder.removePeriodicEvent(readCGroupData); + + if (cgroupTelemetryEmitter.get() != null) { + FlightRecorder.removePeriodicEvent(cgroupTelemetryEmitter.get()); + cgroupTelemetryEmitter.set(null); + } SystemStatsProvider.close(); } } diff --git a/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/jfr/SystemStatsProvider.java b/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/jfr/SystemStatsProvider.java index 0455ce9dd2c..953ffd970f8 100644 --- a/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/jfr/SystemStatsProvider.java +++ b/agent/agent-profiler/agent-diagnostics/src/main/java/com/microsoft/applicationinsights/diagnostics/jfr/SystemStatsProvider.java @@ -20,6 +20,8 @@ import com.microsoft.applicationinsights.diagnostics.collection.libos.os.linux.LinuxProcessDumper; import com.microsoft.applicationinsights.diagnostics.collection.libos.os.linux.cgroups.LinuxCGroupDataReader; import com.microsoft.applicationinsights.diagnostics.collection.libos.os.linux.cgroups.LinuxCGroupUsageDataReader; +import com.microsoft.applicationinsights.diagnostics.collection.libos.os.linux.cgroupsv2.LinuxCGroupV2DataReader; +import com.microsoft.applicationinsights.diagnostics.collection.libos.os.linux.cgroupsv2.LinuxCGroupV2UsageDataReader; import com.microsoft.applicationinsights.diagnostics.collection.libos.os.nop.NoOpCGroupDataReader; import com.microsoft.applicationinsights.diagnostics.collection.libos.os.nop.NoOpCGroupUsageDataReader; import com.microsoft.applicationinsights.diagnostics.collection.libos.os.nop.NoOpKernelMonitor; @@ -30,6 +32,7 @@ import com.microsoft.applicationinsights.diagnostics.collection.libos.process.ThisPidSupplier; import java.io.Closeable; import java.io.IOException; +import java.nio.file.Path; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -53,7 +56,7 @@ public class SystemStatsProvider { private SystemStatsProvider() {} - public static void init(int thisPid) { + public static void init(int thisPid, Path cgroupBasePath) { // Ensure we only initialize once if (initialised.compareAndSet(false, true)) { singletons.put(ThisPidSupplier.class, new AtomicReference<>((ThisPidSupplier) () -> thisPid)); @@ -62,7 +65,7 @@ public static void init(int thisPid) { try { getCalibration(); getMachineStats(); - getCGroupData(); + getCGroupData(cgroupBasePath); // Close until needed close(); @@ -108,12 +111,12 @@ private static T getSingleton(Class clazz, Supplier supplier) { "checkstyle:AbbreviationAsWordInName", "MemberName" }) // CGroup is the standard abbreviation for Control Group - public static CGroupData getCGroupData() { + public static CGroupData getCGroupData(Path cgroupBasePath) { return getSingleton( CGroupData.class, () -> { try { - CGroupDataReader reader = buildCGroupDataReader(); + CGroupDataReader reader = buildCGroupDataReader(cgroupBasePath); CGroupData data = new CGroupData(); return data.setKmemLimit(reader.getKmemLimit()) @@ -169,10 +172,21 @@ private static Process getThisProcess() { @SuppressWarnings( "checkstyle:AbbreviationAsWordInName") // CGroup is the standard abbreviation for Control // Group - private static CGroupDataReader buildCGroupDataReader() { + private static CGroupDataReader buildCGroupDataReader(Path cgroupBasePath) { switch (OperatingSystemDetector.getOperatingSystem()) { case LINUX: - return new LinuxCGroupDataReader(); + CGroupDataReader dataReader = new LinuxCGroupDataReader(cgroupBasePath); + if (dataReader.isAvailable()) { + return dataReader; + } + + dataReader = new LinuxCGroupV2DataReader(cgroupBasePath); + if (dataReader.isAvailable()) { + return dataReader; + } + + logger.info("No CGroup limits data not found"); + return new NoOpCGroupDataReader(); default: return new NoOpCGroupDataReader(); } @@ -194,13 +208,29 @@ private static ProcessDumper getProcessDumper() { @SuppressWarnings( "checkstyle:AbbreviationAsWordInName") // CGroup is the standard abbreviation for Control // Group - private static CGroupUsageDataReader buildCGroupUsageDataReader() { + private static CGroupUsageDataReader buildCGroupUsageDataReader(Path cgroupBasePath) { return getSingleton( CGroupUsageDataReader.class, () -> { + if (cgroupBasePath == null) { + logger.info("No CGroup data present"); + return new NoOpCGroupUsageDataReader(); + } + switch (OperatingSystemDetector.getOperatingSystem()) { case LINUX: - return new LinuxCGroupUsageDataReader(); + CGroupUsageDataReader usageReader = new LinuxCGroupUsageDataReader(cgroupBasePath); + if (usageReader.isAvailable()) { + return usageReader; + } + + usageReader = new LinuxCGroupV2UsageDataReader(cgroupBasePath); + if (usageReader.isAvailable()) { + return usageReader; + } + + logger.warn("CGroup data not found"); + return new NoOpCGroupUsageDataReader(); default: return new NoOpCGroupUsageDataReader(); } @@ -220,15 +250,15 @@ private static MemoryInfoReader buildMemoryInfoReader() { }); } - private static SystemStatsReader getSystemStatsReader() { - return getSingleton(SystemStatsReader.class, SystemStatsProvider::buildSystemStatsReader); + private static SystemStatsReader getSystemStatsReader(Path cgroupBasePath) { + return getSingleton(SystemStatsReader.class, () -> buildSystemStatsReader(cgroupBasePath)); } - private static SystemStatsReader buildSystemStatsReader() { + private static SystemStatsReader buildSystemStatsReader(Path cgroupBasePath) { SystemStatsReader ssr = new SystemStatsReader( getKernelMonitor(), - buildCGroupUsageDataReader(), + buildCGroupUsageDataReader(cgroupBasePath), getThisProcess().getCpuStats(), getThisProcess().getIoStats(), buildMemoryInfoReader()); @@ -257,9 +287,9 @@ private static KernelMonitorDeviceDriver getKernelMonitor() { }); } - public static SystemStatsReader getStatsReader(int thisPidSupplier) { - init(thisPidSupplier); - return getSystemStatsReader(); + public static SystemStatsReader getStatsReader(int thisPidSupplier, Path cgroupBasePath) { + init(thisPidSupplier, cgroupBasePath); + return getSystemStatsReader(cgroupBasePath); } public static void close() { diff --git a/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/configuration/Configuration.java b/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/configuration/Configuration.java index a39a66b454c..26005a97585 100644 --- a/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/configuration/Configuration.java +++ b/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/configuration/Configuration.java @@ -1530,6 +1530,7 @@ public static class ProfilerConfiguration { public boolean enableDiagnostics = false; public boolean enableRequestTriggering = false; public List requestTriggerEndpoints = new ArrayList<>(); + @Nullable public String cgroupPath = null; } public static class GcEventConfiguration { diff --git a/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/PerformanceMonitoringService.java b/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/PerformanceMonitoringService.java index a49d2f24d5f..8a4860ab594 100644 --- a/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/PerformanceMonitoringService.java +++ b/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/PerformanceMonitoringService.java @@ -145,7 +145,8 @@ private DiagnosticEngine startDiagnosticEngine() { 1, ThreadPoolUtils.createNamedDaemonThreadFactory("DiagnosisThreadPool")); DiagnosticEngine diagnosticEngine = - diagnosticEngineFactory.create(diagnosticEngineExecutorService); + diagnosticEngineFactory.create( + diagnosticEngineExecutorService, configuration.cgroupPath); if (diagnosticEngine != null) { diagnosticEngine.init(Integer.parseInt(new PidFinder().getValue(System::getenv))); diff --git a/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/service/ServiceProfilerClient.java b/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/service/ServiceProfilerClient.java index b4388ab2d5b..829a165eaf4 100644 --- a/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/service/ServiceProfilerClient.java +++ b/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/service/ServiceProfilerClient.java @@ -123,19 +123,23 @@ private static Mono reportUploadFinish(HttpResponse response) { // this shouldn't happen, the mono should complete with a response or a failure return Mono.error(new AssertionError("http response mono returned empty")); } - try { - int statusCode = response.getStatusCode(); - if (statusCode != 201 && statusCode != 202) { - logger.error("Trace upload failed: {}", statusCode); - return Mono.error(new AssertionError("http request failed")); - } - return response.getBodyAsString(); - } finally { - // need to consume the body or close the response, otherwise get netty ByteBuf leak warnings: - // io.netty.util.ResourceLeakDetector - LEAK: ByteBuf.release() was not called before - // it's garbage-collected (see https://github.com/Azure/azure-sdk-for-java/issues/10467) - response.close(); + int statusCode = response.getStatusCode(); + if (statusCode != 201 && statusCode != 202) { + logger.error("Trace upload failed: {}", statusCode); + return Mono.error(new AssertionError("http request failed")); } + + return response + .getBodyAsString() + .doFinally( + done -> { + // need to consume the body or close the response, otherwise get netty ByteBuf leak + // warnings: + // io.netty.util.ResourceLeakDetector - LEAK: ByteBuf.release() was not called before + // it's garbage-collected (see + // https://github.com/Azure/azure-sdk-for-java/issues/10467) + response.close(); + }); } /** Obtain current settings that have been configured within the UI. */ diff --git a/smoke-tests/apps/DiagnosticExtension/MockExtension/src/main/java/com/microsoft/applicationinsights/smoketestextension/MockDiagnosticEngineFactory.java b/smoke-tests/apps/DiagnosticExtension/MockExtension/src/main/java/com/microsoft/applicationinsights/smoketestextension/MockDiagnosticEngineFactory.java index e1114d2fb9e..c278cb61a38 100644 --- a/smoke-tests/apps/DiagnosticExtension/MockExtension/src/main/java/com/microsoft/applicationinsights/smoketestextension/MockDiagnosticEngineFactory.java +++ b/smoke-tests/apps/DiagnosticExtension/MockExtension/src/main/java/com/microsoft/applicationinsights/smoketestextension/MockDiagnosticEngineFactory.java @@ -13,7 +13,7 @@ public class MockDiagnosticEngineFactory implements DiagnosticEngineFactory { @Override - public DiagnosticEngine create(ScheduledExecutorService executorService) { + public DiagnosticEngine create(ScheduledExecutorService executorService, String cgroupBasePath) { return new DiagnosticEngine() { @Override From e044de049e86540fdaada20db7e178eb8b6e068b Mon Sep 17 00:00:00 2001 From: John Oliver <1615532+johnoliver@users.noreply.github.com> Date: Tue, 27 Jan 2026 12:35:25 +0000 Subject: [PATCH 2/3] Add tests for cgroup file reading --- .../cgroups/CGroupv2CpuReaderTest.java | 170 ++++++++++++++++++ .../cgroups/LinuxCGroupDataReaderTest.java | 111 ++++++++++++ .../LinuxCGroupUsageDataReaderTest.java | 121 +++++++++++++ .../cgroups/LinuxCGroupV2DataReaderTest.java | 164 +++++++++++++++++ .../LinuxCGroupV2UsageDataReaderTest.java | 146 +++++++++++++++ 5 files changed, 712 insertions(+) create mode 100644 agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/diagnostics/cgroups/CGroupv2CpuReaderTest.java create mode 100644 agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/diagnostics/cgroups/LinuxCGroupDataReaderTest.java create mode 100644 agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/diagnostics/cgroups/LinuxCGroupUsageDataReaderTest.java create mode 100644 agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/diagnostics/cgroups/LinuxCGroupV2DataReaderTest.java create mode 100644 agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/diagnostics/cgroups/LinuxCGroupV2UsageDataReaderTest.java diff --git a/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/diagnostics/cgroups/CGroupv2CpuReaderTest.java b/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/diagnostics/cgroups/CGroupv2CpuReaderTest.java new file mode 100644 index 00000000000..e9bcbb21679 --- /dev/null +++ b/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/diagnostics/cgroups/CGroupv2CpuReaderTest.java @@ -0,0 +1,170 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.applicationinsights.agent.internal.diagnostics.cgroups; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.microsoft.applicationinsights.diagnostics.collection.libos.os.linux.cgroupsv2.CGroupv2CpuReader; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** Tests for {@link CGroupv2CpuReader} verifying cgroup v2 CPU stat file parsing. */ +@SuppressWarnings("checkstyle:AbbreviationAsWordInName") +class CGroupv2CpuReaderTest { + + @TempDir Path tempDir; + + private Path cgroupDir; + + @BeforeEach + void setUp() throws IOException { + cgroupDir = tempDir.resolve("cgroup2"); + Files.createDirectories(cgroupDir); + } + + public static void writeString(Path path, String content) throws IOException { + Files.write(path, Collections.singletonList(content), StandardCharsets.UTF_8); + } + + public static void writeLines(Path path, String... lines) throws IOException { + Files.write(path, Arrays.asList(lines), StandardCharsets.UTF_8); + } + + @Test + void parsesCpuStatFields() throws Exception { + // Arrange + createCpuStatFile(1000000, 600000, 400000); + CGroupv2CpuReader reader = new CGroupv2CpuReader(cgroupDir); + + // Act - first poll/update to set baseline + reader.poll(); + reader.update(); + + // Update file and poll again + createCpuStatFile(2000000, 1200000, 800000); + reader.poll(); + reader.update(); + + // Assert + assertThat(reader.getCpuUsage().getIncrement()).isEqualTo(1000000L); + assertThat(reader.getCpuUser().getIncrement()).isEqualTo(600000L); + assertThat(reader.getCpuSystem().getIncrement()).isEqualTo(400000L); + } + + @Test + void handlesAdditionalFieldsInCpuStat() throws Exception { + // Arrange - cpu.stat may contain additional fields like nr_periods, nr_throttled + writeLines( + cgroupDir.resolve("cpu.stat"), + "usage_usec 1000000", + "user_usec 600000", + "system_usec 400000", + "nr_periods 100", + "nr_throttled 5", + "throttled_usec 50000", + "nr_bursts 0", + "burst_usec 0"); + + CGroupv2CpuReader reader = new CGroupv2CpuReader(cgroupDir); + + // Act + reader.poll(); + reader.update(); + + // Update with new values + writeLines( + cgroupDir.resolve("cpu.stat"), + "usage_usec 2000000", + "user_usec 1200000", + "system_usec 800000", + "nr_periods 200", + "nr_throttled 10", + "throttled_usec 100000", + "nr_bursts 0", + "burst_usec 0"); + reader.poll(); + reader.update(); + + // Assert - should only parse the usage, user, and system fields + assertThat(reader.getCpuUsage().getIncrement()).isEqualTo(1000000L); + assertThat(reader.getCpuUser().getIncrement()).isEqualTo(600000L); + assertThat(reader.getCpuSystem().getIncrement()).isEqualTo(400000L); + } + + @Test + void closesResourcesProperly() throws Exception { + // Arrange + createCpuStatFile(1000000, 600000, 400000); + CGroupv2CpuReader reader = new CGroupv2CpuReader(cgroupDir); + + reader.poll(); + reader.update(); + + // Act & Assert - should not throw + reader.close(); + } + + @Test + void incrementIsNullOnFirstPoll() throws Exception { + // Arrange + createCpuStatFile(1000000, 600000, 400000); + CGroupv2CpuReader reader = new CGroupv2CpuReader(cgroupDir); + + // Act - only first poll/update + reader.poll(); + reader.update(); + + // Assert - no increment yet since we need two values to calculate + assertThat(reader.getCpuUsage().getIncrement()).isNull(); + assertThat(reader.getCpuUser().getIncrement()).isNull(); + assertThat(reader.getCpuSystem().getIncrement()).isNull(); + } + + @Test + void worksWithCustomMountPath() throws Exception { + // Arrange - deeply nested custom path + Path customPath = tempDir.resolve("sys").resolve("fs").resolve("cgroup").resolve("app"); + Files.createDirectories(customPath); + + writeLines( + customPath.resolve("cpu.stat"), + "usage_usec 5000000", + "user_usec 3000000", + "system_usec 2000000"); + + CGroupv2CpuReader reader = new CGroupv2CpuReader(customPath); + + // Act + reader.poll(); + reader.update(); + + writeLines( + customPath.resolve("cpu.stat"), + "usage_usec 10000000", + "user_usec 6000000", + "system_usec 4000000"); + reader.poll(); + reader.update(); + + // Assert + assertThat(reader.getCpuUsage().getIncrement()).isEqualTo(5000000L); + assertThat(reader.getCpuUser().getIncrement()).isEqualTo(3000000L); + assertThat(reader.getCpuSystem().getIncrement()).isEqualTo(2000000L); + } + + private void createCpuStatFile(long usage, long user, long system) throws IOException { + writeLines( + cgroupDir.resolve("cpu.stat"), + "usage_usec " + usage, + "user_usec " + user, + "system_usec " + system); + } +} diff --git a/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/diagnostics/cgroups/LinuxCGroupDataReaderTest.java b/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/diagnostics/cgroups/LinuxCGroupDataReaderTest.java new file mode 100644 index 00000000000..0d9ef298f13 --- /dev/null +++ b/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/diagnostics/cgroups/LinuxCGroupDataReaderTest.java @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.applicationinsights.agent.internal.diagnostics.cgroups; + +import static com.microsoft.applicationinsights.agent.internal.diagnostics.cgroups.CGroupv2CpuReaderTest.writeString; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.microsoft.applicationinsights.diagnostics.collection.libos.OperatingSystemInteractionException; +import com.microsoft.applicationinsights.diagnostics.collection.libos.os.linux.cgroups.LinuxCGroupDataReader; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** + * Tests for {@link LinuxCGroupDataReader} verifying cgroup v1 data reading with arbitrary mount + * locations. + */ +@SuppressWarnings("checkstyle:AbbreviationAsWordInName") +class LinuxCGroupDataReaderTest { + + @TempDir Path tempDir; + + private Path cgroupRoot; + + @BeforeEach + void setUp() throws IOException { + cgroupRoot = tempDir.resolve("cgroup"); + Files.createDirectories(cgroupRoot); + } + + @Test + void readsMemoryLimitsFromCustomMountLocation() throws Exception { + // Arrange + Path memoryDir = cgroupRoot.resolve("memory"); + Files.createDirectories(memoryDir); + writeString(memoryDir.resolve("memory.limit_in_bytes"), "1073741824"); + writeString(memoryDir.resolve("memory.soft_limit_in_bytes"), "536870912"); + writeString(memoryDir.resolve("memory.kmem.limit_in_bytes"), "268435456"); + + LinuxCGroupDataReader reader = new LinuxCGroupDataReader(cgroupRoot); + + // Act & Assert + assertThat(reader.getMemoryLimit()).isEqualTo(1073741824L); + assertThat(reader.getMemorySoftLimit()).isEqualTo(536870912L); + assertThat(reader.getKmemLimit()).isEqualTo(268435456L); + } + + @Test + void readsCpuLimitsFromCustomMountLocation() throws Exception { + // Arrange + Path cpuDir = cgroupRoot.resolve("cpu,cpuacct"); + Files.createDirectories(cpuDir); + writeString(cpuDir.resolve("cpu.cfs_quota_us"), "50000"); + writeString(cpuDir.resolve("cpu.cfs_period_us"), "100000"); + + LinuxCGroupDataReader reader = new LinuxCGroupDataReader(cgroupRoot); + + // Act & Assert + assertThat(reader.isAvailable()).isTrue(); + assertThat(reader.getCpuLimit()).isEqualTo(50000L); + assertThat(reader.getCpuPeriod()).isEqualTo(100000L); + } + + @Test + void isAvailableReturnsFalseWhenNoFilesExist() { + // Arrange - cgroupRoot exists but has no cgroup files + LinuxCGroupDataReader reader = new LinuxCGroupDataReader(cgroupRoot); + + // Act & Assert + assertThat(reader.isAvailable()).isFalse(); + } + + @Test + void throwsExceptionWhenFileDoesNotExist() { + // Arrange + LinuxCGroupDataReader reader = new LinuxCGroupDataReader(cgroupRoot); + + // Act & Assert + assertThatThrownBy(reader::getMemoryLimit) + .isInstanceOf(OperatingSystemInteractionException.class); + } + + @Test + void worksWithDeeplyNestedCustomMountLocation() throws Exception { + // Arrange - simulate a deeply nested custom mount point + Path deepPath = tempDir.resolve("custom").resolve("mount").resolve("point").resolve("cgroup"); + Files.createDirectories(deepPath); + + Path memoryDir = deepPath.resolve("memory"); + Files.createDirectories(memoryDir); + writeString(memoryDir.resolve("memory.limit_in_bytes"), "2147483648"); + + Path cpuDir = deepPath.resolve("cpu,cpuacct"); + Files.createDirectories(cpuDir); + writeString(cpuDir.resolve("cpu.cfs_quota_us"), "100000"); + writeString(cpuDir.resolve("cpu.cfs_period_us"), "100000"); + + LinuxCGroupDataReader reader = new LinuxCGroupDataReader(deepPath); + + // Act & Assert + assertThat(reader.isAvailable()).isTrue(); + assertThat(reader.getMemoryLimit()).isEqualTo(2147483648L); + assertThat(reader.getCpuLimit()).isEqualTo(100000L); + assertThat(reader.getCpuPeriod()).isEqualTo(100000L); + } +} diff --git a/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/diagnostics/cgroups/LinuxCGroupUsageDataReaderTest.java b/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/diagnostics/cgroups/LinuxCGroupUsageDataReaderTest.java new file mode 100644 index 00000000000..3950993b785 --- /dev/null +++ b/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/diagnostics/cgroups/LinuxCGroupUsageDataReaderTest.java @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.applicationinsights.agent.internal.diagnostics.cgroups; + +import static com.microsoft.applicationinsights.agent.internal.diagnostics.cgroups.CGroupv2CpuReaderTest.writeLines; +import static com.microsoft.applicationinsights.agent.internal.diagnostics.cgroups.CGroupv2CpuReaderTest.writeString; +import static org.assertj.core.api.Assertions.assertThat; + +import com.microsoft.applicationinsights.diagnostics.collection.libos.os.linux.cgroups.LinuxCGroupUsageDataReader; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** + * Tests for {@link LinuxCGroupUsageDataReader} verifying cgroup v1 usage data reading with + * arbitrary mount locations. + */ +@SuppressWarnings("checkstyle:AbbreviationAsWordInName") +class LinuxCGroupUsageDataReaderTest { + + @TempDir Path tempDir; + + private Path cgroupRoot; + private Path cpuCgroupDir; + + @BeforeEach + void setUp() throws IOException { + cgroupRoot = tempDir.resolve("cgroup"); + cpuCgroupDir = cgroupRoot.resolve("cpu,cpuacct"); + Files.createDirectories(cpuCgroupDir); + } + + @Test + void isAvailableReturnsTrueWhenCpuDirectoryExists() throws Exception { + // Arrange + LinuxCGroupUsageDataReader reader = new LinuxCGroupUsageDataReader(cgroupRoot); + + // Act & Assert + assertThat(reader.isAvailable()).isTrue(); + } + + @Test + void isAvailableReturnsFalseWhenCpuDirectoryDoesNotExist() throws Exception { + // Arrange - use a path without the cpu,cpuacct directory + Path emptyRoot = tempDir.resolve("empty"); + Files.createDirectories(emptyRoot); + + LinuxCGroupUsageDataReader reader = new LinuxCGroupUsageDataReader(emptyRoot); + + // Act & Assert + assertThat(reader.isAvailable()).isFalse(); + } + + @Test + void worksWithCustomMountLocation() throws Exception { + // Arrange - simulate a custom mount location like /custom/cgroup + Path customMount = tempDir.resolve("custom").resolve("cgroup"); + Path customCpuDir = customMount.resolve("cpu,cpuacct"); + Files.createDirectories(customCpuDir); + + createCgroupV1CpuFiles(customCpuDir, "5000000", "2500000", "2500000", "50", "50"); + + LinuxCGroupUsageDataReader reader = new LinuxCGroupUsageDataReader(customMount); + + // Act & Assert + assertThat(reader.isAvailable()).isTrue(); + + reader.poll(); + reader.update(); + + List telemetry = reader.getTelemetry(); + assertThat(telemetry).hasSize(5); + } + + @Test + void closesResourcesProperly() throws Exception { + // Arrange + createCgroupV1CpuFiles(cpuCgroupDir, "1000000", "500000", "500000", "100", "100"); + LinuxCGroupUsageDataReader reader = new LinuxCGroupUsageDataReader(cgroupRoot); + + reader.poll(); + reader.update(); + + // Act & Assert - should not throw + reader.close(); + } + + @Test + void returnsNegativeOneForUnreadableValues() throws Exception { + // Arrange - create directory but no files + LinuxCGroupUsageDataReader reader = new LinuxCGroupUsageDataReader(cgroupRoot); + + // Act + reader.poll(); + reader.update(); + List telemetry = reader.getTelemetry(); + + // Assert - all values should be -1.0 since files don't exist + assertThat(telemetry).hasSize(5); + assertThat(telemetry).allMatch(value -> value == -1.0d); + } + + private static void createCgroupV1CpuFiles( + Path cpuDir, + String usage, + String userUsage, + String systemUsage, + String statUser, + String statSystem) + throws IOException { + writeString(cpuDir.resolve("cpuacct.usage"), usage); + writeString(cpuDir.resolve("cpuacct.usage_user"), userUsage); + writeString(cpuDir.resolve("cpuacct.usage_sys"), systemUsage); + writeLines(cpuDir.resolve("cpuacct.stat"), "user " + statUser, "system " + statSystem); + } +} diff --git a/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/diagnostics/cgroups/LinuxCGroupV2DataReaderTest.java b/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/diagnostics/cgroups/LinuxCGroupV2DataReaderTest.java new file mode 100644 index 00000000000..8d5a6f4ad0a --- /dev/null +++ b/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/diagnostics/cgroups/LinuxCGroupV2DataReaderTest.java @@ -0,0 +1,164 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.applicationinsights.agent.internal.diagnostics.cgroups; + +import static com.microsoft.applicationinsights.agent.internal.diagnostics.cgroups.CGroupv2CpuReaderTest.writeString; +import static org.assertj.core.api.Assertions.assertThat; + +import com.microsoft.applicationinsights.diagnostics.collection.libos.os.linux.cgroupsv2.LinuxCGroupV2DataReader; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** + * Tests for {@link LinuxCGroupV2DataReader} verifying cgroup v2 data reading with arbitrary mount + * locations. + */ +@SuppressWarnings("checkstyle:AbbreviationAsWordInName") +class LinuxCGroupV2DataReaderTest { + + @TempDir Path tempDir; + + private Path cgroupDir; + + @BeforeEach + void setUp() throws IOException { + cgroupDir = tempDir.resolve("cgroup2"); + Files.createDirectories(cgroupDir); + } + + @Test + void readsMemoryMaxFromCustomMountLocation() throws Exception { + // Arrange + writeString(cgroupDir.resolve("memory.max"), "1073741824"); + writeString(cgroupDir.resolve("memory.high"), "536870912"); + writeString(cgroupDir.resolve("cpu.max"), "50000 100000"); + + LinuxCGroupV2DataReader reader = new LinuxCGroupV2DataReader(cgroupDir); + + // Act & Assert + assertThat(reader.getMemoryLimit()).isEqualTo(1073741824L); + } + + @Test + void readsMemoryHighAsSoftLimit() throws Exception { + // Arrange + writeString(cgroupDir.resolve("memory.max"), "1073741824"); + writeString(cgroupDir.resolve("memory.high"), "536870912"); + writeString(cgroupDir.resolve("cpu.max"), "50000 100000"); + + LinuxCGroupV2DataReader reader = new LinuxCGroupV2DataReader(cgroupDir); + + // Act & Assert + assertThat(reader.getMemorySoftLimit()).isEqualTo(536870912L); + } + + @Test + void parsesMaxValueAsLongMaxValue() throws Exception { + // Arrange - "max" means no limit in cgroup v2 + writeString(cgroupDir.resolve("memory.max"), "max"); + writeString(cgroupDir.resolve("memory.high"), "max"); + writeString(cgroupDir.resolve("cpu.max"), "max 100000"); + + LinuxCGroupV2DataReader reader = new LinuxCGroupV2DataReader(cgroupDir); + + // Act & Assert + assertThat(reader.getMemoryLimit()).isEqualTo(Long.MAX_VALUE); + assertThat(reader.getMemorySoftLimit()).isEqualTo(Long.MAX_VALUE); + } + + @Test + void readsCpuQuotaFromCpuMax() throws Exception { + // Arrange - cpu.max format is "quota period" + writeString(cgroupDir.resolve("memory.max"), "1073741824"); + writeString(cgroupDir.resolve("memory.high"), "536870912"); + writeString(cgroupDir.resolve("cpu.max"), "50000 100000"); + + LinuxCGroupV2DataReader reader = new LinuxCGroupV2DataReader(cgroupDir); + + // Act & Assert + assertThat(reader.getCpuLimit()).isEqualTo(50000L); + } + + @Test + void readsCpuPeriodFromCpuMax() throws Exception { + // Arrange - cpu.max format is "quota period" + writeString(cgroupDir.resolve("memory.max"), "1073741824"); + writeString(cgroupDir.resolve("memory.high"), "536870912"); + writeString(cgroupDir.resolve("cpu.max"), "50000 100000"); + + LinuxCGroupV2DataReader reader = new LinuxCGroupV2DataReader(cgroupDir); + + // Act & Assert + assertThat(reader.getCpuPeriod()).isEqualTo(100000L); + } + + @Test + void cpuQuotaReturnsNegativeOneWhenMax() throws Exception { + // Arrange - "max" for cpu quota means unlimited + writeString(cgroupDir.resolve("memory.max"), "1073741824"); + writeString(cgroupDir.resolve("memory.high"), "536870912"); + writeString(cgroupDir.resolve("cpu.max"), "max 100000"); + + LinuxCGroupV2DataReader reader = new LinuxCGroupV2DataReader(cgroupDir); + + // Act & Assert + assertThat(reader.getCpuLimit()).isEqualTo(-1L); + } + + @Test + void kmemLimitReturnsMaxValueForCgroupV2() throws Exception { + // Arrange - cgroup v2 doesn't separately expose kernel memory + writeString(cgroupDir.resolve("memory.max"), "1073741824"); + writeString(cgroupDir.resolve("memory.high"), "536870912"); + writeString(cgroupDir.resolve("cpu.max"), "50000 100000"); + + LinuxCGroupV2DataReader reader = new LinuxCGroupV2DataReader(cgroupDir); + + // Act & Assert + assertThat(reader.getKmemLimit()).isEqualTo(Long.MAX_VALUE); + } + + @Test + void isAvailableReturnsTrueWhenAtLeastOneFileExists() throws Exception { + // Arrange + writeString(cgroupDir.resolve("memory.max"), "1073741824"); + + LinuxCGroupV2DataReader reader = new LinuxCGroupV2DataReader(cgroupDir); + + // Act & Assert + assertThat(reader.isAvailable()).isTrue(); + } + + @Test + void isAvailableReturnsFalseWhenNoFilesExist() { + // Arrange - cgroupDir exists but has no cgroup v2 files + LinuxCGroupV2DataReader reader = new LinuxCGroupV2DataReader(cgroupDir); + + // Act & Assert + assertThat(reader.isAvailable()).isFalse(); + } + + @Test + void worksWithDeeplyNestedCustomMountLocation() throws Exception { + // Arrange - simulate a deeply nested custom mount point + Path deepPath = tempDir.resolve("sys").resolve("fs").resolve("cgroup").resolve("user.slice"); + Files.createDirectories(deepPath); + writeString(deepPath.resolve("memory.max"), "2147483648"); + writeString(deepPath.resolve("memory.high"), "1073741824"); + writeString(deepPath.resolve("cpu.max"), "200000 100000"); + + LinuxCGroupV2DataReader reader = new LinuxCGroupV2DataReader(deepPath); + + // Act & Assert + assertThat(reader.isAvailable()).isTrue(); + assertThat(reader.getMemoryLimit()).isEqualTo(2147483648L); + assertThat(reader.getMemorySoftLimit()).isEqualTo(1073741824L); + assertThat(reader.getCpuLimit()).isEqualTo(200000L); + assertThat(reader.getCpuPeriod()).isEqualTo(100000L); + } +} diff --git a/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/diagnostics/cgroups/LinuxCGroupV2UsageDataReaderTest.java b/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/diagnostics/cgroups/LinuxCGroupV2UsageDataReaderTest.java new file mode 100644 index 00000000000..fe12c0136d6 --- /dev/null +++ b/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/diagnostics/cgroups/LinuxCGroupV2UsageDataReaderTest.java @@ -0,0 +1,146 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.applicationinsights.agent.internal.diagnostics.cgroups; + +import static com.microsoft.applicationinsights.agent.internal.diagnostics.cgroups.CGroupv2CpuReaderTest.writeLines; +import static com.microsoft.applicationinsights.agent.internal.diagnostics.cgroups.CGroupv2CpuReaderTest.writeString; +import static org.assertj.core.api.Assertions.assertThat; + +import com.microsoft.applicationinsights.diagnostics.collection.libos.os.linux.cgroupsv2.LinuxCGroupV2UsageDataReader; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** + * Tests for {@link LinuxCGroupV2UsageDataReader} verifying cgroup v2 usage data reading with + * arbitrary mount locations. + */ +@SuppressWarnings("checkstyle:AbbreviationAsWordInName") +class LinuxCGroupV2UsageDataReaderTest { + + @TempDir Path tempDir; + + private Path cgroupDir; + + @BeforeEach + void setUp() throws IOException { + cgroupDir = tempDir.resolve("cgroup2"); + Files.createDirectories(cgroupDir); + } + + @Test + void isAvailableReturnsTrueWhenCgroupControllersExists() throws Exception { + // Arrange - cgroup.controllers is the indicator file for cgroup v2 + writeString(cgroupDir.resolve("cgroup.controllers"), "cpu memory io"); + + LinuxCGroupV2UsageDataReader reader = new LinuxCGroupV2UsageDataReader(cgroupDir); + + // Act & Assert + assertThat(reader.isAvailable()).isTrue(); + } + + @Test + void isAvailableReturnsFalseWhenCgroupControllersDoesNotExist() { + // Arrange - cgroupDir exists but no cgroup.controllers file + LinuxCGroupV2UsageDataReader reader = new LinuxCGroupV2UsageDataReader(cgroupDir); + + // Act & Assert + assertThat(reader.isAvailable()).isFalse(); + } + + @Test + void parsesCpuStatFileCorrectly() throws Exception { + // Arrange + createCgroupV2CpuStatFile(cgroupDir, 1000000, 600000, 400000); + writeString(cgroupDir.resolve("cgroup.controllers"), "cpu memory io"); + + LinuxCGroupV2UsageDataReader reader = new LinuxCGroupV2UsageDataReader(cgroupDir); + + // First poll/update + reader.poll(); + reader.update(); + + // Update with new values + createCgroupV2CpuStatFile(cgroupDir, 3000000, 1800000, 1200000); + reader.poll(); + reader.update(); + + // Act + List telemetry = reader.getTelemetry(); + + // Assert + assertThat(telemetry).hasSize(5); + // The increments should be: usage=2000000, user=1200000, system=800000 + assertThat(telemetry.get(0)).isEqualTo(2000000.0d); // usage increment + assertThat(telemetry.get(1)).isEqualTo(1200000.0d); // user increment + assertThat(telemetry.get(2)).isEqualTo(800000.0d); // system increment + } + + @Test + void worksWithCustomMountLocation() throws Exception { + // Arrange - simulate a custom cgroup v2 mount like /sys/fs/cgroup/user.slice/user-1000.slice + Path customMount = tempDir.resolve("sys").resolve("fs").resolve("cgroup").resolve("user.slice"); + Files.createDirectories(customMount); + + createCgroupV2CpuStatFile(customMount, 5000000, 2500000, 2500000); + writeString(customMount.resolve("cgroup.controllers"), "cpu memory"); + + LinuxCGroupV2UsageDataReader reader = new LinuxCGroupV2UsageDataReader(customMount); + + // Act & Assert + assertThat(reader.isAvailable()).isTrue(); + + reader.poll(); + reader.update(); + + List telemetry = reader.getTelemetry(); + assertThat(telemetry).hasSize(5); + } + + @Test + void closesResourcesProperly() throws Exception { + // Arrange + createCgroupV2CpuStatFile(cgroupDir, 1000000, 500000, 500000); + writeString(cgroupDir.resolve("cgroup.controllers"), "cpu memory io"); + + LinuxCGroupV2UsageDataReader reader = new LinuxCGroupV2UsageDataReader(cgroupDir); + + reader.poll(); + reader.update(); + + // Act & Assert - should not throw + reader.close(); + } + + @Test + void returnsNegativeOneForFirstPoll() throws Exception { + // Arrange + createCgroupV2CpuStatFile(cgroupDir, 1000000, 500000, 500000); + writeString(cgroupDir.resolve("cgroup.controllers"), "cpu memory io"); + + LinuxCGroupV2UsageDataReader reader = new LinuxCGroupV2UsageDataReader(cgroupDir); + + // Act - only first poll/update (no increment available yet) + reader.poll(); + reader.update(); + List telemetry = reader.getTelemetry(); + + // Assert - first read has no increment, so values should be -1.0 + assertThat(telemetry).hasSize(5); + assertThat(telemetry).allMatch(value -> value == -1.0d); + } + + private static void createCgroupV2CpuStatFile(Path dir, long usage, long user, long system) + throws IOException { + writeLines( + dir.resolve("cpu.stat"), + "usage_usec " + usage, + "user_usec " + user, + "system_usec " + system); + } +} From d3877eb2eb5dcbd591b04cea91619dfd85af26b8 Mon Sep 17 00:00:00 2001 From: John Oliver <1615532+johnoliver@users.noreply.github.com> Date: Tue, 27 Jan 2026 13:09:19 +0000 Subject: [PATCH 3/3] Limit tests to linux only --- .../internal/diagnostics/cgroups/CGroupv2CpuReaderTest.java | 3 +++ .../diagnostics/cgroups/LinuxCGroupDataReaderTest.java | 3 +++ .../diagnostics/cgroups/LinuxCGroupUsageDataReaderTest.java | 3 +++ .../diagnostics/cgroups/LinuxCGroupV2DataReaderTest.java | 3 +++ .../diagnostics/cgroups/LinuxCGroupV2UsageDataReaderTest.java | 3 +++ 5 files changed, 15 insertions(+) diff --git a/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/diagnostics/cgroups/CGroupv2CpuReaderTest.java b/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/diagnostics/cgroups/CGroupv2CpuReaderTest.java index e9bcbb21679..a50aaff23dd 100644 --- a/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/diagnostics/cgroups/CGroupv2CpuReaderTest.java +++ b/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/diagnostics/cgroups/CGroupv2CpuReaderTest.java @@ -14,9 +14,12 @@ import java.util.Collections; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; import org.junit.jupiter.api.io.TempDir; /** Tests for {@link CGroupv2CpuReader} verifying cgroup v2 CPU stat file parsing. */ +@EnabledOnOs(OS.LINUX) @SuppressWarnings("checkstyle:AbbreviationAsWordInName") class CGroupv2CpuReaderTest { diff --git a/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/diagnostics/cgroups/LinuxCGroupDataReaderTest.java b/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/diagnostics/cgroups/LinuxCGroupDataReaderTest.java index 0d9ef298f13..dbd842f53f0 100644 --- a/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/diagnostics/cgroups/LinuxCGroupDataReaderTest.java +++ b/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/diagnostics/cgroups/LinuxCGroupDataReaderTest.java @@ -14,12 +14,15 @@ import java.nio.file.Path; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; import org.junit.jupiter.api.io.TempDir; /** * Tests for {@link LinuxCGroupDataReader} verifying cgroup v1 data reading with arbitrary mount * locations. */ +@EnabledOnOs(OS.LINUX) @SuppressWarnings("checkstyle:AbbreviationAsWordInName") class LinuxCGroupDataReaderTest { diff --git a/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/diagnostics/cgroups/LinuxCGroupUsageDataReaderTest.java b/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/diagnostics/cgroups/LinuxCGroupUsageDataReaderTest.java index 3950993b785..f5ce8d268c4 100644 --- a/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/diagnostics/cgroups/LinuxCGroupUsageDataReaderTest.java +++ b/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/diagnostics/cgroups/LinuxCGroupUsageDataReaderTest.java @@ -14,12 +14,15 @@ import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; import org.junit.jupiter.api.io.TempDir; /** * Tests for {@link LinuxCGroupUsageDataReader} verifying cgroup v1 usage data reading with * arbitrary mount locations. */ +@EnabledOnOs(OS.LINUX) @SuppressWarnings("checkstyle:AbbreviationAsWordInName") class LinuxCGroupUsageDataReaderTest { diff --git a/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/diagnostics/cgroups/LinuxCGroupV2DataReaderTest.java b/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/diagnostics/cgroups/LinuxCGroupV2DataReaderTest.java index 8d5a6f4ad0a..2d34be55725 100644 --- a/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/diagnostics/cgroups/LinuxCGroupV2DataReaderTest.java +++ b/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/diagnostics/cgroups/LinuxCGroupV2DataReaderTest.java @@ -12,12 +12,15 @@ import java.nio.file.Path; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; import org.junit.jupiter.api.io.TempDir; /** * Tests for {@link LinuxCGroupV2DataReader} verifying cgroup v2 data reading with arbitrary mount * locations. */ +@EnabledOnOs(OS.LINUX) @SuppressWarnings("checkstyle:AbbreviationAsWordInName") class LinuxCGroupV2DataReaderTest { diff --git a/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/diagnostics/cgroups/LinuxCGroupV2UsageDataReaderTest.java b/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/diagnostics/cgroups/LinuxCGroupV2UsageDataReaderTest.java index fe12c0136d6..0e4cef69928 100644 --- a/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/diagnostics/cgroups/LinuxCGroupV2UsageDataReaderTest.java +++ b/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/diagnostics/cgroups/LinuxCGroupV2UsageDataReaderTest.java @@ -14,12 +14,15 @@ import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; import org.junit.jupiter.api.io.TempDir; /** * Tests for {@link LinuxCGroupV2UsageDataReader} verifying cgroup v2 usage data reading with * arbitrary mount locations. */ +@EnabledOnOs(OS.LINUX) @SuppressWarnings("checkstyle:AbbreviationAsWordInName") class LinuxCGroupV2UsageDataReaderTest {