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 ff851fe1 feat(metrics): bundle Prometheus alert rule YAML templates
(#933)
ff851fe1 is described below
commit ff851fe11ef2851e33bec14d406a1b15ca40355a
Author: zhaohai <[email protected]>
AuthorDate: Tue Aug 4 18:05:21 2026 +0800
feat(metrics): bundle Prometheus alert rule YAML templates (#933)
* feat(metrics): bundle Prometheus alert rule YAML templates (METRICS-01)
Add 23 Prometheus alert rule YAML assets under resources/alerts and expose
them through AlertRuleAssetService/Controller (/api/alert-rules/assets):
list, get raw yaml, and export as attachment.
AlertService.defaultPrometheusRules()
now seeds from these bundled assets when the in-memory repository is empty,
satisfying the "load as default" requirement. Frontend adds a template
browser
(nav + page + list component) with mock/real data mode and export download,
plus api/service/component unit tests.
Backend: 10 tests (service 5, controller 3, default-seed 2).
Frontend: 13 tests (api 3, service 7, component 3); tsc + eslint clean.
Co-Authored-By: WorkBuddy <[email protected]>
* fix(ci): resolve tsc -b unused React import in alert rule asset frontend
AlertRuleAssetList.tsx: drop unused default `React` import (automatic JSX
runtime). The page component uses `React.FC` so it is left untouched. Fixes
the TS6133 failure under `tsc -b` in the Docker build.
Co-Authored-By: WorkBuddy <[email protected]>
* test: wire AlertRuleAssetService into AlertServiceTest after template
refactor
---------
Co-authored-by: WorkBuddy <[email protected]>
Co-authored-by: lizhimins <[email protected]>
---
server/scripts/gen_alert_rule_yaml.py | 115 +++++++++++
.../studio/ops/alert/AlertRuleAssetController.java | 71 +++++++
.../studio/ops/alert/AlertRuleAssetInfo.java | 26 +++
.../studio/ops/alert/AlertRuleAssetService.java | 180 ++++++++++++++++++
.../rocketmq/studio/ops/alert/AlertService.java | 41 +---
.../studio/ops/alert/PrometheusAlertRule.java | 32 ++++
.../resources/alerts/rocketmq-broker-cpu-high.yaml | 16 ++
.../alerts/rocketmq-broker-disk-high.yaml | 16 ++
.../resources/alerts/rocketmq-broker-down.yaml | 16 ++
.../alerts/rocketmq-broker-memory-high.yaml | 16 ++
.../alerts/rocketmq-broker-replication-lag.yaml | 16 ++
.../alerts/rocketmq-client-connection-drop.yaml | 16 ++
.../resources/alerts/rocketmq-client-timeout.yaml | 16 ++
.../alerts/rocketmq-consumer-group-empty.yaml | 16 ++
.../alerts/rocketmq-consumer-lag-critical.yaml | 16 ++
.../alerts/rocketmq-consumer-lag-high.yaml | 16 ++
.../alerts/rocketmq-consumer-rebalance.yaml | 16 ++
.../resources/alerts/rocketmq-dlq-resend-high.yaml | 16 ++
.../resources/alerts/rocketmq-exception-rate.yaml | 16 ++
.../resources/alerts/rocketmq-jvm-gc-cpu-high.yaml | 16 ++
.../alerts/rocketmq-producer-failure.yaml | 16 ++
.../alerts/rocketmq-producer-latency-high.yaml | 16 ++
.../alerts/rocketmq-producer-tps-drop.yaml | 16 ++
.../main/resources/alerts/rocketmq-proxy-down.yaml | 16 ++
.../alerts/rocketmq-proxy-latency-high.yaml | 16 ++
.../alerts/rocketmq-threadpool-reject.yaml | 16 ++
.../alerts/rocketmq-topic-accumulation.yaml | 16 ++
.../alerts/rocketmq-topic-dispatch-latency.yaml | 16 ++
.../resources/alerts/rocketmq-topic-in-drop.yaml | 16 ++
.../ops/alert/AlertRuleAssetControllerTest.java | 81 ++++++++
.../ops/alert/AlertRuleAssetServiceTest.java | 84 +++++++++
.../ops/alert/AlertServiceDefaultRulesTest.java | 68 +++++++
.../studio/ops/alert/AlertServiceTest.java | 11 +-
web/src/App.tsx | 2 +
web/src/api/alertRuleAssets.test.ts | 71 +++++++
web/src/api/alertRuleAssets.ts | 42 +++++
web/src/components/AlertRuleAssetList.tsx | 197 +++++++++++++++++++
.../__tests__/AlertRuleAssetList.test.tsx | 127 +++++++++++++
web/src/i18n/translations.ts | 10 +
web/src/layouts/MainLayout.tsx | 7 +
web/src/mock/alertRuleAssets.ts | 210 +++++++++++++++++++++
web/src/pages/studio/AlertRuleAssets.tsx | 44 +++++
web/src/services/alertRuleAssetService.test.ts | 101 ++++++++++
web/src/services/alertRuleAssetService.ts | 39 ++++
44 files changed, 1884 insertions(+), 43 deletions(-)
diff --git a/server/scripts/gen_alert_rule_yaml.py
b/server/scripts/gen_alert_rule_yaml.py
new file mode 100644
index 00000000..cf1b0d3a
--- /dev/null
+++ b/server/scripts/gen_alert_rule_yaml.py
@@ -0,0 +1,115 @@
+#!/usr/bin/env python3
+"""Generate RocketMQ Prometheus alert rule YAML assets.
+
+Each asset is a standalone Prometheus rule file (one alert per file) covering
+broker, consumer, producer, topic, client, proxy and error conditions. The
+dashboard loads these as the default alert rule set.
+"""
+import os
+
+OUT_DIR = os.path.join(os.path.dirname(__file__), "..", "src", "main",
"resources", "alerts")
+os.makedirs(OUT_DIR, exist_ok=True)
+
+# (file_slug, alert, group, expr, for_, severity, team, summary, description)
+RULES = [
+ ("rocketmq-broker-down", "RocketMQBrokerDown", "rocketmq-broker.rules",
+ 'up{job=~".*rocketmq.*broker.*"} == 0', "1m", "critical", "broker",
+ "RocketMQ broker is down", "A RocketMQ broker scrape target has been
unavailable for more than 1 minute."),
+ ("rocketmq-broker-cpu-high", "RocketMQBrokerCPUHigh",
"rocketmq-broker.rules",
+ '100 * (1 - avg
by(instance)(rate(node_cpu_seconds_total{mode="idle"}[5m]))) > 85', "5m",
"warning", "broker",
+ "Broker CPU usage high", "Broker CPU usage has stayed above 85% for 5
minutes."),
+ ("rocketmq-broker-memory-high", "RocketMQBrokerMemoryHigh",
"rocketmq-broker.rules",
+ 'jvm_memory_used_bytes{area="heap"} / jvm_memory_max_bytes{area="heap"} *
100 > 85', "5m", "warning", "broker",
+ "Broker JVM heap high", "Broker JVM heap usage is above 85%."),
+ ("rocketmq-broker-disk-high", "RocketMQBrokerDiskHigh",
"rocketmq-broker.rules",
+ 'rocketmq_disk_use_ratio > 85', "5m", "critical", "broker",
+ "Broker disk usage high", "Broker disk usage ratio is above 85%."),
+ ("rocketmq-broker-replication-lag", "RocketMQBrokerReplicationLag",
"rocketmq-broker.rules",
+ 'rocketmq_broker_replication_fall_behind_bytes > 1073741824', "5m",
"warning", "broker",
+ "Replication lag high", "Master-slave replication fall-behind is above
1GiB."),
+ ("rocketmq-consumer-lag-high", "RocketMQConsumerLagHigh",
"rocketmq-consumer.rules",
+ 'rocketmq_consumer_lag_messages > 100000', "5m", "warning", "consumer",
+ "Consumer lag high", "A consumer group lag is above 100000 messages."),
+ ("rocketmq-consumer-lag-critical", "RocketMQConsumerLagCritical",
"rocketmq-consumer.rules",
+ 'rocketmq_consumer_lag_messages > 1000000', "5m", "critical", "consumer",
+ "Consumer lag critical", "A consumer group lag is above 1 million
messages."),
+ ("rocketmq-consumer-rebalance", "RocketMQConsumerRebalance",
"rocketmq-consumer.rules",
+ 'increase(rocketmq_consumer_rebalance_times[5m]) > 10', "5m", "warning",
"consumer",
+ "Frequent consumer rebalances", "More than 10 consumer rebalances
happened in 5 minutes."),
+ ("rocketmq-consumer-group-empty", "RocketMQConsumerGroupEmpty",
"rocketmq-consumer.rules",
+ 'absent(rocketmq_consumer_lag_messages) == 1', "10m", "info", "consumer",
+ "Consumer group missing", "No consumer lag metrics observed for a
consumer group."),
+ ("rocketmq-producer-latency-high", "RocketMQProducerSendLatencyHigh",
"rocketmq-client.rules",
+ 'rocketmq_producer_send_to_back_rt > 1000', "5m", "warning", "client",
+ "Producer send latency high", "Producer send-to-broker latency is above
1000 ms."),
+ ("rocketmq-producer-failure", "RocketMQProducerSendFailure",
"rocketmq-client.rules",
+ 'rate(rocketmq_producer_send_failure_count[5m]) > 0', "5m", "critical",
"client",
+ "Producer send failures", "Producer message send failures have been
observed."),
+ ("rocketmq-producer-tps-drop", "RocketMQProducerTPSDrop",
"rocketmq-client.rules",
+ 'rate(rocketmq_messages_in_total[5m]) < 0.1 *
rate(rocketmq_messages_in_total[1h] offset 1h)', "10m", "warning", "client",
+ "Producer TPS dropped", "Ingress TPS dropped by more than 90% compared
with one hour ago."),
+ ("rocketmq-topic-in-drop", "RocketMQTopicMessageInDrop",
"rocketmq-topic.rules",
+ 'rate(rocketmq_messages_in_total[10m]) == 0', "10m", "info", "topic",
+ "No incoming messages", "No incoming messages have been observed for 10
minutes."),
+ ("rocketmq-topic-accumulation", "RocketMQTopicAccumulation",
"rocketmq-topic.rules",
+ 'rocketmq_dispatch_behind_bytes > 1073741824', "5m", "warning", "topic",
+ "Topic dispatch backlog high", "Topic dispatch behind bytes is above
1GiB."),
+ ("rocketmq-topic-dispatch-latency", "RocketMQTopicDispatchLatency",
"rocketmq-topic.rules",
+ 'histogram_quantile(0.99, rate(rocketmq_dispatch_latency_bucket[5m])) >
1', "5m", "warning", "topic",
+ "Dispatch latency high", "99th percentile dispatch latency is above 1
second."),
+ ("rocketmq-client-connection-drop", "RocketMQClientConnectionDrop",
"rocketmq-client.rules",
+ 'changes(rocketmq_producer_count[5m]) < -5', "5m", "warning", "client",
+ "Client connections dropped", "More than 5 producer connections dropped
in 5 minutes."),
+ ("rocketmq-client-timeout", "RocketMQClientTimeout",
"rocketmq-client.rules",
+ 'rocketmq_send_to_client_latency > 3000', "5m", "warning", "client",
+ "Client push latency high", "Push-to-client latency is above 3000 ms."),
+ ("rocketmq-proxy-down", "RocketMQProxyDown", "rocketmq-proxy.rules",
+ 'up{job=~".*rocketmq.*proxy.*"} == 0', "1m", "critical", "proxy",
+ "RocketMQ proxy is down", "A RocketMQ 5.x proxy target has been down for
more than 1 minute."),
+ ("rocketmq-proxy-latency-high", "RocketMQProxyLatencyHigh",
"rocketmq-proxy.rules",
+ 'histogram_quantile(0.99, rate(rocketmq_proxy_process_time_bucket[5m])) >
1', "5m", "warning", "proxy",
+ "Proxy latency high", "99th percentile proxy process time is above 1
second."),
+ ("rocketmq-exception-rate", "RocketMQBrokerExceptions",
"rocketmq-errors.rules",
+ 'rate(rocketmq_broker_exception_count[5m]) > 0', "5m", "critical",
"broker",
+ "Broker exceptions", "Broker runtime exceptions have been observed."),
+ ("rocketmq-dlq-resend-high", "RocketMQDLQResendHigh",
"rocketmq-errors.rules",
+ 'rate(rocketmq_dlq_resend_count[5m]) > 10', "5m", "warning", "consumer",
+ "DLQ resends high", "More than 10 dead-letter queue resends occurred in 5
minutes."),
+ ("rocketmq-threadpool-reject", "RocketMQThreadPoolReject",
"rocketmq-broker.rules",
+ 'increase(rocketmq_threadpool_reject_count[5m]) > 0', "5m", "critical",
"broker",
+ "Thread pool rejections", "The broker thread pool rejected tasks,
indicating saturation."),
+ ("rocketmq-jvm-gc-cpu-high", "RocketMQJVMCpuHigh", "rocketmq-broker.rules",
+ 'rate(jvm_gc_pause_seconds_count[5m]) *
avg(rate(jvm_gc_pause_seconds_sum[5m])) > 0.3', "5m", "warning", "broker",
+ "JVM GC CPU high", "The broker spends more than 30% of CPU time in GC
pauses."),
+]
+
+TEMPLATE = """\
+# ============================================================================
+# RocketMQ 5.x Monitoring - Alert Rule
+# Compatible with Prometheus / VictoriaMetrics / Thanos / Cortex / Mimir
+# ============================================================================
+groups:
+ - name: {group}
+ rules:
+ - alert: {alert}
+ expr: {expr}
+ for: {for_}
+ labels:
+ severity: {severity}
+ team: {team}
+ annotations:
+ summary: "{summary}"
+ description: "{description}"
+"""
+
+for slug, alert, group, expr, for_, severity, team, summary, description in
RULES:
+ content = TEMPLATE.format(
+ group=group, alert=alert, expr=expr, for_=for_,
+ severity=severity, team=team, summary=summary, description=description,
+ )
+ path = os.path.join(OUT_DIR, f"{slug}.yaml")
+ with open(path, "w", encoding="utf-8") as f:
+ f.write(content)
+ print(f"wrote {path}")
+
+print(f"TOTAL alert rule assets: {len(RULES)}")
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/ops/alert/AlertRuleAssetController.java
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/AlertRuleAssetController.java
new file mode 100644
index 00000000..aa991290
--- /dev/null
+++
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/AlertRuleAssetController.java
@@ -0,0 +1,71 @@
+/*
+ * 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.alert;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import lombok.RequiredArgsConstructor;
+import org.apache.rocketmq.studio.common.domain.Result;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/alert-rules/assets")
+@RequiredArgsConstructor
+public class AlertRuleAssetController {
+
+ private final AlertRuleAssetService alertRuleAssetService;
+
+ @Operation(summary = "List bundled alert rule assets",
+ description = "Returns metadata for every RocketMQ Prometheus
alert rule YAML shipped with the dashboard")
+ @ApiResponse(responseCode = "200", description = "Assets listed
successfully", useReturnTypeSchema = true)
+ @GetMapping
+ public Result<List<AlertRuleAssetInfo>> listAssets() {
+ return Result.ok(alertRuleAssetService.listAssets());
+ }
+
+ @Operation(summary = "Get an alert rule asset",
+ description = "Returns the raw Prometheus alert rule YAML for the
given asset name")
+ @ApiResponse(responseCode = "200", description = "Asset returned
successfully", useReturnTypeSchema = true)
+ @ApiResponse(responseCode = "404", description = "Asset name is unknown")
+ @GetMapping("/{name}")
+ public Result<String> getAsset(@PathVariable("name") String name) {
+ return Result.ok(alertRuleAssetService.getAssetYaml(name));
+ }
+
+ @Operation(summary = "Export an alert rule asset",
+ description = "Returns the raw Prometheus alert rule YAML as a
downloadable attachment")
+ @ApiResponse(responseCode = "200", description = "Asset YAML returned")
+ @ApiResponse(responseCode = "404", description = "Asset name is unknown")
+ @GetMapping("/{name}/export")
+ public ResponseEntity<byte[]> exportAsset(@PathVariable("name") String
name) {
+ String yaml = alertRuleAssetService.getAssetYaml(name);
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.parseMediaType("application/x-yaml"));
+ headers.setContentDispositionFormData("attachment", name + ".yaml");
+ return new ResponseEntity<>(yaml.getBytes(StandardCharsets.UTF_8),
headers, HttpStatus.OK);
+ }
+}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/ops/alert/AlertRuleAssetInfo.java
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/AlertRuleAssetInfo.java
new file mode 100644
index 00000000..ea7a144d
--- /dev/null
+++
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/AlertRuleAssetInfo.java
@@ -0,0 +1,26 @@
+/*
+ * 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.alert;
+
+import java.util.List;
+
+/**
+ * Metadata describing a bundled alert rule YAML asset. The {@code name}
matches
+ * the file name (without the {@code .yaml} suffix).
+ */
+public record AlertRuleAssetInfo(String name, String group, int ruleCount,
List<String> severities) {
+}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/ops/alert/AlertRuleAssetService.java
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/AlertRuleAssetService.java
new file mode 100644
index 00000000..3c2d173f
--- /dev/null
+++
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/AlertRuleAssetService.java
@@ -0,0 +1,180 @@
+/*
+ * 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.alert;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.rocketmq.studio.common.exception.BusinessException;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import org.springframework.core.io.support.ResourcePatternResolver;
+import org.springframework.stereotype.Service;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Loads the Prometheus alert rule YAML assets bundled under
+ * {@code classpath*:alerts/*.yaml} and exposes them for listing, viewing,
+ * exporting and as the default alert rule set.
+ */
+@Slf4j
+@Service
+public class AlertRuleAssetService {
+
+ private static final String LOCATION_PATTERN = "classpath*:alerts/*.yaml";
+
+ private final ObjectMapper yamlMapper = new ObjectMapper(new
YAMLFactory());
+ private final ResourcePatternResolver resourceResolver = new
PathMatchingResourcePatternResolver();
+
+ /**
+ * Lists metadata for every bundled alert rule asset.
+ */
+ public List<AlertRuleAssetInfo> listAssets() {
+ List<AlertRuleAssetInfo> infos = new ArrayList<>();
+ for (Resource resource : resolveResources()) {
+ String name = nameOf(resource);
+ if (name == null) {
+ continue;
+ }
+ try (InputStream in = resource.getInputStream()) {
+ JsonNode root = yamlMapper.readTree(in);
+ List<PrometheusAlertRule> rules = parseRules(root);
+ Set<String> severities = new LinkedHashSet<>();
+ String group = rules.isEmpty() ? "" : rules.get(0).group();
+ rules.forEach(rule -> severities.add(rule.severity()));
+ infos.add(new AlertRuleAssetInfo(name, group, rules.size(),
new ArrayList<>(severities)));
+ } catch (IOException e) {
+ log.warn("Skipping unreadable alert rule asset {}: {}",
resource, e.getMessage());
+ }
+ }
+ infos.sort((a, b) -> a.name().compareTo(b.name()));
+ return infos;
+ }
+
+ /**
+ * Returns the raw YAML content for the given asset name.
+ *
+ * @throws BusinessException with code 404 when the asset is unknown
+ */
+ public String getAssetYaml(String name) {
+ Resource resource = findResource(name);
+ if (resource == null) {
+ throw new BusinessException(404, "Alert rule asset not found: " +
name);
+ }
+ try (InputStream in = resource.getInputStream()) {
+ return new String(in.readAllBytes(), StandardCharsets.UTF_8);
+ } catch (IOException e) {
+ throw new BusinessException(500, "Failed to read alert rule asset:
" + name);
+ }
+ }
+
+ /**
+ * Loads every rule from every bundled asset as the default alert set.
+ */
+ public List<PrometheusAlertRule> loadDefaultRules() {
+ List<PrometheusAlertRule> rules = new ArrayList<>();
+ for (Resource resource : resolveResources()) {
+ if (nameOf(resource) == null) {
+ continue;
+ }
+ try (InputStream in = resource.getInputStream()) {
+ rules.addAll(parseRules(yamlMapper.readTree(in)));
+ } catch (IOException e) {
+ log.warn("Skipping unreadable alert rule asset {}: {}",
resource, e.getMessage());
+ }
+ }
+ return rules;
+ }
+
+ private List<PrometheusAlertRule> parseRules(JsonNode root) {
+ List<PrometheusAlertRule> rules = new ArrayList<>();
+ JsonNode groups = root.get("groups");
+ if (groups == null || !groups.isArray()) {
+ return rules;
+ }
+ for (JsonNode group : groups) {
+ String groupName = textOr(group, "name", "");
+ JsonNode ruleNodes = group.get("rules");
+ if (ruleNodes == null || !ruleNodes.isArray()) {
+ continue;
+ }
+ for (JsonNode rule : ruleNodes) {
+ if (!rule.isObject()) {
+ continue;
+ }
+ String alert = textOr(rule, "alert", "");
+ String expr = textOr(rule, "expr", "");
+ String duration = textOr(rule, "for", "5m");
+ String severity = textOr(labelsOf(rule), "severity",
"warning");
+ String team = textOr(labelsOf(rule), "team", "broker");
+ String summary = textOr(annotationsOf(rule), "summary", alert);
+ String description = textOr(annotationsOf(rule),
"description", "");
+ rules.add(new PrometheusAlertRule(groupName, alert, expr,
duration, severity, team, summary, description));
+ }
+ }
+ return rules;
+ }
+
+ private Resource findResource(String name) {
+ for (Resource resource : resolveResources()) {
+ if (name.equals(nameOf(resource))) {
+ return resource;
+ }
+ }
+ return null;
+ }
+
+ private Resource[] resolveResources() {
+ try {
+ return resourceResolver.getResources(LOCATION_PATTERN);
+ } catch (IOException e) {
+ log.warn("Unable to resolve alert rule assets: {}",
e.getMessage());
+ return new Resource[0];
+ }
+ }
+
+ private static String nameOf(Resource resource) {
+ String filename = resource.getFilename();
+ if (filename == null || !filename.endsWith(".yaml")) {
+ return null;
+ }
+ return filename.substring(0, filename.length() - ".yaml".length());
+ }
+
+ private static String textOr(JsonNode node, String field, String fallback)
{
+ JsonNode value = node.get(field);
+ return value != null && value.isTextual() ? value.asText() : fallback;
+ }
+
+ private static JsonNode labelsOf(JsonNode rule) {
+ JsonNode labels = rule.get("labels");
+ return labels != null && labels.isObject() ? labels : rule;
+ }
+
+ private static JsonNode annotationsOf(JsonNode rule) {
+ JsonNode annotations = rule.get("annotations");
+ return annotations != null && annotations.isObject() ? annotations :
rule;
+ }
+}
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 418a7fc0..cd405ebc 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
@@ -21,7 +21,6 @@ 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;
@@ -31,6 +30,7 @@ import java.util.UUID;
public class AlertService {
private final AlertRepository alertRepository;
+ private final AlertRuleAssetService alertRuleAssetService;
public List<AlertRuleVO> listRules() {
@@ -126,40 +126,7 @@ public class AlertService {
}
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);
+ return alertRuleAssetService.loadDefaultRules();
}
private PrometheusAlertRule toPrometheusRule(AlertRuleVO rule) {
@@ -301,10 +268,6 @@ public class AlertService {
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) {
- }
-
private void validateRuleId(String id) {
if (id == null || id.isBlank()) {
throw new BusinessException(400, "Alert rule ID is required");
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/ops/alert/PrometheusAlertRule.java
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/PrometheusAlertRule.java
new file mode 100644
index 00000000..8ad18d62
--- /dev/null
+++
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/PrometheusAlertRule.java
@@ -0,0 +1,32 @@
+/*
+ * 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.alert;
+
+/**
+ * A single Prometheus alerting rule in its serialized form.
+ */
+public record PrometheusAlertRule(
+ String group,
+ String alert,
+ String expr,
+ String duration,
+ String severity,
+ String team,
+ String summary,
+ String description
+) {
+}
diff --git a/server/src/main/resources/alerts/rocketmq-broker-cpu-high.yaml
b/server/src/main/resources/alerts/rocketmq-broker-cpu-high.yaml
new file mode 100644
index 00000000..ce0d1443
--- /dev/null
+++ b/server/src/main/resources/alerts/rocketmq-broker-cpu-high.yaml
@@ -0,0 +1,16 @@
+# ============================================================================
+# RocketMQ 5.x Monitoring - Alert Rule
+# Compatible with Prometheus / VictoriaMetrics / Thanos / Cortex / Mimir
+# ============================================================================
+groups:
+ - name: rocketmq-broker.rules
+ rules:
+ - alert: RocketMQBrokerCPUHigh
+ expr: 100 * (1 - avg
by(instance)(rate(node_cpu_seconds_total{mode="idle"}[5m]))) > 85
+ for: 5m
+ labels:
+ severity: warning
+ team: broker
+ annotations:
+ summary: "Broker CPU usage high"
+ description: "Broker CPU usage has stayed above 85% for 5 minutes."
diff --git a/server/src/main/resources/alerts/rocketmq-broker-disk-high.yaml
b/server/src/main/resources/alerts/rocketmq-broker-disk-high.yaml
new file mode 100644
index 00000000..ff71d44e
--- /dev/null
+++ b/server/src/main/resources/alerts/rocketmq-broker-disk-high.yaml
@@ -0,0 +1,16 @@
+# ============================================================================
+# RocketMQ 5.x Monitoring - Alert Rule
+# Compatible with Prometheus / VictoriaMetrics / Thanos / Cortex / Mimir
+# ============================================================================
+groups:
+ - name: rocketmq-broker.rules
+ rules:
+ - alert: RocketMQBrokerDiskHigh
+ expr: rocketmq_disk_use_ratio > 85
+ for: 5m
+ labels:
+ severity: critical
+ team: broker
+ annotations:
+ summary: "Broker disk usage high"
+ description: "Broker disk usage ratio is above 85%."
diff --git a/server/src/main/resources/alerts/rocketmq-broker-down.yaml
b/server/src/main/resources/alerts/rocketmq-broker-down.yaml
new file mode 100644
index 00000000..7a3c9e63
--- /dev/null
+++ b/server/src/main/resources/alerts/rocketmq-broker-down.yaml
@@ -0,0 +1,16 @@
+# ============================================================================
+# RocketMQ 5.x Monitoring - Alert Rule
+# Compatible with Prometheus / VictoriaMetrics / Thanos / Cortex / Mimir
+# ============================================================================
+groups:
+ - name: rocketmq-broker.rules
+ rules:
+ - alert: RocketMQBrokerDown
+ expr: up{job=~".*rocketmq.*broker.*"} == 0
+ for: 1m
+ labels:
+ severity: critical
+ team: broker
+ annotations:
+ summary: "RocketMQ broker is down"
+ description: "A RocketMQ broker scrape target has been unavailable
for more than 1 minute."
diff --git a/server/src/main/resources/alerts/rocketmq-broker-memory-high.yaml
b/server/src/main/resources/alerts/rocketmq-broker-memory-high.yaml
new file mode 100644
index 00000000..171d5ffb
--- /dev/null
+++ b/server/src/main/resources/alerts/rocketmq-broker-memory-high.yaml
@@ -0,0 +1,16 @@
+# ============================================================================
+# RocketMQ 5.x Monitoring - Alert Rule
+# Compatible with Prometheus / VictoriaMetrics / Thanos / Cortex / Mimir
+# ============================================================================
+groups:
+ - name: rocketmq-broker.rules
+ rules:
+ - alert: RocketMQBrokerMemoryHigh
+ expr: jvm_memory_used_bytes{area="heap"} /
jvm_memory_max_bytes{area="heap"} * 100 > 85
+ for: 5m
+ labels:
+ severity: warning
+ team: broker
+ annotations:
+ summary: "Broker JVM heap high"
+ description: "Broker JVM heap usage is above 85%."
diff --git
a/server/src/main/resources/alerts/rocketmq-broker-replication-lag.yaml
b/server/src/main/resources/alerts/rocketmq-broker-replication-lag.yaml
new file mode 100644
index 00000000..271e3989
--- /dev/null
+++ b/server/src/main/resources/alerts/rocketmq-broker-replication-lag.yaml
@@ -0,0 +1,16 @@
+# ============================================================================
+# RocketMQ 5.x Monitoring - Alert Rule
+# Compatible with Prometheus / VictoriaMetrics / Thanos / Cortex / Mimir
+# ============================================================================
+groups:
+ - name: rocketmq-broker.rules
+ rules:
+ - alert: RocketMQBrokerReplicationLag
+ expr: rocketmq_broker_replication_fall_behind_bytes > 1073741824
+ for: 5m
+ labels:
+ severity: warning
+ team: broker
+ annotations:
+ summary: "Replication lag high"
+ description: "Master-slave replication fall-behind is above 1GiB."
diff --git
a/server/src/main/resources/alerts/rocketmq-client-connection-drop.yaml
b/server/src/main/resources/alerts/rocketmq-client-connection-drop.yaml
new file mode 100644
index 00000000..9857c9c2
--- /dev/null
+++ b/server/src/main/resources/alerts/rocketmq-client-connection-drop.yaml
@@ -0,0 +1,16 @@
+# ============================================================================
+# RocketMQ 5.x Monitoring - Alert Rule
+# Compatible with Prometheus / VictoriaMetrics / Thanos / Cortex / Mimir
+# ============================================================================
+groups:
+ - name: rocketmq-client.rules
+ rules:
+ - alert: RocketMQClientConnectionDrop
+ expr: changes(rocketmq_producer_count[5m]) < -5
+ for: 5m
+ labels:
+ severity: warning
+ team: client
+ annotations:
+ summary: "Client connections dropped"
+ description: "More than 5 producer connections dropped in 5 minutes."
diff --git a/server/src/main/resources/alerts/rocketmq-client-timeout.yaml
b/server/src/main/resources/alerts/rocketmq-client-timeout.yaml
new file mode 100644
index 00000000..5a321b00
--- /dev/null
+++ b/server/src/main/resources/alerts/rocketmq-client-timeout.yaml
@@ -0,0 +1,16 @@
+# ============================================================================
+# RocketMQ 5.x Monitoring - Alert Rule
+# Compatible with Prometheus / VictoriaMetrics / Thanos / Cortex / Mimir
+# ============================================================================
+groups:
+ - name: rocketmq-client.rules
+ rules:
+ - alert: RocketMQClientTimeout
+ expr: rocketmq_send_to_client_latency > 3000
+ for: 5m
+ labels:
+ severity: warning
+ team: client
+ annotations:
+ summary: "Client push latency high"
+ description: "Push-to-client latency is above 3000 ms."
diff --git
a/server/src/main/resources/alerts/rocketmq-consumer-group-empty.yaml
b/server/src/main/resources/alerts/rocketmq-consumer-group-empty.yaml
new file mode 100644
index 00000000..bfbb5bce
--- /dev/null
+++ b/server/src/main/resources/alerts/rocketmq-consumer-group-empty.yaml
@@ -0,0 +1,16 @@
+# ============================================================================
+# RocketMQ 5.x Monitoring - Alert Rule
+# Compatible with Prometheus / VictoriaMetrics / Thanos / Cortex / Mimir
+# ============================================================================
+groups:
+ - name: rocketmq-consumer.rules
+ rules:
+ - alert: RocketMQConsumerGroupEmpty
+ expr: absent(rocketmq_consumer_lag_messages) == 1
+ for: 10m
+ labels:
+ severity: info
+ team: consumer
+ annotations:
+ summary: "Consumer group missing"
+ description: "No consumer lag metrics observed for a consumer group."
diff --git
a/server/src/main/resources/alerts/rocketmq-consumer-lag-critical.yaml
b/server/src/main/resources/alerts/rocketmq-consumer-lag-critical.yaml
new file mode 100644
index 00000000..b805efb7
--- /dev/null
+++ b/server/src/main/resources/alerts/rocketmq-consumer-lag-critical.yaml
@@ -0,0 +1,16 @@
+# ============================================================================
+# RocketMQ 5.x Monitoring - Alert Rule
+# Compatible with Prometheus / VictoriaMetrics / Thanos / Cortex / Mimir
+# ============================================================================
+groups:
+ - name: rocketmq-consumer.rules
+ rules:
+ - alert: RocketMQConsumerLagCritical
+ expr: rocketmq_consumer_lag_messages > 1000000
+ for: 5m
+ labels:
+ severity: critical
+ team: consumer
+ annotations:
+ summary: "Consumer lag critical"
+ description: "A consumer group lag is above 1 million messages."
diff --git a/server/src/main/resources/alerts/rocketmq-consumer-lag-high.yaml
b/server/src/main/resources/alerts/rocketmq-consumer-lag-high.yaml
new file mode 100644
index 00000000..92820c3c
--- /dev/null
+++ b/server/src/main/resources/alerts/rocketmq-consumer-lag-high.yaml
@@ -0,0 +1,16 @@
+# ============================================================================
+# RocketMQ 5.x Monitoring - Alert Rule
+# Compatible with Prometheus / VictoriaMetrics / Thanos / Cortex / Mimir
+# ============================================================================
+groups:
+ - name: rocketmq-consumer.rules
+ rules:
+ - alert: RocketMQConsumerLagHigh
+ expr: rocketmq_consumer_lag_messages > 100000
+ for: 5m
+ labels:
+ severity: warning
+ team: consumer
+ annotations:
+ summary: "Consumer lag high"
+ description: "A consumer group lag is above 100000 messages."
diff --git a/server/src/main/resources/alerts/rocketmq-consumer-rebalance.yaml
b/server/src/main/resources/alerts/rocketmq-consumer-rebalance.yaml
new file mode 100644
index 00000000..f7f1b4ac
--- /dev/null
+++ b/server/src/main/resources/alerts/rocketmq-consumer-rebalance.yaml
@@ -0,0 +1,16 @@
+# ============================================================================
+# RocketMQ 5.x Monitoring - Alert Rule
+# Compatible with Prometheus / VictoriaMetrics / Thanos / Cortex / Mimir
+# ============================================================================
+groups:
+ - name: rocketmq-consumer.rules
+ rules:
+ - alert: RocketMQConsumerRebalance
+ expr: increase(rocketmq_consumer_rebalance_times[5m]) > 10
+ for: 5m
+ labels:
+ severity: warning
+ team: consumer
+ annotations:
+ summary: "Frequent consumer rebalances"
+ description: "More than 10 consumer rebalances happened in 5
minutes."
diff --git a/server/src/main/resources/alerts/rocketmq-dlq-resend-high.yaml
b/server/src/main/resources/alerts/rocketmq-dlq-resend-high.yaml
new file mode 100644
index 00000000..ab0fa3f3
--- /dev/null
+++ b/server/src/main/resources/alerts/rocketmq-dlq-resend-high.yaml
@@ -0,0 +1,16 @@
+# ============================================================================
+# RocketMQ 5.x Monitoring - Alert Rule
+# Compatible with Prometheus / VictoriaMetrics / Thanos / Cortex / Mimir
+# ============================================================================
+groups:
+ - name: rocketmq-errors.rules
+ rules:
+ - alert: RocketMQDLQResendHigh
+ expr: rate(rocketmq_dlq_resend_count[5m]) > 10
+ for: 5m
+ labels:
+ severity: warning
+ team: consumer
+ annotations:
+ summary: "DLQ resends high"
+ description: "More than 10 dead-letter queue resends occurred in 5
minutes."
diff --git a/server/src/main/resources/alerts/rocketmq-exception-rate.yaml
b/server/src/main/resources/alerts/rocketmq-exception-rate.yaml
new file mode 100644
index 00000000..c497da1f
--- /dev/null
+++ b/server/src/main/resources/alerts/rocketmq-exception-rate.yaml
@@ -0,0 +1,16 @@
+# ============================================================================
+# RocketMQ 5.x Monitoring - Alert Rule
+# Compatible with Prometheus / VictoriaMetrics / Thanos / Cortex / Mimir
+# ============================================================================
+groups:
+ - name: rocketmq-errors.rules
+ rules:
+ - alert: RocketMQBrokerExceptions
+ expr: rate(rocketmq_broker_exception_count[5m]) > 0
+ for: 5m
+ labels:
+ severity: critical
+ team: broker
+ annotations:
+ summary: "Broker exceptions"
+ description: "Broker runtime exceptions have been observed."
diff --git a/server/src/main/resources/alerts/rocketmq-jvm-gc-cpu-high.yaml
b/server/src/main/resources/alerts/rocketmq-jvm-gc-cpu-high.yaml
new file mode 100644
index 00000000..cd9e4a99
--- /dev/null
+++ b/server/src/main/resources/alerts/rocketmq-jvm-gc-cpu-high.yaml
@@ -0,0 +1,16 @@
+# ============================================================================
+# RocketMQ 5.x Monitoring - Alert Rule
+# Compatible with Prometheus / VictoriaMetrics / Thanos / Cortex / Mimir
+# ============================================================================
+groups:
+ - name: rocketmq-broker.rules
+ rules:
+ - alert: RocketMQJVMCpuHigh
+ expr: rate(jvm_gc_pause_seconds_count[5m]) *
avg(rate(jvm_gc_pause_seconds_sum[5m])) > 0.3
+ for: 5m
+ labels:
+ severity: warning
+ team: broker
+ annotations:
+ summary: "JVM GC CPU high"
+ description: "The broker spends more than 30% of CPU time in GC
pauses."
diff --git a/server/src/main/resources/alerts/rocketmq-producer-failure.yaml
b/server/src/main/resources/alerts/rocketmq-producer-failure.yaml
new file mode 100644
index 00000000..b3d9d733
--- /dev/null
+++ b/server/src/main/resources/alerts/rocketmq-producer-failure.yaml
@@ -0,0 +1,16 @@
+# ============================================================================
+# RocketMQ 5.x Monitoring - Alert Rule
+# Compatible with Prometheus / VictoriaMetrics / Thanos / Cortex / Mimir
+# ============================================================================
+groups:
+ - name: rocketmq-client.rules
+ rules:
+ - alert: RocketMQProducerSendFailure
+ expr: rate(rocketmq_producer_send_failure_count[5m]) > 0
+ for: 5m
+ labels:
+ severity: critical
+ team: client
+ annotations:
+ summary: "Producer send failures"
+ description: "Producer message send failures have been observed."
diff --git
a/server/src/main/resources/alerts/rocketmq-producer-latency-high.yaml
b/server/src/main/resources/alerts/rocketmq-producer-latency-high.yaml
new file mode 100644
index 00000000..970cd0bf
--- /dev/null
+++ b/server/src/main/resources/alerts/rocketmq-producer-latency-high.yaml
@@ -0,0 +1,16 @@
+# ============================================================================
+# RocketMQ 5.x Monitoring - Alert Rule
+# Compatible with Prometheus / VictoriaMetrics / Thanos / Cortex / Mimir
+# ============================================================================
+groups:
+ - name: rocketmq-client.rules
+ rules:
+ - alert: RocketMQProducerSendLatencyHigh
+ expr: rocketmq_producer_send_to_back_rt > 1000
+ for: 5m
+ labels:
+ severity: warning
+ team: client
+ annotations:
+ summary: "Producer send latency high"
+ description: "Producer send-to-broker latency is above 1000 ms."
diff --git a/server/src/main/resources/alerts/rocketmq-producer-tps-drop.yaml
b/server/src/main/resources/alerts/rocketmq-producer-tps-drop.yaml
new file mode 100644
index 00000000..bb10bb9f
--- /dev/null
+++ b/server/src/main/resources/alerts/rocketmq-producer-tps-drop.yaml
@@ -0,0 +1,16 @@
+# ============================================================================
+# RocketMQ 5.x Monitoring - Alert Rule
+# Compatible with Prometheus / VictoriaMetrics / Thanos / Cortex / Mimir
+# ============================================================================
+groups:
+ - name: rocketmq-client.rules
+ rules:
+ - alert: RocketMQProducerTPSDrop
+ expr: rate(rocketmq_messages_in_total[5m]) < 0.1 *
rate(rocketmq_messages_in_total[1h] offset 1h)
+ for: 10m
+ labels:
+ severity: warning
+ team: client
+ annotations:
+ summary: "Producer TPS dropped"
+ description: "Ingress TPS dropped by more than 90% compared with one
hour ago."
diff --git a/server/src/main/resources/alerts/rocketmq-proxy-down.yaml
b/server/src/main/resources/alerts/rocketmq-proxy-down.yaml
new file mode 100644
index 00000000..68b5b330
--- /dev/null
+++ b/server/src/main/resources/alerts/rocketmq-proxy-down.yaml
@@ -0,0 +1,16 @@
+# ============================================================================
+# RocketMQ 5.x Monitoring - Alert Rule
+# Compatible with Prometheus / VictoriaMetrics / Thanos / Cortex / Mimir
+# ============================================================================
+groups:
+ - name: rocketmq-proxy.rules
+ rules:
+ - alert: RocketMQProxyDown
+ expr: up{job=~".*rocketmq.*proxy.*"} == 0
+ for: 1m
+ labels:
+ severity: critical
+ team: proxy
+ annotations:
+ summary: "RocketMQ proxy is down"
+ description: "A RocketMQ 5.x proxy target has been down for more
than 1 minute."
diff --git a/server/src/main/resources/alerts/rocketmq-proxy-latency-high.yaml
b/server/src/main/resources/alerts/rocketmq-proxy-latency-high.yaml
new file mode 100644
index 00000000..03d22895
--- /dev/null
+++ b/server/src/main/resources/alerts/rocketmq-proxy-latency-high.yaml
@@ -0,0 +1,16 @@
+# ============================================================================
+# RocketMQ 5.x Monitoring - Alert Rule
+# Compatible with Prometheus / VictoriaMetrics / Thanos / Cortex / Mimir
+# ============================================================================
+groups:
+ - name: rocketmq-proxy.rules
+ rules:
+ - alert: RocketMQProxyLatencyHigh
+ expr: histogram_quantile(0.99,
rate(rocketmq_proxy_process_time_bucket[5m])) > 1
+ for: 5m
+ labels:
+ severity: warning
+ team: proxy
+ annotations:
+ summary: "Proxy latency high"
+ description: "99th percentile proxy process time is above 1 second."
diff --git a/server/src/main/resources/alerts/rocketmq-threadpool-reject.yaml
b/server/src/main/resources/alerts/rocketmq-threadpool-reject.yaml
new file mode 100644
index 00000000..8967efe1
--- /dev/null
+++ b/server/src/main/resources/alerts/rocketmq-threadpool-reject.yaml
@@ -0,0 +1,16 @@
+# ============================================================================
+# RocketMQ 5.x Monitoring - Alert Rule
+# Compatible with Prometheus / VictoriaMetrics / Thanos / Cortex / Mimir
+# ============================================================================
+groups:
+ - name: rocketmq-broker.rules
+ rules:
+ - alert: RocketMQThreadPoolReject
+ expr: increase(rocketmq_threadpool_reject_count[5m]) > 0
+ for: 5m
+ labels:
+ severity: critical
+ team: broker
+ annotations:
+ summary: "Thread pool rejections"
+ description: "The broker thread pool rejected tasks, indicating
saturation."
diff --git a/server/src/main/resources/alerts/rocketmq-topic-accumulation.yaml
b/server/src/main/resources/alerts/rocketmq-topic-accumulation.yaml
new file mode 100644
index 00000000..a9b5907e
--- /dev/null
+++ b/server/src/main/resources/alerts/rocketmq-topic-accumulation.yaml
@@ -0,0 +1,16 @@
+# ============================================================================
+# RocketMQ 5.x Monitoring - Alert Rule
+# Compatible with Prometheus / VictoriaMetrics / Thanos / Cortex / Mimir
+# ============================================================================
+groups:
+ - name: rocketmq-topic.rules
+ rules:
+ - alert: RocketMQTopicAccumulation
+ expr: rocketmq_dispatch_behind_bytes > 1073741824
+ for: 5m
+ labels:
+ severity: warning
+ team: topic
+ annotations:
+ summary: "Topic dispatch backlog high"
+ description: "Topic dispatch behind bytes is above 1GiB."
diff --git
a/server/src/main/resources/alerts/rocketmq-topic-dispatch-latency.yaml
b/server/src/main/resources/alerts/rocketmq-topic-dispatch-latency.yaml
new file mode 100644
index 00000000..cd7bfe57
--- /dev/null
+++ b/server/src/main/resources/alerts/rocketmq-topic-dispatch-latency.yaml
@@ -0,0 +1,16 @@
+# ============================================================================
+# RocketMQ 5.x Monitoring - Alert Rule
+# Compatible with Prometheus / VictoriaMetrics / Thanos / Cortex / Mimir
+# ============================================================================
+groups:
+ - name: rocketmq-topic.rules
+ rules:
+ - alert: RocketMQTopicDispatchLatency
+ expr: histogram_quantile(0.99,
rate(rocketmq_dispatch_latency_bucket[5m])) > 1
+ for: 5m
+ labels:
+ severity: warning
+ team: topic
+ annotations:
+ summary: "Dispatch latency high"
+ description: "99th percentile dispatch latency is above 1 second."
diff --git a/server/src/main/resources/alerts/rocketmq-topic-in-drop.yaml
b/server/src/main/resources/alerts/rocketmq-topic-in-drop.yaml
new file mode 100644
index 00000000..1fb7ccf2
--- /dev/null
+++ b/server/src/main/resources/alerts/rocketmq-topic-in-drop.yaml
@@ -0,0 +1,16 @@
+# ============================================================================
+# RocketMQ 5.x Monitoring - Alert Rule
+# Compatible with Prometheus / VictoriaMetrics / Thanos / Cortex / Mimir
+# ============================================================================
+groups:
+ - name: rocketmq-topic.rules
+ rules:
+ - alert: RocketMQTopicMessageInDrop
+ expr: rate(rocketmq_messages_in_total[10m]) == 0
+ for: 10m
+ labels:
+ severity: info
+ team: topic
+ annotations:
+ summary: "No incoming messages"
+ description: "No incoming messages have been observed for 10
minutes."
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertRuleAssetControllerTest.java
b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertRuleAssetControllerTest.java
new file mode 100644
index 00000000..cad19f82
--- /dev/null
+++
b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertRuleAssetControllerTest.java
@@ -0,0 +1,81 @@
+/*
+ * 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.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 java.util.List;
+
+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.header;
+import static
org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static
org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@WebMvcTest(AlertRuleAssetController.class)
+@AutoConfigureMockMvc(addFilters = false)
+class AlertRuleAssetControllerTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @MockBean
+ private AlertRuleAssetService alertRuleAssetService;
+
+ @Test
+ void listAssetsShouldReturnMetadatas() throws Exception {
+ when(alertRuleAssetService.listAssets()).thenReturn(List.of(
+ new AlertRuleAssetInfo("rocketmq-broker-down",
"rocketmq-broker.rules", 1, List.of("critical")),
+ new AlertRuleAssetInfo("rocketmq-consumer-lag-high",
"rocketmq-consumer.rules", 1, List.of("warning"))
+ ));
+
+ mockMvc.perform(get("/api/alert-rules/assets"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(200))
+ .andExpect(jsonPath("$.data.length()").value(2))
+
.andExpect(jsonPath("$.data[0].name").value("rocketmq-broker-down"))
+ .andExpect(jsonPath("$.data[0].ruleCount").value(1));
+ }
+
+ @Test
+ void getAssetShouldReturnRawYaml() throws Exception {
+ when(alertRuleAssetService.getAssetYaml("rocketmq-broker-down"))
+ .thenReturn("groups:\n - name: rocketmq-broker.rules\n
rules:\n - alert: RocketMQBrokerDown\n");
+
+ mockMvc.perform(get("/api/alert-rules/assets/rocketmq-broker-down"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(200))
+ .andExpect(jsonPath("$.data").exists());
+ }
+
+ @Test
+ void exportAssetShouldReturnAttachment() throws Exception {
+ when(alertRuleAssetService.getAssetYaml("rocketmq-broker-down"))
+ .thenReturn("groups:\n - name: rocketmq-broker.rules\n");
+
+
mockMvc.perform(get("/api/alert-rules/assets/rocketmq-broker-down/export"))
+ .andExpect(status().isOk())
+ .andExpect(header().string("Content-Type",
"application/x-yaml"))
+ .andExpect(header().string("Content-Disposition",
+ "form-data; name=\"attachment\";
filename=\"rocketmq-broker-down.yaml\""));
+ }
+}
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertRuleAssetServiceTest.java
b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertRuleAssetServiceTest.java
new file mode 100644
index 00000000..ba1016cd
--- /dev/null
+++
b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertRuleAssetServiceTest.java
@@ -0,0 +1,84 @@
+/*
+ * 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.alert;
+
+import org.apache.rocketmq.studio.common.exception.BusinessException;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class AlertRuleAssetServiceTest {
+
+ private final AlertRuleAssetService service =
+ new AlertRuleAssetService();
+
+ @Test
+ void listAssetsShouldExposeBundledYamlFiles() {
+ List<AlertRuleAssetInfo> assets = service.listAssets();
+
+ assertFalse(assets.isEmpty(), "expected bundled alert rule assets");
+ assertTrue(assets.size() >= 10, "expected at least 10 asset files, got
" + assets.size());
+ for (AlertRuleAssetInfo asset : assets) {
+ assertFalse(asset.name().isBlank());
+ assertTrue(asset.ruleCount() >= 1, "asset should contain at least
one rule");
+ }
+ }
+
+ @Test
+ void loadDefaultRulesShouldReturnAtLeastTwentyRules() {
+ List<PrometheusAlertRule> rules = service.loadDefaultRules();
+
+ assertTrue(rules.size() >= 20, "expected at least 20 default alert
rules, got " + rules.size());
+ for (PrometheusAlertRule rule : rules) {
+ assertFalse(rule.alert().isBlank(), "rule alert name must not be
blank");
+ assertFalse(rule.expr().isBlank(), "rule expr must not be blank");
+ }
+ }
+
+ @Test
+ void getAssetYamlShouldReturnRawContent() {
+ List<AlertRuleAssetInfo> assets = service.listAssets();
+ String name = assets.get(0).name();
+
+ String yaml = service.getAssetYaml(name);
+
+ assertFalse(yaml.isBlank());
+ assertTrue(yaml.contains("alert:"));
+ assertTrue(yaml.contains("expr:"));
+ }
+
+ @Test
+ void getAssetYamlShouldThrowWhenNameUnknown() {
+ BusinessException exception = assertThrows(BusinessException.class,
+ () -> service.getAssetYaml("no-such-asset"));
+ assertEquals(404, exception.getCode());
+ }
+
+ @Test
+ void parseRulesShouldMapSeverityAndTeamLabels() {
+ List<PrometheusAlertRule> rules = service.loadDefaultRules();
+ boolean hasCritical = rules.stream().anyMatch(r ->
"critical".equals(r.severity()));
+ boolean hasBroker = rules.stream().anyMatch(r ->
"broker".equals(r.team()));
+ assertTrue(hasCritical, "expected at least one critical rule");
+ assertTrue(hasBroker, "expected at least one broker rule");
+ }
+}
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertServiceDefaultRulesTest.java
b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertServiceDefaultRulesTest.java
new file mode 100644
index 00000000..786ebf65
--- /dev/null
+++
b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertServiceDefaultRulesTest.java
@@ -0,0 +1,68 @@
+/*
+ * 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.alert;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class AlertServiceDefaultRulesTest {
+
+ private static final Pattern ALERT_PATTERN = Pattern.compile("^\\s*-
alert:");
+
+ @Test
+ void exportUsesBundledAssetsAsDefaultWhenRepositoryIsEmpty() {
+ AlertRepository repository = mock(AlertRepository.class);
+ when(repository.findAllRules()).thenReturn(Collections.emptyList());
+
+ AlertService service = new AlertService(repository, new
AlertRuleAssetService());
+ String yaml = service.exportPrometheusRulesYaml();
+
+ int ruleCount = countRules(yaml);
+ assertTrue(ruleCount >= 20, "expected at least 20 default alert rules,
got " + ruleCount);
+ }
+
+ @Test
+ void listRulesDelegatesToRepositoryWhenEmptyUsesDefaultsViaExport() {
+ AlertRepository repository = mock(AlertRepository.class);
+ when(repository.findAllRules()).thenReturn(List.of());
+
+ AlertService service = new AlertService(repository, new
AlertRuleAssetService());
+ String yaml = service.exportPrometheusRulesYaml();
+
+ assertTrue(yaml.contains("rocketmq-broker.rules"));
+ assertTrue(yaml.contains("RocketMQBrokerDown"));
+ }
+
+ private int countRules(String yaml) {
+ int count = 0;
+ for (String line : yaml.split("\n")) {
+ Matcher matcher = ALERT_PATTERN.matcher(line);
+ if (matcher.find()) {
+ count++;
+ }
+ }
+ return count;
+ }
+}
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertServiceTest.java
b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertServiceTest.java
index 0e717c3f..3b11ad02 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertServiceTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertServiceTest.java
@@ -23,7 +23,6 @@ import
org.apache.rocketmq.studio.common.domain.enums.AlertLevel;
import org.apache.rocketmq.studio.common.exception.BusinessException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@@ -45,9 +44,13 @@ class AlertServiceTest {
@Mock
private AlertRepository alertRepository;
- @InjectMocks
private AlertService alertService;
+ @org.junit.jupiter.api.BeforeEach
+ void setUp() {
+ alertService = new AlertService(alertRepository, new
AlertRuleAssetService());
+ }
+
@Test
void listRulesShouldReturnAllRules() {
AlertRuleVO rule1 = AlertRuleVO.builder().id("1").name("High
CPU").metric("cpu_usage")
@@ -82,8 +85,8 @@ class AlertServiceTest {
assertThat(result)
.contains("groups:")
- .contains("# Rule 1: RocketMQBrokerDown")
- .contains("up{job=~\".*rocketmq.*\"} == 0")
+ .contains("RocketMQBrokerDown")
+ .contains("up{job=~\".*rocketmq.*broker.*\"} == 0")
.contains("rocketmq_consumer_lag_messages > 100000")
.contains("rocketmq_producer_send_to_back_rt > 1000")
.contains("severity: critical");
diff --git a/web/src/App.tsx b/web/src/App.tsx
index cd9fba6c..db56997c 100644
--- a/web/src/App.tsx
+++ b/web/src/App.tsx
@@ -51,6 +51,7 @@ const AlertManagementPage = lazy(() =>
import('./pages/studio/AlertManagement'))
const GrafanaDashboardsPage = lazy(() =>
import('./pages/studio/GrafanaDashboards'));
const ProducerPage = lazy(() => import('./pages/studio/Producer'));
const OpsPage = lazy(() => import('./pages/studio/Ops'));
+const AlertRuleAssetsPage = lazy(() =>
import('./pages/studio/AlertRuleAssets'));
type AuthGateState = 'checking' | 'allowed' | 'denied' | 'error';
@@ -174,6 +175,7 @@ function App() {
<Route path="studio/alert-management"
element={<AlertManagementPage />} />
<Route path="studio/producer" element={<ProducerPage />} />
<Route path="studio/ops" element={<OpsPage />} />
+ <Route path="ops/alert-rule-templates"
element={<AlertRuleAssetsPage />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Route>
</Route>
diff --git a/web/src/api/alertRuleAssets.test.ts
b/web/src/api/alertRuleAssets.test.ts
new file mode 100644
index 00000000..ad137784
--- /dev/null
+++ b/web/src/api/alertRuleAssets.test.ts
@@ -0,0 +1,71 @@
+/*
+ * 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.
+ */
+
+import MockAdapter from 'axios-mock-adapter';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import client from './client';
+import { exportAlertRuleAsset, getAlertRuleAsset, listAlertRuleAssets } from
'./alertRuleAssets';
+
+const mock = new MockAdapter(client);
+
+describe('alertRuleAssets API', () => {
+ beforeEach(() => {
+ mock.reset();
+ vi.stubGlobal('localStorage', { getItem: vi.fn().mockReturnValue(null) });
+ });
+
+ afterEach(() => {
+ mock.reset();
+ vi.unstubAllGlobals();
+ });
+
+ it('lists alert rule assets', async () => {
+ const assets = [
+ {
+ name: 'rocketmq-broker-down',
+ group: 'rocketmq-broker.rules',
+ ruleCount: 1,
+ severities: ['critical'],
+ },
+ {
+ name: 'rocketmq-consumer-lag-high',
+ group: 'rocketmq-consumer.rules',
+ ruleCount: 1,
+ severities: ['warning'],
+ },
+ ];
+ mock.onGet('/alert-rules/assets').reply(200, { code: 200, data: assets });
+
+ await expect(listAlertRuleAssets()).resolves.toEqual(assets);
+ });
+
+ it('gets a single asset yaml by name', async () => {
+ const yaml = 'groups:\n - name: rocketmq-broker.rules\n';
+ mock.onGet('/alert-rules/assets/rocketmq-broker-down').reply(200, { code:
200, data: yaml });
+
+ await
expect(getAlertRuleAsset('rocketmq-broker-down')).resolves.toBe(yaml);
+ });
+
+ it('exports an asset as a blob', async () => {
+ const blob = new Blob(['groups:\n - name: rocketmq-broker.rules\n'], {
type: 'text/yaml' });
+ mock.onGet('/alert-rules/assets/rocketmq-broker-down/export').reply(200,
blob);
+
+ const result = await exportAlertRuleAsset('rocketmq-broker-down');
+ expect(result).toBeInstanceOf(Blob);
+ expect(result.type).toBe('text/yaml');
+ });
+});
diff --git a/web/src/api/alertRuleAssets.ts b/web/src/api/alertRuleAssets.ts
new file mode 100644
index 00000000..fbf2db94
--- /dev/null
+++ b/web/src/api/alertRuleAssets.ts
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+
+import client from './client';
+
+export interface AlertRuleAssetInfo {
+ name: string;
+ group: string;
+ ruleCount: number;
+ severities: string[];
+}
+
+export async function listAlertRuleAssets(): Promise<AlertRuleAssetInfo[]> {
+ const res = await client.get<{ data: AlertRuleAssetInfo[]
}>('/alert-rules/assets');
+ return res.data.data;
+}
+
+export async function getAlertRuleAsset(name: string): Promise<string> {
+ const res = await client.get<{ data: string
}>(`/alert-rules/assets/${encodeURIComponent(name)}`);
+ return res.data.data;
+}
+
+export async function exportAlertRuleAsset(name: string): Promise<Blob> {
+ const res = await
client.get<Blob>(`/alert-rules/assets/${encodeURIComponent(name)}/export`, {
+ responseType: 'blob',
+ });
+ return res.data;
+}
diff --git a/web/src/components/AlertRuleAssetList.tsx
b/web/src/components/AlertRuleAssetList.tsx
new file mode 100644
index 00000000..32b2a232
--- /dev/null
+++ b/web/src/components/AlertRuleAssetList.tsx
@@ -0,0 +1,197 @@
+/*
+ * 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.
+ */
+
+import { useEffect, useState } from 'react';
+import { App, Button, Modal, Space, Table, Tag, Typography } from 'antd';
+import type { ColumnsType } from 'antd/es/table';
+import { DownloadSimple, Eye } from '@phosphor-icons/react';
+import { useLang } from '../i18n/LangContext';
+import {
+ exportAlertRuleAsset,
+ getAlertRuleAsset,
+ listAlertRuleAssets,
+} from '../services/alertRuleAssetService';
+import type { AlertRuleAssetInfo } from '../api/alertRuleAssets';
+
+const { Text } = Typography;
+
+const SEVERITY_COLORS: Record<string, string> = {
+ critical: 'red',
+ warning: 'orange',
+ info: 'blue',
+};
+
+export const AlertRuleAssetList: React.FC = () => {
+ const { t } = useLang();
+ const { message } = App.useApp();
+ const [assets, setAssets] = useState<AlertRuleAssetInfo[]>([]);
+ const [loading, setLoading] = useState(true);
+ const [viewing, setViewing] = useState<AlertRuleAssetInfo | null>(null);
+ const [viewContent, setViewContent] = useState('');
+ const [viewLoading, setViewLoading] = useState(false);
+ const [exportingName, setExportingName] = useState<string | null>(null);
+
+ useEffect(() => {
+ let cancelled = false;
+ const load = async () => {
+ try {
+ const data = await listAlertRuleAssets();
+ if (!cancelled) setAssets(data);
+ } catch {
+ if (!cancelled) message.error(t('alertAssets.loadFailed'));
+ } finally {
+ if (!cancelled) setLoading(false);
+ }
+ };
+ void load();
+ return () => {
+ cancelled = true;
+ };
+ }, [t, message]);
+
+ const handleView = async (info: AlertRuleAssetInfo) => {
+ setViewing(info);
+ setViewLoading(true);
+ try {
+ const yaml = await getAlertRuleAsset(info.name);
+ setViewContent(yaml);
+ } catch {
+ message.error(t('alertAssets.loadFailed'));
+ } finally {
+ setViewLoading(false);
+ }
+ };
+
+ const triggerDownload = (name: string, content: Blob | string) => {
+ const blob = typeof content === 'string' ? new Blob([content], { type:
'text/yaml' }) : content;
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `${name}.yaml`;
+ a.click();
+ URL.revokeObjectURL(url);
+ };
+
+ const handleExport = async (info: AlertRuleAssetInfo) => {
+ setExportingName(info.name);
+ try {
+ const blob = await exportAlertRuleAsset(info.name);
+ triggerDownload(info.name, blob);
+ message.success(t('alertAssets.exported'));
+ } catch {
+ message.error(t('alertAssets.exportFailed'));
+ } finally {
+ setExportingName(null);
+ }
+ };
+
+ const columns: ColumnsType<AlertRuleAssetInfo> = [
+ {
+ title: t('alertAssets.name'),
+ dataIndex: 'name',
+ key: 'name',
+ },
+ {
+ title: t('alertAssets.group'),
+ dataIndex: 'group',
+ key: 'group',
+ render: (group: string) => <Tag color="blue">{group}</Tag>,
+ },
+ {
+ title: t('alertAssets.ruleCount'),
+ dataIndex: 'ruleCount',
+ key: 'ruleCount',
+ width: 110,
+ },
+ {
+ title: t('alertAssets.severity'),
+ dataIndex: 'severities',
+ key: 'severities',
+ width: 180,
+ render: (severities: string[]) => (
+ <Space size={[0, 4]} wrap>
+ {(severities || []).map((severity) => (
+ <Tag key={severity} color={SEVERITY_COLORS[severity] || 'default'}>
+ {severity.toUpperCase()}
+ </Tag>
+ ))}
+ </Space>
+ ),
+ },
+ {
+ title: t('common.actions'),
+ key: 'actions',
+ width: 180,
+ render: (_: unknown, record: AlertRuleAssetInfo) => (
+ <Space size="small">
+ <Button size="small" icon={<Eye size={16} />} onClick={() =>
handleView(record)}>
+ {t('common.view')}
+ </Button>
+ <Button
+ size="small"
+ icon={<DownloadSimple size={16} />}
+ loading={exportingName === record.name}
+ onClick={() => handleExport(record)}
+ >
+ {t('common.export')}
+ </Button>
+ </Space>
+ ),
+ },
+ ];
+
+ return (
+ <div>
+ <Table
+ columns={columns}
+ dataSource={assets}
+ loading={loading}
+ rowKey="name"
+ pagination={false}
+ size="small"
+ />
+
+ <Modal
+ title={viewing ? viewing.name : t('alertAssets.title')}
+ open={viewing !== null}
+ footer={<Button onClick={() =>
setViewing(null)}>{t('common.close')}</Button>}
+ onCancel={() => setViewing(null)}
+ width={760}
+ destroyOnHidden
+ >
+ {viewLoading ? (
+ <Text type="secondary">{t('common.loading')}</Text>
+ ) : (
+ <pre
+ style={{
+ maxHeight: 480,
+ overflow: 'auto',
+ background: '#f5f5f5',
+ padding: 16,
+ borderRadius: 6,
+ fontSize: 12,
+ }}
+ >
+ {viewContent}
+ </pre>
+ )}
+ </Modal>
+ </div>
+ );
+};
+
+export default AlertRuleAssetList;
diff --git a/web/src/components/__tests__/AlertRuleAssetList.test.tsx
b/web/src/components/__tests__/AlertRuleAssetList.test.tsx
new file mode 100644
index 00000000..cf606cdc
--- /dev/null
+++ b/web/src/components/__tests__/AlertRuleAssetList.test.tsx
@@ -0,0 +1,127 @@
+/*
+ * 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.
+ */
+
+import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
+import { fireEvent, render, screen, waitFor, within } from
'@testing-library/react';
+import { App as AntdApp } from 'antd';
+import AlertRuleAssetList from '../AlertRuleAssetList';
+import { LangProvider } from '../../i18n/LangContext';
+import * as alertRuleAssetService from '../../services/alertRuleAssetService';
+
+vi.mock('../../services/alertRuleAssetService', () => ({
+ listAlertRuleAssets: vi.fn(),
+ getAlertRuleAsset: vi.fn(),
+ exportAlertRuleAsset: vi.fn(),
+}));
+
+const sampleAssets = [
+ {
+ name: 'rocketmq-broker-down',
+ group: 'rocketmq-broker.rules',
+ ruleCount: 1,
+ severities: ['critical'],
+ },
+ {
+ name: 'rocketmq-consumer-lag-high',
+ group: 'rocketmq-consumer.rules',
+ ruleCount: 1,
+ severities: ['warning'],
+ },
+];
+
+const renderWithProviders = (ui: React.ReactElement) =>
+ render(
+ <LangProvider>
+ <AntdApp>{ui}</AntdApp>
+ </LangProvider>,
+ );
+
+describe('AlertRuleAssetList', () => {
+ beforeAll(() => {
+ window.matchMedia =
+ window.matchMedia ||
+ ((query: string) =>
+ ({
+ matches: false,
+ media: query,
+ onchange: null,
+ addListener: () => {},
+ removeListener: () => {},
+ addEventListener: () => {},
+ removeEventListener: () => {},
+ dispatchEvent: () => false,
+ }) as unknown as MediaQueryList);
+ });
+
+ afterEach(() => {
+ vi.clearAllMocks();
+ localStorage.clear();
+ });
+
+ it('renders asset rows from the service', async () => {
+
vi.mocked(alertRuleAssetService.listAlertRuleAssets).mockResolvedValue(sampleAssets);
+
+ renderWithProviders(<AlertRuleAssetList />);
+
+ expect(await
screen.findByText('rocketmq-broker-down')).toBeInTheDocument();
+ expect(screen.getByText('rocketmq-consumer-lag-high')).toBeInTheDocument();
+ });
+
+ it('opens a modal with yaml content when View is clicked', async () => {
+
vi.mocked(alertRuleAssetService.listAlertRuleAssets).mockResolvedValue(sampleAssets);
+ vi.mocked(alertRuleAssetService.getAlertRuleAsset).mockResolvedValue(
+ 'groups:\n - name: rocketmq-broker.rules\n rules:\n - alert:
RocketMQBrokerDown\n',
+ );
+
+ renderWithProviders(<AlertRuleAssetList />);
+
+ const viewButtons = await screen.findAllByRole('button', { name: /查看|View/
});
+ fireEvent.click(viewButtons[0]);
+
+ const dialog = await screen.findByRole('dialog');
+
expect(within(dialog).getByText(/rocketmq-broker.rules/)).toBeInTheDocument();
+
expect(alertRuleAssetService.getAlertRuleAsset).toHaveBeenCalledWith('rocketmq-broker-down');
+ });
+
+ it('downloads the yaml when Export is clicked', async () => {
+ const createObjectURLSpy = vi.spyOn(URL,
'createObjectURL').mockReturnValue('blob:url');
+ const revokeSpy = vi.spyOn(URL, 'revokeObjectURL').mockImplementation(()
=> {});
+ const clickSpy = vi.spyOn(HTMLAnchorElement.prototype,
'click').mockImplementation(() => {});
+
+
vi.mocked(alertRuleAssetService.listAlertRuleAssets).mockResolvedValue(sampleAssets);
+ vi.mocked(alertRuleAssetService.exportAlertRuleAsset).mockResolvedValue(
+ new Blob(['groups:\n - name: rocketmq-broker.rules\n'], { type:
'text/yaml' }),
+ );
+
+ renderWithProviders(<AlertRuleAssetList />);
+
+ const exportButtons = await screen.findAllByRole('button', { name:
/导出|Export/ });
+ fireEvent.click(exportButtons[0]);
+
+ await waitFor(() =>
+ expect(alertRuleAssetService.exportAlertRuleAsset).toHaveBeenCalledWith(
+ 'rocketmq-broker-down',
+ ),
+ );
+ expect(createObjectURLSpy).toHaveBeenCalled();
+ expect(clickSpy).toHaveBeenCalled();
+
+ createObjectURLSpy.mockRestore();
+ revokeSpy.mockRestore();
+ clickSpy.mockRestore();
+ });
+});
diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts
index 9afb2e54..3838f340 100644
--- a/web/src/i18n/translations.ts
+++ b/web/src/i18n/translations.ts
@@ -34,6 +34,7 @@ const translations: Record<string, Record<Lang, string>> = {
'nav.clients': { zh: '客户端连接', en: 'Client Connections' },
'nav.alertEvents': { zh: '告警事件', en: 'Alert Events' },
'nav.alertRules': { zh: '告警规则', en: 'Alert Rules' },
+ 'nav.alertRuleAssets': { zh: '告警规则模板', en: 'Alert Rule Templates' },
'nav.audit': { zh: '审计日志', en: 'Audit Log' },
'nav.grafanaDashboards': { zh: 'Grafana 看板', en: 'Grafana Dashboards' },
'nav.ai': { zh: 'AI 交互', en: 'AI Chat' },
@@ -741,6 +742,15 @@ const translations: Record<string, Record<Lang, string>> =
{
'grafana.loadFailed': { zh: '加载看板失败', en: 'Failed to load dashboards' },
'grafana.exported': { zh: '看板已导出', en: 'Dashboard exported' },
'grafana.exportFailed': { zh: '导出看板失败', en: 'Failed to export dashboard' },
+ // ─── Alert rule templates ───
+ 'alertAssets.title': { zh: '告警规则模板', en: 'Alert Rule Templates' },
+ 'alertAssets.name': { zh: '名称', en: 'Name' },
+ 'alertAssets.group': { zh: '规则组', en: 'Group' },
+ 'alertAssets.ruleCount': { zh: '规则数', en: 'Rules' },
+ 'alertAssets.severity': { zh: '级别', en: 'Severity' },
+ 'alertAssets.loadFailed': { zh: '加载告警规则失败', en: 'Failed to load alert rules'
},
+ 'alertAssets.exported': { zh: '告警规则已导出', en: 'Alert rule exported' },
+ 'alertAssets.exportFailed': { zh: '导出告警规则失败', en: 'Failed to export alert
rule' },
// ─── Topic (detailed) ───
'topic.subtitle': {
diff --git a/web/src/layouts/MainLayout.tsx b/web/src/layouts/MainLayout.tsx
index d85024ad..8803cf1e 100644
--- a/web/src/layouts/MainLayout.tsx
+++ b/web/src/layouts/MainLayout.tsx
@@ -38,6 +38,7 @@ import {
PlugsConnected,
BellRinging,
Notebook,
+ Warning,
} from '@phosphor-icons/react';
import { useLang } from '../i18n/LangContext';
import { useTheme } from '../theme/ThemeContext';
@@ -133,6 +134,11 @@ const MainLayout = () => {
{ key: '/ops/alerts', icon: <BellRinging size={16} />, label:
t('nav.alertRules') },
{ key: '/ops/system-alerts', icon: <BellRinging size={16} />, label:
t('nav.alertEvents') },
{ key: '/ops/audit', icon: <Notebook size={16} />, label:
t('nav.audit') },
+ {
+ key: '/ops/alert-rule-templates',
+ icon: <Warning size={16} />,
+ label: t('nav.alertRuleAssets'),
+ },
],
},
{ key: '/ai', icon: <Sparkle size={iconSize} weight="duotone" />, label:
t('nav.ai') },
@@ -160,6 +166,7 @@ const MainLayout = () => {
'/ops/system-alerts': t('nav.alertEvents'),
'/ops/alerts': t('nav.alertRules'),
'/ops/audit': t('nav.audit'),
+ '/ops/alert-rule-templates': t('nav.alertRuleAssets'),
'/ai': t('nav.ai'),
'/settings': t('nav.settings'),
};
diff --git a/web/src/mock/alertRuleAssets.ts b/web/src/mock/alertRuleAssets.ts
new file mode 100644
index 00000000..5ea80470
--- /dev/null
+++ b/web/src/mock/alertRuleAssets.ts
@@ -0,0 +1,210 @@
+/*
+ * 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.
+ */
+
+import type { AlertRuleAssetInfo } from '../api/alertRuleAssets';
+
+export interface MockAlertRuleAsset extends AlertRuleAssetInfo {
+ yaml: string;
+}
+
+const yaml = (name: string, group: string, alert: string, expr: string,
severity: string) =>
+ `groups:\n - name: ${group}\n rules:\n - alert: ${alert}\n
expr: ${expr}\n for: 5m\n labels:\n severity:
${severity}\n team: broker\n annotations:\n summary:
"${alert}"\n description: "Bundled alert rule ${name}"\n`;
+
+export const mockAlertRuleAssets: MockAlertRuleAsset[] = [
+ {
+ name: 'rocketmq-broker-down',
+ group: 'rocketmq-broker.rules',
+ ruleCount: 1,
+ severities: ['critical'],
+ yaml: yaml(
+ 'rocketmq-broker-down',
+ 'rocketmq-broker.rules',
+ 'RocketMQBrokerDown',
+ 'up{job=~".*rocketmq.*broker.*"} == 0',
+ 'critical',
+ ),
+ },
+ {
+ name: 'rocketmq-broker-disk-high',
+ group: 'rocketmq-broker.rules',
+ ruleCount: 1,
+ severities: ['critical'],
+ yaml: yaml(
+ 'rocketmq-broker-disk-high',
+ 'rocketmq-broker.rules',
+ 'RocketMQBrokerDiskHigh',
+ 'rocketmq_disk_use_ratio > 85',
+ 'critical',
+ ),
+ },
+ {
+ name: 'rocketmq-consumer-lag-high',
+ group: 'rocketmq-consumer.rules',
+ ruleCount: 1,
+ severities: ['warning'],
+ yaml: yaml(
+ 'rocketmq-consumer-lag-high',
+ 'rocketmq-consumer.rules',
+ 'RocketMQConsumerLagHigh',
+ 'rocketmq_consumer_lag_messages > 100000',
+ 'warning',
+ ),
+ },
+ {
+ name: 'rocketmq-consumer-lag-critical',
+ group: 'rocketmq-consumer.rules',
+ ruleCount: 1,
+ severities: ['critical'],
+ yaml: yaml(
+ 'rocketmq-consumer-lag-critical',
+ 'rocketmq-consumer.rules',
+ 'RocketMQConsumerLagCritical',
+ 'rocketmq_consumer_lag_messages > 1000000',
+ 'critical',
+ ),
+ },
+ {
+ name: 'rocketmq-producer-latency-high',
+ group: 'rocketmq-client.rules',
+ ruleCount: 1,
+ severities: ['warning'],
+ yaml: yaml(
+ 'rocketmq-producer-latency-high',
+ 'rocketmq-client.rules',
+ 'RocketMQProducerSendLatencyHigh',
+ 'rocketmq_producer_send_to_back_rt > 1000',
+ 'warning',
+ ),
+ },
+ {
+ name: 'rocketmq-producer-failure',
+ group: 'rocketmq-client.rules',
+ ruleCount: 1,
+ severities: ['critical'],
+ yaml: yaml(
+ 'rocketmq-producer-failure',
+ 'rocketmq-client.rules',
+ 'RocketMQProducerSendFailure',
+ 'rate(rocketmq_producer_send_failure_count[5m]) > 0',
+ 'critical',
+ ),
+ },
+ {
+ name: 'rocketmq-topic-in-drop',
+ group: 'rocketmq-topic.rules',
+ ruleCount: 1,
+ severities: ['info'],
+ yaml: yaml(
+ 'rocketmq-topic-in-drop',
+ 'rocketmq-topic.rules',
+ 'RocketMQTopicMessageInDrop',
+ 'rate(rocketmq_messages_in_total[10m]) == 0',
+ 'info',
+ ),
+ },
+ {
+ name: 'rocketmq-topic-accumulation',
+ group: 'rocketmq-topic.rules',
+ ruleCount: 1,
+ severities: ['warning'],
+ yaml: yaml(
+ 'rocketmq-topic-accumulation',
+ 'rocketmq-topic.rules',
+ 'RocketMQTopicAccumulation',
+ 'rocketmq_dispatch_behind_bytes > 1073741824',
+ 'warning',
+ ),
+ },
+ {
+ name: 'rocketmq-client-connection-drop',
+ group: 'rocketmq-client.rules',
+ ruleCount: 1,
+ severities: ['warning'],
+ yaml: yaml(
+ 'rocketmq-client-connection-drop',
+ 'rocketmq-client.rules',
+ 'RocketMQClientConnectionDrop',
+ 'changes(rocketmq_producer_count[5m]) < -5',
+ 'warning',
+ ),
+ },
+ {
+ name: 'rocketmq-proxy-down',
+ group: 'rocketmq-proxy.rules',
+ ruleCount: 1,
+ severities: ['critical'],
+ yaml: yaml(
+ 'rocketmq-proxy-down',
+ 'rocketmq-proxy.rules',
+ 'RocketMQProxyDown',
+ 'up{job=~".*rocketmq.*proxy.*"} == 0',
+ 'critical',
+ ),
+ },
+ {
+ name: 'rocketmq-exception-rate',
+ group: 'rocketmq-errors.rules',
+ ruleCount: 1,
+ severities: ['critical'],
+ yaml: yaml(
+ 'rocketmq-exception-rate',
+ 'rocketmq-errors.rules',
+ 'RocketMQBrokerExceptions',
+ 'rate(rocketmq_broker_exception_count[5m]) > 0',
+ 'critical',
+ ),
+ },
+ {
+ name: 'rocketmq-dlq-resend-high',
+ group: 'rocketmq-errors.rules',
+ ruleCount: 1,
+ severities: ['warning'],
+ yaml: yaml(
+ 'rocketmq-dlq-resend-high',
+ 'rocketmq-errors.rules',
+ 'RocketMQDLQResendHigh',
+ 'rate(rocketmq_dlq_resend_count[5m]) > 10',
+ 'warning',
+ ),
+ },
+ {
+ name: 'rocketmq-threadpool-reject',
+ group: 'rocketmq-broker.rules',
+ ruleCount: 1,
+ severities: ['critical'],
+ yaml: yaml(
+ 'rocketmq-threadpool-reject',
+ 'rocketmq-broker.rules',
+ 'RocketMQThreadPoolReject',
+ 'increase(rocketmq_threadpool_reject_count[5m]) > 0',
+ 'critical',
+ ),
+ },
+ {
+ name: 'rocketmq-jvm-gc-cpu-high',
+ group: 'rocketmq-broker.rules',
+ ruleCount: 1,
+ severities: ['warning'],
+ yaml: yaml(
+ 'rocketmq-jvm-gc-cpu-high',
+ 'rocketmq-broker.rules',
+ 'RocketMQJVMCpuHigh',
+ 'rate(jvm_gc_pause_seconds_count[5m]) *
avg(rate(jvm_gc_pause_seconds_sum[5m])) > 0.3',
+ 'warning',
+ ),
+ },
+];
diff --git a/web/src/pages/studio/AlertRuleAssets.tsx
b/web/src/pages/studio/AlertRuleAssets.tsx
new file mode 100644
index 00000000..4b55f38e
--- /dev/null
+++ b/web/src/pages/studio/AlertRuleAssets.tsx
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+
+import React from 'react';
+import { Card, Space } from 'antd';
+import { Warning } from '@phosphor-icons/react';
+import { useLang } from '../../i18n/LangContext';
+import AlertRuleAssetList from '../../components/AlertRuleAssetList';
+
+const AlertRuleAssetsPage: React.FC = () => {
+ const { t } = useLang();
+
+ return (
+ <div style={{ padding: '0 0 24px' }}>
+ <Card
+ size="small"
+ title={
+ <Space>
+ <Warning size={18} />
+ <span>{t('alertAssets.title')}</span>
+ </Space>
+ }
+ >
+ <AlertRuleAssetList />
+ </Card>
+ </div>
+ );
+};
+
+export default AlertRuleAssetsPage;
diff --git a/web/src/services/alertRuleAssetService.test.ts
b/web/src/services/alertRuleAssetService.test.ts
new file mode 100644
index 00000000..281a014b
--- /dev/null
+++ b/web/src/services/alertRuleAssetService.test.ts
@@ -0,0 +1,101 @@
+/*
+ * 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.
+ */
+
+import { describe, expect, it, vi } from 'vitest';
+import type { AlertRuleAssetInfo } from '../api/alertRuleAssets';
+import * as api from '../api/alertRuleAssets';
+import { mockAlertRuleAssets } from '../mock/alertRuleAssets';
+
+const { mode } = vi.hoisted(() => ({ mode: { mock: true } }));
+
+vi.mock('./dataMode', () => ({ isMockMode: () => mode.mock }));
+vi.mock('../api/alertRuleAssets', () => ({
+ listAlertRuleAssets: vi.fn(),
+ getAlertRuleAsset: vi.fn(),
+ exportAlertRuleAsset: vi.fn(),
+}));
+
+import {
+ exportAlertRuleAsset,
+ getAlertRuleAsset,
+ listAlertRuleAssets,
+} from './alertRuleAssetService';
+
+describe('alertRuleAssetService (mock mode)', () => {
+ it('maps mock assets to AlertRuleAssetInfo list', async () => {
+ mode.mock = true;
+ const assets = await listAlertRuleAssets();
+ expect(assets.length).toBe(mockAlertRuleAssets.length);
+ expect(assets[0]).toEqual({
+ name: mockAlertRuleAssets[0].name,
+ group: mockAlertRuleAssets[0].group,
+ ruleCount: mockAlertRuleAssets[0].ruleCount,
+ severities: mockAlertRuleAssets[0].severities,
+ });
+ });
+
+ it('returns yaml for a known asset', async () => {
+ mode.mock = true;
+ const yaml = await getAlertRuleAsset('rocketmq-broker-down');
+ expect(yaml).toContain('RocketMQBrokerDown');
+ });
+
+ it('throws for an unknown asset', async () => {
+ mode.mock = true;
+ await expect(getAlertRuleAsset('does-not-exist')).rejects.toThrow();
+ });
+
+ it('exports a yaml blob', async () => {
+ mode.mock = true;
+ const blob = await exportAlertRuleAsset('rocketmq-broker-down');
+ expect(blob).toBeInstanceOf(Blob);
+ expect(blob.type).toBe('text/yaml');
+ });
+});
+
+describe('alertRuleAssetService (real mode)', () => {
+ it('delegates list to the api module', async () => {
+ mode.mock = false;
+ const data: AlertRuleAssetInfo[] = [
+ { name: 'a', group: 'g', ruleCount: 1, severities: ['info'] },
+ ];
+ vi.mocked(api.listAlertRuleAssets).mockResolvedValue(data);
+
+ const result = await listAlertRuleAssets();
+ expect(api.listAlertRuleAssets).toHaveBeenCalled();
+ expect(result).toEqual(data);
+ });
+
+ it('delegates get to the api module', async () => {
+ mode.mock = false;
+ vi.mocked(api.getAlertRuleAsset).mockResolvedValue('yaml-content');
+
+ const result = await getAlertRuleAsset('a');
+ expect(api.getAlertRuleAsset).toHaveBeenCalledWith('a');
+ expect(result).toBe('yaml-content');
+ });
+
+ it('delegates export to the api module', async () => {
+ mode.mock = false;
+ const blob = new Blob(['x']);
+ vi.mocked(api.exportAlertRuleAsset).mockResolvedValue(blob);
+
+ const result = await exportAlertRuleAsset('a');
+ expect(api.exportAlertRuleAsset).toHaveBeenCalledWith('a');
+ expect(result).toBe(blob);
+ });
+});
diff --git a/web/src/services/alertRuleAssetService.ts
b/web/src/services/alertRuleAssetService.ts
new file mode 100644
index 00000000..cd82389f
--- /dev/null
+++ b/web/src/services/alertRuleAssetService.ts
@@ -0,0 +1,39 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.
+
+import { isMockMode } from './dataMode';
+import * as alertRuleAssetsApi from '../api/alertRuleAssets';
+import type { AlertRuleAssetInfo } from '../api/alertRuleAssets';
+import { mockAlertRuleAssets } from '../mock/alertRuleAssets';
+
+export async function listAlertRuleAssets(): Promise<AlertRuleAssetInfo[]> {
+ if (isMockMode()) {
+ return mockAlertRuleAssets.map(({ name, group, ruleCount, severities }) =>
({
+ name,
+ group,
+ ruleCount,
+ severities,
+ }));
+ }
+ return alertRuleAssetsApi.listAlertRuleAssets();
+}
+
+export async function getAlertRuleAsset(name: string): Promise<string> {
+ if (isMockMode()) {
+ const found = mockAlertRuleAssets.find((asset) => asset.name === name);
+ if (!found) {
+ throw new Error(`Alert rule asset not found: ${name}`);
+ }
+ return found.yaml;
+ }
+ return alertRuleAssetsApi.getAlertRuleAsset(name);
+}
+
+export async function exportAlertRuleAsset(name: string): Promise<Blob> {
+ if (isMockMode()) {
+ const found = mockAlertRuleAssets.find((asset) => asset.name === name);
+ const yaml = found ? found.yaml : '';
+ return new Blob([yaml], { type: 'text/yaml' });
+ }
+ return alertRuleAssetsApi.exportAlertRuleAsset(name);
+}