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 e4960b708 fix(metrics): preserve cloud group failure diagnostic scope 
(#6199)
e4960b708 is described below

commit e4960b708bf81af20a384e8c731651b1fc62beeb
Author: zmuxuny <[email protected]>
AuthorDate: Mon Oct 12 00:19:12 2026 -0700

    fix(metrics): preserve cloud group failure diagnostic scope (#6199)
    
    The group-level failure branch in 
`CloudRocketMqBusinessMetricsCollector.java:107-109` built its three 
UNAVAILABLE samples through the four-argument `unavailable(...)` helper at 
`:119-123`, which hardcodes a null cluster id even though the group's cluster 
is known at that point. `NativeAlertRuleScopeMatcher.java:38` selects on 
`matchesSelector(rule.getClusterName(), sample.clusterId())` and 
`NativeAlertRuleTestService.java:77-79` filters samples through that matcher 
before returning t [...]
    
    Seven new instances drive the real `TencentInstanceProvider` through the 
collector, the rule-test service and `AlertRuleEvaluator`, asserting first that 
the provider maps the injected `ClusterIdV4` onto `group.getClusterId()`. 
Passing null instead of the cluster id empties 
`clusterScopedRuleTestReportsTheKnownGroupOutageTest` for all three metrics. 
`wholeInstanceFailureRemainsUnscopedTest`, 
`unscopedRuleStillReportsFailureWithoutAProviderClusterIdTest`, 
`failureDoesNotAppearInAnotherC [...]
---
 .../CloudRocketMqBusinessMetricsCollector.java     |  13 +-
 .../tencent/TencentCloudLagFailureScopeTest.java   | 151 +++++++++++++++++++++
 2 files changed, 160 insertions(+), 4 deletions(-)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/collectors/CloudRocketMqBusinessMetricsCollector.java
 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/collectors/CloudRocketMqBusinessMetricsCollector.java
index 8503c84a3..ae98df3a6 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/collectors/CloudRocketMqBusinessMetricsCollector.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/collectors/CloudRocketMqBusinessMetricsCollector.java
@@ -105,9 +105,9 @@ public class CloudRocketMqBusinessMetricsCollector 
implements BusinessMetricsCol
         } catch (RuntimeException error) {
             log.warn("Failed to collect cloud consumer lag for group {} on 
instance {}: {}", group.getName(),
                     instance.getName(), error.getMessage());
-            return List.of(unavailable(CONSUMER_LAG_TOTAL, instance, labels, 
collectedAt),
-                    unavailable(CONSUMER_LAG_MAX_QUEUE, instance, labels, 
collectedAt),
-                    unavailable(TOPIC_BACKLOG_TOTAL, instance, labels, 
collectedAt));
+            return List.of(unavailable(CONSUMER_LAG_TOTAL, instance, 
group.getClusterId(), labels, collectedAt),
+                    unavailable(CONSUMER_LAG_MAX_QUEUE, instance, 
group.getClusterId(), labels, collectedAt),
+                    unavailable(TOPIC_BACKLOG_TOTAL, instance, 
group.getClusterId(), labels, collectedAt));
         }
     }
 
@@ -119,7 +119,12 @@ public class CloudRocketMqBusinessMetricsCollector 
implements BusinessMetricsCol
 
     private static MetricSample unavailable(String metric, InstanceVO 
instance, Map<String, String> labels,
             Instant collectedAt) {
-        return new MetricSample(metric, AlertDomain.BUSINESS, 
instance.getName(), null, labels, null,
+        return unavailable(metric, instance, null, labels, collectedAt);
+    }
+
+    private static MetricSample unavailable(String metric, InstanceVO 
instance, String clusterId,
+            Map<String, String> labels, Instant collectedAt) {
+        return new MetricSample(metric, AlertDomain.BUSINESS, 
instance.getName(), clusterId, labels, null,
                 MetricAvailability.UNAVAILABLE, collectedAt);
     }
 }
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/provider/tencent/TencentCloudLagFailureScopeTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/provider/tencent/TencentCloudLagFailureScopeTest.java
new file mode 100644
index 000000000..454a3121e
--- /dev/null
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/provider/tencent/TencentCloudLagFailureScopeTest.java
@@ -0,0 +1,151 @@
+/*
+ * 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.provider.tencent;
+
+import com.tencentcloudapi.trocket.v20230308.TrocketClient;
+import com.tencentcloudapi.trocket.v20230308.models.ConsumeGroupItem;
+import 
com.tencentcloudapi.trocket.v20230308.models.DescribeConsumerGroupListResponse;
+import 
com.tencentcloudapi.trocket.v20230308.models.DescribeTopicListByGroupResponse;
+import com.tencentcloudapi.trocket.v20230308.models.SubscriptionData;
+import org.apache.rocketmq.studio.cluster.metrics.MetricAvailability;
+import 
org.apache.rocketmq.studio.cluster.metrics.collectors.CloudRocketMqBusinessMetricsCollector;
+import org.apache.rocketmq.studio.common.domain.enums.InstanceVendor;
+import org.apache.rocketmq.studio.common.exception.BusinessException;
+import org.apache.rocketmq.studio.instance.InstanceRepository;
+import org.apache.rocketmq.studio.instance.InstanceVO;
+import org.apache.rocketmq.studio.ops.alert.AlertDomain;
+import org.apache.rocketmq.studio.ops.alert.AlertRuleEvaluator;
+import org.apache.rocketmq.studio.ops.alert.AlertRuleTestResultVO;
+import org.apache.rocketmq.studio.ops.alert.AlertRuleVO;
+import org.apache.rocketmq.studio.ops.alert.NativeAlertRuleTestService;
+import org.apache.rocketmq.studio.provider.InstanceProviderRegistry;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.util.List;
+import java.util.Optional;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class TencentCloudLagFailureScopeTest {
+    @ParameterizedTest
+    @ValueSource(strings = {"consumer.lag.total", "consumer.lag.max_queue", 
"topic.backlog.total"})
+    void clusterScopedRuleTestReportsTheKnownGroupOutageTest(String metric) 
throws Exception {
+        NativeAlertRuleTestService service = 
serviceWithFailedProgress("legacy-cluster");
+
+        AlertRuleTestResultVO result = service.test(rule(metric, 
"legacy-cluster"));
+
+        assertThat(result.samples()).singleElement().satisfies(sample -> {
+            assertThat(sample.availability()).isEqualTo("UNAVAILABLE");
+            assertThat(sample.currentValue()).isNull();
+            assertThat(sample.conditionMet()).isFalse();
+            assertThat(sample.labels()).containsEntry("consumerGroup", 
"orders");
+        });
+    }
+
+    @Test
+    void failureDoesNotAppearInAnotherClustersRuleTest() throws Exception {
+        assertThat(serviceWithFailedProgress("legacy-cluster")
+                .test(rule("consumer.lag.total", 
"other-cluster")).samples()).isEmpty();
+    }
+
+    @Test
+    void unscopedRuleStillReportsFailureWithoutAProviderClusterIdTest() throws 
Exception {
+        
assertThat(serviceWithFailedProgress(null).test(rule("consumer.lag.total", 
null)).samples())
+                .singleElement().satisfies(sample -> 
assertThat(sample.availability()).isEqualTo("UNAVAILABLE"));
+    }
+
+    @Test
+    void successfulClusterScopedRuleStillReportsItsMeasuredLagTest() throws 
Exception {
+        var result = serviceWithProgress("legacy-cluster", false)
+                .test(rule("consumer.lag.total", "legacy-cluster"));
+
+        assertThat(result.samples()).singleElement().satisfies(sample -> {
+            assertThat(sample.availability()).isEqualTo("AVAILABLE");
+            assertThat(sample.currentValue()).isEqualTo(80D);
+            assertThat(sample.conditionMet()).isTrue();
+        });
+    }
+
+    @Test
+    void wholeInstanceFailureRemainsUnscopedTest() {
+        InstanceProviderRegistry registry = 
mock(InstanceProviderRegistry.class);
+        when(registry.byInstanceId("tencent-prod"))
+                .thenThrow(new BusinessException(502, "provider unavailable"));
+        InstanceVO instance = 
InstanceVO.builder().name("tencent-prod").vendor(InstanceVendor.TENCENT).build();
+
+        assertThat(new 
CloudRocketMqBusinessMetricsCollector(registry).collect(instance))
+                .hasSize(3).allSatisfy(sample -> {
+                    assertThat(sample.clusterId()).isNull();
+                    assertThat(sample.labels()).isEmpty();
+                    assertThat(sample.value()).isNull();
+                    
assertThat(sample.availability()).isEqualTo(MetricAvailability.UNAVAILABLE);
+                });
+    }
+
+    private static AlertRuleVO rule(String metric, String cluster) {
+        return 
AlertRuleVO.builder().domain(AlertDomain.BUSINESS).instanceId("tencent-prod")
+                .consumerGroup("orders").clusterName(cluster).metric(metric)
+                .operator(">").threshold(50).consecutiveSamples(1).build();
+    }
+
+    private static NativeAlertRuleTestService serviceWithFailedProgress(String 
cluster) throws Exception {
+        return serviceWithProgress(cluster, true);
+    }
+
+    private static NativeAlertRuleTestService serviceWithProgress(String 
cluster, boolean failure) throws Exception {
+        InstanceRepository instances = mock(InstanceRepository.class);
+        InstanceVO instance = 
InstanceVO.builder().name("tencent-prod").vendor(InstanceVendor.TENCENT)
+                
.cloudInstanceId("rmq-test").regionId("ap-chengdu").credentialId(1L).build();
+        
when(instances.findByIdentifier("tencent-prod")).thenReturn(Optional.of(instance));
+        TrocketClient client = mock(TrocketClient.class);
+        TencentClientFactory factory = mock(TencentClientFactory.class);
+        when(factory.call(any(Long.class), anyString(), 
any())).thenAnswer(invocation ->
+                
invocation.<TencentClientFactory.TencentCall<Object>>getArgument(2).execute(client));
+        ConsumeGroupItem item = new ConsumeGroupItem();
+        item.setConsumerGroup("orders");
+        // Real Tencent mapping retains the source cluster of a migrated 
consumer group.
+        item.setClusterIdV4(cluster);
+        DescribeConsumerGroupListResponse groups = new 
DescribeConsumerGroupListResponse();
+        groups.setData(new ConsumeGroupItem[] {item});
+        groups.setTotalCount(1L);
+        when(client.DescribeConsumerGroupList(any())).thenReturn(groups);
+        if (failure) {
+            when(client.DescribeTopicListByGroup(any())).thenThrow(new 
BusinessException(502, "progress unavailable"));
+        } else {
+            SubscriptionData subscription = new SubscriptionData();
+            subscription.setTopic("orders-topic");
+            subscription.setConsumerLag(80L);
+            DescribeTopicListByGroupResponse progress = new 
DescribeTopicListByGroupResponse();
+            progress.setData(new SubscriptionData[] {subscription});
+            progress.setTotalCount(1L);
+            when(client.DescribeTopicListByGroup(any())).thenReturn(progress);
+        }
+        TencentInstanceProvider provider = new 
TencentInstanceProvider(factory, instances);
+        assertThat(provider.listConsumerGroups("tencent-prod", 
null)).singleElement()
+                .satisfies(group -> 
assertThat(group.getClusterId()).isEqualTo(cluster));
+        InstanceProviderRegistry registry = 
mock(InstanceProviderRegistry.class);
+        
when(registry.byInstanceId("tencent-prod")).thenReturn(Optional.of(provider));
+        return new NativeAlertRuleTestService(instances, List.of(),
+                List.of(new CloudRocketMqBusinessMetricsCollector(registry)), 
new AlertRuleEvaluator());
+    }
+}

Reply via email to