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 fd7001d feat: add alert rules YAML endpoint (#507)
fd7001d is described below
commit fd7001dcef60d4ab39f4b898e73283c8003a040d
Author: aias00 <[email protected]>
AuthorDate: Thu Jul 23 22:51:35 2026 -0700
feat: add alert rules YAML endpoint (#507)
Add alert rules Prometheus YAML export endpoint with default RocketMQ alert
template fallback.
---
.../studio/ops/alert/AlertRulesYamlVO.java | 28 ++++
.../rocketmq/studio/ops/alert/AlertService.java | 155 +++++++++++++++++++++
.../studio/ops/alert/LegacyAlertController.java | 36 +++++
.../studio/ops/alert/AlertServiceTest.java | 37 +++++
.../ops/alert/LegacyAlertControllerTest.java | 55 ++++++++
5 files changed, 311 insertions(+)
diff --git
a/server/src/main/java/com/rocketmq/studio/ops/alert/AlertRulesYamlVO.java
b/server/src/main/java/com/rocketmq/studio/ops/alert/AlertRulesYamlVO.java
new file mode 100644
index 0000000..472b6a5
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/ops/alert/AlertRulesYamlVO.java
@@ -0,0 +1,28 @@
+/*
+ * 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 com.rocketmq.studio.ops.alert;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class AlertRulesYamlVO {
+ private String rules;
+}
diff --git
a/server/src/main/java/com/rocketmq/studio/ops/alert/AlertService.java
b/server/src/main/java/com/rocketmq/studio/ops/alert/AlertService.java
index 7336481..9de4f2c 100644
--- a/server/src/main/java/com/rocketmq/studio/ops/alert/AlertService.java
+++ b/server/src/main/java/com/rocketmq/studio/ops/alert/AlertService.java
@@ -20,6 +20,7 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
+import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@@ -36,6 +37,32 @@ public class AlertService {
return alertRepository.findAllRules();
}
+ public String exportPrometheusRulesYaml() {
+ List<AlertRuleVO> rules = alertRepository.findAllRules();
+ List<PrometheusAlertRule> prometheusRules = rules.isEmpty()
+ ? defaultPrometheusRules()
+ : rules.stream().map(this::toPrometheusRule).toList();
+
+ StringBuilder yaml = new StringBuilder();
+ yaml.append("groups:\n");
+ int index = 1;
+ for (PrometheusAlertRule rule : prometheusRules) {
+ yaml.append(" - name: ").append(rule.group()).append('\n');
+ yaml.append(" rules:\n");
+ yaml.append(" # Rule ").append(index++).append(":
").append(rule.alert()).append('\n');
+ yaml.append(" - alert: ").append(rule.alert()).append('\n');
+ yaml.append(" expr: ").append(rule.expr()).append('\n');
+ yaml.append(" for: ").append(rule.duration()).append('\n');
+ yaml.append(" labels:\n");
+ yaml.append(" severity:
").append(rule.severity()).append('\n');
+ yaml.append(" team: ").append(rule.team()).append('\n');
+ yaml.append(" annotations:\n");
+ yaml.append(" summary:
\"").append(escapeYaml(rule.summary())).append("\"\n");
+ yaml.append(" description:
\"").append(escapeYaml(rule.description())).append("\"\n");
+ }
+ return yaml.toString();
+ }
+
public AlertRuleVO createRule(AlertRuleVO rule) {
log.info("Creating alert rule: {}", rule.getName());
@@ -90,4 +117,132 @@ public class AlertService {
log.info("Clearing acknowledged system alerts");
return alertRepository.deleteAcknowledgedAlerts();
}
+
+ private List<PrometheusAlertRule> defaultPrometheusRules() {
+ List<PrometheusAlertRule> rules = new ArrayList<>();
+ rules.add(rule("rocketmq-broker.rules", "RocketMQBrokerDown",
"up{job=~\".*rocketmq.*\"} == 0", "1m",
+ "critical", "broker", "RocketMQ broker is down",
+ "A RocketMQ broker scrape target has been unavailable for more
than 1 minute."));
+ rules.add(rule("rocketmq-consumer.rules", "RocketMQConsumerLagHigh",
+ "rocketmq_consumer_lag_messages > 100000", "5m",
+ "warning", "consumer", "Consumer lag is high",
+ "A consumer group has accumulated more than 100000
messages."));
+ rules.add(rule("rocketmq-consumer.rules",
"RocketMQConsumerLagCritical",
+ "rocketmq_consumer_lag_messages > 1000000", "5m",
+ "critical", "consumer", "Consumer lag is critical",
+ "A consumer group has accumulated more than 1000000
messages."));
+ rules.add(rule("rocketmq-client.rules",
"RocketMQProducerSendLatencyHigh",
+ "rocketmq_producer_send_to_back_rt > 1000", "5m",
+ "warning", "client", "Producer send latency is high",
+ "Producer send-to-broker latency has stayed above 1000 ms."));
+ rules.add(rule("rocketmq-broker.rules",
"RocketMQProcessorWatermarkHigh",
+ "rocketmq_processor_watermark > 80", "5m",
+ "warning", "broker", "Processor watermark is high",
+ "Broker processor watermark is above 80 percent."));
+ rules.add(rule("rocketmq-topic.rules", "RocketMQMessageInDrop",
+ "rate(rocketmq_messages_in_total[5m]) == 0", "10m",
+ "info", "topic", "No incoming messages",
+ "No incoming messages have been observed for 10 minutes."));
+ rules.add(rule("rocketmq-consumer.rules", "RocketMQMessageOutDrop",
+ "rate(rocketmq_messages_out_total[5m]) == 0", "10m",
+ "info", "consumer", "No outgoing messages",
+ "No outgoing messages have been observed for 10 minutes."));
+ return rules;
+ }
+
+ private PrometheusAlertRule rule(String group, String alert, String expr,
String duration,
+ String severity, String team, String
summary, String description) {
+ return new PrometheusAlertRule(group, alert, expr, duration, severity,
team, summary, description);
+ }
+
+ private PrometheusAlertRule toPrometheusRule(AlertRuleVO rule) {
+ String team = inferTeam(rule.getMetric());
+ return new PrometheusAlertRule(
+ groupName(team),
+ alertName(rule),
+ expression(rule),
+ duration(rule),
+ "warning",
+ team,
+ summary(rule),
+ description(rule));
+ }
+
+ private String groupName(String team) {
+ if ("client".equals(team)) {
+ return "rocketmq-client.rules";
+ }
+ if ("consumer".equals(team)) {
+ return "rocketmq-consumer.rules";
+ }
+ if ("topic".equals(team)) {
+ return "rocketmq-topic.rules";
+ }
+ return "rocketmq-broker.rules";
+ }
+
+ private String alertName(AlertRuleVO rule) {
+ return hasText(rule.getName()) ?
rule.getName().replaceAll("[^A-Za-z0-9_]", "") : "RocketMQAlert";
+ }
+
+ private String expression(AlertRuleVO rule) {
+ String metric = hasText(rule.getMetric()) ? rule.getMetric() :
"rocketmq_consumer_lag_messages";
+ String operator = hasText(rule.getOperator()) ? rule.getOperator() :
">";
+ return metric + " " + operator + " " +
formatThreshold(rule.getThreshold());
+ }
+
+ private String formatThreshold(double threshold) {
+ if (threshold == Math.rint(threshold)) {
+ return Long.toString((long) threshold);
+ }
+ return Double.toString(threshold);
+ }
+
+ private String duration(AlertRuleVO rule) {
+ return hasText(rule.getDuration()) ? rule.getDuration() : "5m";
+ }
+
+ private String inferTeam(String metric) {
+ if (!hasText(metric)) {
+ return "broker";
+ }
+ if (metric.contains("consumer") || metric.contains("lag")) {
+ return "consumer";
+ }
+ if (metric.contains("producer") || metric.contains("client")) {
+ return "client";
+ }
+ if (metric.contains("topic") || metric.contains("messages_in") ||
metric.contains("messages_out")) {
+ return "topic";
+ }
+ return "broker";
+ }
+
+ private String summary(AlertRuleVO rule) {
+ String description = rule.getDescription();
+ if (hasText(description) && description.contains(" - ")) {
+ return description.substring(0, description.indexOf(" - "));
+ }
+ return hasText(rule.getName()) ? rule.getName() : "RocketMQ alert";
+ }
+
+ private String description(AlertRuleVO rule) {
+ String description = rule.getDescription();
+ if (hasText(description) && description.contains(" - ")) {
+ return description.substring(description.indexOf(" - ") + 3);
+ }
+ return hasText(description) ? description : "RocketMQ alert condition
matched.";
+ }
+
+ private String escapeYaml(String value) {
+ return value.replace("\\", "\\\\").replace("\"", "\\\"");
+ }
+
+ private boolean hasText(String value) {
+ return value != null && !value.trim().isEmpty();
+ }
+
+ private record PrometheusAlertRule(String group, String alert, String
expr, String duration,
+ String severity, String team, String
summary, String description) {
+ }
}
diff --git
a/server/src/main/java/com/rocketmq/studio/ops/alert/LegacyAlertController.java
b/server/src/main/java/com/rocketmq/studio/ops/alert/LegacyAlertController.java
new file mode 100644
index 0000000..88d24d8
--- /dev/null
+++
b/server/src/main/java/com/rocketmq/studio/ops/alert/LegacyAlertController.java
@@ -0,0 +1,36 @@
+/*
+ * 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 com.rocketmq.studio.ops.alert;
+
+import com.rocketmq.studio.common.domain.Result;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/api/alert")
+@RequiredArgsConstructor
+public class LegacyAlertController {
+
+ private final AlertService alertService;
+
+ @GetMapping("/rules")
+ public Result<AlertRulesYamlVO> getRules() {
+ return Result.ok(new
AlertRulesYamlVO(alertService.exportPrometheusRulesYaml()));
+ }
+}
diff --git
a/server/src/test/java/com/rocketmq/studio/ops/alert/AlertServiceTest.java
b/server/src/test/java/com/rocketmq/studio/ops/alert/AlertServiceTest.java
index 2fd408c..c32d444 100644
--- a/server/src/test/java/com/rocketmq/studio/ops/alert/AlertServiceTest.java
+++ b/server/src/test/java/com/rocketmq/studio/ops/alert/AlertServiceTest.java
@@ -70,6 +70,43 @@ class AlertServiceTest {
assertThat(result).isEmpty();
}
+ @Test
+ void
exportPrometheusRulesYamlShouldReturnDefaultRulesWhenRepositoryIsEmpty() {
+
when(alertRepository.findAllRules()).thenReturn(Collections.emptyList());
+
+ String result = alertService.exportPrometheusRulesYaml();
+
+ assertThat(result)
+ .contains("groups:")
+ .contains("# Rule 1: RocketMQBrokerDown")
+ .contains("up{job=~\".*rocketmq.*\"} == 0")
+ .contains("rocketmq_consumer_lag_messages > 100000")
+ .contains("rocketmq_producer_send_to_back_rt > 1000")
+ .contains("severity: critical");
+ }
+
+ @Test
+ void exportPrometheusRulesYamlShouldConvertConfiguredRules() {
+ AlertRuleVO rule = AlertRuleVO.builder()
+ .name("High Lag Alert")
+ .metric("rocketmq_consumer_lag_messages")
+ .operator(">")
+ .threshold(5000)
+ .duration("3m")
+ .description("Lag too high")
+ .build();
+ when(alertRepository.findAllRules()).thenReturn(List.of(rule));
+
+ String result = alertService.exportPrometheusRulesYaml();
+
+ assertThat(result)
+ .contains("rocketmq-consumer.rules")
+ .contains("# Rule 1: HighLagAlert")
+ .contains("expr: rocketmq_consumer_lag_messages > 5000")
+ .contains("for: 3m")
+ .contains("description: \"Lag too high\"");
+ }
+
@Test
void createRuleShouldAssignId() {
AlertRuleVO input = AlertRuleVO.builder().name("New
Rule").metric("tps")
diff --git
a/server/src/test/java/com/rocketmq/studio/ops/alert/LegacyAlertControllerTest.java
b/server/src/test/java/com/rocketmq/studio/ops/alert/LegacyAlertControllerTest.java
new file mode 100644
index 0000000..6c50ce2
--- /dev/null
+++
b/server/src/test/java/com/rocketmq/studio/ops/alert/LegacyAlertControllerTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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 com.rocketmq.studio.ops.alert;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import
org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static
org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static
org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@WebMvcTest(LegacyAlertController.class)
+@AutoConfigureMockMvc(addFilters = false)
+class LegacyAlertControllerTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @MockBean
+ private AlertService alertService;
+
+ @Test
+ void getRulesShouldReturnYamlPayloadForLegacyStudioPage() throws Exception
{
+ String yaml = "groups:\n - name: rocketmq-broker.rules\n rules:\n"
+ + " # Rule 1: RocketMQBrokerDown\n - alert:
RocketMQBrokerDown\n";
+ when(alertService.exportPrometheusRulesYaml()).thenReturn(yaml);
+
+ mockMvc.perform(get("/api/alert/rules"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(200))
+ .andExpect(jsonPath("$.data.rules").value(yaml));
+
+ verify(alertService).exportPrometheusRulesYaml();
+ }
+}