This is an automated email from the ASF dual-hosted git repository.

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git


The following commit(s) were added to refs/heads/rocketmq-studio by this push:
     new 9c180ae64 fix(alert): require metric equality when reconciling native 
alert presence (#3145)
9c180ae64 is described below

commit 9c180ae641976873e16aec7cd41544af49bb627b
Author: 烤化の初雪 <[email protected]>
AuthorDate: Mon Sep 7 17:46:56 2026 +0800

    fix(alert): require metric equality when reconciling native alert presence 
(#3145)
    
    Registry rows were not the only identity confusion: native alert
    reconciliation treated any sample matching a rule's instance and
    resource labels as proof that the rule's own metric is still reported.
    Because fingerprints are derived from ruleId + instanceId + labels, a
    different metric emitted with the same labels (e.g. consumer.lag.total
    for a group whose consumer.delay.seconds sample disappeared) kept the
    stale FIRING/ACKED state active forever and suppressed the RESOLVED
    event and recovery notification.
    
    Require the sample metric to equal the rule metric when building the
    reconciliation presentKeys, so a missing metric in a successful
    collection scope resolves its prior active state even when another
    same-label metric remains.
    
    Regression test: 
resolvesMissingMetricEvenWhenAnotherMetricSharesTheSameLabelsTest
    (fails before the fix: state was never saved as RESOLVED).
    
    Fixes #3104
    
    Co-authored-by: unbridled-41 
<[email protected]>
---
 .../studio/ops/alert/NativeAlertProcessor.java     |  4 +-
 .../studio/ops/alert/NativeAlertProcessorTest.java | 43 ++++++++++++++++++++++
 2 files changed, 46 insertions(+), 1 deletion(-)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/ops/alert/NativeAlertProcessor.java
 
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/NativeAlertProcessor.java
index fad40db82..2ae7b22d1 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/ops/alert/NativeAlertProcessor.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/NativeAlertProcessor.java
@@ -24,6 +24,7 @@ import 
org.apache.rocketmq.studio.cluster.metrics.MetricSample;
 import org.apache.rocketmq.studio.common.domain.enums.AlertLevel;
 import org.springframework.stereotype.Component;
 import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
 
 import java.time.Instant;
 import java.time.LocalDateTime;
@@ -97,7 +98,7 @@ public class NativeAlertProcessor {
         List<AlertRuleVO> rules = 
alertService.listRules(scope.domain()).stream()
                 .filter(rule -> rule.getId() != null)
                 .filter(AlertRuleVO::isEnabled)
-                .filter(rule -> scope.metricKeys().contains(rule.getMetric()))
+                .filter(rule -> 
scope.metricKeys().contains(StringUtils.trimWhitespace(rule.getMetric())))
                 .filter(rule -> rule.getInstanceId() == null || 
scope.instanceId().equals(rule.getInstanceId()))
                 .toList();
         if (rules.isEmpty()) {
@@ -106,6 +107,7 @@ public class NativeAlertProcessor {
         Set<AlertStateKey> presentKeys = samples.stream()
                 .filter(scope::contains)
                 .flatMap(sample -> rules.stream()
+                        .filter(rule -> 
sample.metricKey().equals(StringUtils.trimWhitespace(rule.getMetric())))
                         .filter(rule -> 
NativeAlertRuleScopeMatcher.matches(rule, sample))
                         .map(rule -> new AlertStateKey(rule.getId(),
                                 AlertFingerprint.of(rule.getId(), 
sample.instanceId(), sample.labels()))))
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/ops/alert/NativeAlertProcessorTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/NativeAlertProcessorTest.java
index e7f530711..54c462407 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/ops/alert/NativeAlertProcessorTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/NativeAlertProcessorTest.java
@@ -477,6 +477,49 @@ class NativeAlertProcessorTest {
         verify(alerts, never()).saveAlert(any(SystemAlertVO.class));
     }
 
+    @Test
+    void resolvesMissingMetricEvenWhenAnotherMetricSharesTheSameLabelsTest() {
+        AlertService service = mock(AlertService.class);
+        AlertRuleVO rule = 
AlertRuleVO.builder().id(1L).domain(AlertDomain.BUSINESS).name("Orders delay")
+                
.metric("consumer.delay.seconds").operator(">").threshold(10).enabled(true)
+                
.instanceId("local").consumerGroup("orders").consecutiveSamples(1).build();
+        
when(service.listRules(AlertDomain.BUSINESS)).thenReturn(List.of(rule));
+        MetricSample previousDelay = new 
MetricSample("consumer.delay.seconds", AlertDomain.BUSINESS, "local",
+                null, Map.of("consumerGroup", "orders"), 120D, 
MetricAvailability.AVAILABLE, Instant.now());
+        AlertStateKey key = new AlertStateKey(rule.getId(),
+                AlertFingerprint.of(rule.getId(), previousDelay.instanceId(), 
previousDelay.labels()));
+        ActiveAlertState active = new ActiveAlertState(key,
+                new AlertRuleState(AlertStateStatus.FIRING, 1, 120D, 
previousDelay.collectedAt().minusSeconds(60),
+                        previousDelay.collectedAt().minusSeconds(60), 
previousDelay.collectedAt().minusSeconds(60),
+                        null),
+                previousDelay.instanceId(), previousDelay.labels());
+        AlertStateRepository states = mock(AlertStateRepository.class);
+        when(states.findActive(any(MetricCollectionScope.class), 
eq(List.of(rule)))).thenReturn(List.of(active));
+        when(states.save(eq(key), any(AlertRuleState.class))).thenReturn(true);
+        AlertRepository alerts = mock(AlertRepository.class);
+        when(alerts.saveAlert(any(SystemAlertVO.class))).thenAnswer(invocation 
-> invocation.getArgument(0));
+        NotificationOutboxService outbox = 
mock(NotificationOutboxService.class);
+
+        MetricSample lagSample = new MetricSample("consumer.lag.total", 
AlertDomain.BUSINESS, "local",
+                null, Map.of("consumerGroup", "orders"), 5D, 
MetricAvailability.AVAILABLE, Instant.now());
+
+        new NativeAlertProcessor(service,
+                new NativeAlertEvaluationService(new AlertRuleEvaluator(), new 
AlertStateMachine(), states,
+                        mock(MetricSnapshotRepository.class), alerts, outbox, 
suppression()),
+                new AlertStateMachine(), states, alerts, outbox, suppression())
+                .processSuccessfulCollection(new 
MetricCollectionScope(AlertDomain.BUSINESS, "local",
+                        java.util.Set.of("consumer.delay.seconds", 
"consumer.lag.total")), List.of(lagSample));
+
+        org.mockito.ArgumentCaptor<AlertRuleState> state = 
org.mockito.ArgumentCaptor.forClass(AlertRuleState.class);
+        org.mockito.ArgumentCaptor<SystemAlertVO> event = 
org.mockito.ArgumentCaptor.forClass(SystemAlertVO.class);
+        verify(states).save(eq(key), state.capture());
+        
assertThat(state.getValue().status()).isEqualTo(AlertStateStatus.RESOLVED);
+        verify(alerts).saveAlert(event.capture());
+        
assertThat(event.getValue().getTransition()).isEqualTo(AlertStateTransition.RESOLVED.name());
+        
assertThat(event.getValue().getLabels()).isEqualTo(previousDelay.labels());
+        verify(outbox).enqueue(any(SystemAlertVO.class), eq(rule), 
eq(previousDelay.labels()));
+    }
+
     @Test
     void 
doesNotResolveMissingActiveStateWhenCollectionReportsWholeScopeUnavailableTest()
 {
         AlertService service = mock(AlertService.class);

Reply via email to