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 0000a1382 fix(alert): keep consumer group and topic scope in 
Prometheus export (#3989)
0000a1382 is described below

commit 0000a13824baf057383bdb7fdb8eec818415e868
Author: Zhao Jianing <[email protected]>
AuthorDate: Tue Sep 15 20:07:51 2026 +0800

    fix(alert): keep consumer group and topic scope in Prometheus export (#3989)
    
    labelSelector only forwarded clusterName and brokerName, so an alert
    rule scoped to a consumer group (or topic) exported as a bare
    'metric > threshold' expression: the exported alert evaluated every
    series of the metric, silently widening the rule's boundary to the
    whole cluster.
    
    Forward the rule's consumerGroup and topic fields as PromQL labels,
    with the label names resolved from the active metric profile through
    MetricProfileService.resolveCurrentScopeLabel — each profile mapping
    now carries scopeLabels mapping canonical scope dimensions to the
    label the profile actually exposes (the 4.x exporter labels
    consumer-group series "group", the 5.x native metrics
    "consumer_group"). A hardcoded "group" selector would match an empty
    series set on a 5.x deployment and silently disable the alert; an
    unmapped scope drops the selector. Wildcard and empty scopes keep
    exporting without the label.
    
    Signed-off-by: zjncs <[email protected]>
---
 .../cluster/metrics/MetricProfileService.java      |  57 +++++++-
 .../studio/cluster/metrics/MetricProfileVO.java    |   8 ++
 .../rocketmq/studio/ops/alert/AlertService.java    |  39 +++++-
 .../cluster/metrics/MetricProfileServiceTest.java  |  27 ++++
 .../studio/ops/alert/AlertServiceTest.java         | 155 +++++++++++++++++++++
 5 files changed, 284 insertions(+), 2 deletions(-)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/MetricProfileService.java
 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/MetricProfileService.java
index bfd2b8dbb..15b641623 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/MetricProfileService.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/MetricProfileService.java
@@ -22,6 +22,7 @@ import org.springframework.stereotype.Service;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 import java.util.Optional;
 
 @Service
@@ -72,6 +73,40 @@ public class MetricProfileService {
                 .findFirst();
     }
 
+    /**
+     * Resolves the label name the active profile uses for a scope dimension 
of a semantic
+     * metric's series — e.g. the consumer_group dimension is labeled {@code 
group} by the
+     * 4.x exporter profile and {@code consumer_group} by the 5.x native 
profile. Returns
+     * empty when the active profile has no mapping for the semantic metric or 
its series
+     * are not broken out by that dimension, so callers drop the selector 
instead of guessing
+     * a label name that would silently match an empty series set.
+     */
+    public Optional<String> resolveCurrentScopeLabel(String semanticMetric, 
String scope) {
+        return listProfiles().get(0).getMetrics().stream()
+                .filter(metric -> 
metric.getSemanticMetric().equals(semanticMetric))
+                .findFirst()
+                .flatMap(metric -> Optional.ofNullable(metric.getScopeLabels())
+                        .map(labels -> labels.get(scope)));
+    }
+
+    /**
+     * Semantic metric that a rule's metric string refers to, matched against 
both the semantic key
+     * and the active profile's exporter name. A rule may store either form, 
and guessing the
+     * semantic metric for an exporter name that belongs to a different 
mapping yields label names
+     * the referenced series do not carry. Returns empty when the active 
profile has no such metric.
+     */
+    public Optional<String> resolveSemanticMetric(String metric) {
+        if (metric == null || metric.isBlank()) {
+            return Optional.empty();
+        }
+        String normalized = metric.trim();
+        return listProfiles().get(0).getMetrics().stream()
+                .filter(mapping -> 
normalized.equals(mapping.getSemanticMetric())
+                        || normalized.equals(mapping.getPrometheusMetric()))
+                .map(MetricProfileVO.MetricMappingVO::getSemanticMetric)
+                .findFirst();
+    }
+
     private MetricProfileVO profile(MetricProfile profile,
                                     List<MetricProfileVO.MetricMappingVO> 
metrics) {
         return MetricProfileVO.builder()
@@ -86,24 +121,31 @@ public class MetricProfileService {
         return List.of(
                 mapping(SemanticMetric.MESSAGE_IN_TPS, "rocketmq_broker_tps",
                         "sum(rocketmq_broker_tps) by (cluster, broker)",
+                        Map.of("cluster", "cluster", "broker", "broker"),
                         "cluster", "broker"),
                 mapping(SemanticMetric.MESSAGE_OUT_TPS, 
"rocketmq_consumer_tps",
                         "sum(rocketmq_consumer_tps) by (cluster, group, 
topic)",
+                        Map.of("cluster", "cluster", "consumer_group", 
"group", "topic", "topic"),
                         "cluster", "group", "topic"),
                 mapping(SemanticMetric.THROUGHPUT_IN, 
"rocketmq_producer_message_size",
                         "sum(rocketmq_producer_message_size) by (cluster, 
topic)",
+                        Map.of("cluster", "cluster", "topic", "topic"),
                         "cluster", "topic"),
                 mapping(SemanticMetric.THROUGHPUT_OUT, 
"rocketmq_consumer_message_size",
                         "sum(rocketmq_consumer_message_size) by (cluster, 
group, topic)",
+                        Map.of("cluster", "cluster", "consumer_group", 
"group", "topic", "topic"),
                         "cluster", "group", "topic"),
                 mapping(SemanticMetric.CONSUMER_LAG_MESSAGES, 
"rocketmq_message_accumulation",
                         "sum(rocketmq_message_accumulation) by (cluster, 
group, topic)",
+                        Map.of("cluster", "cluster", "consumer_group", 
"group", "topic", "topic"),
                         "cluster", "group", "topic"),
                 mapping(SemanticMetric.CONSUMER_LAG_LATENCY, 
"rocketmq_group_get_latency_by_storetime",
                         "max(rocketmq_group_get_latency_by_storetime) by 
(cluster, group, topic)",
+                        Map.of("cluster", "cluster", "consumer_group", 
"group", "topic", "topic"),
                         "cluster", "group", "topic"),
                 mapping(SemanticMetric.BROKER_HEALTH, "up",
                         "min(up{job=~\".*rocketmq.*\"}) by (job, instance)",
+                        Map.of(),
                         "job", "instance")
         );
     }
@@ -112,43 +154,56 @@ public class MetricProfileService {
         return List.of(
                 mapping(SemanticMetric.MESSAGE_IN_TPS, 
"rocketmq_messages_in_total",
                         "sum(rate(rocketmq_messages_in_total[1m])) by 
(cluster, node_id)",
+                        Map.of("cluster", "cluster", "broker", "node_id", 
"topic", "topic"),
                         "cluster", "node_id", "topic", "message_type"),
                 mapping(SemanticMetric.MESSAGE_OUT_TPS, 
"rocketmq_messages_out_total",
                         "sum(rate(rocketmq_messages_out_total[1m])) by 
(cluster, node_id, consumer_group)",
+                        Map.of("cluster", "cluster", "broker", "node_id", 
"topic", "topic",
+                                "consumer_group", "consumer_group"),
                         "cluster", "node_id", "topic", "consumer_group"),
                 mapping(SemanticMetric.THROUGHPUT_IN, 
"rocketmq_throughput_in_total",
                         "sum(rate(rocketmq_throughput_in_total[1m])) by 
(cluster, node_id)",
+                        Map.of("cluster", "cluster", "broker", "node_id", 
"topic", "topic"),
                         "cluster", "node_id", "topic", "message_type"),
                 mapping(SemanticMetric.THROUGHPUT_OUT, 
"rocketmq_throughput_out_total",
                         "sum(rate(rocketmq_throughput_out_total[1m])) by 
(cluster, node_id, consumer_group)",
+                        Map.of("cluster", "cluster", "broker", "node_id", 
"topic", "topic",
+                                "consumer_group", "consumer_group"),
                         "cluster", "node_id", "topic", "consumer_group"),
                 mapping(SemanticMetric.CONSUMER_LAG_MESSAGES, 
"rocketmq_consumer_lag_messages",
                         "sum(rocketmq_consumer_lag_messages) by (cluster, 
topic, consumer_group)",
+                        Map.of("cluster", "cluster", "topic", "topic", 
"consumer_group", "consumer_group"),
                         "cluster", "topic", "consumer_group"),
                 mapping(SemanticMetric.CONSUMER_LAG_LATENCY, 
"rocketmq_consumer_lag_latency_milliseconds",
                         "max(rocketmq_consumer_lag_latency_milliseconds) by 
(cluster, topic, consumer_group)",
+                        Map.of("cluster", "cluster", "topic", "topic", 
"consumer_group", "consumer_group"),
                         "cluster", "topic", "consumer_group"),
                 // Every broker reports the same cluster-level count; max 
avoids double counting.
                 mapping(SemanticMetric.TOPIC_NUMBER, "rocketmq_topic_number",
                         "max(rocketmq_topic_number) by (cluster)",
+                        Map.of("cluster", "cluster"),
                         "cluster"),
                 mapping(SemanticMetric.CONSUMER_GROUP_NUMBER, 
"rocketmq_consumer_group_number",
                         "max(rocketmq_consumer_group_number) by (cluster)",
+                        Map.of("cluster", "cluster"),
                         "cluster"),
                 mapping(SemanticMetric.BROKER_HEALTH, "up",
                         "min(up{job=~\".*rocketmq.*\"}) by (job, instance)",
+                        Map.of(),
                         "job", "instance")
         );
     }
 
     private MetricProfileVO.MetricMappingVO mapping(SemanticMetric 
semanticMetric, String prometheusMetric,
-                                                    String promql, String... 
labels) {
+                                                    String promql, Map<String, 
String> scopeLabels,
+                                                    String... labels) {
         return MetricProfileVO.MetricMappingVO.builder()
                 .semanticMetric(semanticMetric.getKey())
                 .name(semanticMetric.getDisplayName())
                 .unit(semanticMetric.getUnit())
                 .prometheusMetric(prometheusMetric)
                 .promql(promql)
+                .scopeLabels(scopeLabels)
                 .labels(List.of(labels))
                 .build();
     }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/MetricProfileVO.java
 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/MetricProfileVO.java
index d03a28a57..8674ed8e7 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/MetricProfileVO.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/MetricProfileVO.java
@@ -22,6 +22,7 @@ import lombok.Data;
 import lombok.NoArgsConstructor;
 
 import java.util.List;
+import java.util.Map;
 
 @Data
 @Builder
@@ -45,5 +46,12 @@ public class MetricProfileVO {
         private String prometheusMetric;
         private String promql;
         private List<String> labels;
+        /**
+         * Canonical scope dimension (cluster, broker, topic, consumer_group) 
to the label name
+         * this profile exposes on the metric's series. Exported rules must 
emit selectors with
+         * these label names — the consumer_group dimension is "group" on the 
4.x exporter
+         * profile and "consumer_group" on the 5.x native profile.
+         */
+        private Map<String, String> scopeLabels;
     }
 }
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 9a38cae45..f456991e7 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
@@ -36,6 +36,7 @@ import java.util.HashSet;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
 import java.util.regex.Pattern;
 import java.time.LocalDateTime;
@@ -695,12 +696,48 @@ public class AlertService {
     }
 
     private String labelSelector(AlertRuleVO rule) {
+        Optional<String> semantic = scopeSemanticMetric(rule.getMetric());
         StringBuilder selector = new StringBuilder();
+        // cluster is spelled identically by both profiles. The broker 
dimension is node_id on the
+        // 5.x native profile, so use the profile's own name when it maps one; 
a metric no profile
+        // knows (a custom exporter rule) keeps the literal name, because 
dropping the scope would
+        // silently widen the rule.
         appendLabel(selector, "cluster", rule.getClusterName());
-        appendLabel(selector, "broker", rule.getBrokerName());
+        appendLabel(selector, resolveScopeLabel(semantic, "broker", "broker"), 
rule.getBrokerName());
+        appendScopeLabel(selector, semantic, "consumer_group", 
rule.getConsumerGroup());
+        appendScopeLabel(selector, semantic, "topic", rule.getTopic());
         return selector.isEmpty() ? "" : "{" + selector + "}";
     }
 
+    /**
+     * Semantic metric the rule actually references — a native key through
+     * {@link #NATIVE_METRIC_SEMANTIC}, otherwise matched against the active 
profile's semantic keys
+     * and exporter names. Empty when no profile knows the metric: falling 
back to the consumer-lag
+     * mapping would borrow that metric's label names and emit, say, {@code 
consumer_group} on a
+     * series that carries no such label, silently matching an empty set.
+     */
+    private Optional<String> scopeSemanticMetric(String metric) {
+        String normalized = hasText(metric) ? metric.trim() : "";
+        return Optional.ofNullable(NATIVE_METRIC_SEMANTIC.get(normalized))
+                .or(() -> 
metricProfileService.resolveSemanticMetric(normalized));
+    }
+
+    private String resolveScopeLabel(Optional<String> semantic, String scope, 
String fallback) {
+        return semantic.flatMap(key -> 
metricProfileService.resolveCurrentScopeLabel(key, scope))
+                .orElse(fallback);
+    }
+
+    private void appendScopeLabel(StringBuilder selector, Optional<String> 
semantic, String scope, String value) {
+        if (!hasText(value) || "*".equals(value.trim())) {
+            return;
+        }
+        // A label name guessed for the wrong profile (e.g. "group" on a 5.x 
deployment)
+        // matches an empty series set and silently disables the alert, so the 
name must
+        // come from the active profile's mapping; an unmapped scope drops the 
selector.
+        semantic.flatMap(key -> 
metricProfileService.resolveCurrentScopeLabel(key, scope))
+                .ifPresent(label -> appendLabel(selector, label, value));
+    }
+
     private void appendLabel(StringBuilder selector, String label, String 
value) {
         if (!hasText(value) || "*".equals(value.trim())) {
             return;
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/cluster/metrics/MetricProfileServiceTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/cluster/metrics/MetricProfileServiceTest.java
index 73024103d..cb5718b07 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/cluster/metrics/MetricProfileServiceTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/cluster/metrics/MetricProfileServiceTest.java
@@ -159,6 +159,33 @@ class MetricProfileServiceTest {
                 "Unknown semantic metric 'queue_depth' for profile 
'rocketmq5-native'");
     }
 
+    @Test
+    void resolveCurrentScopeLabelShouldFollowActiveProfileTest() {
+        // 5.x native profile (the default): consumer-group series carry the 
consumer_group label.
+        assertThat(service.resolveCurrentScopeLabel("consumer_lag_messages", 
"consumer_group"))
+                .contains("consumer_group");
+        assertThat(service.resolveCurrentScopeLabel("consumer_lag_messages", 
"topic"))
+                .contains("topic");
+        assertThat(service.resolveCurrentScopeLabel("consumer_lag_messages", 
"broker"))
+                .isEmpty();
+
+        // 4.x exporter profile: the same dimension is the exporter's "group" 
label, and the
+        // lag series are not broken out by broker either.
+        PrometheusProperties exporterProperties = new PrometheusProperties();
+        exporterProperties.setProfile("rocketmq4-exporter");
+        MetricProfileService exporterService = new 
MetricProfileService(exporterProperties);
+        
assertThat(exporterService.resolveCurrentScopeLabel("consumer_lag_messages", 
"consumer_group"))
+                .contains("group");
+        
assertThat(exporterService.resolveCurrentScopeLabel("consumer_lag_messages", 
"topic"))
+                .contains("topic");
+        
assertThat(exporterService.resolveCurrentScopeLabel("consumer_lag_messages", 
"broker"))
+                .isEmpty();
+
+        // An unknown semantic metric resolves to empty so callers drop the 
selector instead
+        // of guessing a label name that would match an empty series set.
+        assertThat(service.resolveCurrentScopeLabel("queue_depth", 
"consumer_group")).isEmpty();
+    }
+
     private MetricProfileVO findProfile(String id) {
         return service.listProfiles().stream()
                 .filter(profile -> profile.getId().equals(id))
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 fa0d31c60..ebde0b84f 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,9 @@ import org.apache.rocketmq.studio.common.domain.PageResult;
 import org.apache.rocketmq.studio.common.domain.enums.AlertLevel;
 import org.apache.rocketmq.studio.common.exception.BusinessException;
 import org.apache.rocketmq.studio.audit.OperationAuditService;
+import org.apache.rocketmq.studio.cluster.metrics.MetricProfile;
 import org.apache.rocketmq.studio.cluster.metrics.MetricProfileService;
+import org.apache.rocketmq.studio.cluster.metrics.MetricProfileVO;
 import org.apache.rocketmq.studio.cluster.metrics.PrometheusProperties;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
@@ -477,6 +479,159 @@ class AlertServiceTest {
                 .contains("- name: rocketmq-consumer.rules");
     }
 
+    private AlertService alertServiceWithProfile(String profileId) {
+        PrometheusProperties properties = new PrometheusProperties();
+        properties.setProfile(profileId);
+        return new AlertService(alertRepository, alertStateRepository, new 
AlertRuleAssetService(),
+                operationAuditService, new MetricProfileService(properties));
+    }
+
+    private MetricProfileVO.MetricMappingVO activeLagMapping(String profileId) 
{
+        PrometheusProperties properties = new PrometheusProperties();
+        properties.setProfile(profileId);
+        return new 
MetricProfileService(properties).listProfiles().get(0).getMetrics().stream()
+                .filter(metric -> 
"consumer_lag_messages".equals(metric.getSemanticMetric()))
+                .findFirst()
+                .orElseThrow();
+    }
+
+    @Test
+    void 
exportPrometheusRulesYamlShouldKeepConsumerGroupScopeOnNativeProfileTest() {
+        AlertRuleVO rule = AlertRuleVO.builder()
+                .name("Orders Group Lag")
+                .metric("rocketmq_consumer_lag_messages")
+                .operator(">")
+                .threshold(5000)
+                .consumerGroup("cg-orders")
+                .enabled(true)
+                .build();
+        when(alertRepository.findAllRules()).thenReturn(List.of(rule));
+
+        String result = alertService.exportPrometheusRulesYaml();
+
+        // The 5.x native profile labels consumer-group series 
"consumer_group"; a hardcoded
+        // "group" selector would match an empty series set and silently 
disable the alert.
+        assertThat(result)
+                .contains("expr: 
rocketmq_consumer_lag_messages{consumer_group=\"cg-orders\"} > 5000")
+                .doesNotContain("rocketmq_consumer_lag_messages{group=")
+                .doesNotContain("expr: rocketmq_consumer_lag_messages >");
+    }
+
+    @Test
+    void exportPrometheusRulesYamlShouldUseTheNativeBrokerLabelTest() {
+        AlertRuleVO rule = AlertRuleVO.builder()
+                .name("Broker Inbound TPS")
+                .metric("rocketmq_messages_in_total")
+                .operator(">")
+                .threshold(1000)
+                .brokerName("broker-a")
+                .enabled(true)
+                .build();
+        when(alertRepository.findAllRules()).thenReturn(List.of(rule));
+
+        String result = alertService.exportPrometheusRulesYaml();
+
+        // The 5.x native profile aggregates the broker dimension as node_id, 
so a literal
+        // broker="..." selector matches an empty series set and the rule 
never fires.
+        assertThat(result)
+                .contains("expr: 
rocketmq_messages_in_total{node_id=\"broker-a\"} > 1000")
+                .doesNotContain("rocketmq_messages_in_total{broker=");
+    }
+
+    @Test
+    void 
exportPrometheusRulesYamlShouldDropScopesTheReferencedMetricDoesNotCarryTest() {
+        AlertRuleVO rule = AlertRuleVO.builder()
+                .name("Payments Inbound TPS")
+                .metric("rocketmq_messages_in_total")
+                .operator(">")
+                .threshold(1000)
+                .consumerGroup("cg-payments")
+                .enabled(true)
+                .build();
+        when(alertRepository.findAllRules()).thenReturn(List.of(rule));
+
+        String result = alertService.exportPrometheusRulesYaml();
+
+        // rocketmq_messages_in_total series carry no consumer_group label. 
Borrowing the
+        // consumer-lag mapping's label names for an unrelated metric emits a 
selector that
+        // matches nothing, which silently disables the rule instead of 
failing loudly.
+        assertThat(result)
+                .contains("expr: rocketmq_messages_in_total > 1000")
+                .doesNotContain("consumer_group=");
+    }
+
+    @Test
+    void 
exportPrometheusRulesYamlShouldKeepConsumerGroupScopeOnExporterProfileTest() {
+        AlertRuleVO rule = AlertRuleVO.builder()
+                .name("Orders Group Lag")
+                .metric("consumer.lag.total")
+                .operator(">")
+                .threshold(5000)
+                .consumerGroup("cg-orders")
+                .enabled(true)
+                .build();
+        when(alertRepository.findAllRules()).thenReturn(List.of(rule));
+
+        String result = 
alertServiceWithProfile(MetricProfile.ROCKETMQ_4_EXPORTER.getId())
+                .exportPrometheusRulesYaml();
+
+        // On the 4.x exporter profile the metric resolves to 
rocketmq_message_accumulation and
+        // the consumer-group dimension is the "group" label, per the 
profile's own mapping.
+        MetricProfileVO.MetricMappingVO lagMapping = 
activeLagMapping(MetricProfile.ROCKETMQ_4_EXPORTER.getId());
+        
assertThat(lagMapping.getPrometheusMetric()).isEqualTo("rocketmq_message_accumulation");
+        
assertThat(lagMapping.getScopeLabels()).containsEntry("consumer_group", 
"group");
+        assertThat(result)
+                .contains("expr: 
rocketmq_message_accumulation{group=\"cg-orders\"} > 5000")
+                .doesNotContain("consumer_group=\"")
+                .doesNotContain("expr: rocketmq_message_accumulation >");
+    }
+
+    @Test
+    void exportPrometheusRulesYamlShouldKeepTopicScopeOnBothProfilesTest() {
+        AlertRuleVO rule = AlertRuleVO.builder()
+                .name("Payments Backlog")
+                .metric("consumer.lag.total")
+                .operator(">")
+                .threshold(2000)
+                .clusterName("DefaultCluster")
+                .consumerGroup("cg-payments")
+                .topic("payments")
+                .enabled(true)
+                .build();
+        when(alertRepository.findAllRules()).thenReturn(List.of(rule));
+
+        // Both profiles spell the cluster and topic dimensions identically, 
so the same rule
+        // exports with the profile-correct metric name and consumer-group 
label either way.
+        assertThat(alertService.exportPrometheusRulesYaml())
+                .contains("expr: 
rocketmq_consumer_lag_messages{cluster=\"DefaultCluster\","
+                        + "consumer_group=\"cg-payments\",topic=\"payments\"} 
> 2000");
+        
assertThat(alertServiceWithProfile(MetricProfile.ROCKETMQ_4_EXPORTER.getId())
+                .exportPrometheusRulesYaml())
+                .contains("expr: 
rocketmq_message_accumulation{cluster=\"DefaultCluster\","
+                        + "group=\"cg-payments\",topic=\"payments\"} > 2000");
+    }
+
+    @Test
+    void exportPrometheusRulesYamlShouldIgnoreWildcardGroupAndTopicScopeTest() 
{
+        AlertRuleVO rule = AlertRuleVO.builder()
+                .name("Any Group Lag")
+                .metric("rocketmq_consumer_lag_messages")
+                .operator(">")
+                .threshold(1000)
+                .consumerGroup("*")
+                .topic(" ")
+                .enabled(true)
+                .build();
+        when(alertRepository.findAllRules()).thenReturn(List.of(rule));
+
+        String result = alertService.exportPrometheusRulesYaml();
+
+        assertThat(result)
+                .contains("expr: rocketmq_consumer_lag_messages > 1000")
+                .doesNotContain("consumer_group=\"")
+                .doesNotContain("topic=\"");
+    }
+
     @Test
     void 
exportPrometheusRulesYamlShouldEmitEmptyGroupsWhenAllRulesUseUnexportableNativeMetricsTest()
 {
         AlertRuleVO rule = AlertRuleVO.builder()

Reply via email to