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 ec94c7d7 feat: add AI tool for listing alert rules (#712)
ec94c7d7 is described below

commit ec94c7d75ab0808f0220a8e37fcbe934aa7a5adf
Author: yx9o <[email protected]>
AuthorDate: Mon Aug 3 11:11:22 2026 +0800

    feat: add AI tool for listing alert rules (#712)
---
 .../ops/ai/tool/AlertRuleListToolHandler.java      | 100 +++++++++++++++++++++
 .../src/main/resources/tool-catalog/rmq-tools.yaml |  63 +++++++++++++
 .../studio/ops/ai/tool/ToolCatalogTest.java        |   3 +-
 .../studio/ops/ai/tool/ToolGatewayServiceTest.java |  76 ++++++++++++++--
 4 files changed, 234 insertions(+), 8 deletions(-)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/AlertRuleListToolHandler.java
 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/AlertRuleListToolHandler.java
new file mode 100644
index 00000000..46344838
--- /dev/null
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/AlertRuleListToolHandler.java
@@ -0,0 +1,100 @@
+/*
+ * 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.rocketmq.studio.ops.ai.tool;
+
+import lombok.RequiredArgsConstructor;
+import org.apache.rocketmq.studio.ops.alert.AlertRuleVO;
+import org.apache.rocketmq.studio.ops.alert.AlertService;
+import org.springframework.stereotype.Component;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+@Component
+@RequiredArgsConstructor
+public class AlertRuleListToolHandler implements ToolHandler {
+
+    private static final String NAME = "rmq.alert.rule.list";
+
+    private final AlertService alertService;
+
+    @Override
+    public String name() {
+        return NAME;
+    }
+
+    @Override
+    public Object execute(Map<String, Object> input) {
+        String search = (String) input.get("search");
+        Boolean enabled = (Boolean) input.get("enabled");
+        return alertService.listRules().stream()
+                .filter(rule -> matchesEnabled(rule, enabled))
+                .filter(rule -> matchesSearch(rule, search))
+                .map(AlertRuleListToolHandler::safeProjection)
+                .toList();
+    }
+
+    private static boolean matchesEnabled(AlertRuleVO rule, Boolean enabled) {
+        return enabled == null || rule.isEnabled() == enabled;
+    }
+
+    private static boolean matchesSearch(AlertRuleVO rule, String search) {
+        if (search == null || search.isBlank()) {
+            return true;
+        }
+        String normalizedSearch = search.trim().toLowerCase(Locale.ROOT);
+        return contains(rule.getName(), normalizedSearch)
+                || contains(rule.getMetric(), normalizedSearch)
+                || contains(rule.getDescription(), normalizedSearch);
+    }
+
+    private static boolean contains(String value, String normalizedSearch) {
+        return value != null && 
value.toLowerCase(Locale.ROOT).contains(normalizedSearch);
+    }
+
+    private static Map<String, Object> safeProjection(AlertRuleVO rule) {
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("id", blankIfNull(rule.getId()));
+        result.put("name", require(rule.getName(), "name"));
+        result.put("metric", require(rule.getMetric(), "metric"));
+        result.put("operator", blankIfNull(rule.getOperator()));
+        result.put("threshold", rule.getThreshold());
+        result.put("thresholdUnit", blankIfNull(rule.getThresholdUnit()));
+        result.put("duration", blankIfNull(rule.getDuration()));
+        result.put("channels", copyList(rule.getChannels()));
+        result.put("enabled", rule.isEnabled());
+        result.put("description", blankIfNull(rule.getDescription()));
+        return result;
+    }
+
+    private static String require(String value, String field) {
+        if (value == null || value.isBlank()) {
+            throw new IllegalStateException("Alert rule " + field + " is 
unavailable");
+        }
+        return value;
+    }
+
+    private static List<String> copyList(List<String> value) {
+        return value == null ? List.of() : List.copyOf(value);
+    }
+
+    private static String blankIfNull(String value) {
+        return value == null ? "" : value;
+    }
+}
diff --git a/server/src/main/resources/tool-catalog/rmq-tools.yaml 
b/server/src/main/resources/tool-catalog/rmq-tools.yaml
index 1ce2d105..4b36f087 100644
--- a/server/src/main/resources/tool-catalog/rmq-tools.yaml
+++ b/server/src/main/resources/tool-catalog/rmq-tools.yaml
@@ -303,3 +303,66 @@ tools:
             type: integer
     viewHint: table
     deprecated: false
+  - name: rmq.alert.rule.list
+    cli:
+      resource: alert-rule
+      verb: list
+    description: List RocketMQ Studio alert rules.
+    riskLevel: L1
+    permission: alert:read
+    requiredCapabilities: []
+    inputSchema:
+      type: object
+      required:
+        - cluster
+      additionalProperties: false
+      properties:
+        cluster:
+          type: string
+          minLength: 1
+        search:
+          type: string
+          minLength: 1
+        enabled:
+          type: boolean
+    outputSchema:
+      type: array
+      items:
+        type: object
+        required:
+          - id
+          - name
+          - metric
+          - operator
+          - threshold
+          - thresholdUnit
+          - duration
+          - channels
+          - enabled
+          - description
+        additionalProperties: false
+        properties:
+          id:
+            type: string
+          name:
+            type: string
+          metric:
+            type: string
+          operator:
+            type: string
+          threshold:
+            type: number
+          thresholdUnit:
+            type: string
+          duration:
+            type: string
+          channels:
+            type: array
+            items:
+              type: string
+          enabled:
+            type: boolean
+          description:
+            type: string
+    viewHint: table
+    deprecated: false
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/ToolCatalogTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/ToolCatalogTest.java
index 50f10175..5c098e8c 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/ToolCatalogTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/ToolCatalogTest.java
@@ -43,7 +43,8 @@ class ToolCatalogTest {
                         "rmq.capabilities",
                         "rmq.dashboard.summary",
                         "rmq.topic.list",
-                        "rmq.group.list");
+                        "rmq.group.list",
+                        "rmq.alert.rule.list");
         assertThat(catalog.find("rmq.cluster.list")).isPresent();
         assertThat(catalog.find("rmq.unknown")).isEmpty();
     }
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/ToolGatewayServiceTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/ToolGatewayServiceTest.java
index 46a0699d..ae5ef118 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/ToolGatewayServiceTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/ToolGatewayServiceTest.java
@@ -30,6 +30,8 @@ import 
org.apache.rocketmq.studio.instance.group.ConsumerGroupVO;
 import org.apache.rocketmq.studio.instance.topic.MetadataService;
 import org.apache.rocketmq.studio.instance.topic.TopicVO;
 import org.apache.rocketmq.studio.ops.ai.AiToolVO;
+import org.apache.rocketmq.studio.ops.alert.AlertRuleVO;
+import org.apache.rocketmq.studio.ops.alert.AlertService;
 import org.apache.rocketmq.studio.ops.dashboard.ClusterOverviewVO;
 import org.apache.rocketmq.studio.ops.dashboard.DashboardDataVO;
 import org.apache.rocketmq.studio.ops.dashboard.DashboardService;
@@ -56,12 +58,14 @@ class ToolGatewayServiceTest {
     private ClusterService clusterService;
     private DashboardService dashboardService;
     private MetadataService metadataService;
+    private AlertService alertService;
     private CapabilityResolver capabilityResolver;
     private ClusterListToolHandler clusterListHandler;
     private CapabilitiesToolHandler capabilitiesHandler;
     private DashboardSummaryToolHandler dashboardSummaryHandler;
     private TopicListToolHandler topicListHandler;
     private ConsumerGroupListToolHandler consumerGroupListHandler;
+    private AlertRuleListToolHandler alertRuleListHandler;
     private ToolGatewayService gateway;
 
     @BeforeEach
@@ -70,19 +74,22 @@ class ToolGatewayServiceTest {
         clusterService = mock(ClusterService.class);
         dashboardService = mock(DashboardService.class);
         metadataService = mock(MetadataService.class);
+        alertService = mock(AlertService.class);
         capabilityResolver = new CapabilityResolver(clusterService);
         clusterListHandler = new ClusterListToolHandler(clusterService);
         capabilitiesHandler = new CapabilitiesToolHandler(clusterService, 
capabilityResolver);
         dashboardSummaryHandler = new 
DashboardSummaryToolHandler(dashboardService);
         topicListHandler = new TopicListToolHandler(metadataService);
         consumerGroupListHandler = new 
ConsumerGroupListToolHandler(metadataService);
+        alertRuleListHandler = new AlertRuleListToolHandler(alertService);
         gateway = gateway(
                 catalog,
                 clusterListHandler,
                 capabilitiesHandler,
                 dashboardSummaryHandler,
                 topicListHandler,
-                consumerGroupListHandler);
+                consumerGroupListHandler,
+                alertRuleListHandler);
     }
 
     @Test
@@ -104,7 +111,8 @@ class ToolGatewayServiceTest {
                         "rmq.capabilities",
                         "rmq.dashboard.summary",
                         "rmq.topic.list",
-                        "rmq.group.list");
+                        "rmq.group.list",
+                        "rmq.alert.rule.list");
     }
 
     @Test
@@ -318,6 +326,39 @@ class ToolGatewayServiceTest {
         verifyNoInteractions(metadataService);
     }
 
+    @Test
+    void executesAlertRuleListThroughADataMinimizingProjection() {
+        when(alertService.listRules()).thenReturn(List.of(
+                alertRule("rule-1", "High Lag", 
"rocketmq_consumer_lag_messages", true),
+                alertRule("rule-2", "Broker Down", "up", false)));
+
+        Object output = gateway.execute("rmq.alert.rule.list", Map.of(
+                "cluster", "cluster-v5",
+                "search", "  LAG  ",
+                "enabled", true));
+
+        assertThat(output).isEqualTo(List.of(Map.of(
+                "id", "rule-1",
+                "name", "High Lag",
+                "metric", "rocketmq_consumer_lag_messages",
+                "operator", ">",
+                "threshold", 100000D,
+                "thresholdUnit", "messages",
+                "duration", "5m",
+                "channels", List.of("email"),
+                "enabled", true,
+                "description", "Consumer lag is high")));
+        assertThat(output.toString()).doesNotContain("lastTriggered");
+    }
+
+    @Test
+    void rejectsAlertRuleListWithoutAClusterBeforeHandlerRuns() {
+        assertThatThrownBy(() -> gateway.execute("rmq.alert.rule.list", 
Map.of()))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("input validation failed");
+        verifyNoInteractions(alertService);
+    }
+
     @Test
     void rejectsCapabilitiesExecutionWhenClusterTypeIsMissing() {
         
when(clusterService.getCluster("unknown")).thenReturn(cluster("unknown", null));
@@ -373,7 +414,8 @@ class ToolGatewayServiceTest {
                 capabilitiesHandler,
                 dashboardSummaryHandler,
                 topicListHandler,
-                consumerGroupListHandler);
+                consumerGroupListHandler,
+                alertRuleListHandler);
 
         assertThatThrownBy(() -> l2Gateway.execute("rmq.cluster.list", 
Map.of()))
                 .isInstanceOf(BusinessException.class)
@@ -390,7 +432,8 @@ class ToolGatewayServiceTest {
                 capabilitiesHandler,
                 dashboardSummaryHandler,
                 topicListHandler,
-                consumerGroupListHandler))
+                consumerGroupListHandler,
+                alertRuleListHandler))
                 .isInstanceOf(IllegalStateException.class)
                 .hasMessageContaining("duplicate handler");
     }
@@ -424,7 +467,8 @@ class ToolGatewayServiceTest {
                 capabilitiesHandler,
                 dashboardSummaryHandler,
                 topicListHandler,
-                consumerGroupListHandler))
+                consumerGroupListHandler,
+                alertRuleListHandler))
                 .isInstanceOf(IllegalStateException.class)
                 .hasMessageContaining("input schema")
                 .hasMessageContaining("rmq.cluster.list");
@@ -451,7 +495,8 @@ class ToolGatewayServiceTest {
                 capabilitiesHandler,
                 dashboardSummaryHandler,
                 topicListHandler,
-                consumerGroupListHandler))
+                consumerGroupListHandler,
+                alertRuleListHandler))
                 .isInstanceOf(IllegalStateException.class)
                 .hasMessageContaining("input schema")
                 .hasMessageContaining("rmq.cluster.list");
@@ -476,7 +521,8 @@ class ToolGatewayServiceTest {
                 capabilitiesHandler,
                 dashboardSummaryHandler,
                 topicListHandler,
-                consumerGroupListHandler);
+                consumerGroupListHandler,
+                alertRuleListHandler);
 
         assertThatThrownBy(() -> invalidGateway.execute("rmq.cluster.list", 
Map.of()))
                 .isInstanceOf(IllegalStateException.class)
@@ -545,4 +591,20 @@ class ToolGatewayServiceTest {
         group.setRetryMaxTimes(16);
         return group;
     }
+
+    private static AlertRuleVO alertRule(String id, String name, String 
metric, boolean enabled) {
+        return AlertRuleVO.builder()
+                .id(id)
+                .name(name)
+                .metric(metric)
+                .operator(">")
+                .threshold(100000D)
+                .thresholdUnit("messages")
+                .duration("5m")
+                .channels(List.of("email"))
+                .enabled(enabled)
+                .lastTriggered("do-not-expose")
+                .description("Consumer lag is high")
+                .build();
+    }
 }

Reply via email to