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()