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

commit 1aa3c23dc715340b61b2d475d9652026a84a61ac
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 00000000..472b6a57
--- /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 7336481e..9de4f2c5 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 00000000..88d24d80
--- /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 2fd408c2..c32d4448 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 00000000..6c50ce2a
--- /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();
+    }
+}

Reply via email to