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();
+ }
}