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) {