From 8cec38f7b3f2861a92ccfad039c7fb473f1687e0 Mon Sep 17 00:00:00 2001 From: dahoud <41957584+DahoudG@users.noreply.github.com> Date: Tue, 21 Apr 2026 13:38:31 +0000 Subject: [PATCH] =?UTF-8?q?fix(monitoring):=20corriger=20calcul=20CPU=20?= =?UTF-8?q?=E2=80=94=20getProcessCpuLoad=20au=20lieu=20de=20loadAverage/pr?= =?UTF-8?q?ocessors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le calcul précédent `getSystemLoadAverage() / getAvailableProcessors() * 100` utilisait : - getSystemLoadAverage() : charge 1min du NODE Linux hôte entier (12 CPU sur prod VPS) - getAvailableProcessors() : limite conteneur K8s (1 CPU pour le pod UnionFlow) Résultat : load 1.5 sur le node → cpuUsage = (1.5 / 1) * 100 = 150% capé à 100%. Déclenchement constant d'alertes "CPU 100%" (19 faux positifs / 24h sur prod le 20-21/04/2026) dès que le node fait autre chose que dormir. Correctif : utilise com.sun.management.OperatingSystemMXBean.getProcessCpuLoad() qui retourne la charge CPU du process JVM courant (0.0-1.0), conscient du conteneur. Branche -1 préservée (API non dispo sur certaines JVM). Tests mis à jour : AlertMonitoringServiceMockStaticCoverageTest injecte désormais un com.sun.management.OperatingSystemMXBean et mocke getProcessCpuLoad() (compatible mock-maker-inline déjà configuré). AlertMonitoringServiceTest conserve sa logique OS-agnostique via des thresholds extrêmes (-1 toujours / 100 jamais). --- .../service/AlertMonitoringService.java | 15 +- ...nitoringServiceMockStaticCoverageTest.java | 138 +++++++++--------- .../service/AlertMonitoringServiceTest.java | 8 +- 3 files changed, 83 insertions(+), 78 deletions(-) diff --git a/src/main/java/dev/lions/unionflow/server/service/AlertMonitoringService.java b/src/main/java/dev/lions/unionflow/server/service/AlertMonitoringService.java index 893c37e..3c5f576 100644 --- a/src/main/java/dev/lions/unionflow/server/service/AlertMonitoringService.java +++ b/src/main/java/dev/lions/unionflow/server/service/AlertMonitoringService.java @@ -82,12 +82,15 @@ public class AlertMonitoringService { */ private void checkCpuThreshold(AlertConfiguration config) { try { - OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean(); - double loadAvg = osBean.getSystemLoadAverage(); - int processors = osBean.getAvailableProcessors(); - - // Calculer l'utilisation CPU en pourcentage - double cpuUsage = loadAvg < 0 ? 0.0 : Math.min(100.0, (loadAvg / processors) * 100.0); + // getProcessCpuLoad() renvoie la charge CPU de CE process JVM (0.0-1.0), + // ce qui est correct en conteneur K8s/Docker. + // getSystemLoadAverage() renvoie la charge du NODE entier (hôte Linux), + // divisée par availableProcessors() limité par le conteneur (ex: 1), + // ce qui produit des faux positifs dès que le node est actif. + com.sun.management.OperatingSystemMXBean osBean = + (com.sun.management.OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean(); + double processCpuLoad = osBean.getProcessCpuLoad(); + double cpuUsage = processCpuLoad < 0 ? 0.0 : Math.min(100.0, processCpuLoad * 100.0); lastCpuUsage = cpuUsage; int threshold = config.getCpuThresholdPercent(); diff --git a/src/test/java/dev/lions/unionflow/server/service/AlertMonitoringServiceMockStaticCoverageTest.java b/src/test/java/dev/lions/unionflow/server/service/AlertMonitoringServiceMockStaticCoverageTest.java index f47ab04..05d4529 100644 --- a/src/test/java/dev/lions/unionflow/server/service/AlertMonitoringServiceMockStaticCoverageTest.java +++ b/src/test/java/dev/lions/unionflow/server/service/AlertMonitoringServiceMockStaticCoverageTest.java @@ -18,7 +18,6 @@ import dev.lions.unionflow.server.repository.SystemLogRepository; import java.lang.management.ManagementFactory; import java.lang.management.MemoryMXBean; import java.lang.management.MemoryUsage; -import java.lang.management.OperatingSystemMXBean; import java.lang.reflect.Field; import java.time.LocalDateTime; import java.util.Collections; @@ -35,11 +34,12 @@ import org.mockito.junit.jupiter.MockitoExtension; * Tests unitaires purs (sans Quarkus) pour {@link AlertMonitoringService}. * *
Utilise {@code mockStatic(ManagementFactory.class)} pour injecter des beans MXBean - * contrôlés et couvrir les branches inaccessibles sur certains systèmes (notamment Windows - * où {@code getSystemLoadAverage()} retourne toujours -1). + * contrôlés et couvrir les branches du calcul CPU basé sur + * {@code com.sun.management.OperatingSystemMXBean.getProcessCpuLoad()} (0.0-1.0, -1 si indispo). * *
Note : utilise {@code mock-maker-inline} (configuré via - * {@code src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker}). + * {@code src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker}) — + * nécessaire pour mocker {@code com.sun.management.OperatingSystemMXBean} (type JDK interne). */ @ExtendWith(MockitoExtension.class) @DisplayName("AlertMonitoringService — branches via mockStatic ManagementFactory") @@ -69,31 +69,31 @@ class AlertMonitoringServiceMockStaticCoverageTest { } // ========================================================================= - // L85 : branche ternaire loadAvg >= 0 → Math.min(100.0, (loadAvg / processors) * 100.0) + // checkCpuThreshold : branche processCpuLoad >= 0 + // cpuUsage = Math.min(100.0, processCpuLoad * 100.0) // ========================================================================= /** - * Couvre la branche {@code loadAvg >= 0} à la ligne 85 : - * {@code Math.min(100.0, (loadAvg / processors) * 100.0)}. + * Couvre la branche {@code processCpuLoad >= 0} : + * {@code cpuUsage = Math.min(100.0, processCpuLoad * 100.0)}. * - *
On injecte un {@code OperatingSystemMXBean} mock qui retourne - * {@code getSystemLoadAverage() = 0.5} (valeur positive ≥ 0) et - * {@code getAvailableProcessors() = 4}. - * → {@code cpuUsage = Math.min(100.0, (0.5/4)*100.0) = Math.min(100.0, 12.5) = 12.5}. + *
On injecte un {@code com.sun.management.OperatingSystemMXBean} mock qui retourne + * {@code getProcessCpuLoad() = 0.125} (valeur positive ≥ 0). + * → {@code cpuUsage = Math.min(100.0, 0.125 * 100.0) = 12.5}. * *
Avec {@code threshold = 0}, {@code 12.5 > 0} → branche cpuUsage > threshold → true
- * → {@code lastCpuHighTime} est mis à null initialement → il est settté à now().
+ * → {@code lastCpuHighTime} est mis à null initialement → il est setté à now().
* Aucune alerte n'est créée (première itération).
*/
@Test
- @DisplayName("checkCpuThreshold : loadAvg >= 0 → Math.min branch (L85) via OperatingSystemMXBean mock")
- void checkCpuThreshold_loadAvgPositive_coversL85MathMinBranch() {
+ @DisplayName("checkCpuThreshold : processCpuLoad >= 0 → Math.min branch via OperatingSystemMXBean mock")
+ void checkCpuThreshold_processCpuLoadPositive_coversMathMinBranch() {
AlertConfiguration config = buildConfig(true, 0, 60, false, 100, false);
when(alertConfigurationRepository.getConfiguration()).thenReturn(config);
- OperatingSystemMXBean osMock = mock(OperatingSystemMXBean.class);
- when(osMock.getSystemLoadAverage()).thenReturn(0.5); // >= 0 → branche Math.min à L85
- when(osMock.getAvailableProcessors()).thenReturn(4);
+ com.sun.management.OperatingSystemMXBean osMock =
+ mock(com.sun.management.OperatingSystemMXBean.class);
+ when(osMock.getProcessCpuLoad()).thenReturn(0.125); // >= 0 → branche Math.min
try (MockedStatic On simule un CPU à 200% (loadAvg=8, processors=4 → cpuUsage=200, cappé à 100%).
+ * On simule un CPU à 200% brut (getProcessCpuLoad=2.0 — cas théorique) cappé à 100% via Math.min.
* Après deux appels avec {@code durationMinutes=0}, l'alerte est créée.
*/
@Test
- @DisplayName("checkCpuThreshold : loadAvg élevé → cpuUsage 100% → alerte créée en 2 appels (L85 Math.min branch)")
- void checkCpuThreshold_loadAvgHighPositive_triggersAlert_coversL85() {
+ @DisplayName("checkCpuThreshold : processCpuLoad élevé → cpuUsage cappé à 100% → alerte créée en 2 appels")
+ void checkCpuThreshold_processCpuLoadHighPositive_triggersAlert() {
// Premier appel : reset lastCpuHighTime via threshold élevé
AlertConfiguration resetConfig = buildConfig(true, 100, 0, false, 100, false);
when(alertConfigurationRepository.getConfiguration()).thenReturn(resetConfig);
- OperatingSystemMXBean osMock = mock(OperatingSystemMXBean.class);
- when(osMock.getSystemLoadAverage()).thenReturn(2.0); // >= 0 → Math.min branch
- when(osMock.getAvailableProcessors()).thenReturn(4);
+ com.sun.management.OperatingSystemMXBean osMock =
+ mock(com.sun.management.OperatingSystemMXBean.class);
+ when(osMock.getProcessCpuLoad()).thenReturn(0.5); // >= 0 → Math.min branch (50%)
try (MockedStatic On force {@code ManagementFactory.getOperatingSystemMXBean()} à lancer une
- * {@code RuntimeException}. Cette exception est capturée par le catch à L113,
- * loggée à L114, et la méthode retourne silencieusement.
+ * {@code RuntimeException}. Cette exception est capturée par le catch,
+ * loggée, et la méthode retourne silencieusement.
*/
@Test
- @DisplayName("checkCpuThreshold : ManagementFactory.getOperatingSystemMXBean() throw → catch L113-114 couvert")
- void checkCpuThreshold_managementFactoryThrows_coversCatchL113to114() {
+ @DisplayName("checkCpuThreshold : ManagementFactory.getOperatingSystemMXBean() throw → catch couvert")
+ void checkCpuThreshold_managementFactoryThrows_coversCatch() {
AlertConfiguration config = buildConfig(true, 80, 5, false, 100, false);
when(alertConfigurationRepository.getConfiguration()).thenReturn(config);
try (MockedStatic On injecte un {@code MemoryMXBean} mock dont {@code getHeapMemoryUsage().getMax()}
@@ -219,14 +219,14 @@ class AlertMonitoringServiceMockStaticCoverageTest {
* Avec threshold=0 : {@code 0.0 > 0} est false → pas d'alerte.
*/
@Test
- @DisplayName("checkMemoryThreshold : maxMemory = -1 (non défini) → memoryUsage = 0.0 → branche false L127")
- void checkMemoryThreshold_maxMemoryMinusOne_coversL127FalseBranch() {
+ @DisplayName("checkMemoryThreshold : maxMemory = -1 (non défini) → memoryUsage = 0.0 → branche false")
+ void checkMemoryThreshold_maxMemoryMinusOne_coversFalseBranch() {
AlertConfiguration config = buildConfig(false, 80, 5, true, 0, false);
when(alertConfigurationRepository.getConfiguration()).thenReturn(config);
MemoryMXBean memMock = mock(MemoryMXBean.class);
MemoryUsage heapUsageMock = mock(MemoryUsage.class);
- when(heapUsageMock.getMax()).thenReturn(-1L); // maxMemory = -1 → branche false à L127
+ when(heapUsageMock.getMax()).thenReturn(-1L); // maxMemory = -1 → branche false
when(heapUsageMock.getUsed()).thenReturn(100L);
when(memMock.getHeapMemoryUsage()).thenReturn(heapUsageMock);
@@ -242,19 +242,19 @@ class AlertMonitoringServiceMockStaticCoverageTest {
}
/**
- * Couvre L127 branche false avec maxMemory = 0.
+ * Couvre la branche false du ternaire avec maxMemory = 0.
*
* La condition {@code maxMemory > 0} est false pour maxMemory = 0 aussi.
*/
@Test
- @DisplayName("checkMemoryThreshold : maxMemory = 0 → memoryUsage = 0.0 → branche false L127")
- void checkMemoryThreshold_maxMemoryZero_coversL127FalseBranch() {
+ @DisplayName("checkMemoryThreshold : maxMemory = 0 → memoryUsage = 0.0 → branche false")
+ void checkMemoryThreshold_maxMemoryZero_coversFalseBranch() {
AlertConfiguration config = buildConfig(false, 80, 5, true, 0, false);
when(alertConfigurationRepository.getConfiguration()).thenReturn(config);
MemoryMXBean memMock = mock(MemoryMXBean.class);
MemoryUsage heapUsageMock = mock(MemoryUsage.class);
- when(heapUsageMock.getMax()).thenReturn(0L); // maxMemory = 0 → branche false à L127
+ when(heapUsageMock.getMax()).thenReturn(0L); // maxMemory = 0 → branche false
when(heapUsageMock.getUsed()).thenReturn(50L);
when(memMock.getHeapMemoryUsage()).thenReturn(heapUsageMock);
@@ -270,11 +270,11 @@ class AlertMonitoringServiceMockStaticCoverageTest {
}
/**
- * Couvre L127 branche true (maxMemory > 0) + alerte mémoire créée.
+ * Couvre la branche true (maxMemory > 0) + alerte mémoire créée.
* Vérifie que le mock MemoryMXBean avec une valeur positive couvre la branche vraie.
*/
@Test
- @DisplayName("checkMemoryThreshold : maxMemory > 0 + usage > threshold → alerte créée (L127 branche true)")
+ @DisplayName("checkMemoryThreshold : maxMemory > 0 + usage > threshold → alerte créée (branche true)")
void checkMemoryThreshold_maxMemoryPositive_usageAboveThreshold_createsAlert() {
AlertConfiguration config = buildConfig(false, 80, 5, true, 0, false);
when(alertConfigurationRepository.getConfiguration()).thenReturn(config);
@@ -299,25 +299,27 @@ class AlertMonitoringServiceMockStaticCoverageTest {
}
// =========================================================================
- // L85 : combinaison de loadAvg >= 0 avec threshold dépassé (couvre Math.min)
+ // checkCpuThreshold : branche processCpuLoad < 0 (API non disponible)
+ // cpuUsage = 0.0 (ternaire : processCpuLoad < 0 ? 0.0 : ...)
// =========================================================================
/**
- * Couvre explicitement la branche {@code loadAvg < 0} (L85, branche true du ternaire)
- * pour compléter la couverture des deux branches du ternaire à L85.
+ * Couvre explicitement la branche {@code processCpuLoad < 0} du ternaire
+ * pour compléter la couverture des deux branches du ternaire.
*
- * On injecte {@code getSystemLoadAverage() = -1.0} (loadAvg < 0 → cpuUsage = 0.0).
- * Cela simule le comportement Windows où loadAvg = -1.
+ * On injecte {@code getProcessCpuLoad() = -1.0} → cpuUsage = 0.0.
+ * Cela simule le cas où l'API n'est pas disponible (le contrat JDK dit que
+ * {@code getProcessCpuLoad()} peut retourner -1 si la valeur n'est pas disponible).
*/
@Test
- @DisplayName("checkCpuThreshold : loadAvg = -1 (Windows) → cpuUsage = 0.0 → branche true L85 ternaire")
- void checkCpuThreshold_loadAvgNegative_coversL85TrueBranch() {
+ @DisplayName("checkCpuThreshold : processCpuLoad = -1 (indispo) → cpuUsage = 0.0 → branche true ternaire")
+ void checkCpuThreshold_processCpuLoadNegative_coversTrueBranch() {
AlertConfiguration config = buildConfig(true, 100, 5, false, 100, false);
when(alertConfigurationRepository.getConfiguration()).thenReturn(config);
- OperatingSystemMXBean osMock = mock(OperatingSystemMXBean.class);
- when(osMock.getSystemLoadAverage()).thenReturn(-1.0); // < 0 → cpuUsage = 0.0
- when(osMock.getAvailableProcessors()).thenReturn(4);
+ com.sun.management.OperatingSystemMXBean osMock =
+ mock(com.sun.management.OperatingSystemMXBean.class);
+ when(osMock.getProcessCpuLoad()).thenReturn(-1.0); // < 0 → cpuUsage = 0.0
try (MockedStatic La lambda à L229 : {@code "CPU".equals(alert.getSource()) && !alert.getAcknowledged()}
+ * Lambda : {@code "CPU".equals(alert.getSource()) && !alert.getAcknowledged()}
* — branche : source="CPU" true ET acknowledged=false → true → anyMatch=true.
*/
@Test
- @DisplayName("hasRecentCpuAlert lambda L229 : source=CPU + acknowledged=false → anyMatch true → pas d'alerte")
+ @DisplayName("hasRecentCpuAlert lambda : source=CPU + acknowledged=false → anyMatch true → pas d'alerte")
void hasRecentCpuAlert_lambda_sourceCpu_acknowledgedFalse_anyMatchTrue_noNewAlert() throws Exception {
// Préparer : reset lastCpuHighTime à null d'abord
injectField(service, "lastCpuHighTime", null);
@@ -362,9 +364,9 @@ class AlertMonitoringServiceMockStaticCoverageTest {
when(systemAlertRepository.findByTimestampBetween(any(LocalDateTime.class), any(LocalDateTime.class)))
.thenReturn(List.of(existingCpuAlert));
- OperatingSystemMXBean osMock = mock(OperatingSystemMXBean.class);
- when(osMock.getSystemLoadAverage()).thenReturn(2.0); // loadAvg >= 0 → Math.min branch L85
- when(osMock.getAvailableProcessors()).thenReturn(4); // cpuUsage = 50% > -1
+ com.sun.management.OperatingSystemMXBean osMock =
+ mock(com.sun.management.OperatingSystemMXBean.class);
+ when(osMock.getProcessCpuLoad()).thenReturn(0.5); // >= 0 → Math.min branch → cpuUsage=50%
try (MockedStatic