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

Aias00 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hertzbeat.git


The following commit(s) were added to refs/heads/master by this push:
     new 4cf968a3f4 [fix] Fix notice rule label matching against grouped alerts 
(#4191)
4cf968a3f4 is described below

commit 4cf968a3f49bf71029ec3bb1476fd82f31cfe20e
Author: Duansg <[email protected]>
AuthorDate: Sun Jul 12 23:34:45 2026 +0800

    [fix] Fix notice rule label matching against grouped alerts (#4191)
---
 .../service/impl/NoticeConfigServiceImpl.java      | 26 +++---
 .../alert/service/NoticeConfigServiceTest.java     | 95 ++++++++++++++++++++++
 2 files changed, 111 insertions(+), 10 deletions(-)

diff --git 
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/NoticeConfigServiceImpl.java
 
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/NoticeConfigServiceImpl.java
index f7f62ab8b7..dc4521aabb 100644
--- 
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/NoticeConfigServiceImpl.java
+++ 
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/NoticeConfigServiceImpl.java
@@ -69,18 +69,18 @@ import java.util.stream.Collectors;
 @Transactional(rollbackFor = Exception.class)
 @Slf4j
 public class NoticeConfigServiceImpl implements NoticeConfigService, 
CommandLineRunner {
-    
+
     private static final Map<Byte, NoticeTemplate> PRESET_TEMPLATE = new 
HashMap<>(16);
-    
+
     @Autowired
     private NoticeReceiverDao noticeReceiverDao;
 
     @Autowired
     private NoticeRuleDao noticeRuleDao;
-    
+
     @Autowired
     private NoticeTemplateDao noticeTemplateDao;
-    
+
     @Autowired
     @Lazy
     private AlertNoticeDispatch dispatcherAlarm;
@@ -212,24 +212,30 @@ public class NoticeConfigServiceImpl implements 
NoticeConfigService, CommandLine
         }
 
         // The temporary rule is to forward all, and then implement more 
matching rules: alarm status selection, monitoring type selection, etc.
+        // TODO: This matches an already-grouped alert against notice rules 
(group-then-route). It cannot fully
+        //  separate alerts that were grouped together but should reach 
different receivers, so a rule matched by
+        //  one alert still notifies the whole group. The ideal design is 
route-then-group (like Alertmanager):
+        //  route each single alert by its labels first, then group per 
receiver. Tracked as a follow-up to #3852.
         return rules.stream()
                 .filter(rule -> {
                     if (!rule.isFilterAll()) {
-                        // filter labels
+                        // filter labels: a rule matches when ANY single alert 
in the group carries
                         if (rule.getLabels() != null && 
!rule.getLabels().isEmpty()) {
-                            boolean labelMatch = 
rule.getLabels().entrySet().stream().allMatch(labelItem -> {
-                                if 
(!alert.getCommonLabels().containsKey(labelItem.getKey())) {
+                            List<SingleAlert> singleAlerts = alert.getAlerts();
+                            boolean labelMatch = singleAlerts != null && 
singleAlerts.stream().anyMatch(singleAlert -> {
+                                Map<String, String> alertLabels = 
singleAlert.getLabels();
+                                if (alertLabels == null) {
                                     return false;
                                 }
-                                String alertLabelValue = 
alert.getCommonLabels().get(labelItem.getKey());
-                                return Objects.equals(labelItem.getValue(), 
alertLabelValue);
+                                return 
rule.getLabels().entrySet().stream().allMatch(labelItem ->
+                                        Objects.equals(labelItem.getValue(), 
alertLabels.get(labelItem.getKey())));
                             });
                             if (!labelMatch) {
                                 return false;
                             }
                         }
                     }
-                    
+
                     LocalDateTime nowDate = LocalDateTime.now();
                     // filter day
                     int currentDayOfWeek = 
nowDate.toLocalDate().getDayOfWeek().getValue();
diff --git 
a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/NoticeConfigServiceTest.java
 
b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/NoticeConfigServiceTest.java
index 9d06aa26db..81a9c2c24b 100644
--- 
a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/NoticeConfigServiceTest.java
+++ 
b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/NoticeConfigServiceTest.java
@@ -22,10 +22,13 @@ import org.apache.hertzbeat.alert.dao.NoticeRuleDao;
 import org.apache.hertzbeat.alert.dao.NoticeTemplateDao;
 import org.apache.hertzbeat.alert.notice.AlertNoticeDispatch;
 import org.apache.hertzbeat.alert.service.impl.NoticeConfigServiceImpl;
+import org.apache.hertzbeat.common.cache.CacheFactory;
 import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
 import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
 import org.apache.hertzbeat.common.entity.alerter.NoticeRule;
 import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
+import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
+import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
@@ -39,9 +42,12 @@ import org.springframework.data.domain.Sort;
 import org.springframework.data.jpa.domain.Specification;
 
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
@@ -284,4 +290,93 @@ class NoticeConfigServiceTest {
         noticeConfigService.sendTestMsg(noticeReceiver);
         verify(dispatcherAlarm, times(1)).sendNoticeMsg(eq(noticeReceiver), 
eq(noticeTemplate), any(GroupAlert.class));
     }
+
+    @AfterEach
+    void tearDown() {
+        CacheFactory.clearNoticeCache();
+    }
+
+    private GroupAlert buildGroupAlertWithPartialLabel() {
+        SingleAlert alertA = SingleAlert.builder()
+                .labels(new HashMap<>(Map.of("alertname", "cpu", "instance", 
"s1", "department", "algorithm")))
+                .build();
+        SingleAlert alertB = SingleAlert.builder()
+                .labels(new HashMap<>(Map.of("alertname", "cpu", "instance", 
"s2")))
+                .build();
+        return GroupAlert.builder()
+                .commonLabels(new HashMap<>(Map.of("alertname", "cpu")))
+                .alerts(Arrays.asList(alertA, alertB))
+                .build();
+    }
+
+    /**
+     * A rule scoped to a label carried by only part of the group's alerts 
must still match,
+     * even though that label is absent from commonLabels. Regression test for 
issue #3852.
+     */
+    @Test
+    void getReceiverFilterRuleMatchesLabelOnPartialAlert() {
+        NoticeRule rule = new NoticeRule();
+        rule.setId(1L);
+        rule.setName("algorithm-team");
+        rule.setFilterAll(false);
+        rule.setLabels(new HashMap<>(Map.of("department", "algorithm")));
+        CacheFactory.setNoticeCache(List.of(rule));
+
+        List<NoticeRule> matched = 
noticeConfigService.getReceiverFilterRule(buildGroupAlertWithPartialLabel());
+
+        assertEquals(1, matched.size());
+        assertEquals(1L, matched.get(0).getId());
+    }
+
+    /**
+     * A rule whose label value is not present on any alert in the group must 
not match.
+     */
+    @Test
+    void getReceiverFilterRuleSkipsRuleWithUnmatchedLabel() {
+        NoticeRule rule = new NoticeRule();
+        rule.setId(2L);
+        rule.setName("infra-team");
+        rule.setFilterAll(false);
+        rule.setLabels(new HashMap<>(Map.of("department", "infra")));
+        CacheFactory.setNoticeCache(List.of(rule));
+
+        List<NoticeRule> matched = 
noticeConfigService.getReceiverFilterRule(buildGroupAlertWithPartialLabel());
+
+        assertTrue(matched.isEmpty());
+    }
+
+    /**
+     * A rule requiring several labels matches only when a single alert 
carries all of them,
+     * preventing false matches assembled across different alerts in the group.
+     */
+    @Test
+    void getReceiverFilterRuleRequiresAllLabelsOnSameAlert() {
+        NoticeRule rule = new NoticeRule();
+        rule.setId(3L);
+        rule.setName("algorithm-on-s2");
+        rule.setFilterAll(false);
+        rule.setLabels(new HashMap<>(Map.of("department", "algorithm", 
"instance", "s2")));
+        CacheFactory.setNoticeCache(List.of(rule));
+
+        List<NoticeRule> matched = 
noticeConfigService.getReceiverFilterRule(buildGroupAlertWithPartialLabel());
+
+        assertTrue(matched.isEmpty());
+    }
+
+    /**
+     * A forward-all rule keeps matching regardless of labels.
+     */
+    @Test
+    void getReceiverFilterRuleForwardAllAlwaysMatches() {
+        NoticeRule rule = new NoticeRule();
+        rule.setId(4L);
+        rule.setName("forward-all");
+        rule.setFilterAll(true);
+        CacheFactory.setNoticeCache(List.of(rule));
+
+        List<NoticeRule> matched = 
noticeConfigService.getReceiverFilterRule(buildGroupAlertWithPartialLabel());
+
+        assertEquals(1, matched.size());
+        assertEquals(4L, matched.get(0).getId());
+    }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to