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 be5265a8 fix(alert): validation, dedup and formatting hardening 
(#1405, #1420, #1432, #1440)
be5265a8 is described below

commit be5265a8e067ea4990d271238a82321be6d77f22
Author: Yu Xinqiang <[email protected]>
AuthorDate: Tue Aug 11 00:25:57 2026 +0800

    fix(alert): validation, dedup and formatting hardening (#1405, #1420, 
#1432, #1440)
    
    * [ISSUE #1404] Deduplicate alert names in Prometheus YAML export
    
    The alertName() method strips non-alphanumeric characters from rule
    names, causing collisions when two rules normalize to the same name
    (e.g. 'Broker Down!' and 'Broker_Down' both become 'BrokerDown').
    Prometheus rejects rule files with duplicate alert names.
    
    This fix:
    - Tracks used alert names per group during YAML export
    - Appends _2, _3, etc. suffixes to resolve collisions
    - Adds ensureUniqueAlertName() helper method
    
    Fixes #1404
    
    * [ISSUE #1419] Validate alert rule operator, metric, and duration fields
    
    The alert rule operator, metric name, and duration fields were injected
    directly into PromQL expressions without validation. Malformed values
    could produce invalid PromQL that causes Prometheus rule loading to
    fail. This fix:
    
    - Validates operator against a whitelist of valid comparison operators
    - Validates metric name against Prometheus metric name pattern
    - Validates duration against Prometheus duration format
    - Falls back to safe defaults for invalid values
    
    Fixes #1419
    
    * [ISSUE #1431] Reject NaN and Infinity in alert threshold formatting
    
    formatThreshold produced 'NaN' or 'Infinity' strings for invalid
    double values, causing Prometheus to reject the exported rules YAML.
    
    Fixes #1431
    
    * [ISSUE #1439] Prefix alert names starting with a digit
    
    Prometheus alert names must start with [a-zA-Z_], not a digit.
    The alertName method stripped non-alphanumeric chars but did not
    validate the first character, producing invalid names like '5xxErrorRate'.
    
    Fixes #1439
---
 .../rocketmq/studio/ops/alert/AlertService.java    | 50 +++++++++++++++++++---
 1 file changed, 44 insertions(+), 6 deletions(-)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/ops/alert/AlertService.java 
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/AlertService.java
index 6f9c2ffb..a163bd76 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/ops/alert/AlertService.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/AlertService.java
@@ -25,8 +25,10 @@ import org.springframework.stereotype.Service;
 import java.util.ArrayList;
 import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.HashSet;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Set;
 import java.util.UUID;
 
 @Slf4j
@@ -34,6 +36,10 @@ import java.util.UUID;
 @RequiredArgsConstructor
 public class AlertService {
 
+    private static final Set<String> VALID_OPERATORS = Set.of(">", ">=", "<", 
"<=", "==", "!=");
+    private static final Pattern METRIC_NAME_PATTERN = 
Pattern.compile("^[a-zA-Z_:][a-zA-Z0-9_:]*$");
+    private static final Pattern DURATION_PATTERN = 
Pattern.compile("^\\d+(ms|s|m|h|d|w|y)$");
+
     private final AlertRepository alertRepository;
     private final AlertRuleAssetService alertRuleAssetService;
     private final OperationAuditService operationAuditService;
@@ -64,9 +70,11 @@ public class AlertService {
         for (Map.Entry<String, List<PrometheusAlertRule>> group : 
rulesByGroup.entrySet()) {
             yaml.append("  - name: ").append(group.getKey()).append('\n');
             yaml.append("    rules:\n");
+            Set<String> usedAlertNames = new HashSet<>();
             for (PrometheusAlertRule rule : group.getValue()) {
-                yaml.append("      # Rule ").append(index++).append(": 
").append(rule.alert()).append('\n');
-                yaml.append("      - alert: 
").append(rule.alert()).append('\n');
+                String uniqueAlertName = ensureUniqueAlertName(rule.alert(), 
usedAlertNames);
+                yaml.append("      # Rule ").append(index++).append(": 
").append(uniqueAlertName).append('\n');
+                yaml.append("      - alert: 
").append(uniqueAlertName).append('\n');
                 yaml.append("        expr: ").append(rule.expr()).append('\n');
                 yaml.append("        for: 
").append(rule.duration()).append('\n');
                 yaml.append("        labels:\n");
@@ -191,17 +199,43 @@ public class AlertService {
         return "rocketmq-broker.rules";
     }
 
+    private String ensureUniqueAlertName(String baseName, Set<String> 
usedAlertNames) {
+        String uniqueName = baseName;
+        int suffix = 2;
+        while (!usedAlertNames.add(uniqueName)) {
+            uniqueName = baseName + "_" + suffix++;
+        }
+        return uniqueName;
+    }
+
     private String alertName(AlertRuleVO rule) {
         String alertName = hasText(rule.getName()) ? 
rule.getName().replaceAll("[^A-Za-z0-9_]", "") : "";
-        return alertName.isEmpty() ? "RocketMQAlert" : alertName;
+        if (alertName.isEmpty()) {
+            return "RocketMQAlert";
+        }
+        // Prometheus alert names must start with [a-zA-Z_], not a digit
+        if (Character.isDigit(alertName.charAt(0))) {
+            alertName = "A_" + alertName;
+        }
+        return alertName;
     }
 
     private String expression(AlertRuleVO rule) {
-        String metric = hasText(rule.getMetric()) ? rule.getMetric() : 
"rocketmq_consumer_lag_messages";
-        String operator = hasText(rule.getOperator()) ? rule.getOperator() : 
">";
+        String metric = validateMetric(rule.getMetric());
+        String operator = validateOperator(rule.getOperator());
         return metric + labelSelector(rule) + " " + operator + " " + 
formatThreshold(rule.getThreshold());
     }
 
+    private String validateMetric(String metric) {
+        String normalized = hasText(metric) ? metric.trim() : 
"rocketmq_consumer_lag_messages";
+        return METRIC_NAME_PATTERN.matcher(normalized).matches() ? normalized 
: "rocketmq_consumer_lag_messages";
+    }
+
+    private String validateOperator(String operator) {
+        String normalized = hasText(operator) ? operator.trim() : ">";
+        return VALID_OPERATORS.contains(normalized) ? normalized : ">";
+    }
+
     private String labelSelector(AlertRuleVO rule) {
         StringBuilder selector = new StringBuilder();
         appendLabel(selector, "cluster", rule.getClusterName());
@@ -237,6 +271,9 @@ public class AlertService {
     }
 
     private String formatThreshold(double threshold) {
+        if (!Double.isFinite(threshold)) {
+            return "0";
+        }
         if (threshold == Math.rint(threshold)) {
             return Long.toString((long) threshold);
         }
@@ -244,7 +281,8 @@ public class AlertService {
     }
 
     private String duration(AlertRuleVO rule) {
-        return hasText(rule.getDuration()) ? rule.getDuration() : "5m";
+        String dur = hasText(rule.getDuration()) ? rule.getDuration().trim() : 
"5m";
+        return DURATION_PATTERN.matcher(dur).matches() ? dur : "5m";
     }
 
     private String inferTeam(String metric) {

Reply via email to