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

Duansg 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 4c9ebb7d0a maintenance: canonicalize alert label fingerprints (#4283)
4c9ebb7d0a is described below

commit 4c9ebb7d0a448dd03acd901fb2c67cecb2049661
Author: Logic <[email protected]>
AuthorDate: Mon Aug 3 13:51:02 2026 +0800

    maintenance: canonicalize alert label fingerprints (#4283)
    
    Co-authored-by: Duansg <[email protected]>
---
 .../org/apache/hertzbeat/alert/util/AlertUtil.java | 34 ++++++++----
 .../alert/calculate/AlarmCacheManagerTest.java     | 27 +++++++++-
 .../alert/reduce/AlarmCommonReduceTest.java        | 24 +++++++++
 .../apache/hertzbeat/alert/util/AlertUtilTest.java | 61 ++++++++++++++++++++++
 4 files changed, 136 insertions(+), 10 deletions(-)

diff --git 
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/util/AlertUtil.java
 
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/util/AlertUtil.java
index a1ed180c3f..06cc39600a 100644
--- 
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/util/AlertUtil.java
+++ 
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/util/AlertUtil.java
@@ -17,10 +17,8 @@
 
 package org.apache.hertzbeat.alert.util;
 
-import java.util.Arrays;
-import java.util.List;
+import java.util.Comparator;
 import java.util.Map;
-import java.util.Objects;
 
 /**
  * alert util
@@ -28,13 +26,31 @@ import java.util.Objects;
 public class AlertUtil {
 
     /**
-     * calculate fingerprint
-     * @param fingerPrints finger prints
+     * Calculate an in-memory alert cache coordinate.
+     *
+     * <p>This value is rebuilt from persisted alert labels when the process
+     * starts. It is not the durable {@code SingleAlert.fingerprint} used by
+     * persistence, grouping, silence, or inhibition.</p>
+     *
+     * @param fingerPrints labels used by the calculator cache
+     * @return deterministic cache coordinate
      */
     public static String calculateFingerprint(Map<String, String> 
fingerPrints) {
-        List<String> keyList = 
fingerPrints.keySet().stream().filter(Objects::nonNull).sorted().toList();
-        List<String> valueList = 
fingerPrints.values().stream().filter(Objects::nonNull).sorted().toList();
-        return Arrays.hashCode(keyList.toArray(new String[0])) + "-"
-                + Arrays.hashCode(valueList.toArray(new String[0]));
+        StringBuilder canonicalLabels = new StringBuilder();
+        fingerPrints.entrySet().stream()
+                
.sorted(Map.Entry.comparingByKey(Comparator.nullsFirst(Comparator.naturalOrder())))
+                .forEach(entry -> {
+                    appendLengthPrefixed(canonicalLabels, entry.getKey());
+                    appendLengthPrefixed(canonicalLabels, entry.getValue());
+                });
+        return CryptoUtils.sha256Hex(canonicalLabels.toString());
+    }
+
+    private static void appendLengthPrefixed(StringBuilder target, String 
value) {
+        if (value == null) {
+            target.append("-1:");
+            return;
+        }
+        target.append(value.length()).append(':').append(value);
     }
 }
diff --git 
a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/AlarmCacheManagerTest.java
 
b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/AlarmCacheManagerTest.java
index 95a43ffbfa..c2a76092e3 100644
--- 
a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/AlarmCacheManagerTest.java
+++ 
b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/AlarmCacheManagerTest.java
@@ -34,6 +34,7 @@ import java.util.Map;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.mockito.Mockito.when;
 
 /**
@@ -132,4 +133,28 @@ public class AlarmCacheManagerTest {
         historicalSingleAlert = alarmCacheManager.getFiring(4L, fingerprint);
         assertNull(historicalSingleAlert);
     }
-}
\ No newline at end of file
+
+    @Test
+    void restartShouldRebuildCacheKeyWithoutChangingPersistedFingerprint() {
+        Map<String, String> labels = Map.of(
+                CommonConstants.LABEL_DEFINE_ID, "7",
+                CommonConstants.LABEL_ALERT_NAME, "disk_full",
+                "instance", "db-1");
+        SingleAlert persistedAlert = SingleAlert.builder()
+                .id(99L)
+                .fingerprint("alertname:disk_full,define_id:7,instance:db-1")
+                .labels(labels)
+                .status(CommonConstants.ALERT_STATUS_FIRING)
+                .build();
+        
when(singleAlertDao.querySingleAlertsByStatus(CommonConstants.ALERT_STATUS_FIRING))
+                .thenReturn(Collections.singletonList(persistedAlert));
+
+        alarmCacheManager = new AlarmCacheManager(singleAlertDao);
+        String rebuiltCacheKey = AlertUtil.calculateFingerprint(labels);
+        SingleAlert resolved = alarmCacheManager.removeFiring(7L, 
rebuiltCacheKey);
+
+        assertSame(persistedAlert, resolved);
+        assertEquals("alertname:disk_full,define_id:7,instance:db-1", 
resolved.getFingerprint());
+        assertNull(alarmCacheManager.getFiring(7L, rebuiltCacheKey));
+    }
+}
diff --git 
a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/reduce/AlarmCommonReduceTest.java
 
b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/reduce/AlarmCommonReduceTest.java
index 0208188529..bd8cdfe217 100644
--- 
a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/reduce/AlarmCommonReduceTest.java
+++ 
b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/reduce/AlarmCommonReduceTest.java
@@ -17,6 +17,7 @@
 
 package org.apache.hertzbeat.alert.reduce;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
@@ -28,6 +29,7 @@ import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
 import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
 import org.apache.hertzbeat.common.config.VirtualThreadProperties;
 import org.junit.jupiter.api.AfterEach;
@@ -85,6 +87,28 @@ class AlarmCommonReduceTest {
         assertTrue(virtualThread.get());
     }
 
+    @Test
+    void durableFingerprintShouldRemainIndependentFromCalculatorCacheKey() 
throws Exception {
+        CountDownLatch latch = new CountDownLatch(1);
+        AtomicReference<String> durableFingerprint = new AtomicReference<>();
+        doAnswer(invocation -> {
+            durableFingerprint.set(invocation.getArgument(0, 
SingleAlert.class).getFingerprint());
+            latch.countDown();
+            return null;
+        }).when(alarmGroupReduce).processGroupAlert(any(SingleAlert.class));
+        SingleAlert alert = SingleAlert.builder()
+                .labels(new HashMap<>(Map.of(
+                        "instance", "db-1",
+                        "alertname", "disk_full",
+                        "timestamp", "not-part-of-identity")))
+                .build();
+
+        alarmCommonReduce.reduceAndSendAlarm(alert);
+
+        assertTrue(latch.await(5, TimeUnit.SECONDS));
+        assertEquals("alertname:disk_full,instance:db-1", 
durableFingerprint.get());
+    }
+
     @Test
     void testReduceAndSendAlarmQueuesWhenConcurrencyLimitReached() throws 
Exception {
         VirtualThreadProperties properties = new VirtualThreadProperties(
diff --git 
a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/util/AlertUtilTest.java
 
b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/util/AlertUtilTest.java
new file mode 100644
index 0000000000..b634713068
--- /dev/null
+++ 
b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/util/AlertUtilTest.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.alert.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test case for {@link AlertUtil}.
+ */
+class AlertUtilTest {
+
+    @Test
+    void calculateFingerprintPreservesLabelPairing() {
+        Map<String, String> first = new LinkedHashMap<>();
+        first.put("environment", "production");
+        first.put("team", "payments");
+
+        Map<String, String> swapped = new LinkedHashMap<>();
+        swapped.put("environment", "payments");
+        swapped.put("team", "production");
+
+        assertNotEquals(
+                AlertUtil.calculateFingerprint(first),
+                AlertUtil.calculateFingerprint(swapped));
+    }
+
+    @Test
+    void calculateFingerprintIsIndependentOfMapIterationOrder() {
+        Map<String, String> first = new LinkedHashMap<>();
+        first.put("environment", "production");
+        first.put("team", "payments");
+
+        Map<String, String> reversed = new LinkedHashMap<>();
+        reversed.put("team", "payments");
+        reversed.put("environment", "production");
+
+        assertEquals(
+                AlertUtil.calculateFingerprint(first),
+                AlertUtil.calculateFingerprint(reversed));
+    }
+}


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

Reply via email to