You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@eventmesh.apache.org by xi...@apache.org on 2026/10/10 19:00:37 UTC
(eventmesh-dashboard) branch dev-maven-release updated: feat: 采集 RocketMQ (#232)
This is an automated email from the ASF dual-hosted git repository.
Pil0tXia pushed a commit to branch dev-maven-release
in repository https://gitbox.apache.org/repos/asf/eventmesh-dashboard.git
The following commit(s) were added to refs/heads/dev-maven-release by this push:
new d1a81761 feat: 采集 RocketMQ (#232)
d1a81761 is described below
commit d1a817619d3da9d717e6c14a4e2ba9bd922e7319
Author: SUPERSAIYAN <17...@users.noreply.github.com>
AuthorDate: Sun Oct 11 03:00:30 2026 +0800
feat: 采集 RocketMQ (#232)
* 实现 RocketMQ Topic 和消费位点异步采集
* 抽象成一个采集topic方法
* 重构 RocketMQ 采集流程,拆分回调并补充中文注释
- 提取单 Topic 采集入口,统一组织位点和消费组采集
- 使用具名内部类封装各类响应处理
* 新增2个模型类
* 重构 RocketMQ 采集流程并完善独立指标模型
* 改为使用计数器来控制调用开始和结束
* 增加Licensed
* 新增采集
Broker 消息统计 写入、取出消息的分钟/小时/日窗口计数,以及对应 TPS
Broker 运行状态 分发积压字节数、刷盘积压字节数、最早保留消息距今时间、各线程池排队数量
* 拆分采集轮次管理与 Topic、Broker 采集职责
* 补充 RocketMQ 消费状态、磁盘和消息统计采集
* 补充 RocketMQ 累计消息指标采集
* 新增单测
* 失败的时候写为0,不再跳过
* 失败的时候写为0,不再跳过,使用3位运算符
* 把null 设置为 0 抽象为一个方法
* test: 新增带生产者消费者的采集手动测试,验证消费侧指标采集
* 修复采集 PR 的依赖版本和许可证冲突
* 修复采集缓存并发写入丢失样本
* 采集写入前检查关闭状态并补充测试
---------
Co-authored-by: sifu <14...@qq.com>
---
eventmesh-dashboard-console/pom.xml | 55 +
.../function/report/ReportHandlerManage.java | 1 +
.../ReportTag.java} | 22 +-
.../function/report/collect/CollectManage.java | 1 +
.../function/report/collect/RestoreData.java | 7 +-
.../report/collect/exporter/RocketMQCollect.java | 724 ++++++++
.../report/iotdb/IotDBReportMetaHandler.java | 3 +-
.../model/rocketmq/Rocketmq2ProducerOffset.java | 16 +-
...sInTotal.java => RocketmqBrokerMessagesIn.java} | 18 +-
...InTotal.java => RocketmqBrokerMessagesOut.java} | 18 +-
....java => RocketmqConsumerConnectionNumber.java} | 16 +-
...InTotal.java => RocketmqConsumerFailedTps.java} | 21 +-
.../rocketmq/RocketmqConsumerGroupNumber.java | 4 +-
.../model/rocketmq/RocketmqConsumerLagLatency.java | 22 +-
...gesInTotal.java => RocketmqConsumerOffset.java} | 26 +-
...Total.java => RocketmqConsumerProcessTime.java} | 21 +-
...Number.java => RocketmqConsumerSuccessTps.java} | 22 +-
...sInTotal.java => RocketmqGroupMessagesOut.java} | 24 +-
.../model/rocketmq/RocketmqMessagesInTotal.java | 12 +-
.../model/rocketmq/RocketmqMessagesOutTotal.java | 12 +-
...mber.java => RocketmqStorageDiskFreeBytes.java} | 10 +-
...sInTotal.java => RocketmqStorageDiskUsage.java} | 18 +-
.../RocketmqStorageMessageReserveTime.java | 6 +-
.../rocketmq/RocketmqThreadPoolWartermark.java | 11 +-
.../model/rocketmq/RocketmqThroughputInTotal.java | 6 +-
...esInTotal.java => RocketmqTopicMessagesIn.java} | 21 +-
.../report/model/rocketmq/RocketmqTopicNumber.java | 2 +-
.../mapstruct/report/RocketMQCollectMapper.java | 194 ++
.../report/collect/DataSyncHandlerTest.java | 156 ++
.../report/collect/RocketMQCollectManualTest.java | 123 ++
.../RocketMQCollectWithClientManualTest.java | 229 +++
.../collect/exporter/RocketMQCollectTest.java | 1891 ++++++++++++++++++++
32 files changed, 3590 insertions(+), 122 deletions(-)
diff --git a/eventmesh-dashboard-console/pom.xml b/eventmesh-dashboard-console/pom.xml
index 59513177..967fbbf3 100644
--- a/eventmesh-dashboard-console/pom.xml
+++ b/eventmesh-dashboard-console/pom.xml
@@ -243,4 +243,59 @@
</plugin>
</plugins>
</build>
+ <profiles>
+ <profile>
+ <id>rocketmq-collect-tests</id>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <testIncludes combine.self="override">
+ <testInclude>**/RocketMQCollectTest.java</testInclude>
+ <testInclude>**/DataSyncHandlerTest.java</testInclude>
+ </testIncludes>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <configuration>
+ <testFailureIgnore>false</testFailureIgnore>
+ <includes>
+ <include>**/RocketMQCollectTest.java</include>
+ <include>**/DataSyncHandlerTest.java</include>
+ </includes>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ <profile>
+ <id>rocketmq-manual-collect</id>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <testIncludes combine.self="override">
+ <testInclude>**/RocketMQCollectManualTest.java</testInclude>
+ </testIncludes>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <configuration>
+ <includes>
+ <include>**/RocketMQCollectManualTest.java</include>
+ </includes>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ </profiles>
</project>
diff --git a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/ReportHandlerManage.java b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/ReportHandlerManage.java
index a8516bfe..a97404d8 100644
--- a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/ReportHandlerManage.java
+++ b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/ReportHandlerManage.java
@@ -117,6 +117,7 @@ public class ReportHandlerManage {
@Getter
private final Map<String, ReportMetaData> aggregationMetaDataMap = new HashMap<>();
@Setter
+ @Getter
private ReportEngine reportEngine;
@Setter
private ReportConfig reportConfig;
diff --git a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/collect/exporter/RocketMQCollect.java b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/annotation/ReportTag.java
similarity index 56%
copy from eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/collect/exporter/RocketMQCollect.java
copy to eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/annotation/ReportTag.java
index c566a743..c1d07640 100644
--- a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/collect/exporter/RocketMQCollect.java
+++ b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/annotation/ReportTag.java
@@ -15,19 +15,15 @@
* limitations under the License.
*/
-package org.apache.eventmesh.dashboard.console.function.report.collect.exporter;
+package org.apache.eventmesh.dashboard.console.function.report.annotation;
-import org.apache.eventmesh.dashboard.common.annotation.ClusterTypeMark;
-import org.apache.eventmesh.dashboard.common.enums.ClusterType;
-import org.apache.eventmesh.dashboard.console.function.report.collect.AbstractCollect;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
-
-@ClusterTypeMark(clusterType = {ClusterType.STORAGE_ROCKETMQ_CLUSTER, ClusterType.STORAGE_ROCKETMQ_BROKER,
- ClusterType.STORAGE_ROCKETMQ_BROKER_MAIN_SLAVE, ClusterType.STORAGE_ROCKETMQ_BROKER_RAFT})
-public class RocketMQCollect extends AbstractCollect {
-
- @Override
- protected void doCollect() {
-
- }
+/** 显式声明报表维度,允许资源名称作为标签,无需将名称伪装成 ID。 */
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ReportTag {
}
diff --git a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/collect/CollectManage.java b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/collect/CollectManage.java
index f3b7369d..fe5fb5fd 100644
--- a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/collect/CollectManage.java
+++ b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/collect/CollectManage.java
@@ -164,6 +164,7 @@ public class CollectManage {
public void init() {
this.dataSyncHandler.setPaddingService(paddingService);
+ this.dataSyncHandler.setReportEngine(this.reportHandlerManage.getReportEngine());
}
public void collect() {
diff --git a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/collect/RestoreData.java b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/collect/RestoreData.java
index ee046618..366ec966 100644
--- a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/collect/RestoreData.java
+++ b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/collect/RestoreData.java
@@ -20,9 +20,10 @@ package org.apache.eventmesh.dashboard.console.function.report.collect;
import org.apache.eventmesh.dashboard.common.model.metadata.CollectMetadata;
import java.util.ArrayList;
-import java.util.HashMap;
+import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
import lombok.Getter;
import lombok.Setter;
@@ -42,12 +43,12 @@ public class RestoreData {
private int index;
@Getter
- private Map<Class<?>, List<Object>> dataMap = new HashMap<>();
+ private Map<Class<?>, List<Object>> dataMap = new ConcurrentHashMap<>();
public void setData(Object data) {
Class<?> clazz = data.getClass();
- dataMap.computeIfAbsent(clazz, k -> new ArrayList<>()).add(data);
+ dataMap.computeIfAbsent(clazz, k -> Collections.synchronizedList(new ArrayList<>())).add(data);
}
public void restore() {
diff --git a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/collect/exporter/RocketMQCollect.java b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/collect/exporter/RocketMQCollect.java
index c566a743..c87ff860 100644
--- a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/collect/exporter/RocketMQCollect.java
+++ b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/collect/exporter/RocketMQCollect.java
@@ -19,15 +19,739 @@ package org.apache.eventmesh.dashboard.console.function.report.collect.exporter;
import org.apache.eventmesh.dashboard.common.annotation.ClusterTypeMark;
import org.apache.eventmesh.dashboard.common.enums.ClusterType;
+import org.apache.eventmesh.dashboard.common.model.metadata.RuntimeMetadata;
import org.apache.eventmesh.dashboard.console.function.report.collect.AbstractCollect;
+import org.apache.eventmesh.dashboard.console.function.report.model.base.OrganizationId;
+import org.apache.eventmesh.dashboard.console.mapstruct.report.RocketMQCollectMapper;
+import org.apache.eventmesh.dashboard.core.function.SDK.SDKManage;
+import org.apache.eventmesh.dashboard.core.function.SDK.SDKTypeEnum;
+import org.apache.eventmesh.dashboard.core.function.SDK.operation.rocketmq.RocketMQRemotingSDKOperation.DefaultRemotingClient;
+import org.apache.rocketmq.common.MixAll;
+import org.apache.rocketmq.remoting.InvokeCallback;
+import org.apache.rocketmq.remoting.netty.ResponseFuture;
+import org.apache.rocketmq.remoting.protocol.RemotingCommand;
+import org.apache.rocketmq.remoting.protocol.RemotingSerializable;
+import org.apache.rocketmq.remoting.protocol.RequestCode;
+import org.apache.rocketmq.remoting.protocol.ResponseCode;
+import org.apache.rocketmq.remoting.protocol.admin.ConsumeStats;
+import org.apache.rocketmq.remoting.protocol.admin.TopicStatsTable;
+import org.apache.rocketmq.remoting.protocol.body.ConsumerConnection;
+import org.apache.rocketmq.remoting.protocol.body.GroupList;
+import org.apache.rocketmq.remoting.protocol.body.TopicConfigSerializeWrapper;
+import org.apache.rocketmq.remoting.protocol.header.GetAllSubscriptionGroupRequestHeader;
+import org.apache.rocketmq.remoting.protocol.header.GetConsumeStatsRequestHeader;
+import org.apache.rocketmq.remoting.protocol.header.GetConsumerConnectionListRequestHeader;
+import org.apache.rocketmq.remoting.protocol.header.GetConsumerRunningInfoRequestHeader;
+import org.apache.rocketmq.remoting.protocol.header.GetTopicStatsInfoRequestHeader;
+import org.apache.rocketmq.remoting.protocol.header.QueryConsumeTimeSpanRequestHeader;
+import org.apache.rocketmq.remoting.protocol.header.QueryTopicConsumeByWhoRequestHeader;
+import org.apache.rocketmq.remoting.protocol.header.ViewBrokerStatsDataRequestHeader;
+
+import java.io.StringReader;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import lombok.Setter;
@ClusterTypeMark(clusterType = {ClusterType.STORAGE_ROCKETMQ_CLUSTER, ClusterType.STORAGE_ROCKETMQ_BROKER,
ClusterType.STORAGE_ROCKETMQ_BROKER_MAIN_SLAVE, ClusterType.STORAGE_ROCKETMQ_BROKER_RAFT})
public class RocketMQCollect extends AbstractCollect {
+ private static final RocketMQCollectMapper MAPPER = RocketMQCollectMapper.INSTANCE;
+ private static final Pattern BYTE_COUNT = Pattern.compile("([0-9]+(?:\\.[0-9]+)?)\\s*(B|[KMGTPE]iB)");
+
+ private static final Pattern DISK_CAPACITY = Pattern.compile("Total : (.+), Free : (.+)\\.");
+
+ private String runtimeUnique;
+
+ @Setter
+ private DefaultRemotingClient defaultRemotingClient;
+
+ @Override
+ public void setRuntimeMetadata(RuntimeMetadata runtimeMetadata) {
+ super.setRuntimeMetadata(runtimeMetadata);
+ this.runtimeUnique = runtimeMetadata == null ? null : runtimeMetadata.getUnique();
+ }
@Override
protected void doCollect() {
+ if (this.runtimeUnique == null) {
+ return;
+ }
+ this.defaultRemotingClient = SDKManage.getInstance().getClient(SDKTypeEnum.ADMIN, this.runtimeUnique);
+ new CollectRound().executeAndAwait();
+ }
+
+ /**
+ * 一轮采集流程:
+ * 1. 先注册 Topic 和 Broker 两个方向的根请求,再分别启动,统一管理请求计数。
+ * 2. TopicCollect 采集 Topic 总数、位点、积压时间、消息统计及客户端处理指标;死信 Topic 不查询消费组。
+ * 3. BrokerCollect 采集配置消费组总数、消息统计和运行状态;响应直接调用父类 setData。
+ * 4. 等待所有异步请求完成后返回,父类再统一交付本轮数据。
+ * 5. 超时或中断时保留已经采集的数据,关闭本轮,拒绝尚未开始处理的迟到回调。
+ */
+ private class CollectRound {
+ private final DefaultRemotingClient client = defaultRemotingClient;
+ private final AtomicInteger pending = new AtomicInteger();
+ private final CountDownLatch completed = new CountDownLatch(1);
+ private final long deadline = System.currentTimeMillis() + 4000;
+ private volatile boolean closed;
+
+ private void executeAndAwait() {
+ try {
+ // 先注册所有根请求,避免同步回调在后续请求注册前让计数归零。
+ TopicCollect topics = new TopicCollect();
+ BrokerCollect broker = new BrokerCollect();
+ topics.start();
+ broker.start();
+ // 所有请求完成时计数归零并唤醒;超时则保留已采集的数据。
+ this.completed.await(Math.max(1, this.deadline - System.currentTimeMillis()), TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ } finally {
+ this.close();
+ }
+ }
+
+ private void close() {
+ this.closed = true;
+ }
+
+ /** 每次追加样本前检查本轮状态,已关闭则放弃本次写入。 */
+ private void setData(OrganizationId data) {
+ if (this.closed) {
+ return;
+ }
+ RocketMQCollect.this.setData(data);
+ }
+
+ /** 采集 Topic 总数、生产和消费位点、积压量及时间、消息统计、消费组连接数和客户端处理指标。 */
+ private class TopicCollect {
+ private final Set<String> connectedGroups = ConcurrentHashMap.newKeySet();
+ private final TopicsCallback topics =
+ new TopicsCallback(RemotingCommand.createRequestCommand(RequestCode.GET_ALL_TOPIC_CONFIG, null));
+
+ private void start() {
+ this.topics.execute();
+ }
+
+ private void collectTopic(String topic) {
+ this.collectTopicStats(topic);
+ this.collectMessages(topic, null);
+ if (!topic.startsWith(MixAll.DLQ_GROUP_TOPIC_PREFIX)) {
+ this.collectGroups(topic);
+ }
+ }
+
+ /** 原生统计键:写入按 Topic,取出按 Topic@消费组。 */
+ private void collectMessages(String topic, String group) {
+ ViewBrokerStatsDataRequestHeader header = new ViewBrokerStatsDataRequestHeader();
+ header.setStatsName(group == null ? "TOPIC_PUT_NUMS" : "GROUP_GET_NUMS");
+ header.setStatsKey(group == null ? topic : topic + "@" + group);
+ new MessageStatsCallback(RemotingCommand.createRequestCommand(RequestCode.VIEW_BROKER_STATS_DATA, header),
+ topic, group).execute();
+ }
+
+ private void collectTopicStats(String topic) {
+ GetTopicStatsInfoRequestHeader topicHeader = new GetTopicStatsInfoRequestHeader();
+ topicHeader.setTopic(topic);
+ RemotingCommand topicRequest = RemotingCommand.createRequestCommand(RequestCode.GET_TOPIC_STATS_INFO, topicHeader);
+ new TopicStatsCallback(topicRequest, topic).execute();
+ }
+
+ private void collectGroups(String topic) {
+ QueryTopicConsumeByWhoRequestHeader groupHeader = new QueryTopicConsumeByWhoRequestHeader();
+ groupHeader.setTopic(topic);
+ RemotingCommand groupRequest = RemotingCommand.createRequestCommand(RequestCode.QUERY_TOPIC_CONSUME_BY_WHO, groupHeader);
+ new GroupsCallback(groupRequest, topic).execute();
+ }
+
+ private void collectConnections(String group) {
+ GetConsumerConnectionListRequestHeader connectionHeader = new GetConsumerConnectionListRequestHeader();
+ connectionHeader.setConsumerGroup(group);
+ RemotingCommand connectionRequest = RemotingCommand.createRequestCommand(
+ RequestCode.GET_CONSUMER_CONNECTION_LIST, connectionHeader);
+ new ConnectionsCallback(connectionRequest, group).execute();
+ }
+
+ private void collectConsumeStats(String topic, String group) {
+ GetConsumeStatsRequestHeader consumeHeader = new GetConsumeStatsRequestHeader();
+ consumeHeader.setTopic(topic);
+ consumeHeader.setConsumerGroup(group);
+ RemotingCommand consumeRequest = RemotingCommand.createRequestCommand(RequestCode.GET_CONSUME_STATS, consumeHeader);
+ new ConsumeStatsCallback(consumeRequest, topic, group).execute();
+ }
+
+ /** 步骤一:解析 Broker 的 Topic 列表,逐个启动采集。 */
+ private class TopicsCallback extends CollectCallback {
+ private TopicsCallback(RemotingCommand request) {
+ super(request);
+ }
+
+ @Override
+ protected void handleResponse(ResponseFuture responseFuture) {
+ TopicConfigSerializeWrapper topics =
+ RemotingSerializable.decode(responseFuture.getResponseCommand().getBody(), TopicConfigSerializeWrapper.class);
+ // 缺少列表不是零;仅对成功返回的完整配置表计数。
+ Map<?, ?> body = RemotingSerializable.decode(responseFuture.getResponseCommand().getBody(), Map.class);
+ if (body == null || !(body.get("topicConfigTable") instanceof Map<?, ?>)) {
+ return;
+ }
+ CollectRound.this.setData(MAPPER.topicNumber((long) topics.getTopicConfigTable().size()));
+ for (String topic : topics.getTopicConfigTable().keySet()) {
+ collectTopic(topic);
+ }
+ }
+ }
+
+ private class MessageStatsCallback extends WindowStatsCallback {
+ private final String topic;
+ private final String group;
+
+ private MessageStatsCallback(RemotingCommand request, String topic, String group) {
+ super(request);
+ this.topic = topic;
+ this.group = group;
+ }
+
+ @Override
+ protected void recordWindow(String window, long count, float rate) {
+ CollectRound.this.setData(this.group == null
+ ? MAPPER.topicMessages(this.topic, window, count, rate)
+ : MAPPER.groupMessages(this.topic, this.group, window, count, rate));
+ }
+ }
+
+ /** 步骤二:采集队列位点,并计算 Topic 汇总位点。 */
+ private class TopicStatsCallback extends CollectCallback {
+ private final String topic;
+
+ private TopicStatsCallback(RemotingCommand request, String topic) {
+ super(request);
+ this.topic = topic;
+ }
+
+ @Override
+ protected void handleResponse(ResponseFuture responseFuture) {
+ TopicStatsTable stats =
+ RemotingSerializable.decode(responseFuture.getResponseCommand().getBody(), TopicStatsTable.class);
+ stats.getOffsetTable().forEach((queue, offset) -> {
+ String queueId = String.valueOf(queue.getQueueId());
+
+ CollectRound.this.setData(MAPPER.topicOffset(topic, queueId, offset));
+ });
+ long min = stats.getOffsetTable().values().stream().mapToLong(offset -> offset.getMinOffset()).sum();
+ long max = stats.getOffsetTable().values().stream().mapToLong(offset -> offset.getMaxOffset()).sum();
+ long lastUpdate = stats.getOffsetTable().values().stream()
+ .mapToLong(offset -> offset.getLastUpdateTimestamp()).max().orElse(0L);
+
+ CollectRound.this.setData(MAPPER.aggregateOffset(topic, min, max, lastUpdate));
+ }
+ }
+
+ /** 步骤三:发现 Topic 对应的消费组,启动连接与消费进度采集。 */
+ private class GroupsCallback extends CollectCallback {
+ private final String topic;
+
+ private GroupsCallback(RemotingCommand request, String topic) {
+ super(request);
+ this.topic = topic;
+ }
+
+ @Override
+ protected void handleResponse(ResponseFuture responseFuture) {
+ GroupList groups = RemotingSerializable.decode(responseFuture.getResponseCommand().getBody(), GroupList.class);
+ for (String group : groups.getGroupList()) {
+ if (TopicCollect.this.connectedGroups.add(group)) {
+ collectConnections(group);
+ }
+ collectConsumeStats(topic, group);
+ collectMessages(topic, group);
+ }
+ }
+ }
+
+ /** 步骤四:采集消费组连接数;同一消费组每轮只查询一次。 */
+ private class ConnectionsCallback extends CollectCallback {
+ private final String group;
+
+ private ConnectionsCallback(RemotingCommand request, String group) {
+ super(request);
+ this.group = group;
+ }
+
+ @Override
+ protected void handleResponse(ResponseFuture responseFuture) {
+ ConsumerConnection connections = RemotingSerializable.decode(
+ responseFuture.getResponseCommand().getBody(), ConsumerConnection.class);
+
+ CollectRound.this.setData(MAPPER.connections(group, connections.getConnectionSet().size()));
+ // 每个在线客户端分别采样,不把 Broker 取消息 TPS 当成处理成功 TPS。
+ Set<String> clients = ConcurrentHashMap.newKeySet();
+ connections.getConnectionSet().forEach(connection -> {
+ String clientId = connection.getClientId();
+ if (clientId != null && !clientId.isBlank() && clients.add(clientId)) {
+ GetConsumerRunningInfoRequestHeader header = new GetConsumerRunningInfoRequestHeader();
+ header.setConsumerGroup(this.group);
+ header.setClientId(clientId);
+ header.setJstackEnable(false);
+ new ClientStatsCallback(RemotingCommand.createRequestCommand(
+ RequestCode.GET_CONSUMER_RUNNING_INFO, header), this.group, clientId).execute();
+ }
+ });
+ }
+ }
+
+ /** 按 Topic、消费组和客户端采集消费成功 TPS、失败 TPS 和平均处理耗时。 */
+ private class ClientStatsCallback extends CollectCallback {
+ private final String group;
+ private final String clientId;
+
+ private ClientStatsCallback(RemotingCommand request, String group, String clientId) {
+ super(request);
+ this.group = group;
+ this.clientId = clientId;
+ }
+
+ @Override
+ protected void handleResponse(ResponseFuture responseFuture) {
+ Map<?, ?> body = RemotingSerializable.decode(responseFuture.getResponseCommand().getBody(), Map.class);
+ if (body == null || !(body.get("statusTable") instanceof Map<?, ?> statuses)) {
+ return;
+ }
+ statuses.forEach((key, value) -> {
+ if (!(key instanceof String topic) || !(value instanceof Map<?, ?> status)) {
+ return;
+ }
+ Float success = nonnegativeFloatOrZero(status.get("consumeOKTPS"));
+ Float failed = nonnegativeFloatOrZero(status.get("consumeFailedTPS"));
+ Float elapsed = nonnegativeFloatOrZero(status.get("consumeRT"));
+ CollectRound.this.setData(MAPPER.consumerSuccess(topic, this.group, this.clientId, success));
+ CollectRound.this.setData(MAPPER.consumerFailed(topic, this.group, this.clientId, failed));
+ CollectRound.this.setData(MAPPER.consumerProcessTime(topic, this.group, this.clientId, elapsed));
+ });
+ }
+ }
+
+ /** 采集各 Topic、消费组和队列中最早未消费消息的积压时间。 */
+ private class LagCallback extends CollectCallback {
+ private final String topic;
+ private final String group;
+
+ private LagCallback(RemotingCommand request, String topic, String group) {
+ super(request);
+ this.topic = topic;
+ this.group = group;
+ }
+
+ @Override
+ protected void handleResponse(ResponseFuture responseFuture) {
+ Map<?, ?> body = RemotingSerializable.decode(responseFuture.getResponseCommand().getBody(), Map.class);
+ if (body == null || !(body.get("consumeTimeSpanSet") instanceof List<?> spans)) {
+ return;
+ }
+ for (Object item : spans) {
+ if (!(item instanceof Map<?, ?> span) || !(span.get("messageQueue") instanceof Map<?, ?> queue)
+ || !this.topic.equals(queue.get("topic"))) {
+ continue;
+ }
+ Long queueId = nonnegativeLong(queue.get("queueId"));
+ Long delay = nonnegativeLong(span.get("delayTime"));
+ Long earliest = nonnegativeLong(span.get("minTimeStamp"));
+ // 位点已过期或未初始化时,原生接口可能用 -1 时间戳计算出近似当前纪元的延迟。
+ if (queueId != null && delay != null && earliest != null && earliest > 0
+ && delay <= System.currentTimeMillis() - earliest) {
+ CollectRound.this.setData(MAPPER.consumerLag(this.topic, this.group, queueId.toString(), delay));
+ }
+ }
+ }
+ }
+
+ /** 步骤五:采集 Topic 与消费组对应的队列位点和积压量。 */
+ private class ConsumeStatsCallback extends CollectCallback {
+ private final String topic;
+ private final String group;
+
+ private ConsumeStatsCallback(RemotingCommand request, String topic, String group) {
+ super(request);
+ this.topic = topic;
+ this.group = group;
+ }
+
+ @Override
+ protected void handleResponse(ResponseFuture responseFuture) {
+ ConsumeStats stats = RemotingSerializable.decode(
+ responseFuture.getResponseCommand().getBody(), ConsumeStats.class);
+ stats.getOffsetTable().forEach((queue, offset) -> {
+ if (!topic.equals(queue.getTopic())) {
+ return;
+ }
+ String queueId = String.valueOf(queue.getQueueId());
+
+ CollectRound.this.setData(MAPPER.consumerOffset(topic, group, queueId, offset));
+ });
+ if (!stats.getOffsetTable().isEmpty()) {
+ QueryConsumeTimeSpanRequestHeader header = new QueryConsumeTimeSpanRequestHeader();
+ header.setTopic(this.topic);
+ header.setGroup(this.group);
+ new LagCallback(RemotingCommand.createRequestCommand(RequestCode.QUERY_CONSUME_TIME_SPAN, header),
+ this.topic, this.group).execute();
+ }
+ }
+ }
+ }
+
+ /** 采集消费组总数、Broker 消息统计、存储积压、消息保留时间、磁盘使用率及剩余空间、线程池队列长度。 */
+ private class BrokerCollect {
+ private final BrokerConfigCallback config = new BrokerConfigCallback();
+ private final BrokerRuntimeCallback runtime = new BrokerRuntimeCallback();
+ private final GroupCountCallback groups = new GroupCountCallback();
+
+ private void start() {
+ this.config.execute();
+ this.runtime.execute();
+ this.groups.execute();
+ }
+
+ /** 采集 Broker 配置的消费组总数。 */
+ private class GroupCountCallback extends CollectCallback {
+ private GroupCountCallback() {
+ super(RemotingCommand.createRequestCommand(RequestCode.GET_ALL_SUBSCRIPTIONGROUP_CONFIG,
+ new GetAllSubscriptionGroupRequestHeader()));
+ }
+
+ @Override
+ protected void handleResponse(ResponseFuture responseFuture) {
+ Map<?, ?> body = RemotingSerializable.decode(responseFuture.getResponseCommand().getBody(), Map.class);
+ if (body != null && body.get("subscriptionGroupTable") instanceof Map<?, ?> groups) {
+ CollectRound.this.setData(MAPPER.groupNumber((long) groups.size()));
+ }
+ }
+ }
+
+ private void collectStats(String cluster, boolean incoming) {
+ ViewBrokerStatsDataRequestHeader header = new ViewBrokerStatsDataRequestHeader();
+ header.setStatsName(incoming ? "BROKER_PUT_NUMS" : "BROKER_GET_NUMS");
+ header.setStatsKey(cluster);
+ new BrokerStatsCallback(RemotingCommand.createRequestCommand(RequestCode.VIEW_BROKER_STATS_DATA, header), incoming).execute();
+ }
+
+ /** 步骤一:获取 Broker 所属集群名,启动消息写入和取出统计采集。 */
+ private class BrokerConfigCallback extends CollectCallback {
+ private BrokerConfigCallback() {
+ super(RemotingCommand.createRequestCommand(RequestCode.GET_BROKER_CONFIG, null));
+ }
+
+ @Override
+ protected void handleResponse(ResponseFuture responseFuture) throws Exception {
+ Properties properties = new Properties();
+ properties.load(new StringReader(new String(responseFuture.getResponseCommand().getBody(), StandardCharsets.UTF_8)));
+ String cluster = properties.getProperty("brokerClusterName");
+ if (cluster == null || cluster.isBlank()) {
+ return;
+ }
+ collectStats(cluster, true);
+ collectStats(cluster, false);
+ }
+ }
+
+ /** 步骤二:采集分钟、小时、日窗口的消息写入和取出数量及 TPS。 */
+ private class BrokerStatsCallback extends WindowStatsCallback {
+ private final boolean incoming;
+
+ private BrokerStatsCallback(RemotingCommand request, boolean incoming) {
+ super(request);
+ this.incoming = incoming;
+ }
+
+ @Override
+ protected void recordWindow(String window, long count, float rate) {
+ CollectRound.this.setData(this.incoming
+ ? MAPPER.brokerMessagesIn(window, count, rate) : MAPPER.brokerMessagesOut(window, count, rate));
+ }
+ }
+
+ /** 步骤三:采集存储分发和刷盘积压、消息保留时间、磁盘使用率及剩余空间、线程池队列长度。 */
+ private class BrokerRuntimeCallback extends CollectCallback {
+ private BrokerRuntimeCallback() {
+ super(RemotingCommand.createRequestCommand(RequestCode.GET_BROKER_RUNTIME_INFO, null));
+ }
+
+ @Override
+ protected void handleResponse(ResponseFuture responseFuture) {
+ Map<?, ?> response = RemotingSerializable.decode(responseFuture.getResponseCommand().getBody(), Map.class);
+ if (response == null || !(response.get("table") instanceof Map<?, ?> table)) {
+ return;
+ }
+ Long messagesIn = nonnegativeLong(table.get("msgPutTotalTodayNow"));
+ if (messagesIn != null) {
+ CollectRound.this.setData(MAPPER.messagesInTotal(messagesIn));
+ }
+ Long bytesIn = nonnegativeLong(table.get("putMessageSizeTotal"));
+ if (bytesIn != null) {
+ CollectRound.this.setData(MAPPER.throughputInTotal(bytesIn));
+ }
+ Long messagesOut = nonnegativeLong(table.get("msgGetTotalTodayNow"));
+ if (messagesOut != null) {
+ CollectRound.this.setData(MAPPER.messagesOutTotal(messagesOut));
+ }
+ Long dispatch = nonnegativeLong(table.get("dispatchBehindBytes"));
+ if (dispatch != null) {
+ CollectRound.this.setData(MAPPER.dispatchBytes(dispatch));
+ }
+ this.collectFlush(table);
+ this.collectDisk(table);
+ Long earliest = nonnegativeLong(table.get("earliestMessageTimeStamp"));
+ long now = System.currentTimeMillis();
+ if (earliest != null && earliest > 0 && earliest <= now) {
+ CollectRound.this.setData(MAPPER.reserveTime(now - earliest));
+ }
+ for (String pool : new String[] {"send", "pull", "litePull", "query", "ack"}) {
+ this.collectThreadPool(table, pool, pool + "ThreadPoolQueueSize");
+ }
+ this.collectThreadPool(table, "endTransaction", "EndTransactionQueueSize");
+ }
+
+ private void collectDisk(Map<?, ?> table) {
+ table.forEach((key, value) -> {
+ if (!(key instanceof String name)) {
+ return;
+ }
+ String type;
+ String path = "";
+ if (name.equals("commitLogDiskRatio")) {
+ type = "commitlog_min";
+ } else if (name.startsWith("commitLogDiskRatio_")) {
+ type = "commitlog";
+ path = name.substring("commitLogDiskRatio_".length());
+ } else if (name.equals("consumeQueueDiskRatio")) {
+ type = "consumequeue";
+ } else {
+ return;
+ }
+ Float ratio = nonnegativeFloat(value);
+ if (ratio != null && ratio <= 1) {
+ CollectRound.this.setData(MAPPER.diskUsage(type, path, ratio));
+ }
+ });
+ Object capacity = table.get("commitLogDirCapacity");
+ if (capacity instanceof String text) {
+ Matcher matcher = DISK_CAPACITY.matcher(text.trim());
+ if (matcher.matches()) {
+ Long total = byteCount(matcher.group(1));
+ Long free = byteCount(matcher.group(2));
+ if (total != null && total > 0 && free != null && free <= total) {
+ CollectRound.this.setData(MAPPER.diskFreeBytes(free));
+ }
+ }
+ }
+ }
+
+ private void collectFlush(Map<?, ?> table) {
+ Long flush = byteCount(table.get("remainHowManyDataToFlush"));
+ // 启用暂存池时,原生刷盘积压还包括待提交字节;字段缺席表示未启用该分支。
+ Long commit = table.containsKey("remainHowManyDataToCommit")
+ ? byteCount(table.get("remainHowManyDataToCommit")) : Long.valueOf(0L);
+ if (flush != null && commit != null && flush <= Long.MAX_VALUE - commit) {
+ CollectRound.this.setData(MAPPER.flushBytes(flush + commit));
+ }
+ }
+
+ private void collectThreadPool(Map<?, ?> table, String pool, String key) {
+ Long size = nonnegativeLong(table.get(key));
+ if (size != null) {
+ CollectRound.this.setData(MAPPER.threadPool(pool, size));
+ }
+ }
+ }
+
+ private Long byteCount(Object value) {
+ if (!(value instanceof String text)) {
+ return null;
+ }
+ Matcher matcher = BYTE_COUNT.matcher(text.trim());
+ if (!matcher.matches()) {
+ return null;
+ }
+ String unit = matcher.group(2);
+ int exponent = "B".equals(unit) ? 0 : "KMGTPE".indexOf(unit.charAt(0)) + 1;
+ try {
+ // Broker 已将二进制字节数格式化并舍入,此处只能还原近似值,不能声称字节级精确。
+ return new BigDecimal(matcher.group(1)).multiply(BigDecimal.valueOf(1024).pow(exponent))
+ .setScale(0, RoundingMode.HALF_UP).longValueExact();
+ } catch (ArithmeticException e) {
+ return null;
+ }
+ }
+ }
+
+ private Long nonnegativeLong(Object value) {
+ if (value == null) {
+ return null;
+ }
+ try {
+ long number = Long.parseLong(value.toString());
+ return number >= 0 ? number : null;
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ }
+
+ private Float nonnegativeFloatOrZero(Object value) {
+ Float result = this.nonnegativeFloat(value);
+ return result == null ? 0f : result;
+ }
+
+ private Float nonnegativeFloat(Object value) {
+ if (value == null) {
+ return null;
+ }
+ try {
+ double number = Double.parseDouble(value.toString());
+ float result = (float) number;
+ return number >= 0 && Float.isFinite(result) ? result : null;
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ }
+
+ /** 解析 Broker、Topic 和消费组的分钟、小时、日消息统计窗口。 */
+ private abstract class WindowStatsCallback extends CollectCallback {
+ private WindowStatsCallback(RemotingCommand request) {
+ super(request);
+ }
+
+ @Override
+ protected final void handleResponse(ResponseFuture responseFuture) {
+ Map<?, ?> stats = RemotingSerializable.decode(responseFuture.getResponseCommand().getBody(), Map.class);
+ this.collectWindow(stats, "statsMinute", "minute");
+ this.collectWindow(stats, "statsHour", "hour");
+ this.collectWindow(stats, "statsDay", "day");
+ }
+
+ private void collectWindow(Map<?, ?> stats, String key, String window) {
+ if (stats == null || !(stats.get(key) instanceof Map<?, ?> sample)
+ || !(sample.get("sum") instanceof Number count) || !(sample.get("tps") instanceof Number rate)) {
+ return;
+ }
+ try {
+ long value = Long.parseLong(count.toString());
+ float tps = rate.floatValue();
+ if (value >= 0 && Float.isFinite(tps) && rate.doubleValue() >= 0) {
+ this.recordWindow(window, value, tps);
+ }
+ } catch (NumberFormatException e) {
+ // 小数、溢出或格式无效的消息数不参与采集。
+ }
+ }
+
+ protected abstract void recordWindow(String window, long count, float rate);
+ }
+
+ /** 统一处理请求发送、响应校验、异常和完成计数,具体响应由各采集回调解析。 */
+ private abstract class CollectCallback implements InvokeCallback {
+ private final RemotingCommand request;
+ private final AtomicBoolean finished = new AtomicBoolean();
+
+ private CollectCallback(RemotingCommand request) {
+ this.request = request;
+ // 请求发送前 +1;子请求先计数,父请求处理结束后才 -1。
+ CollectRound.this.pending.incrementAndGet();
+ }
+
+ // 发送异常也进入失败回调,每个请求只扣减一次。
+ protected final void execute() {
+ executeSafely(this,
+ () -> client.invokeAsync(this.request, this.timeoutMillis(), this));
+ }
+
+ private long timeoutMillis() throws TimeoutException {
+ long remaining = CollectRound.this.deadline - System.currentTimeMillis();
+ if (CollectRound.this.closed || remaining <= 0) {
+ throw new TimeoutException("RocketMQ collection deadline reached");
+ }
+ return Math.min(3000, remaining);
+ }
+
+ @Override
+ public final void operationComplete(ResponseFuture responseFuture) {
+ if (!this.finished.compareAndSet(false, true)) {
+ return;
+ }
+ try {
+ if (CollectRound.this.closed) {
+ return;
+ }
+ RemotingCommand response = responseFuture.getResponseCommand();
+ if (response == null) {
+ throw new IllegalStateException("Missing RocketMQ response");
+ }
+ if (response.getCode() != ResponseCode.SUCCESS || response.getBody() == null || response.getBody().length == 0) {
+ return;
+ }
+ this.handleResponse(responseFuture);
+ } catch (Exception e) {
+ this.restoreInterrupt(e);
+ } finally {
+ this.completeRequest();
+ }
+ }
+
+ @Override
+ public final void operationFail(Throwable cause) {
+ if (!this.finished.compareAndSet(false, true)) {
+ return;
+ }
+ try {
+ this.restoreInterrupt(cause);
+ } finally {
+ this.completeRequest();
+ }
+ }
+
+ private void restoreInterrupt(Throwable cause) {
+ if (cause instanceof InterruptedException) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ private void completeRequest() {
+ // 成功、失败都 -1;finished 防止重复回调重复扣减。
+ if (CollectRound.this.pending.decrementAndGet() == 0) {
+ CollectRound.this.completed.countDown();
+ }
+ }
+
+ protected abstract void handleResponse(ResponseFuture responseFuture) throws Exception;
+ }
+
}
+
+ private static void executeSafely(InvokeCallback callback, RequestAction action) {
+ try {
+ action.run();
+ } catch (Exception e) {
+ callback.operationFail(e);
+ }
+ }
+
+ @FunctionalInterface
+ private interface RequestAction {
+ void run() throws Exception;
+ }
+
}
diff --git a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/iotdb/IotDBReportMetaHandler.java b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/iotdb/IotDBReportMetaHandler.java
index 358d1bbf..bd8c3441 100644
--- a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/iotdb/IotDBReportMetaHandler.java
+++ b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/iotdb/IotDBReportMetaHandler.java
@@ -18,6 +18,7 @@
package org.apache.eventmesh.dashboard.console.function.report.iotdb;
import org.apache.eventmesh.dashboard.console.function.report.annotation.AbstractReportMetaHandler;
+import org.apache.eventmesh.dashboard.console.function.report.annotation.ReportTag;
import java.lang.reflect.Field;
import java.time.LocalDateTime;
@@ -155,7 +156,7 @@ public class IotDBReportMetaHandler extends AbstractReportMetaHandler {
if (Objects.equals(filedName, "time")) {
stringBuilder.append(" time");
- } else if (filedName.endsWith("Id")) {
+ } else if (filedName.endsWith("Id") || field.isAnnotationPresent(ReportTag.class)) {
stringBuilder.append(" tag");
} else if (filedName.startsWith("value") || field.getDeclaringClass() == this.reportMeta.getClazz()) {
stringBuilder.append(" field");
diff --git a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/Rocketmq2ProducerOffset.java b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/Rocketmq2ProducerOffset.java
index d89fcb60..893873ad 100644
--- a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/Rocketmq2ProducerOffset.java
+++ b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/Rocketmq2ProducerOffset.java
@@ -20,6 +20,7 @@ package org.apache.eventmesh.dashboard.console.function.report.model.rocketmq;
import org.apache.eventmesh.dashboard.common.enums.ClusterType;
import org.apache.eventmesh.dashboard.console.function.report.ReportViewType;
import org.apache.eventmesh.dashboard.console.function.report.annotation.ReportMeta;
+import org.apache.eventmesh.dashboard.console.function.report.annotation.ReportTag;
import org.apache.eventmesh.dashboard.console.function.report.model.base.RuntimeId.RuntimeLongValue;
import lombok.Data;
@@ -28,8 +29,21 @@ import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@ReportMeta(clusterType = ClusterType.STORAGE_ROCKETMQ, reportName = "rocketmq_producer_offset",
- defaultViewType = ReportViewType.HISTOGRAM, tableName = "rocketmq_producer_offset",
+ defaultViewType = ReportViewType.GAUGE, tableName = "rocketmq_producer_offset",
comment = "topic offset")
public class Rocketmq2ProducerOffset extends RuntimeLongValue {
+ @ReportTag
+ private String topicName;
+
+ private String queueId;
+
+ private Long valueMinOffset;
+
+ private Long valueLastUpdateTime;
+
+ private Long valueMinOffsetSum;
+
+ private Long valueMaxOffsetSum;
+
}
diff --git a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqMessagesInTotal.java b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqBrokerMessagesIn.java
similarity index 69%
copy from eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqMessagesInTotal.java
copy to eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqBrokerMessagesIn.java
index 43d32a80..d5ab65f0 100644
--- a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqMessagesInTotal.java
+++ b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqBrokerMessagesIn.java
@@ -20,20 +20,24 @@ package org.apache.eventmesh.dashboard.console.function.report.model.rocketmq;
import org.apache.eventmesh.dashboard.common.enums.ClusterType;
import org.apache.eventmesh.dashboard.console.function.report.ReportViewType;
import org.apache.eventmesh.dashboard.console.function.report.annotation.ReportMeta;
-import org.apache.eventmesh.dashboard.console.function.report.model.base.not.TopicId.TopicLongValue;
+import org.apache.eventmesh.dashboard.console.function.report.annotation.ReportTag;
+import org.apache.eventmesh.dashboard.console.function.report.model.base.RuntimeId.RuntimeFloatValue;
import lombok.Data;
import lombok.EqualsAndHashCode;
-
+/** Broker 原生滚动采样窗口的写入消息数及消息 TPS;继承的 value 为消息数/秒,并非累计总量。 */
@Data
@EqualsAndHashCode(callSuper = true)
-@ReportMeta(clusterType = ClusterType.STORAGE_ROCKETMQ, reportName = "rocketmq_messages_in_total",
- defaultViewType = ReportViewType.GAUGE, tableName = "rocketmq_messages_in_total",
- comment = "消息生产数量")
-public class RocketmqMessagesInTotal extends TopicLongValue {
+@ReportMeta(clusterType = ClusterType.STORAGE_ROCKETMQ, reportName = "rocketmq_broker_messages_in",
+ defaultViewType = ReportViewType.GAUGE, tableName = "rocketmq_broker_messages_in",
+ comment = "Broker 滚动采样窗口写入消息数及消息 TPS(消息数/秒)")
+public class RocketmqBrokerMessagesIn extends RuntimeFloatValue {
- private String messageType;
+ @ReportTag
+ private String window;
+ /** 滚动采样窗口内的写入消息数,不是进程累计计数。 */
+ private Long valueWindowCount;
}
diff --git a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqMessagesInTotal.java b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqBrokerMessagesOut.java
similarity index 67%
copy from eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqMessagesInTotal.java
copy to eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqBrokerMessagesOut.java
index 43d32a80..1b9abe6a 100644
--- a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqMessagesInTotal.java
+++ b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqBrokerMessagesOut.java
@@ -20,20 +20,24 @@ package org.apache.eventmesh.dashboard.console.function.report.model.rocketmq;
import org.apache.eventmesh.dashboard.common.enums.ClusterType;
import org.apache.eventmesh.dashboard.console.function.report.ReportViewType;
import org.apache.eventmesh.dashboard.console.function.report.annotation.ReportMeta;
-import org.apache.eventmesh.dashboard.console.function.report.model.base.not.TopicId.TopicLongValue;
+import org.apache.eventmesh.dashboard.console.function.report.annotation.ReportTag;
+import org.apache.eventmesh.dashboard.console.function.report.model.base.RuntimeId.RuntimeFloatValue;
import lombok.Data;
import lombok.EqualsAndHashCode;
-
+/** Broker 原生滚动采样窗口的取出消息数及消息 TPS;继承的 value 为消息数/秒,不代表消费确认。 */
@Data
@EqualsAndHashCode(callSuper = true)
-@ReportMeta(clusterType = ClusterType.STORAGE_ROCKETMQ, reportName = "rocketmq_messages_in_total",
- defaultViewType = ReportViewType.GAUGE, tableName = "rocketmq_messages_in_total",
- comment = "消息生产数量")
-public class RocketmqMessagesInTotal extends TopicLongValue {
+@ReportMeta(clusterType = ClusterType.STORAGE_ROCKETMQ, reportName = "rocketmq_broker_messages_out",
+ defaultViewType = ReportViewType.GAUGE, tableName = "rocketmq_broker_messages_out",
+ comment = "Broker 滚动采样窗口取出消息数及消息 TPS(消息数/秒),非消费确认数")
+public class RocketmqBrokerMessagesOut extends RuntimeFloatValue {
- private String messageType;
+ @ReportTag
+ private String window;
+ /** 滚动采样窗口内的取出消息数,不是累计计数或已确认消费数。 */
+ private Long valueWindowCount;
}
diff --git a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqMessagesInTotal.java b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqConsumerConnectionNumber.java
similarity index 78%
copy from eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqMessagesInTotal.java
copy to eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqConsumerConnectionNumber.java
index 43d32a80..f85bb9e1 100644
--- a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqMessagesInTotal.java
+++ b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqConsumerConnectionNumber.java
@@ -20,20 +20,22 @@ package org.apache.eventmesh.dashboard.console.function.report.model.rocketmq;
import org.apache.eventmesh.dashboard.common.enums.ClusterType;
import org.apache.eventmesh.dashboard.console.function.report.ReportViewType;
import org.apache.eventmesh.dashboard.console.function.report.annotation.ReportMeta;
-import org.apache.eventmesh.dashboard.console.function.report.model.base.not.TopicId.TopicLongValue;
+import org.apache.eventmesh.dashboard.console.function.report.annotation.ReportTag;
+import org.apache.eventmesh.dashboard.console.function.report.model.base.RuntimeId;
import lombok.Data;
import lombok.EqualsAndHashCode;
-
@Data
@EqualsAndHashCode(callSuper = true)
-@ReportMeta(clusterType = ClusterType.STORAGE_ROCKETMQ, reportName = "rocketmq_messages_in_total",
- defaultViewType = ReportViewType.GAUGE, tableName = "rocketmq_messages_in_total",
- comment = "消息生产数量")
-public class RocketmqMessagesInTotal extends TopicLongValue {
+@ReportMeta(clusterType = ClusterType.STORAGE_ROCKETMQ, reportName = "rocketmq_consumer_connection_number",
+ defaultViewType = ReportViewType.GAUGE, tableName = "rocketmq_consumer_connection_number",
+ comment = "消费组在当前 Broker 上的连接数")
+public class RocketmqConsumerConnectionNumber extends RuntimeId {
- private String messageType;
+ @ReportTag
+ private String groupName;
+ private Long valueConnectionCount;
}
diff --git a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqMessagesInTotal.java b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqConsumerFailedTps.java
similarity index 72%
copy from eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqMessagesInTotal.java
copy to eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqConsumerFailedTps.java
index 43d32a80..0641f00f 100644
--- a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqMessagesInTotal.java
+++ b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqConsumerFailedTps.java
@@ -20,20 +20,27 @@ package org.apache.eventmesh.dashboard.console.function.report.model.rocketmq;
import org.apache.eventmesh.dashboard.common.enums.ClusterType;
import org.apache.eventmesh.dashboard.console.function.report.ReportViewType;
import org.apache.eventmesh.dashboard.console.function.report.annotation.ReportMeta;
-import org.apache.eventmesh.dashboard.console.function.report.model.base.not.TopicId.TopicLongValue;
+import org.apache.eventmesh.dashboard.console.function.report.annotation.ReportTag;
+import org.apache.eventmesh.dashboard.console.function.report.model.base.RuntimeId.RuntimeFloatValue;
import lombok.Data;
import lombok.EqualsAndHashCode;
-
+/** 客户端最近一分钟消费失败 TPS,消息数每秒。 */
@Data
@EqualsAndHashCode(callSuper = true)
-@ReportMeta(clusterType = ClusterType.STORAGE_ROCKETMQ, reportName = "rocketmq_messages_in_total",
- defaultViewType = ReportViewType.GAUGE, tableName = "rocketmq_messages_in_total",
- comment = "消息生产数量")
-public class RocketmqMessagesInTotal extends TopicLongValue {
+@ReportMeta(clusterType = ClusterType.STORAGE_ROCKETMQ, reportName = "rocketmq_consumer_failed_tps",
+ defaultViewType = ReportViewType.GAUGE, tableName = "rocketmq_consumer_failed_tps",
+ comment = "客户端最近一分钟消费失败 TPS,消息数每秒")
+public class RocketmqConsumerFailedTps extends RuntimeFloatValue {
+
+ @ReportTag
+ private String topicName;
- private String messageType;
+ @ReportTag
+ private String groupName;
+ @ReportTag
+ private String clientId;
}
diff --git a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqConsumerGroupNumber.java b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqConsumerGroupNumber.java
index 7f2a2f1f..14fa5022 100644
--- a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqConsumerGroupNumber.java
+++ b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqConsumerGroupNumber.java
@@ -29,8 +29,8 @@ import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
@ReportMeta(clusterType = ClusterType.STORAGE_ROCKETMQ, reportName = "rocketmq_consumer_group_number",
defaultViewType = ReportViewType.GAUGE, tableName = "rocketmq_consumer_group_number",
- comment = "消费者组数量")
+ comment = "当前 Broker 配置的消费组总数,包含系统组和离线组")
public class RocketmqConsumerGroupNumber extends RuntimeLongValue {
-}
+}
diff --git a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqConsumerLagLatency.java b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqConsumerLagLatency.java
index c1ecd53e..8f6ca0f3 100644
--- a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqConsumerLagLatency.java
+++ b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqConsumerLagLatency.java
@@ -20,11 +20,27 @@ package org.apache.eventmesh.dashboard.console.function.report.model.rocketmq;
import org.apache.eventmesh.dashboard.common.enums.ClusterType;
import org.apache.eventmesh.dashboard.console.function.report.ReportViewType;
import org.apache.eventmesh.dashboard.console.function.report.annotation.ReportMeta;
-import org.apache.eventmesh.dashboard.console.function.report.model.base.not.SubscribeId.SubscribeLongValue;
+import org.apache.eventmesh.dashboard.console.function.report.annotation.ReportTag;
+import org.apache.eventmesh.dashboard.console.function.report.model.base.RuntimeId.RuntimeLongValue;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/** 最早未消费消息的积压时间,毫秒;无积压为零。 */
+@Data
+@EqualsAndHashCode(callSuper = true)
@ReportMeta(clusterType = ClusterType.STORAGE_ROCKETMQ, reportName = "rocketmq_consumer_lag_latency",
defaultViewType = ReportViewType.GAUGE, tableName = "rocketmq_consumer_lag_latency",
- comment = "消费处理延迟时间")
-public class RocketmqConsumerLagLatency extends SubscribeLongValue {
+ comment = "最早未消费消息的积压时间,毫秒;无积压为零")
+public class RocketmqConsumerLagLatency extends RuntimeLongValue {
+
+ @ReportTag
+ private String topicName;
+
+ @ReportTag
+ private String groupName;
+
+ @ReportTag
+ private String queueId;
}
diff --git a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqMessagesInTotal.java b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqConsumerOffset.java
similarity index 71%
copy from eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqMessagesInTotal.java
copy to eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqConsumerOffset.java
index 43d32a80..383c995a 100644
--- a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqMessagesInTotal.java
+++ b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqConsumerOffset.java
@@ -20,20 +20,32 @@ package org.apache.eventmesh.dashboard.console.function.report.model.rocketmq;
import org.apache.eventmesh.dashboard.common.enums.ClusterType;
import org.apache.eventmesh.dashboard.console.function.report.ReportViewType;
import org.apache.eventmesh.dashboard.console.function.report.annotation.ReportMeta;
-import org.apache.eventmesh.dashboard.console.function.report.model.base.not.TopicId.TopicLongValue;
+import org.apache.eventmesh.dashboard.console.function.report.annotation.ReportTag;
+import org.apache.eventmesh.dashboard.console.function.report.model.base.RuntimeId;
import lombok.Data;
import lombok.EqualsAndHashCode;
-
@Data
@EqualsAndHashCode(callSuper = true)
-@ReportMeta(clusterType = ClusterType.STORAGE_ROCKETMQ, reportName = "rocketmq_messages_in_total",
- defaultViewType = ReportViewType.GAUGE, tableName = "rocketmq_messages_in_total",
- comment = "消息生产数量")
-public class RocketmqMessagesInTotal extends TopicLongValue {
+@ReportMeta(clusterType = ClusterType.STORAGE_ROCKETMQ, reportName = "rocketmq_consumer_offset",
+ defaultViewType = ReportViewType.GAUGE, tableName = "rocketmq_consumer_offset",
+ comment = "消费组队列位点及位点差")
+public class RocketmqConsumerOffset extends RuntimeId {
+
+ @ReportTag
+ private String topicName;
+
+ @ReportTag
+ private String groupName;
+
+ private String queueId;
+
+ private Long valueConsumerOffset;
- private String messageType;
+ private Long valueBrokerOffset;
+ /** Broker 位点减去已提交位点,下限为零;表示位点差。 */
+ private Long valueOffsetLag;
}
diff --git a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqMessagesInTotal.java b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqConsumerProcessTime.java
similarity index 69%
copy from eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqMessagesInTotal.java
copy to eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqConsumerProcessTime.java
index 43d32a80..92fc9fac 100644
--- a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqMessagesInTotal.java
+++ b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqConsumerProcessTime.java
@@ -20,20 +20,27 @@ package org.apache.eventmesh.dashboard.console.function.report.model.rocketmq;
import org.apache.eventmesh.dashboard.common.enums.ClusterType;
import org.apache.eventmesh.dashboard.console.function.report.ReportViewType;
import org.apache.eventmesh.dashboard.console.function.report.annotation.ReportMeta;
-import org.apache.eventmesh.dashboard.console.function.report.model.base.not.TopicId.TopicLongValue;
+import org.apache.eventmesh.dashboard.console.function.report.annotation.ReportTag;
+import org.apache.eventmesh.dashboard.console.function.report.model.base.RuntimeId.RuntimeFloatValue;
import lombok.Data;
import lombok.EqualsAndHashCode;
-
+/** 客户端消费平均耗时,毫秒;沿用客户端分钟窗口及小时窗口回退语义。 */
@Data
@EqualsAndHashCode(callSuper = true)
-@ReportMeta(clusterType = ClusterType.STORAGE_ROCKETMQ, reportName = "rocketmq_messages_in_total",
- defaultViewType = ReportViewType.GAUGE, tableName = "rocketmq_messages_in_total",
- comment = "消息生产数量")
-public class RocketmqMessagesInTotal extends TopicLongValue {
+@ReportMeta(clusterType = ClusterType.STORAGE_ROCKETMQ, reportName = "rocketmq_consumer_process_time",
+ defaultViewType = ReportViewType.GAUGE, tableName = "rocketmq_consumer_process_time",
+ comment = "客户端消费平均耗时,毫秒;沿用客户端分钟窗口及小时窗口回退语义")
+public class RocketmqConsumerProcessTime extends RuntimeFloatValue {
+
+ @ReportTag
+ private String topicName;
- private String messageType;
+ @ReportTag
+ private String groupName;
+ @ReportTag
+ private String clientId;
}
diff --git a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqConsumerGroupNumber.java b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqConsumerSuccessTps.java
similarity index 72%
copy from eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqConsumerGroupNumber.java
copy to eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqConsumerSuccessTps.java
index 7f2a2f1f..f752a287 100644
--- a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqConsumerGroupNumber.java
+++ b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqConsumerSuccessTps.java
@@ -20,17 +20,27 @@ package org.apache.eventmesh.dashboard.console.function.report.model.rocketmq;
import org.apache.eventmesh.dashboard.common.enums.ClusterType;
import org.apache.eventmesh.dashboard.console.function.report.ReportViewType;
import org.apache.eventmesh.dashboard.console.function.report.annotation.ReportMeta;
-import org.apache.eventmesh.dashboard.console.function.report.model.base.RuntimeId.RuntimeLongValue;
+import org.apache.eventmesh.dashboard.console.function.report.annotation.ReportTag;
+import org.apache.eventmesh.dashboard.console.function.report.model.base.RuntimeId.RuntimeFloatValue;
import lombok.Data;
import lombok.EqualsAndHashCode;
+/** 客户端最近一分钟消费成功 TPS,消息数每秒。 */
@Data
@EqualsAndHashCode(callSuper = true)
-@ReportMeta(clusterType = ClusterType.STORAGE_ROCKETMQ, reportName = "rocketmq_consumer_group_number",
- defaultViewType = ReportViewType.GAUGE, tableName = "rocketmq_consumer_group_number",
- comment = "消费者组数量")
-public class RocketmqConsumerGroupNumber extends RuntimeLongValue {
+@ReportMeta(clusterType = ClusterType.STORAGE_ROCKETMQ, reportName = "rocketmq_consumer_success_tps",
+ defaultViewType = ReportViewType.GAUGE, tableName = "rocketmq_consumer_success_tps",
+ comment = "客户端最近一分钟消费成功 TPS,消息数每秒")
+public class RocketmqConsumerSuccessTps extends RuntimeFloatValue {
-}
+ @ReportTag
+ private String topicName;
+
+ @ReportTag
+ private String groupName;
+ @ReportTag
+ private String clientId;
+
+}
diff --git a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqMessagesInTotal.java b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqGroupMessagesOut.java
similarity index 66%
copy from eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqMessagesInTotal.java
copy to eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqGroupMessagesOut.java
index 43d32a80..123e1880 100644
--- a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqMessagesInTotal.java
+++ b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqGroupMessagesOut.java
@@ -20,20 +20,30 @@ package org.apache.eventmesh.dashboard.console.function.report.model.rocketmq;
import org.apache.eventmesh.dashboard.common.enums.ClusterType;
import org.apache.eventmesh.dashboard.console.function.report.ReportViewType;
import org.apache.eventmesh.dashboard.console.function.report.annotation.ReportMeta;
-import org.apache.eventmesh.dashboard.console.function.report.model.base.not.TopicId.TopicLongValue;
+import org.apache.eventmesh.dashboard.console.function.report.annotation.ReportTag;
+import org.apache.eventmesh.dashboard.console.function.report.model.base.RuntimeId.RuntimeFloatValue;
import lombok.Data;
import lombok.EqualsAndHashCode;
-
+/** Topic 与消费组滚动窗口取出消息数和 TPS,非消费确认数;继承的 value 单位为消息数/秒。 */
@Data
@EqualsAndHashCode(callSuper = true)
-@ReportMeta(clusterType = ClusterType.STORAGE_ROCKETMQ, reportName = "rocketmq_messages_in_total",
- defaultViewType = ReportViewType.GAUGE, tableName = "rocketmq_messages_in_total",
- comment = "消息生产数量")
-public class RocketmqMessagesInTotal extends TopicLongValue {
+@ReportMeta(clusterType = ClusterType.STORAGE_ROCKETMQ, reportName = "rocketmq_group_messages_out",
+ defaultViewType = ReportViewType.GAUGE, tableName = "rocketmq_group_messages_out",
+ comment = "Topic 与消费组滚动窗口取出消息数和 TPS,非消费确认数")
+public class RocketmqGroupMessagesOut extends RuntimeFloatValue {
+
+ @ReportTag
+ private String topicName;
+
+ @ReportTag
+ private String groupName;
- private String messageType;
+ @ReportTag
+ private String window;
+ /** 滚动采样窗口内的消息数,不是进程累计计数。 */
+ private Long valueWindowCount;
}
diff --git a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqMessagesInTotal.java b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqMessagesInTotal.java
index 43d32a80..e29e25fb 100644
--- a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqMessagesInTotal.java
+++ b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqMessagesInTotal.java
@@ -20,7 +20,7 @@ package org.apache.eventmesh.dashboard.console.function.report.model.rocketmq;
import org.apache.eventmesh.dashboard.common.enums.ClusterType;
import org.apache.eventmesh.dashboard.console.function.report.ReportViewType;
import org.apache.eventmesh.dashboard.console.function.report.annotation.ReportMeta;
-import org.apache.eventmesh.dashboard.console.function.report.model.base.not.TopicId.TopicLongValue;
+import org.apache.eventmesh.dashboard.console.function.report.model.base.RuntimeId.RuntimeLongValue;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -29,11 +29,7 @@ import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@ReportMeta(clusterType = ClusterType.STORAGE_ROCKETMQ, reportName = "rocketmq_messages_in_total",
- defaultViewType = ReportViewType.GAUGE, tableName = "rocketmq_messages_in_total",
- comment = "消息生产数量")
-public class RocketmqMessagesInTotal extends TopicLongValue {
-
- private String messageType;
-
-
+ defaultViewType = ReportViewType.COUNTER, tableName = "rocketmq_messages_in_total",
+ comment = "Broker 启动以来接收的消息数")
+public class RocketmqMessagesInTotal extends RuntimeLongValue {
}
diff --git a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqMessagesOutTotal.java b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqMessagesOutTotal.java
index 8b9c0b2d..48b4e183 100644
--- a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqMessagesOutTotal.java
+++ b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqMessagesOutTotal.java
@@ -20,7 +20,7 @@ package org.apache.eventmesh.dashboard.console.function.report.model.rocketmq;
import org.apache.eventmesh.dashboard.common.enums.ClusterType;
import org.apache.eventmesh.dashboard.console.function.report.ReportViewType;
import org.apache.eventmesh.dashboard.console.function.report.annotation.ReportMeta;
-import org.apache.eventmesh.dashboard.console.function.report.model.base.not.SubscribeId;
+import org.apache.eventmesh.dashboard.console.function.report.model.base.RuntimeId.RuntimeLongValue;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -29,13 +29,7 @@ import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
@ReportMeta(clusterType = ClusterType.STORAGE_ROCKETMQ, reportName = "rocketmq_messages_out_total",
defaultViewType = ReportViewType.COUNTER, tableName = "rocketmq_messages_out_total",
- comment = "已就绪消息量")
-public class RocketmqMessagesOutTotal extends SubscribeId {
-
- private Long topicNum;
-
- private Long minOffset;
-
- private Long maxOffset;
+ comment = "Broker 启动以来从存储读取的消息条数")
+public class RocketmqMessagesOutTotal extends RuntimeLongValue {
}
diff --git a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqConsumerGroupNumber.java b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqStorageDiskFreeBytes.java
similarity index 80%
copy from eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqConsumerGroupNumber.java
copy to eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqStorageDiskFreeBytes.java
index 7f2a2f1f..22348062 100644
--- a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqConsumerGroupNumber.java
+++ b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqStorageDiskFreeBytes.java
@@ -25,12 +25,12 @@ import org.apache.eventmesh.dashboard.console.function.report.model.base.Runtime
import lombok.Data;
import lombok.EqualsAndHashCode;
+/** Broker 存储根目录所在文件系统剩余字节数,格式化容量还原的近似值。 */
@Data
@EqualsAndHashCode(callSuper = true)
-@ReportMeta(clusterType = ClusterType.STORAGE_ROCKETMQ, reportName = "rocketmq_consumer_group_number",
- defaultViewType = ReportViewType.GAUGE, tableName = "rocketmq_consumer_group_number",
- comment = "消费者组数量")
-public class RocketmqConsumerGroupNumber extends RuntimeLongValue {
+@ReportMeta(clusterType = ClusterType.STORAGE_ROCKETMQ, reportName = "rocketmq_storage_disk_free_bytes",
+ defaultViewType = ReportViewType.GAUGE, tableName = "rocketmq_storage_disk_free_bytes",
+ comment = "Broker 存储根目录所在文件系统剩余字节数,格式化容量还原的近似值")
+public class RocketmqStorageDiskFreeBytes extends RuntimeLongValue {
}
-
diff --git a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqMessagesInTotal.java b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqStorageDiskUsage.java
similarity index 72%
copy from eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqMessagesInTotal.java
copy to eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqStorageDiskUsage.java
index 43d32a80..3c62e600 100644
--- a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqMessagesInTotal.java
+++ b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqStorageDiskUsage.java
@@ -20,20 +20,24 @@ package org.apache.eventmesh.dashboard.console.function.report.model.rocketmq;
import org.apache.eventmesh.dashboard.common.enums.ClusterType;
import org.apache.eventmesh.dashboard.console.function.report.ReportViewType;
import org.apache.eventmesh.dashboard.console.function.report.annotation.ReportMeta;
-import org.apache.eventmesh.dashboard.console.function.report.model.base.not.TopicId.TopicLongValue;
+import org.apache.eventmesh.dashboard.console.function.report.annotation.ReportTag;
+import org.apache.eventmesh.dashboard.console.function.report.model.base.RuntimeId.RuntimeFloatValue;
import lombok.Data;
import lombok.EqualsAndHashCode;
-
+/** 存储文件系统使用率,范围 0 到 1;commitlog 汇总为各路径最小值。 */
@Data
@EqualsAndHashCode(callSuper = true)
-@ReportMeta(clusterType = ClusterType.STORAGE_ROCKETMQ, reportName = "rocketmq_messages_in_total",
- defaultViewType = ReportViewType.GAUGE, tableName = "rocketmq_messages_in_total",
- comment = "消息生产数量")
-public class RocketmqMessagesInTotal extends TopicLongValue {
+@ReportMeta(clusterType = ClusterType.STORAGE_ROCKETMQ, reportName = "rocketmq_storage_disk_usage",
+ defaultViewType = ReportViewType.GAUGE, tableName = "rocketmq_storage_disk_usage",
+ comment = "存储文件系统使用率,范围 0 到 1;commitlog 汇总为各路径最小值")
+public class RocketmqStorageDiskUsage extends RuntimeFloatValue {
- private String messageType;
+ @ReportTag
+ private String storeType;
+ @ReportTag
+ private String path;
}
diff --git a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqStorageMessageReserveTime.java b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqStorageMessageReserveTime.java
index ddd5547f..42a84d14 100644
--- a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqStorageMessageReserveTime.java
+++ b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqStorageMessageReserveTime.java
@@ -20,11 +20,11 @@ package org.apache.eventmesh.dashboard.console.function.report.model.rocketmq;
import org.apache.eventmesh.dashboard.common.enums.ClusterType;
import org.apache.eventmesh.dashboard.console.function.report.ReportViewType;
import org.apache.eventmesh.dashboard.console.function.report.annotation.ReportMeta;
-import org.apache.eventmesh.dashboard.console.function.report.model.base.RuntimeId;
+import org.apache.eventmesh.dashboard.console.function.report.model.base.RuntimeId.RuntimeLongValue;
@ReportMeta(clusterType = ClusterType.STORAGE_ROCKETMQ, reportName = "rocketmq_storage_message_reserve_time",
defaultViewType = ReportViewType.GAUGE, tableName = "rocketmq_storage_message_reserve_time",
- comment = "储存层消息保存时间")
-public class RocketmqStorageMessageReserveTime extends RuntimeId {
+ comment = "当前时间与最早保留消息存储时间之差(毫秒),非配置的保留时长")
+public class RocketmqStorageMessageReserveTime extends RuntimeLongValue {
}
diff --git a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqThreadPoolWartermark.java b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqThreadPoolWartermark.java
index 0f0df097..d3c1c065 100644
--- a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqThreadPoolWartermark.java
+++ b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqThreadPoolWartermark.java
@@ -20,11 +20,20 @@ package org.apache.eventmesh.dashboard.console.function.report.model.rocketmq;
import org.apache.eventmesh.dashboard.common.enums.ClusterType;
import org.apache.eventmesh.dashboard.console.function.report.ReportViewType;
import org.apache.eventmesh.dashboard.console.function.report.annotation.ReportMeta;
+import org.apache.eventmesh.dashboard.console.function.report.annotation.ReportTag;
import org.apache.eventmesh.dashboard.console.function.report.model.base.RuntimeId.RuntimeLongValue;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
@ReportMeta(clusterType = ClusterType.STORAGE_ROCKETMQ, reportName = "rocketmq_thread_pool_wartermark",
defaultViewType = ReportViewType.GAUGE, tableName = "rocketmq_thread_pool_wartermark",
- comment = "线程池排队数")
+ comment = "线程池当前队列长度,非历史最大值")
public class RocketmqThreadPoolWartermark extends RuntimeLongValue {
+ @ReportTag
+ private String poolName;
+
}
diff --git a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqThroughputInTotal.java b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqThroughputInTotal.java
index f2874956..38fafc23 100644
--- a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqThroughputInTotal.java
+++ b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqThroughputInTotal.java
@@ -20,7 +20,7 @@ package org.apache.eventmesh.dashboard.console.function.report.model.rocketmq;
import org.apache.eventmesh.dashboard.common.enums.ClusterType;
import org.apache.eventmesh.dashboard.console.function.report.ReportViewType;
import org.apache.eventmesh.dashboard.console.function.report.annotation.ReportMeta;
-import org.apache.eventmesh.dashboard.console.function.report.model.base.not.SubscribeId.SubscribeLongValue;
+import org.apache.eventmesh.dashboard.console.function.report.model.base.RuntimeId.RuntimeLongValue;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -29,7 +29,7 @@ import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
@ReportMeta(clusterType = ClusterType.STORAGE_ROCKETMQ, reportName = "rocketmq_throughput_in_total",
defaultViewType = ReportViewType.COUNTER, tableName = "rocketmq_throughput_in_total",
- comment = "消息生产流入服务端的吞吐量")
-public class RocketmqThroughputInTotal extends SubscribeLongValue {
+ comment = "Broker 启动以来接收的消息字节数")
+public class RocketmqThroughputInTotal extends RuntimeLongValue {
}
diff --git a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqMessagesInTotal.java b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqTopicMessagesIn.java
similarity index 68%
copy from eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqMessagesInTotal.java
copy to eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqTopicMessagesIn.java
index 43d32a80..3dba3300 100644
--- a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqMessagesInTotal.java
+++ b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqTopicMessagesIn.java
@@ -20,20 +20,27 @@ package org.apache.eventmesh.dashboard.console.function.report.model.rocketmq;
import org.apache.eventmesh.dashboard.common.enums.ClusterType;
import org.apache.eventmesh.dashboard.console.function.report.ReportViewType;
import org.apache.eventmesh.dashboard.console.function.report.annotation.ReportMeta;
-import org.apache.eventmesh.dashboard.console.function.report.model.base.not.TopicId.TopicLongValue;
+import org.apache.eventmesh.dashboard.console.function.report.annotation.ReportTag;
+import org.apache.eventmesh.dashboard.console.function.report.model.base.RuntimeId.RuntimeFloatValue;
import lombok.Data;
import lombok.EqualsAndHashCode;
-
+/** Topic 滚动窗口写入消息数和 TPS,非生命周期累计量;继承的 value 单位为消息数/秒。 */
@Data
@EqualsAndHashCode(callSuper = true)
-@ReportMeta(clusterType = ClusterType.STORAGE_ROCKETMQ, reportName = "rocketmq_messages_in_total",
- defaultViewType = ReportViewType.GAUGE, tableName = "rocketmq_messages_in_total",
- comment = "消息生产数量")
-public class RocketmqMessagesInTotal extends TopicLongValue {
+@ReportMeta(clusterType = ClusterType.STORAGE_ROCKETMQ, reportName = "rocketmq_topic_messages_in",
+ defaultViewType = ReportViewType.GAUGE, tableName = "rocketmq_topic_messages_in",
+ comment = "Topic 滚动窗口写入消息数和 TPS,非生命周期累计量")
+public class RocketmqTopicMessagesIn extends RuntimeFloatValue {
+
+ @ReportTag
+ private String topicName;
- private String messageType;
+ @ReportTag
+ private String window;
+ /** 滚动采样窗口内的消息数,不是进程累计计数。 */
+ private Long valueWindowCount;
}
diff --git a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqTopicNumber.java b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqTopicNumber.java
index 968b43c8..24950666 100644
--- a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqTopicNumber.java
+++ b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/function/report/model/rocketmq/RocketmqTopicNumber.java
@@ -24,7 +24,7 @@ import org.apache.eventmesh.dashboard.console.function.report.model.base.Runtime
@ReportMeta(clusterType = ClusterType.STORAGE_ROCKETMQ, reportName = "rocketmq_topic_number",
defaultViewType = ReportViewType.GAUGE, tableName = "rocketmq_topic_number",
- comment = "主题数量")
+ comment = "当前 Broker 配置的 Topic 总数,包含系统、重试和死信 Topic")
public class RocketmqTopicNumber extends RuntimeLongValue {
}
diff --git a/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/mapstruct/report/RocketMQCollectMapper.java b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/mapstruct/report/RocketMQCollectMapper.java
new file mode 100644
index 00000000..6daea830
--- /dev/null
+++ b/eventmesh-dashboard-console/src/main/java/org/apache/eventmesh/dashboard/console/mapstruct/report/RocketMQCollectMapper.java
@@ -0,0 +1,194 @@
+/*
+ * 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.eventmesh.dashboard.console.mapstruct.report;
+
+import org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.Rocketmq2ProducerOffset;
+import org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.RocketmqBrokerMessagesIn;
+import org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.RocketmqBrokerMessagesOut;
+import org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.RocketmqConsumerConnectionNumber;
+import org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.RocketmqConsumerFailedTps;
+import org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.RocketmqConsumerGroupNumber;
+import org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.RocketmqConsumerLagLatency;
+import org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.RocketmqConsumerOffset;
+import org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.RocketmqConsumerProcessTime;
+import org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.RocketmqConsumerSuccessTps;
+import org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.RocketmqGroupMessagesOut;
+import org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.RocketmqMessagesInTotal;
+import org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.RocketmqMessagesOutTotal;
+import org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.RocketmqStorageDiskFreeBytes;
+import org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.RocketmqStorageDiskUsage;
+import org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.RocketmqStorageDispatchBehindBytes;
+import org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.RocketmqStorageFlushBehindBytes;
+import org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.RocketmqStorageMessageReserveTime;
+import org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.RocketmqThreadPoolWartermark;
+import org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.RocketmqThroughputInTotal;
+import org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.RocketmqTopicMessagesIn;
+import org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.RocketmqTopicNumber;
+
+import org.apache.rocketmq.remoting.protocol.admin.OffsetWrapper;
+import org.apache.rocketmq.remoting.protocol.admin.TopicOffset;
+
+import org.mapstruct.BeanMapping;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.factory.Mappers;
+
+/** 将 Broker 采样转换为语义对应的报表模型,由模型注解指定目标表。 */
+@Mapper
+public interface RocketMQCollectMapper {
+ RocketMQCollectMapper INSTANCE = Mappers.getMapper(RocketMQCollectMapper.class);
+
+ @BeanMapping(ignoreByDefault = true)
+ @Mapping(target = "groupName", source = "group")
+ @Mapping(target = "valueConnectionCount", source = "count")
+ RocketmqConsumerConnectionNumber connections(String group, long count);
+
+ @BeanMapping(ignoreByDefault = true)
+ @Mapping(target = "topicName", source = "topic")
+ @Mapping(target = "queueId", source = "queue")
+ @Mapping(target = "value", source = "offset.maxOffset")
+ @Mapping(target = "valueMinOffset", source = "offset.minOffset")
+ @Mapping(target = "valueLastUpdateTime", source = "offset.lastUpdateTimestamp")
+ Rocketmq2ProducerOffset topicOffset(String topic, String queue, TopicOffset offset);
+
+ @BeanMapping(ignoreByDefault = true)
+ @Mapping(target = "topicName", source = "topic")
+ @Mapping(target = "queueId", constant = "")
+ @Mapping(target = "valueMinOffsetSum", source = "min")
+ @Mapping(target = "valueMaxOffsetSum", source = "max")
+ @Mapping(target = "valueLastUpdateTime", source = "lastUpdate")
+ Rocketmq2ProducerOffset aggregateOffset(String topic, long min, long max, long lastUpdate);
+
+ @BeanMapping(ignoreByDefault = true)
+ @Mapping(target = "topicName", source = "topic")
+ @Mapping(target = "groupName", source = "group")
+ @Mapping(target = "queueId", source = "queue")
+ @Mapping(target = "valueConsumerOffset", source = "offset.consumerOffset")
+ @Mapping(target = "valueBrokerOffset", source = "offset.brokerOffset")
+ @Mapping(target = "valueOffsetLag", expression = "java(Math.max(0L, offset.getBrokerOffset() - offset.getConsumerOffset()))")
+ RocketmqConsumerOffset consumerOffset(String topic, String group, String queue, OffsetWrapper offset);
+
+ @BeanMapping(ignoreByDefault = true)
+ @Mapping(target = "window", source = "window")
+ @Mapping(target = "valueWindowCount", source = "count")
+ @Mapping(target = "value", source = "rate")
+ RocketmqBrokerMessagesIn brokerMessagesIn(String window, long count, float rate);
+
+ @BeanMapping(ignoreByDefault = true)
+ @Mapping(target = "window", source = "window")
+ @Mapping(target = "valueWindowCount", source = "count")
+ @Mapping(target = "value", source = "rate")
+ RocketmqBrokerMessagesOut brokerMessagesOut(String window, long count, float rate);
+
+ @BeanMapping(ignoreByDefault = true)
+ @Mapping(target = "value", source = "bytes")
+ RocketmqStorageDispatchBehindBytes dispatchBytes(Long bytes);
+
+ @BeanMapping(ignoreByDefault = true)
+ @Mapping(target = "value", source = "bytes")
+ RocketmqStorageFlushBehindBytes flushBytes(Long bytes);
+
+ @BeanMapping(ignoreByDefault = true)
+ @Mapping(target = "value", source = "age")
+ RocketmqStorageMessageReserveTime reserveTime(Long age);
+
+ @BeanMapping(ignoreByDefault = true)
+ @Mapping(target = "poolName", source = "pool")
+ @Mapping(target = "value", source = "size")
+ RocketmqThreadPoolWartermark threadPool(String pool, long size);
+
+ @BeanMapping(ignoreByDefault = true)
+ @Mapping(target = "value", source = "count")
+ RocketmqTopicNumber topicNumber(Long count);
+
+ @BeanMapping(ignoreByDefault = true)
+ @Mapping(target = "value", source = "count")
+ RocketmqConsumerGroupNumber groupNumber(Long count);
+
+ @BeanMapping(ignoreByDefault = true)
+ @Mapping(target = "value", source = "value")
+ RocketmqMessagesInTotal messagesInTotal(Long value);
+
+ @BeanMapping(ignoreByDefault = true)
+ @Mapping(target = "value", source = "value")
+ RocketmqMessagesOutTotal messagesOutTotal(Long value);
+
+ @BeanMapping(ignoreByDefault = true)
+ @Mapping(target = "value", source = "value")
+ RocketmqThroughputInTotal throughputInTotal(Long value);
+
+ @BeanMapping(ignoreByDefault = true)
+ @Mapping(target = "topicName", source = "topic")
+ @Mapping(target = "window", source = "window")
+ @Mapping(target = "valueWindowCount", source = "count")
+ @Mapping(target = "value", source = "rate")
+ RocketmqTopicMessagesIn topicMessages(String topic, String window, long count, float rate);
+
+ @BeanMapping(ignoreByDefault = true)
+ @Mapping(target = "topicName", source = "topic")
+ @Mapping(target = "groupName", source = "group")
+ @Mapping(target = "window", source = "window")
+ @Mapping(target = "valueWindowCount", source = "count")
+ @Mapping(target = "value", source = "rate")
+ RocketmqGroupMessagesOut groupMessages(String topic, String group, String window, long count, float rate);
+
+
+ @BeanMapping(ignoreByDefault = true)
+ @Mapping(target = "topicName", source = "topicName")
+ @Mapping(target = "groupName", source = "groupName")
+ @Mapping(target = "queueId", source = "queueId")
+ @Mapping(target = "value", source = "value")
+ RocketmqConsumerLagLatency consumerLag(String topicName, String groupName, String queueId, Long value);
+
+
+ @BeanMapping(ignoreByDefault = true)
+ @Mapping(target = "topicName", source = "topicName")
+ @Mapping(target = "groupName", source = "groupName")
+ @Mapping(target = "clientId", source = "clientId")
+ @Mapping(target = "value", source = "value")
+ RocketmqConsumerSuccessTps consumerSuccess(String topicName, String groupName, String clientId, Float value);
+
+
+ @BeanMapping(ignoreByDefault = true)
+ @Mapping(target = "topicName", source = "topicName")
+ @Mapping(target = "groupName", source = "groupName")
+ @Mapping(target = "clientId", source = "clientId")
+ @Mapping(target = "value", source = "value")
+ RocketmqConsumerFailedTps consumerFailed(String topicName, String groupName, String clientId, Float value);
+
+
+ @BeanMapping(ignoreByDefault = true)
+ @Mapping(target = "topicName", source = "topicName")
+ @Mapping(target = "groupName", source = "groupName")
+ @Mapping(target = "clientId", source = "clientId")
+ @Mapping(target = "value", source = "value")
+ RocketmqConsumerProcessTime consumerProcessTime(String topicName, String groupName, String clientId, Float value);
+
+
+ @BeanMapping(ignoreByDefault = true)
+ @Mapping(target = "storeType", source = "storeType")
+ @Mapping(target = "path", source = "path")
+ @Mapping(target = "value", source = "value")
+ RocketmqStorageDiskUsage diskUsage(String storeType, String path, Float value);
+
+
+ @BeanMapping(ignoreByDefault = true)
+ @Mapping(target = "value", source = "value")
+ RocketmqStorageDiskFreeBytes diskFreeBytes(Long value);
+
+}
diff --git a/eventmesh-dashboard-console/src/test/java/org/apache/eventmesh/dashboard/console/function/report/collect/DataSyncHandlerTest.java b/eventmesh-dashboard-console/src/test/java/org/apache/eventmesh/dashboard/console/function/report/collect/DataSyncHandlerTest.java
new file mode 100644
index 00000000..3eaddd37
--- /dev/null
+++ b/eventmesh-dashboard-console/src/test/java/org/apache/eventmesh/dashboard/console/function/report/collect/DataSyncHandlerTest.java
@@ -0,0 +1,156 @@
+/*
+ * 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.eventmesh.dashboard.console.function.report.collect;
+
+import org.apache.eventmesh.dashboard.console.function.report.ReportEngine;
+import org.apache.eventmesh.dashboard.console.function.report.ReportHandlerManage;
+import org.apache.eventmesh.dashboard.console.function.report.collect.padding.PaddingService;
+import org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.RocketmqConsumerConnectionNumber;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+public class DataSyncHandlerTest {
+
+ @Test
+ public void initializationUsesConfiguredEngine() throws Exception {
+ ReportEngine engine = Mockito.mock(ReportEngine.class);
+ ReportHandlerManage reports = new ReportHandlerManage();
+ reports.setReportEngine(engine);
+ CollectManage manager = new CollectManage();
+ manager.setReportHandlerManage(reports);
+ manager.init();
+ var field = CollectManage.class.getDeclaredField("dataSyncHandler");
+ field.setAccessible(true);
+ DataSyncHandler handler = (DataSyncHandler) field.get(manager);
+ // 空结果也经过真实批次交付,验证初始化接线,而不是测试中手动注入引擎。
+ handler.getDataSyncHandlerWrapper(1).sync(data(0));
+ Mockito.verify(engine).batchInsertByClass(Map.of());
+ }
+
+ @Test
+ public void reverseCompletionDeliversFlatIterableRowsOnce() {
+ DataSyncHandler handler = new DataSyncHandler();
+ ReportEngine engine = Mockito.mock(ReportEngine.class);
+ PaddingService padding = Mockito.mock(PaddingService.class);
+ handler.setReportEngine(engine);
+ handler.setPaddingService(padding);
+ var wrapper = handler.getDataSyncHandlerWrapper(3);
+ RestoreData first = data(0, 10, 11);
+ RestoreData empty = data(1);
+ RestoreData last = data(2, 20);
+ List<Long> observed = new ArrayList<>();
+ Mockito.doAnswer(call -> {
+ Map<Class<?>, List<Object>> batch = call.getArgument(0);
+ List<Object> rows = batch.get(RocketmqConsumerConnectionNumber.class);
+ Assertions.assertEquals(3, rows.size());
+ Assertions.assertNotNull(rows.get(2));
+ for (Object row : rows) {
+ observed.add(((RocketmqConsumerConnectionNumber) row).getValueConnectionCount());
+ }
+ Assertions.assertFalse(first.getDataMap().isEmpty(), "Do not recycle before engine consumes the batch");
+ return null;
+ }).when(engine).batchInsertByClass(Mockito.anyMap());
+ wrapper.sync(last);
+ wrapper.sync(empty);
+ Mockito.verifyNoInteractions(engine);
+ wrapper.sync(first);
+ Assertions.assertEquals(List.of(20L, 10L, 11L), observed);
+ Mockito.verify(engine).batchInsertByClass(Mockito.anyMap());
+ Mockito.verify(padding, Mockito.times(3)).padding(Mockito.any());
+ Assertions.assertTrue(first.getDataMap().isEmpty());
+ Assertions.assertTrue(last.getDataMap().isEmpty());
+ Assertions.assertTrue(wrapper.restoreDataList.isEmpty());
+ }
+
+ @Test
+ public void concurrentCompletionAllocatesUniqueIndicesAndLosesNoRows() throws Exception {
+ DataSyncHandler handler = new DataSyncHandler();
+ ReportEngine engine = Mockito.mock(ReportEngine.class);
+ handler.setReportEngine(engine);
+ handler.setPaddingService(Mockito.mock(PaddingService.class));
+ var wrapper = handler.getDataSyncHandlerWrapper(200);
+ Set<Integer> indices = ConcurrentHashMap.newKeySet();
+ Set<Long> observed = ConcurrentHashMap.newKeySet();
+ Mockito.doAnswer(call -> {
+ Map<Class<?>, List<Object>> batch = call.getArgument(0);
+ List<Object> rows = batch.get(RocketmqConsumerConnectionNumber.class);
+ Assertions.assertEquals(200, rows.size());
+ for (Object row : rows) {
+ observed.add(((RocketmqConsumerConnectionNumber) row).getValueConnectionCount());
+ }
+ return null;
+ }).when(engine).batchInsertByClass(Mockito.anyMap());
+ var executor = Executors.newFixedThreadPool(8);
+ try {
+ List<Future<?>> jobs = new ArrayList<>();
+ for (int i = 0; i < 200; i++) {
+ final int value = i;
+ jobs.add(executor.submit(() -> {
+ int index = wrapper.getIndex();
+ Assertions.assertTrue(indices.add(index));
+ wrapper.sync(data(index, value));
+ }));
+ }
+ for (Future<?> job : jobs) {
+ job.get(5, TimeUnit.SECONDS);
+ }
+ } finally {
+ executor.shutdownNow();
+ }
+ Assertions.assertEquals(200, indices.size());
+ Assertions.assertEquals(200, observed.size());
+ Mockito.verify(engine).batchInsertByClass(Mockito.anyMap());
+ }
+
+ @Test
+ public void failedWriteStillReleasesContainers() {
+ DataSyncHandler handler = new DataSyncHandler();
+ ReportEngine engine = Mockito.mock(ReportEngine.class);
+ handler.setReportEngine(engine);
+ handler.setPaddingService(Mockito.mock(PaddingService.class));
+ Mockito.doThrow(new IllegalStateException("write failed")).when(engine).batchInsertByClass(Mockito.anyMap());
+ var wrapper = handler.getDataSyncHandlerWrapper(1);
+ RestoreData data = data(0, 1);
+ Assertions.assertThrows(IllegalStateException.class, () -> wrapper.sync(data));
+ Assertions.assertTrue(data.getDataMap().isEmpty());
+ Assertions.assertTrue(wrapper.restoreDataList.isEmpty());
+ }
+
+ private RestoreData data(int index, long... values) {
+ RestoreData data = new RestoreData();
+ data.setIndex(index);
+ data.setAbstractCollect(Mockito.mock(AbstractCollect.class));
+ for (long value : values) {
+ RocketmqConsumerConnectionNumber row = new RocketmqConsumerConnectionNumber();
+ row.setValueConnectionCount(value);
+ data.setData(row);
+ }
+ return data;
+ }
+}
diff --git a/eventmesh-dashboard-console/src/test/java/org/apache/eventmesh/dashboard/console/function/report/collect/RocketMQCollectManualTest.java b/eventmesh-dashboard-console/src/test/java/org/apache/eventmesh/dashboard/console/function/report/collect/RocketMQCollectManualTest.java
new file mode 100644
index 00000000..cd6cb820
--- /dev/null
+++ b/eventmesh-dashboard-console/src/test/java/org/apache/eventmesh/dashboard/console/function/report/collect/RocketMQCollectManualTest.java
@@ -0,0 +1,123 @@
+/*
+ * 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.eventmesh.dashboard.console.function.report.collect;
+
+import org.apache.eventmesh.dashboard.common.enums.ClusterType;
+import org.apache.eventmesh.dashboard.common.model.metadata.ClusterMetadata;
+import org.apache.eventmesh.dashboard.common.model.metadata.RuntimeMetadata;
+import org.apache.eventmesh.dashboard.console.function.report.collect.DataSyncHandler.DataSyncHandlerWrapper;
+import org.apache.eventmesh.dashboard.console.function.report.collect.exporter.RocketMQCollect;
+import org.apache.eventmesh.dashboard.console.function.report.model.base.OrganizationId;
+import org.apache.eventmesh.dashboard.core.function.SDK.ConfigManage;
+import org.apache.eventmesh.dashboard.core.function.SDK.SDKManage;
+import org.apache.eventmesh.dashboard.core.function.SDK.SDKTypeEnum;
+import org.apache.eventmesh.dashboard.core.function.SDK.config.AbstractSimpleCreateSDKConfig;
+import org.apache.eventmesh.dashboard.core.function.SDK.config.NetAddress;
+import org.apache.eventmesh.dashboard.core.function.SDK.operation.rocketmq.RocketMQRemotingSDKOperation.DefaultRemotingClient;
+
+import org.apache.commons.lang3.reflect.FieldUtils;
+import org.apache.rocketmq.remoting.protocol.RemotingCommand;
+import org.apache.rocketmq.remoting.protocol.RequestCode;
+import org.apache.rocketmq.remoting.protocol.ResponseCode;
+
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import com.alibaba.fastjson.JSON;
+
+import lombok.extern.slf4j.Slf4j;
+
+
+@Slf4j
+public class RocketMQCollectManualTest {
+
+ /** 在 IDE 中运行此方法:调用一次 CollectManage.collect,读取真实 Broker 并打印全部采集结果。 */
+ @Test
+ @DisplayName("手动执行采集任务并打印真实 Broker 指标")
+ @SuppressWarnings("unchecked")
+ public void collectOnceFromRealBrokerAndPrint() throws Exception {
+ RuntimeMetadata runtime = new RuntimeMetadata();
+ runtime.setId(System.nanoTime());
+ runtime.setClusterId(1L);
+ runtime.setClusterType(ClusterType.STORAGE_ROCKETMQ_BROKER_MAIN_SLAVE);
+ ClusterMetadata cluster = new ClusterMetadata();
+ cluster.setId(runtime.getClusterId());
+ cluster.setOrganizationId(7L);
+ cluster.setClusterType(runtime.getClusterType());
+ AtomicInteger printed = new AtomicInteger();
+ RocketMQCollect exporter = new RocketMQCollect() {
+ @Override
+ protected void setData(OrganizationId data) {
+ super.setData(data);
+ log.info("Broker 指标={} 数据={}", data.getClass().getSimpleName(), JSON.toJSONString(data));
+ printed.incrementAndGet();
+ }
+ };
+ exporter.setRuntimeMetadata(runtime);
+ exporter.setClusterMetadata(cluster);
+
+ AbstractSimpleCreateSDKConfig config = ConfigManage.getInstance()
+ .getSimpleCreateSdkConfig(runtime.getClusterType(), SDKTypeEnum.ADMIN);
+ NetAddress address = new NetAddress();
+ address.setAddress(System.getProperty("rocketmq.collect.host", "127.0.0.1"));
+ address.setPort(Integer.getInteger("rocketmq.collect.port", 20911));
+ config.setNetAddress(address);
+ DefaultRemotingClient client = SDKManage.getInstance()
+ .createClient(SDKTypeEnum.ADMIN, runtime, config, runtime.getClusterType());
+
+ CollectManage collectManage = new CollectManage();
+ Map<String, Collect> collectors = (Map<String, Collect>) FieldUtils.readField(collectManage, "executeingCollectMap", true);
+ collectors.put(runtime.getUnique(), exporter);
+
+ // 测试接收器只通知采集完成,不调用 IoTDB 写入链路。
+ CountDownLatch completed = new CountDownLatch(1);
+ DataSyncHandler printHandler = Mockito.mock(DataSyncHandler.class);
+ DataSyncHandlerWrapper wrapper = Mockito.mock(DataSyncHandlerWrapper.class);
+ Mockito.when(printHandler.getDataSyncHandlerWrapper(1)).thenReturn(wrapper);
+ Mockito.doAnswer(call -> {
+ completed.countDown();
+ return null;
+ }).when(wrapper).sync(Mockito.any(RestoreData.class));
+ FieldUtils.writeField(collectManage, "dataSyncHandler", printHandler, true);
+
+ ExecutorService workers = (ExecutorService) FieldUtils.readField(collectManage, "threadPoolExecutor", true);
+ try {
+ RemotingCommand response = client.invokeSync(
+ RemotingCommand.createRequestCommand(RequestCode.GET_BROKER_RUNTIME_INFO, null), 3000);
+ Assertions.assertNotNull(response, "Broker unavailable at " + address);
+ Assertions.assertEquals(ResponseCode.SUCCESS, response.getCode(), "Broker unavailable at " + address);
+
+ // 这一行就是 ReportHandlerManage 定时任务实际调用的采集方法。
+ collectManage.collect();
+ Assertions.assertTrue(completed.await(10, TimeUnit.SECONDS), "Collection did not finish");
+ Assertions.assertTrue(printed.get() > 0, "Broker collection returned no data");
+ } finally {
+ workers.shutdownNow();
+ client.shutdown();
+ SDKManage.getInstance().deleteClient(null, runtime.getUnique());
+ }
+ }
+}
diff --git a/eventmesh-dashboard-console/src/test/java/org/apache/eventmesh/dashboard/console/function/report/collect/RocketMQCollectWithClientManualTest.java b/eventmesh-dashboard-console/src/test/java/org/apache/eventmesh/dashboard/console/function/report/collect/RocketMQCollectWithClientManualTest.java
new file mode 100644
index 00000000..529025c2
--- /dev/null
+++ b/eventmesh-dashboard-console/src/test/java/org/apache/eventmesh/dashboard/console/function/report/collect/RocketMQCollectWithClientManualTest.java
@@ -0,0 +1,229 @@
+/*
+ * 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.eventmesh.dashboard.console.function.report.collect;
+
+import org.apache.eventmesh.dashboard.common.enums.ClusterType;
+import org.apache.eventmesh.dashboard.common.model.metadata.ClusterMetadata;
+import org.apache.eventmesh.dashboard.common.model.metadata.RuntimeMetadata;
+import org.apache.eventmesh.dashboard.console.function.report.collect.DataSyncHandler.DataSyncHandlerWrapper;
+import org.apache.eventmesh.dashboard.console.function.report.collect.exporter.RocketMQCollect;
+import org.apache.eventmesh.dashboard.console.function.report.model.base.OrganizationId;
+import org.apache.eventmesh.dashboard.core.function.SDK.ConfigManage;
+import org.apache.eventmesh.dashboard.core.function.SDK.SDKManage;
+import org.apache.eventmesh.dashboard.core.function.SDK.SDKTypeEnum;
+import org.apache.eventmesh.dashboard.core.function.SDK.config.AbstractSimpleCreateSDKConfig;
+import org.apache.eventmesh.dashboard.core.function.SDK.config.NetAddress;
+import org.apache.eventmesh.dashboard.core.function.SDK.operation.rocketmq.RocketMQRemotingSDKOperation.DefaultRemotingClient;
+
+import org.apache.commons.lang3.reflect.FieldUtils;
+import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
+import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
+import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
+import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
+import org.apache.rocketmq.client.producer.DefaultMQProducer;
+import org.apache.rocketmq.client.producer.SendResult;
+import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
+import org.apache.rocketmq.common.message.Message;
+import org.apache.rocketmq.common.message.MessageExt;
+import org.apache.rocketmq.remoting.protocol.RemotingCommand;
+import org.apache.rocketmq.remoting.protocol.RequestCode;
+import org.apache.rocketmq.remoting.protocol.ResponseCode;
+
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import com.alibaba.fastjson.JSON;
+
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * Manual test that extends {@link RocketMQCollectManualTest} by adding producer and consumer activity
+ * before collection, so that consumer-side metrics (connection count, consume rate, offset lag, etc.)
+ * can be collected.
+ *
+ * <p>Prerequisites: start RocketMQ via {@code docker compose up -d} with broker listening on
+ * {@code 127.0.0.1:20911} and NameServer on {@code 127.0.0.1:9876}. No VM options required.
+ *
+ * <p>Note: the consumer may not consume messages when running against a Docker-based broker due to
+ * network addressing, but this does not affect collection — the consumer registers with the Broker,
+ * so connection count, offset lag, and other consumer-side metrics are still available for collection.
+ */
+@Slf4j
+public class RocketMQCollectWithClientManualTest {
+
+ static {
+ System.setProperty("rocketmq.remoting.vip.channel.enabled", "false");
+ }
+
+ private static final String HOST = "127.0.0.1";
+ private static final int BROKER_PORT = 20911;
+ private static final String NAMESERVER = HOST + ":9876";
+ private static final String TEST_TOPIC = "CollectClientTestTopic";
+ private static final String PRODUCER_GROUP = "CollectClientTestProducerGroup";
+ private static final String CONSUMER_GROUP = "CollectClientTestGroup";
+
+ @Test
+ @DisplayName("带生产者消费者的采集测试")
+ @SuppressWarnings("unchecked")
+ public void collectWithClientActivityAndPrint() throws Exception {
+
+ DefaultMQProducer producer = new DefaultMQProducer(PRODUCER_GROUP);
+ producer.setNamesrvAddr(NAMESERVER);
+ producer.setSendMsgTimeout(10000);
+ producer.setRetryTimesWhenSendFailed(3);
+ producer.start();
+ log.info("生产者已启动, namesrv={}", NAMESERVER);
+
+ boolean initSent = false;
+ for (int retry = 0; retry < 10; retry++) {
+ try {
+ SendResult result = producer.send(new Message(TEST_TOPIC, "TagA",
+ "InitMessage".getBytes(StandardCharsets.UTF_8)));
+ log.info("初始化消息发送成功: msgId={}, status={}", result.getMsgId(), result.getSendStatus());
+ initSent = true;
+ break;
+ } catch (Exception e) {
+ log.warn("发送初始化消息失败(第{}次): {}", retry + 1, e.getMessage());
+ Thread.sleep(3000);
+ }
+ }
+ Assertions.assertTrue(initSent, "无法发送初始化消息, 请检查 RocketMQ 是否正常启动");
+ log.info("Topic={} 已创建", TEST_TOPIC);
+
+ Thread.sleep(3000);
+
+ AtomicInteger consumed = new AtomicInteger();
+ CountDownLatch consumeLatch = new CountDownLatch(10);
+
+ DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(CONSUMER_GROUP);
+ consumer.setNamesrvAddr(NAMESERVER);
+ consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
+ consumer.subscribe(TEST_TOPIC, "*");
+ consumer.registerMessageListener((MessageListenerConcurrently) (List<MessageExt> msgs, ConsumeConcurrentlyContext context) -> {
+ for (MessageExt msg : msgs) {
+ int count = consumed.incrementAndGet();
+ consumeLatch.countDown();
+ log.info("消费消息 #{}: msgId={}", count, msg.getMsgId());
+ }
+ return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
+ });
+ consumer.start();
+ log.info("消费者已启动, group={}", CONSUMER_GROUP);
+ Thread.sleep(5000);
+ log.info("消费者已就绪");
+
+ log.info("开始发送 10 条消息...");
+ for (int i = 0; i < 10; i++) {
+ SendResult result = producer.send(new Message(TEST_TOPIC, "TagA",
+ ("TestMessage_" + i).getBytes(StandardCharsets.UTF_8)));
+ log.info("发送 #{}: msgId={}, status={}", i, result.getMsgId(), result.getSendStatus());
+ }
+ log.info("已发送 10 条消息到 Topic={}", TEST_TOPIC);
+
+ consumeLatch.await(15, TimeUnit.SECONDS);
+ log.info("消费结果: 已消费={} 条", consumed.get());
+
+ Thread.sleep(3000);
+
+ RuntimeMetadata runtime = new RuntimeMetadata();
+ runtime.setId(System.nanoTime());
+ runtime.setClusterId(1L);
+ runtime.setClusterType(ClusterType.STORAGE_ROCKETMQ_BROKER_MAIN_SLAVE);
+ ClusterMetadata cluster = new ClusterMetadata();
+ cluster.setId(runtime.getClusterId());
+ cluster.setOrganizationId(7L);
+ cluster.setClusterType(runtime.getClusterType());
+
+ AtomicInteger printed = new AtomicInteger();
+ Map<String, AtomicInteger> metricCounts = new ConcurrentHashMap<>();
+
+ RocketMQCollect exporter = new RocketMQCollect() {
+ @Override
+ protected void setData(OrganizationId data) {
+ super.setData(data);
+ String name = data.getClass().getSimpleName();
+ metricCounts.computeIfAbsent(name, k -> new AtomicInteger()).incrementAndGet();
+ log.info("采集指标={} 数据={}", name, JSON.toJSONString(data));
+ printed.incrementAndGet();
+ }
+ };
+ exporter.setRuntimeMetadata(runtime);
+ exporter.setClusterMetadata(cluster);
+
+ AbstractSimpleCreateSDKConfig config = ConfigManage.getInstance()
+ .getSimpleCreateSdkConfig(runtime.getClusterType(), SDKTypeEnum.ADMIN);
+ NetAddress address = new NetAddress();
+ address.setAddress(HOST);
+ address.setPort(BROKER_PORT);
+ config.setNetAddress(address);
+ DefaultRemotingClient client = SDKManage.getInstance()
+ .createClient(SDKTypeEnum.ADMIN, runtime, config, runtime.getClusterType());
+
+ CollectManage collectManage = new CollectManage();
+ Map<String, Collect> collectors = (Map<String, Collect>) FieldUtils.readField(collectManage, "executeingCollectMap", true);
+ collectors.put(runtime.getUnique(), exporter);
+
+ CountDownLatch completed = new CountDownLatch(1);
+ DataSyncHandler printHandler = Mockito.mock(DataSyncHandler.class);
+ DataSyncHandlerWrapper wrapper = Mockito.mock(DataSyncHandlerWrapper.class);
+ Mockito.when(printHandler.getDataSyncHandlerWrapper(1)).thenReturn(wrapper);
+ Mockito.doAnswer(call -> {
+ completed.countDown();
+ return null;
+ }).when(wrapper).sync(Mockito.any(RestoreData.class));
+ FieldUtils.writeField(collectManage, "dataSyncHandler", printHandler, true);
+
+ ExecutorService workers = (ExecutorService) FieldUtils.readField(collectManage, "threadPoolExecutor", true);
+ try {
+ RemotingCommand response = client.invokeSync(
+ RemotingCommand.createRequestCommand(RequestCode.GET_BROKER_RUNTIME_INFO, null), 3000);
+ Assertions.assertNotNull(response, "Broker unavailable at " + address);
+ Assertions.assertEquals(ResponseCode.SUCCESS, response.getCode(), "Broker unavailable at " + address);
+
+ collectManage.collect();
+ Assertions.assertTrue(completed.await(15, TimeUnit.SECONDS), "采集未在 15 秒内完成");
+ Assertions.assertTrue(printed.get() > 0, "采集结果为空");
+ log.info("采集完成, 共采集到 {} 条指标数据", printed.get());
+
+ log.info("====== 指标采集汇总 ======");
+ metricCounts.entrySet().stream()
+ .sorted(Map.Entry.comparingByKey())
+ .forEach(e -> log.info(" {} : {} 条", e.getKey(), e.getValue().get()));
+ log.info("==========================");
+
+ } finally {
+ workers.shutdownNow();
+ client.shutdown();
+ SDKManage.getInstance().deleteClient(null, runtime.getUnique());
+ consumer.shutdown();
+ producer.shutdown();
+ log.info("生产者、消费者已关闭");
+ }
+ }
+}
diff --git a/eventmesh-dashboard-console/src/test/java/org/apache/eventmesh/dashboard/console/function/report/collect/exporter/RocketMQCollectTest.java b/eventmesh-dashboard-console/src/test/java/org/apache/eventmesh/dashboard/console/function/report/collect/exporter/RocketMQCollectTest.java
new file mode 100644
index 00000000..8338436f
--- /dev/null
+++ b/eventmesh-dashboard-console/src/test/java/org/apache/eventmesh/dashboard/console/function/report/collect/exporter/RocketMQCollectTest.java
@@ -0,0 +1,1891 @@
+/*
+ * 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.eventmesh.dashboard.console.function.report.collect.exporter;
+
+import org.apache.eventmesh.dashboard.common.enums.ClusterType;
+import org.apache.eventmesh.dashboard.common.model.metadata.RuntimeMetadata;
+import org.apache.eventmesh.dashboard.console.function.report.ReportViewType;
+import org.apache.eventmesh.dashboard.console.function.report.annotation.ReportMeta;
+import org.apache.eventmesh.dashboard.console.function.report.annotation.ReportMetaData;
+import org.apache.eventmesh.dashboard.console.function.report.annotation.ReportTag;
+import org.apache.eventmesh.dashboard.console.function.report.iotdb.IotDBReportMetaHandler;
+import org.apache.eventmesh.dashboard.console.function.report.model.base.RuntimeId.RuntimeFloatValue;
+import org.apache.eventmesh.dashboard.console.function.report.model.base.RuntimeId.RuntimeLongValue;
+import org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.RocketmqBrokerMessagesIn;
+import org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.RocketmqBrokerMessagesOut;
+import org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.RocketmqConsumerConnectionNumber;
+import org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.RocketmqConsumerFailedTps;
+import org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.RocketmqConsumerGroupNumber;
+import org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.RocketmqConsumerLagLatency;
+import org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.RocketmqConsumerOffset;
+import org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.RocketmqConsumerProcessTime;
+import org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.RocketmqConsumerSuccessTps;
+import org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.RocketmqGroupMessagesOut;
+import org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.RocketmqMessagesInTotal;
+import org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.RocketmqMessagesOutTotal;
+import org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.RocketmqStorageDiskFreeBytes;
+import org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.RocketmqStorageDiskUsage;
+import org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.RocketmqStorageDispatchBehindBytes;
+import org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.RocketmqStorageFlushBehindBytes;
+import org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.RocketmqStorageMessageReserveTime;
+import org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.RocketmqThroughputInTotal;
+import org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.RocketmqThreadPoolWartermark;
+import org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.RocketmqTopicMessagesIn;
+import org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.RocketmqTopicNumber;
+import org.apache.eventmesh.dashboard.console.mapstruct.report.RocketMQCollectMapper;
+import org.apache.eventmesh.dashboard.core.function.SDK.ConfigManage;
+import org.apache.eventmesh.dashboard.core.function.SDK.SDKManage;
+import org.apache.eventmesh.dashboard.core.function.SDK.SDKTypeEnum;
+import org.apache.eventmesh.dashboard.core.function.SDK.config.AbstractSimpleCreateSDKConfig;
+import org.apache.eventmesh.dashboard.core.function.SDK.config.NetAddress;
+import org.apache.eventmesh.dashboard.core.function.SDK.operation.rocketmq.RocketMQRemotingSDKOperation.DefaultRemotingClient;
+
+import org.apache.rocketmq.remoting.InvokeCallback;
+import org.apache.rocketmq.remoting.netty.ResponseFuture;
+import org.apache.rocketmq.remoting.protocol.RemotingCommand;
+import org.apache.rocketmq.remoting.protocol.RemotingSerializable;
+import org.apache.rocketmq.remoting.protocol.RequestCode;
+import org.apache.rocketmq.remoting.protocol.ResponseCode;
+import org.apache.rocketmq.remoting.protocol.body.KVTable;
+import org.apache.rocketmq.remoting.protocol.header.CreateTopicRequestHeader;
+import org.apache.rocketmq.remoting.protocol.header.DeleteSubscriptionGroupRequestHeader;
+import org.apache.rocketmq.remoting.protocol.header.DeleteTopicRequestHeader;
+import org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeader;
+import org.apache.rocketmq.remoting.protocol.header.UpdateConsumerOffsetRequestHeader;
+import org.apache.rocketmq.remoting.protocol.header.ViewBrokerStatsDataRequestHeader;
+import org.apache.rocketmq.remoting.protocol.heartbeat.HeartbeatData;
+import org.apache.rocketmq.remoting.protocol.heartbeat.ProducerData;
+import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig;
+
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.slf4j.LoggerFactory;
+
+
+public class RocketMQCollectTest {
+
+ /** 验证实际缓存容器接收并发样本,保留各模型的全部记录。 */
+ @Test
+ @DisplayName("线程安全缓存完整保存并发样本")
+ public void concurrentCacheWritesKeepAllSamples() throws Exception {
+ var cache = new org.apache.eventmesh.dashboard.console.function.report.collect.RestoreData();
+ int threadCount = 8;
+ int samplesPerThread = 5000;
+ var executor = java.util.concurrent.Executors.newFixedThreadPool(threadCount);
+ var start = new java.util.concurrent.CountDownLatch(1);
+ try {
+ List<java.util.concurrent.Future<?>> jobs = new java.util.ArrayList<>();
+ for (int thread = 0; thread < threadCount; thread++) {
+ final int base = thread * samplesPerThread;
+ jobs.add(executor.submit(() -> {
+ try {
+ Assertions.assertTrue(start.await(2, java.util.concurrent.TimeUnit.SECONDS));
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new AssertionError(e);
+ }
+ for (int sample = 0; sample < samplesPerThread; sample++) {
+ long value = base + sample;
+ cache.setData(value % 2 == 0 ? RocketMQCollectMapper.INSTANCE.topicNumber(value)
+ : RocketMQCollectMapper.INSTANCE.groupNumber(value));
+ }
+ }));
+ }
+ start.countDown();
+ for (var job : jobs) {
+ job.get(5, java.util.concurrent.TimeUnit.SECONDS);
+ }
+ } finally {
+ start.countDown();
+ executor.shutdownNow();
+ }
+ int expected = threadCount * samplesPerThread;
+ Assertions.assertEquals(Set.of(RocketmqTopicNumber.class, RocketmqConsumerGroupNumber.class), cache.getDataMap().keySet());
+ Set<Long> values = new java.util.HashSet<>();
+ cache.getDataMap().forEach((model, samples) -> {
+ Assertions.assertEquals(expected / 2, samples.size(), model.getSimpleName());
+ for (Object sample : samples) {
+ RuntimeLongValue row = Assertions.assertInstanceOf(RuntimeLongValue.class, sample);
+ Assertions.assertEquals(model == RocketmqTopicNumber.class ? 0 : 1, row.getValue() % 2);
+ Assertions.assertTrue(values.add(row.getValue()), "Sample values must remain unique");
+ }
+ });
+ Assertions.assertEquals(expected, values.size());
+ LoggerFactory.getLogger(RocketMQCollectTest.class).info("实际缓存并发验证通过:线程数={},样本数={},全部样本完整且唯一",
+ threadCount, values.size());
+ }
+
+ /** 验证消费位点、积压量和连接数正确映射到独立模型,并保留大整数精度。 */
+ @Test
+ @DisplayName("消费位点与连接数映射")
+ public void mapsConsumerSamplesToDedicatedModels() {
+ var offset = new org.apache.rocketmq.remoting.protocol.admin.OffsetWrapper();
+ offset.setBrokerOffset(9007199254740993L);
+ offset.setConsumerOffset(9007199254740991L);
+ RocketmqConsumerOffset row = RocketMQCollectMapper.INSTANCE.consumerOffset("orders", "buyers", "3", offset);
+ Assertions.assertEquals("orders", row.getTopicName());
+ Assertions.assertEquals("buyers", row.getGroupName());
+ Assertions.assertEquals("3", row.getQueueId());
+ Assertions.assertNull(org.apache.commons.lang3.reflect.FieldUtils.getField(RocketmqConsumerOffset.class, "topicId", true));
+ Assertions.assertNull(org.apache.commons.lang3.reflect.FieldUtils.getField(RocketmqConsumerOffset.class, "groupId", true));
+ Assertions.assertEquals(9007199254740993L, row.getValueBrokerOffset());
+ Assertions.assertEquals(9007199254740991L, row.getValueConsumerOffset());
+ Assertions.assertEquals(2L, row.getValueOffsetLag());
+ offset.setConsumerOffset(9007199254740994L);
+ Assertions.assertEquals(0L,
+ RocketMQCollectMapper.INSTANCE.consumerOffset("orders", "buyers", "3", offset).getValueOffsetLag());
+ RocketmqConsumerConnectionNumber connections = RocketMQCollectMapper.INSTANCE.connections("buyers", 2L);
+ Assertions.assertEquals("buyers", connections.getGroupName());
+ Assertions.assertEquals(2L, connections.getValueConnectionCount());
+ Assertions.assertEquals(0L, RocketMQCollectMapper.INSTANCE.connections("buyers", 0L).getValueConnectionCount());
+ }
+
+ /** 验证生产位点、消费位点和连接数使用各自的 GAUGE 表及字段。 */
+ @Test
+ @DisplayName("生产位点、消费位点和连接数表结构")
+ public void dedicatedModelsDescribeSeparateGaugeTables() {
+ assertReportTable(org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.Rocketmq2ProducerOffset.class,
+ "rocketmq_producer_offset", List.of("topic_name", "queue_id"),
+ List.of("value", "value_min_offset", "value_last_update_time", "value_min_offset_sum", "value_max_offset_sum"));
+ assertReportTable(RocketmqConsumerOffset.class, "rocketmq_consumer_offset",
+ List.of("topic_name", "group_name", "queue_id"),
+ List.of("value_consumer_offset", "value_broker_offset", "value_offset_lag"));
+ assertReportTable(RocketmqConsumerConnectionNumber.class, "rocketmq_consumer_connection_number",
+ List.of("group_name"), List.of("value_connection_count"));
+ }
+
+ /** 模拟 Broker 运行状态响应,验证三项启动以来累计值和实例 ID。 */
+ @Test
+ @DisplayName("Broker 启动以来累计值采集")
+ public void collectsBrokerLifetimeCountersFromRuntimeResponse() throws Exception {
+ Map<Class<?>, List<Object>> models = collectRuntime(Map.of(
+ "msgPutTotalTodayNow", "12345",
+ "putMessageSizeTotal", "67890",
+ "msgGetTotalTodayNow", "23456"));
+
+ RocketmqMessagesInTotal messagesIn = rows(models, RocketmqMessagesInTotal.class).get(0);
+ Assertions.assertEquals(12345L, messagesIn.getValue());
+ Assertions.assertNotNull(messagesIn.getRuntimeId());
+ RocketmqThroughputInTotal bytesIn = rows(models, RocketmqThroughputInTotal.class).get(0);
+ Assertions.assertEquals(67890L, bytesIn.getValue());
+ Assertions.assertEquals(messagesIn.getRuntimeId(), bytesIn.getRuntimeId());
+ RocketmqMessagesOutTotal messagesOut = rows(models, RocketmqMessagesOutTotal.class).get(0);
+ Assertions.assertEquals(23456L, messagesOut.getValue());
+ Assertions.assertEquals(messagesIn.getRuntimeId(), messagesOut.getRuntimeId());
+ }
+
+ /** 验证合法零值会保留,缺失或非法的累计值不会生成指标。 */
+ @Test
+ @DisplayName("累计值的零值、缺失值与非法值")
+ public void keepsZeroCountersAndSkipsMissingOrInvalidRuntimeValues() throws Exception {
+ Map<Class<?>, List<Object>> zeros = collectRuntime(Map.of(
+ "msgPutTotalTodayNow", "0",
+ "putMessageSizeTotal", "0",
+ "msgGetTotalTodayNow", "0"));
+ Assertions.assertEquals(0L, rows(zeros, RocketmqMessagesInTotal.class).get(0).getValue());
+ Assertions.assertEquals(0L, rows(zeros, RocketmqThroughputInTotal.class).get(0).getValue());
+ Assertions.assertEquals(0L, rows(zeros, RocketmqMessagesOutTotal.class).get(0).getValue());
+
+ Map<Class<?>, List<Object>> invalid = collectRuntime(Map.of(
+ "msgPutTotalTodayNow", "-1",
+ "putMessageSizeTotal", "NaN",
+ "msgGetTotalTodayNow", "not-a-number"));
+ Assertions.assertFalse(invalid.containsKey(RocketmqMessagesInTotal.class));
+ Assertions.assertFalse(invalid.containsKey(RocketmqThroughputInTotal.class));
+ Assertions.assertFalse(invalid.containsKey(RocketmqMessagesOutTotal.class));
+
+ Map<Class<?>, List<Object>> missing = collectRuntime(Map.of());
+ Assertions.assertFalse(missing.containsKey(RocketmqMessagesInTotal.class));
+ Assertions.assertFalse(missing.containsKey(RocketmqThroughputInTotal.class));
+ Assertions.assertFalse(missing.containsKey(RocketmqMessagesOutTotal.class));
+ }
+
+ /** 验证三项累计指标是按实例区分的 COUNTER,数值字段为 int64。 */
+ @Test
+ @DisplayName("累计指标的 COUNTER 表结构")
+ public void lifetimeCountersHaveRuntimeScopedCounterSchemas() {
+ for (Class<?> model : List.of(RocketmqMessagesInTotal.class, RocketmqThroughputInTotal.class,
+ RocketmqMessagesOutTotal.class)) {
+ ReportMeta annotation = model.getAnnotation(ReportMeta.class);
+ Assertions.assertNotNull(annotation);
+ Assertions.assertEquals(ReportViewType.COUNTER, annotation.defaultViewType());
+ var metadata = new ReportMetaData();
+ metadata.setClazz(model);
+ metadata.setTableName(annotation.tableName());
+ metadata.setComment(annotation.comment());
+ var handler = new IotDBReportMetaHandler();
+ handler.setReportMeta(metadata);
+ handler.setFieldList(org.apache.commons.lang3.reflect.FieldUtils.getAllFieldsList(model));
+ String ddl = handler.createTable();
+ Assertions.assertTrue(ddl.contains("runtime_id string tag"), ddl);
+ Assertions.assertTrue(ddl.contains("value int64 field"), ddl);
+ Assertions.assertFalse(ddl.contains("topic_name"), ddl);
+ Assertions.assertFalse(ddl.contains("group_name"), ddl);
+ }
+ }
+
+ private void assertReportTable(Class<?> model, String table, List<String> tags, List<String> measurements) {
+ assertReportTable(model, table, tags, measurements, List.of());
+ }
+
+ private void assertReportTable(Class<?> model, String table, List<String> tags, List<String> measurements, List<String> floats) {
+ ReportMeta annotation = model.getAnnotation(ReportMeta.class);
+ Assertions.assertNotNull(annotation);
+ Assertions.assertEquals(table, annotation.tableName());
+ Assertions.assertEquals(table, annotation.reportName());
+ Assertions.assertEquals(ReportViewType.GAUGE, annotation.defaultViewType());
+ var metadata = new ReportMetaData();
+ metadata.setClazz(model);
+ metadata.setTableName(annotation.tableName());
+ metadata.setComment(annotation.comment());
+ var handler = new IotDBReportMetaHandler();
+ handler.setReportMeta(metadata);
+ handler.setFieldList(org.apache.commons.lang3.reflect.FieldUtils.getAllFieldsList(model));
+ String ddl = handler.createTable();
+ Assertions.assertTrue(ddl.startsWith("create table if not exists " + table + " "));
+ tags.forEach(tag -> Assertions.assertTrue(ddl.contains(tag + " string tag"), ddl));
+ measurements.forEach(field -> Assertions.assertTrue(ddl.contains(field + " int64 field"), ddl));
+ floats.forEach(field -> Assertions.assertTrue(ddl.contains(field + " float field"), ddl));
+ Assertions.assertTrue(ddl.contains("runtime_id string tag"), ddl);
+ }
+
+ /** 模拟消费响应,验证消费位点和连接数以独立模型交给父采集器。 */
+ @Test
+ @DisplayName("消费指标按独立模型交付")
+ public void handsConsumerSamplesToParentUnderDedicatedClasses() throws Exception {
+ DefaultRemotingClient client = Mockito.mock(DefaultRemotingClient.class);
+ SDKManage sdk = Mockito.mock(SDKManage.class);
+ RuntimeMetadata runtime = runtime();
+ Mockito.when(sdk.getClient(SDKTypeEnum.ADMIN, runtime.getUnique())).thenReturn(client);
+ AtomicInteger connectionRequests = new AtomicInteger();
+ Mockito.doAnswer(invocation -> {
+ RemotingCommand request = invocation.getArgument(0);
+ InvokeCallback callback = invocation.getArgument(2);
+ if (failBrokerRoot(request, callback)) {
+ return null;
+ }
+ byte[] body;
+ switch (request.getCode()) {
+ case RequestCode.GET_ALL_TOPIC_CONFIG:
+ body = "{\"topicConfigTable\":{\"orders\":{},\"payments\":{}}}".getBytes(StandardCharsets.UTF_8);
+ break;
+ case RequestCode.GET_TOPIC_STATS_INFO:
+ body = RemotingSerializable.encode(new org.apache.rocketmq.remoting.protocol.admin.TopicStatsTable());
+ break;
+ case RequestCode.QUERY_TOPIC_CONSUME_BY_WHO:
+ body = "{\"groupList\":[\"buyers\"]}".getBytes(StandardCharsets.UTF_8);
+ break;
+ case RequestCode.GET_CONSUMER_CONNECTION_LIST:
+ connectionRequests.incrementAndGet();
+ body = "{\"connectionSet\":[]}".getBytes(StandardCharsets.UTF_8);
+ break;
+ case RequestCode.GET_CONSUME_STATS:
+ var header = (org.apache.rocketmq.remoting.protocol.header.GetConsumeStatsRequestHeader) request.readCustomHeader();
+ var stats = new org.apache.rocketmq.remoting.protocol.admin.ConsumeStats();
+ var offset = new org.apache.rocketmq.remoting.protocol.admin.OffsetWrapper();
+ offset.setBrokerOffset(3L);
+ offset.setConsumerOffset(2L);
+ stats.getOffsetTable().put(new org.apache.rocketmq.common.message.MessageQueue(header.getTopic(), "broker", 0), offset);
+ body = RemotingSerializable.encode(stats);
+ break;
+ default:
+ throw new AssertionError("Unexpected request " + request.getCode());
+ }
+ reply(callback, ResponseCode.SUCCESS, body);
+ return null;
+ }).when(client).invokeAsync(Mockito.any(), Mockito.anyLong(), Mockito.any());
+ var wrapper = Mockito.mock(org.apache.eventmesh.dashboard.console.function.report.collect.DataSyncHandler.DataSyncHandlerWrapper.class);
+ try (var mocked = Mockito.mockStatic(SDKManage.class)) {
+ mocked.when(SDKManage::getInstance).thenReturn(sdk);
+ collector(runtime).collect(0, wrapper);
+ }
+ var capture = org.mockito.ArgumentCaptor.forClass(org.apache.eventmesh.dashboard.console.function.report.collect.RestoreData.class);
+ Mockito.verify(wrapper).sync(capture.capture());
+ var models = capture.getValue().getDataMap();
+ Assertions.assertEquals(Set.of(RocketmqConsumerOffset.class, RocketmqConsumerConnectionNumber.class,
+ org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.Rocketmq2ProducerOffset.class, RocketmqTopicNumber.class), models.keySet());
+ Assertions.assertEquals(2, models.get(RocketmqConsumerOffset.class).size());
+ models.get(RocketmqConsumerOffset.class).stream().map(RocketmqConsumerOffset.class::cast).forEach(row -> {
+ Assertions.assertEquals("buyers", row.getGroupName());
+ Assertions.assertEquals(2L, row.getValueConsumerOffset());
+ Assertions.assertEquals(3L, row.getValueBrokerOffset());
+ Assertions.assertEquals(1L, row.getValueOffsetLag());
+ Assertions.assertEquals(runtime.getId(), row.getRuntimeId());
+ Assertions.assertEquals(7L, row.getOrganizationId());
+ });
+ Assertions.assertEquals(1, connectionRequests.get());
+ Assertions.assertEquals(1, models.get(RocketmqConsumerConnectionNumber.class).size());
+ }
+
+ /** 验证 Topic 的嵌套异步请求结束后,父采集器才交付本轮结果。 */
+ @Test
+ @DisplayName("等待嵌套请求完成再交付")
+ public void waitsForNestedRequestsThenHandsModelsToParent() throws Exception {
+ DefaultRemotingClient client = Mockito.mock(DefaultRemotingClient.class);
+ SDKManage sdk = Mockito.mock(SDKManage.class);
+ RuntimeMetadata runtime = runtime();
+ RocketMQCollect collector = collector(runtime);
+ Mockito.when(sdk.getClient(SDKTypeEnum.ADMIN, runtime.getUnique())).thenReturn(client);
+ var calls = new java.util.concurrent.LinkedBlockingQueue<Call>();
+ var started = new java.util.concurrent.CountDownLatch(1);
+ Mockito.doAnswer(invocation -> {
+ if (failBrokerRoot(invocation.getArgument(0), invocation.getArgument(2))) {
+ return null;
+ }
+ calls.add(new Call(invocation.getArgument(0), invocation.getArgument(2)));
+ started.countDown();
+ return null;
+ }).when(client).invokeAsync(Mockito.any(), Mockito.anyLong(), Mockito.any());
+ var wrapper = Mockito.mock(org.apache.eventmesh.dashboard.console.function.report.collect.DataSyncHandler.DataSyncHandlerWrapper.class);
+ var executor = java.util.concurrent.Executors.newSingleThreadExecutor();
+ try {
+ var finished = executor.submit(() -> {
+ try (var mocked = Mockito.mockStatic(SDKManage.class)) {
+ mocked.when(SDKManage::getInstance).thenReturn(sdk);
+ collector.collect(0, wrapper);
+ }
+ });
+ Assertions.assertTrue(started.await(2, java.util.concurrent.TimeUnit.SECONDS));
+ Call root = calls.poll(2, java.util.concurrent.TimeUnit.SECONDS);
+ Assertions.assertNotNull(root);
+ Assertions.assertEquals(RequestCode.GET_ALL_TOPIC_CONFIG, root.request().getCode());
+ reply(root.callback(), ResponseCode.SUCCESS,
+ "{\"topicConfigTable\":{\"test-topic\":{\"topicName\":\"test-topic\"}}}".getBytes(StandardCharsets.UTF_8));
+ Call offset = null;
+ for (int i = 0; i < 2; i++) {
+ Call child = calls.poll(2, java.util.concurrent.TimeUnit.SECONDS);
+ Assertions.assertNotNull(child);
+ if (child.request().getCode() == RequestCode.GET_TOPIC_STATS_INFO) {
+ offset = child;
+ } else {
+ reply(child.callback(), ResponseCode.SYSTEM_ERROR, null);
+ child.callback().operationFail(new IllegalStateException("duplicate completion"));
+ }
+ }
+ Assertions.assertNotNull(offset);
+ Assertions.assertFalse(finished.isDone(), "Nested Topic request must still be awaited");
+ Mockito.verifyNoInteractions(wrapper);
+ var stats = new org.apache.rocketmq.remoting.protocol.admin.TopicStatsTable();
+ var position = new org.apache.rocketmq.remoting.protocol.admin.TopicOffset();
+ position.setMinOffset(1L);
+ position.setMaxOffset(9007199254740993L);
+ position.setLastUpdateTimestamp(123L);
+ stats.getOffsetTable().put(new org.apache.rocketmq.common.message.MessageQueue("test-topic", "broker", 0), position);
+ reply(offset.callback(), ResponseCode.SUCCESS, RemotingSerializable.encode(stats));
+ finished.get(2, java.util.concurrent.TimeUnit.SECONDS);
+ var capture = org.mockito.ArgumentCaptor.forClass(org.apache.eventmesh.dashboard.console.function.report.collect.RestoreData.class);
+ Mockito.verify(wrapper).sync(capture.capture());
+ var rows = capture.getValue().getDataMap().get(
+ org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.Rocketmq2ProducerOffset.class);
+ var queue = rows.stream().map(row ->
+ (org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.Rocketmq2ProducerOffset) row)
+ .filter(row -> "0".equals(row.getQueueId())).findFirst().orElseThrow();
+ Assertions.assertEquals(9007199254740993L, queue.getValue());
+ Assertions.assertEquals(1L, queue.getValueMinOffset());
+ Assertions.assertEquals(123L, queue.getValueLastUpdateTime());
+ Assertions.assertEquals(runtime.getId(), queue.getRuntimeId());
+ Assertions.assertEquals(7L, queue.getOrganizationId());
+ } finally {
+ executor.shutdownNow();
+ }
+ }
+
+ /** 验证采集超时后到达的回调不会继续写入本轮结果。 */
+ @Test
+ @DisplayName("超时后的回调不写入结果")
+ public void timeoutDoesNotLetLateCallbacksWriteIntoParent() throws Exception {
+ DefaultRemotingClient client = Mockito.mock(DefaultRemotingClient.class);
+ SDKManage sdk = Mockito.mock(SDKManage.class);
+ RuntimeMetadata runtime = runtime();
+ RocketMQCollect collector = collector(runtime);
+ Mockito.when(sdk.getClient(SDKTypeEnum.ADMIN, runtime.getUnique())).thenReturn(client);
+ List<InvokeCallback> callbacks = new CopyOnWriteArrayList<>();
+ Mockito.doAnswer(call -> {
+ if (failBrokerRoot(call.getArgument(0), call.getArgument(2))) {
+ return null;
+ }
+ callbacks.add(call.getArgument(2));
+ return null;
+ }).when(client).invokeAsync(Mockito.any(), Mockito.anyLong(), Mockito.any());
+ var wrapper = Mockito.mock(org.apache.eventmesh.dashboard.console.function.report.collect.DataSyncHandler.DataSyncHandlerWrapper.class);
+ try (var mocked = Mockito.mockStatic(SDKManage.class)) {
+ mocked.when(SDKManage::getInstance).thenReturn(sdk);
+ Assertions.assertTimeout(java.time.Duration.ofSeconds(6), () -> collector.collect(0, wrapper));
+ var capture = org.mockito.ArgumentCaptor.forClass(org.apache.eventmesh.dashboard.console.function.report.collect.RestoreData.class);
+ Mockito.verify(wrapper).sync(capture.capture());
+ Assertions.assertTrue(capture.getValue().getDataMap().isEmpty());
+ reply(callbacks.get(0), ResponseCode.SUCCESS, "{\"topicConfigTable\":{}}".getBytes(StandardCharsets.UTF_8));
+ Assertions.assertTrue(capture.getValue().getDataMap().isEmpty());
+ Mockito.verify(wrapper, Mockito.times(1)).sync(Mockito.any());
+ }
+ }
+
+ /** 验证已进入解析的旧回调在关闭后不调用父类写入,也不发送子请求。 */
+ @Test
+ @DisplayName("解析中的回调在关闭后放弃写入和子请求")
+ public void processingCallbackChecksClosedBeforeWritingAndSending() throws Exception {
+ DefaultRemotingClient client = Mockito.mock(DefaultRemotingClient.class);
+ SDKManage sdk = Mockito.mock(SDKManage.class);
+ RuntimeMetadata runtime = runtime();
+ AtomicInteger writes = new AtomicInteger();
+ RocketMQCollect collector = new RocketMQCollect() {
+ @Override
+ protected void setData(org.apache.eventmesh.dashboard.console.function.report.model.base.OrganizationId data) {
+ writes.incrementAndGet();
+ super.setData(data);
+ }
+ };
+ var cluster = new org.apache.eventmesh.dashboard.common.model.metadata.ClusterMetadata();
+ cluster.setId(runtime.getClusterId());
+ cluster.setOrganizationId(7L);
+ cluster.setClusterType(runtime.getClusterType());
+ collector.setRuntimeMetadata(runtime);
+ collector.setClusterMetadata(cluster);
+ Mockito.when(sdk.getClient(SDKTypeEnum.ADMIN, runtime.getUnique())).thenReturn(client);
+ var calls = new java.util.concurrent.LinkedBlockingQueue<Call>();
+ AtomicInteger sends = new AtomicInteger();
+ var rootsSent = new java.util.concurrent.CountDownLatch(8);
+ Mockito.doAnswer(invocation -> {
+ sends.incrementAndGet();
+ rootsSent.countDown();
+ if (!failBrokerRoot(invocation.getArgument(0), invocation.getArgument(2))) {
+ calls.add(new Call(invocation.getArgument(0), invocation.getArgument(2)));
+ }
+ return null;
+ }).when(client).invokeAsync(Mockito.any(), Mockito.anyLong(), Mockito.any());
+ var wrapper = Mockito.mock(org.apache.eventmesh.dashboard.console.function.report.collect.DataSyncHandler.DataSyncHandlerWrapper.class);
+ var parsing = new java.util.concurrent.CountDownLatch(1);
+ var resume = new java.util.concurrent.CountDownLatch(1);
+ var executor = java.util.concurrent.Executors.newFixedThreadPool(2);
+ Runnable collect = () -> {
+ try (var mocked = Mockito.mockStatic(SDKManage.class)) {
+ mocked.when(SDKManage::getInstance).thenReturn(sdk);
+ collector.collect(0, wrapper);
+ }
+ };
+ try {
+ var first = executor.submit(collect);
+ Call root = calls.poll(2, java.util.concurrent.TimeUnit.SECONDS);
+ Assertions.assertNotNull(root);
+ byte[] body = "{\"topicConfigTable\":{\"orders\":{}}}".getBytes(StandardCharsets.UTF_8);
+ RemotingCommand response = Mockito.mock(RemotingCommand.class);
+ Mockito.when(response.getCode()).thenReturn(ResponseCode.SUCCESS);
+ AtomicInteger reads = new AtomicInteger();
+ Mockito.when(response.getBody()).thenAnswer(invocation -> {
+ // 前两次读取属于公共校验,第三次读取已经进入 Topic 响应解析。
+ if (reads.incrementAndGet() == 3) {
+ parsing.countDown();
+ Assertions.assertTrue(resume.await(8, java.util.concurrent.TimeUnit.SECONDS));
+ }
+ return body;
+ });
+ ResponseFuture future = Mockito.mock(ResponseFuture.class);
+ Mockito.when(future.getResponseCommand()).thenReturn(response);
+ var oldCallback = executor.submit(() -> root.callback().operationComplete(future));
+ Assertions.assertTrue(parsing.await(2, java.util.concurrent.TimeUnit.SECONDS));
+ first.get(6, java.util.concurrent.TimeUnit.SECONDS);
+ var second = executor.submit(collect);
+ Call nextRoot = calls.poll(2, java.util.concurrent.TimeUnit.SECONDS);
+ Assertions.assertNotNull(nextRoot);
+ Assertions.assertTrue(rootsSent.await(2, java.util.concurrent.TimeUnit.SECONDS));
+ int sentBeforeResume = sends.get();
+ resume.countDown();
+ oldCallback.get(2, java.util.concurrent.TimeUnit.SECONDS);
+ Assertions.assertEquals(0, writes.get(), "Closed round must not invoke parent setData");
+ Assertions.assertEquals(sentBeforeResume, sends.get(), "Closed round must not send child requests");
+ var current = (org.apache.eventmesh.dashboard.console.function.report.collect.RestoreData)
+ org.apache.commons.lang3.reflect.FieldUtils.readField(collector, "current", true);
+ Assertions.assertTrue(current.getDataMap().isEmpty(), "Old samples must not enter the next round");
+ reply(nextRoot.callback(), ResponseCode.SUCCESS, "{\"topicConfigTable\":{}}".getBytes(StandardCharsets.UTF_8));
+ second.get(2, java.util.concurrent.TimeUnit.SECONDS);
+ var capture = org.mockito.ArgumentCaptor.forClass(org.apache.eventmesh.dashboard.console.function.report.collect.RestoreData.class);
+ Mockito.verify(wrapper, Mockito.times(2)).sync(capture.capture());
+ Assertions.assertTrue(capture.getAllValues().get(0).getDataMap().isEmpty());
+ Assertions.assertEquals(1, writes.get());
+ Assertions.assertEquals(0L, rows(capture.getAllValues().get(1).getDataMap(), RocketmqTopicNumber.class).get(0).getValue());
+ LoggerFactory.getLogger(RocketMQCollectTest.class).info("模拟回调验证通过:解析期间超时关闭,旧回调未调用父类写入或发送子请求,下一轮正常采集");
+ } finally {
+ resume.countDown();
+ executor.shutdownNow();
+ }
+ }
+
+ /** 验证当前轮回调直接交付数据,上一轮迟到回调不会污染下一轮。 */
+ @Test
+ @DisplayName("直接交付并隔离前一轮回调")
+ public void publishesDirectlyAndRejectsPreviousRoundCallbacks() throws Exception {
+ DefaultRemotingClient client = Mockito.mock(DefaultRemotingClient.class);
+ SDKManage sdk = Mockito.mock(SDKManage.class);
+ RuntimeMetadata runtime = runtime();
+ RocketMQCollect collector = collector(runtime);
+ Mockito.when(sdk.getClient(SDKTypeEnum.ADMIN, runtime.getUnique())).thenReturn(client);
+ var calls = new java.util.concurrent.LinkedBlockingQueue<Call>();
+ Mockito.doAnswer(invocation -> {
+ if (failBrokerRoot(invocation.getArgument(0), invocation.getArgument(2))) {
+ return null;
+ }
+ calls.add(new Call(invocation.getArgument(0), invocation.getArgument(2)));
+ return null;
+ }).when(client).invokeAsync(Mockito.any(), Mockito.anyLong(), Mockito.any());
+ var wrapper = Mockito.mock(org.apache.eventmesh.dashboard.console.function.report.collect.DataSyncHandler.DataSyncHandlerWrapper.class);
+ var executor = java.util.concurrent.Executors.newSingleThreadExecutor();
+ Runnable collect = () -> {
+ try (var mocked = Mockito.mockStatic(SDKManage.class)) {
+ mocked.when(SDKManage::getInstance).thenReturn(sdk);
+ collector.collect(0, wrapper);
+ }
+ };
+ try {
+ var first = executor.submit(collect);
+ Call root = calls.poll(2, java.util.concurrent.TimeUnit.SECONDS);
+ Assertions.assertNotNull(root);
+ reply(root.callback(), ResponseCode.SUCCESS, "{\"topicConfigTable\":{\"orders\":{}}}".getBytes(StandardCharsets.UTF_8));
+ Call stats = calls.poll(2, java.util.concurrent.TimeUnit.SECONDS);
+ Call groups = calls.poll(2, java.util.concurrent.TimeUnit.SECONDS);
+ Assertions.assertNotNull(stats);
+ Assertions.assertNotNull(groups);
+ Assertions.assertEquals(RequestCode.GET_TOPIC_STATS_INFO, stats.request().getCode());
+ reply(stats.callback(), ResponseCode.SUCCESS,
+ RemotingSerializable.encode(new org.apache.rocketmq.remoting.protocol.admin.TopicStatsTable()));
+ var current = (org.apache.eventmesh.dashboard.console.function.report.collect.RestoreData)
+ org.apache.commons.lang3.reflect.FieldUtils.readField(collector, "current", true);
+ Assertions.assertEquals(1, current.getDataMap().get(org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.Rocketmq2ProducerOffset.class).size(),
+ "Completed metrics must reach setData before the remaining callback finishes");
+ Assertions.assertFalse(first.isDone());
+ first.get(6, java.util.concurrent.TimeUnit.SECONDS);
+ Assertions.assertEquals(1, current.getDataMap().get(org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.Rocketmq2ProducerOffset.class).size(),
+ "Timeout retains samples already delivered through setData");
+ var second = executor.submit(collect);
+ Call nextRoot = calls.poll(2, java.util.concurrent.TimeUnit.SECONDS);
+ Assertions.assertNotNull(nextRoot);
+ reply(groups.callback(), ResponseCode.SUCCESS, "{\"groupList\":[\"late-group\"]}".getBytes(StandardCharsets.UTF_8));
+ Assertions.assertTrue(calls.isEmpty(), "Previous round must not dispatch requests into a new round");
+ reply(nextRoot.callback(), ResponseCode.SUCCESS, "{\"topicConfigTable\":{}}".getBytes(StandardCharsets.UTF_8));
+ second.get(2, java.util.concurrent.TimeUnit.SECONDS);
+ var capture = org.mockito.ArgumentCaptor.forClass(org.apache.eventmesh.dashboard.console.function.report.collect.RestoreData.class);
+ Mockito.verify(wrapper, Mockito.times(2)).sync(capture.capture());
+ Assertions.assertEquals(0L, rows(capture.getAllValues().get(1).getDataMap(), RocketmqTopicNumber.class).get(0).getValue());
+ } finally {
+ executor.shutdownNow();
+ }
+ }
+
+ /** Offset regressions deliberately keep their callback queues independent of the new Broker roots. */
+ private boolean failBrokerRoot(RemotingCommand request, InvokeCallback callback) {
+ if (request.getCode() == RequestCode.GET_BROKER_CONFIG || request.getCode() == RequestCode.GET_BROKER_RUNTIME_INFO
+ || request.getCode() == RequestCode.GET_ALL_SUBSCRIPTIONGROUP_CONFIG
+ || request.getCode() == RequestCode.VIEW_BROKER_STATS_DATA
+ || request.getCode() == RequestCode.QUERY_CONSUME_TIME_SPAN) {
+ callback.operationFail(new IllegalStateException("Broker roots disabled in offset-only regression"));
+ return true;
+ }
+ return false;
+ }
+
+ private void reply(InvokeCallback callback, int code, byte[] body) {
+ RemotingCommand response = RemotingCommand.createResponseCommand(code, "test response");
+ response.setBody(body);
+ ResponseFuture future = Mockito.mock(ResponseFuture.class);
+ Mockito.when(future.getResponseCommand()).thenReturn(response);
+ callback.operationComplete(future);
+ }
+
+ private record Call(RemotingCommand request, InvokeCallback callback) {
+ }
+
+ private RocketMQCollect collector(RuntimeMetadata runtime) {
+ var cluster = new org.apache.eventmesh.dashboard.common.model.metadata.ClusterMetadata();
+ cluster.setId(runtime.getClusterId());
+ cluster.setOrganizationId(7L);
+ cluster.setClusterType(runtime.getClusterType());
+ RocketMQCollect collector = new RocketMQCollect();
+ collector.setRuntimeMetadata(runtime);
+ collector.setClusterMetadata(cluster);
+ return collector;
+ }
+
+ /** 验证发送请求失败时,采集轮次能结束而不等待回调。 */
+ @Test
+ @DisplayName("请求发送失败时结束采集")
+ public void sendFailureFinishesRoundWithoutWaitingForCallback() throws Exception {
+ DefaultRemotingClient client = Mockito.mock(DefaultRemotingClient.class);
+ SDKManage sdk = Mockito.mock(SDKManage.class);
+ RuntimeMetadata runtime = runtime();
+ RocketMQCollect collector = collector(runtime);
+ Mockito.when(sdk.getClient(SDKTypeEnum.ADMIN, runtime.getUnique())).thenReturn(client);
+ Mockito.doThrow(new IllegalStateException("send failed"))
+ .when(client).invokeAsync(Mockito.any(), Mockito.anyLong(), Mockito.any());
+ var wrapper = Mockito.mock(org.apache.eventmesh.dashboard.console.function.report.collect.DataSyncHandler.DataSyncHandlerWrapper.class);
+ try (var mocked = Mockito.mockStatic(SDKManage.class)) {
+ mocked.when(SDKManage::getInstance).thenReturn(sdk);
+ Assertions.assertTimeout(java.time.Duration.ofSeconds(1), () -> collector.collect(0, wrapper));
+ var capture = org.mockito.ArgumentCaptor.forClass(org.apache.eventmesh.dashboard.console.function.report.collect.RestoreData.class);
+ Mockito.verify(wrapper).sync(capture.capture());
+ Assertions.assertTrue(capture.getValue().getDataMap().isEmpty());
+ }
+ }
+
+ /** 验证发送过程被中断后,线程的中断标志得到保留。 */
+ @Test
+ @DisplayName("发送中断后保留中断标志")
+ public void interruptedSendRestoresInterruptFlag() throws Exception {
+ DefaultRemotingClient client = Mockito.mock(DefaultRemotingClient.class);
+ SDKManage sdk = Mockito.mock(SDKManage.class);
+ RuntimeMetadata runtime = runtime();
+ RocketMQCollect collector = collector(runtime);
+ Mockito.when(sdk.getClient(SDKTypeEnum.ADMIN, runtime.getUnique())).thenReturn(client);
+ Mockito.doThrow(new InterruptedException("interrupted"))
+ .when(client).invokeAsync(Mockito.any(), Mockito.anyLong(), Mockito.any());
+ var wrapper = Mockito.mock(org.apache.eventmesh.dashboard.console.function.report.collect.DataSyncHandler.DataSyncHandlerWrapper.class);
+ try (var mocked = Mockito.mockStatic(SDKManage.class)) {
+ mocked.when(SDKManage::getInstance).thenReturn(sdk);
+ collector.collect(0, wrapper);
+ Assertions.assertTrue(Thread.currentThread().isInterrupted());
+ Mockito.verify(wrapper).sync(Mockito.any());
+ } finally {
+ Thread.interrupted();
+ }
+ }
+
+ /** 验证 Broker 窗口速率、累计条数和运行状态数值转换不丢失大整数精度。 */
+ @Test
+ @DisplayName("Broker 速率和运行状态数值映射")
+ public void mapsBrokerRatesAndRuntimeValuesWithoutLosingLongPrecision() {
+ long exact = 9007199254740993L;
+ var incoming = RocketMQCollectMapper.INSTANCE.brokerMessagesIn("minute", exact, 1.25F);
+ Assertions.assertEquals("minute", incoming.getWindow());
+ Assertions.assertEquals(exact, incoming.getValueWindowCount());
+ Assertions.assertEquals(Float.valueOf(1.25F), incoming.getValue());
+ var outgoing = RocketMQCollectMapper.INSTANCE.brokerMessagesOut("day", Long.MAX_VALUE, 0F);
+ Assertions.assertEquals("day", outgoing.getWindow());
+ Assertions.assertEquals(Long.MAX_VALUE, outgoing.getValueWindowCount());
+ Assertions.assertEquals(Float.valueOf(0F), outgoing.getValue());
+ Assertions.assertEquals(exact, RocketMQCollectMapper.INSTANCE.dispatchBytes(exact).getValue());
+ Assertions.assertEquals(exact, RocketMQCollectMapper.INSTANCE.flushBytes(exact).getValue());
+ Assertions.assertEquals(exact, RocketMQCollectMapper.INSTANCE.reserveTime(exact).getValue());
+ var pool = RocketMQCollectMapper.INSTANCE.threadPool("endTransaction", exact);
+ Assertions.assertEquals("endTransaction", pool.getPoolName());
+ Assertions.assertEquals(exact, pool.getValue());
+ }
+
+ /** 验证 Broker 窗口、线程池标签以及整数和浮点数字段类型。 */
+ @Test
+ @DisplayName("Broker 窗口与线程池表结构")
+ public void brokerModelsDescribeWindowPoolAndNumericColumnTypes() throws Exception {
+ assertReportTable(RocketmqBrokerMessagesIn.class, "rocketmq_broker_messages_in", List.of("window"),
+ List.of("value_window_count"), List.of("value"));
+ assertReportTable(RocketmqBrokerMessagesOut.class, "rocketmq_broker_messages_out", List.of("window"),
+ List.of("value_window_count"), List.of("value"));
+ assertReportTable(RocketmqStorageDispatchBehindBytes.class, "rocketmq_storage_dispatch_behind_bytes",
+ List.of(), List.of("value"));
+ assertReportTable(RocketmqStorageFlushBehindBytes.class, "rocketmq_storage_flush_behind_bytes", List.of(), List.of("value"));
+ assertReportTable(RocketmqStorageMessageReserveTime.class, "rocketmq_storage_message_reserve_time", List.of(), List.of("value"));
+ assertReportTable(RocketmqThreadPoolWartermark.class, "rocketmq_thread_pool_wartermark", List.of("pool_name"), List.of("value"));
+ Assertions.assertNotNull(RocketmqBrokerMessagesIn.class.getDeclaredField("window").getAnnotation(ReportTag.class));
+ Assertions.assertNotNull(RocketmqBrokerMessagesOut.class.getDeclaredField("window").getAnnotation(ReportTag.class));
+ Assertions.assertNotNull(RocketmqThreadPoolWartermark.class.getDeclaredField("poolName").getAnnotation(ReportTag.class));
+ Assertions.assertTrue(RuntimeFloatValue.class.isAssignableFrom(RocketmqBrokerMessagesIn.class));
+ Assertions.assertTrue(RuntimeFloatValue.class.isAssignableFrom(RocketmqBrokerMessagesOut.class));
+ Assertions.assertTrue(RuntimeLongValue.class.isAssignableFrom(RocketmqStorageMessageReserveTime.class));
+ }
+
+ /** 验证根请求先注册,同步失败和后续 Broker 统计子请求都正确计入采集轮次。 */
+ @Test
+ @DisplayName("根请求注册与 Broker 子请求等待")
+ public void registersAllRootsBeforeSynchronousFailureAndWaitsForNestedBrokerStats() throws Exception {
+ DefaultRemotingClient client = Mockito.mock(DefaultRemotingClient.class);
+ SDKManage sdk = Mockito.mock(SDKManage.class);
+ RuntimeMetadata runtime = runtime();
+ Mockito.when(sdk.getClient(SDKTypeEnum.ADMIN, runtime.getUnique())).thenReturn(client);
+ var calls = new java.util.concurrent.LinkedBlockingQueue<Call>();
+ var sent = new java.util.concurrent.CountDownLatch(3);
+ AtomicInteger topicRoots = new AtomicInteger();
+ AtomicInteger configRoots = new AtomicInteger();
+ Mockito.doAnswer(invocation -> {
+ RemotingCommand request = invocation.getArgument(0);
+ InvokeCallback callback = invocation.getArgument(2);
+ long timeout = invocation.getArgument(1);
+ Assertions.assertTrue(timeout > 0 && timeout <= 3000);
+ if (request.getCode() == RequestCode.GET_ALL_TOPIC_CONFIG) {
+ topicRoots.incrementAndGet();
+ callback.operationFail(new IllegalStateException("first root fails inline"));
+ } else if (request.getCode() == RequestCode.GET_ALL_SUBSCRIPTIONGROUP_CONFIG) {
+ callback.operationFail(new IllegalStateException("Group count unavailable"));
+ } else if (request.getCode() == RequestCode.GET_BROKER_CONFIG) {
+ configRoots.incrementAndGet();
+ reply(callback, ResponseCode.SUCCESS, "brokerClusterName=cluster-A\nbrokerName=wrong-key\n".getBytes(StandardCharsets.UTF_8));
+ } else {
+ calls.add(new Call(request, callback));
+ sent.countDown();
+ }
+ return null;
+ }).when(client).invokeAsync(Mockito.any(), Mockito.anyLong(), Mockito.any());
+ var wrapper = Mockito.mock(org.apache.eventmesh.dashboard.console.function.report.collect.DataSyncHandler.DataSyncHandlerWrapper.class);
+ var executor = java.util.concurrent.Executors.newSingleThreadExecutor();
+ try {
+ var finished = executor.submit(() -> {
+ try (var mocked = Mockito.mockStatic(SDKManage.class)) {
+ mocked.when(SDKManage::getInstance).thenReturn(sdk);
+ collector(runtime).collect(0, wrapper);
+ }
+ });
+ Assertions.assertTrue(sent.await(2, java.util.concurrent.TimeUnit.SECONDS));
+ Assertions.assertEquals(1, topicRoots.get());
+ Assertions.assertEquals(1, configRoots.get());
+ Assertions.assertEquals(3, calls.size());
+ Call runtimeCall = calls.stream().filter(call -> call.request().getCode() == RequestCode.GET_BROKER_RUNTIME_INFO)
+ .findFirst().orElseThrow();
+ List<Call> stats = calls.stream().filter(call -> call.request().getCode() == RequestCode.VIEW_BROKER_STATS_DATA).toList();
+ Assertions.assertEquals(2, stats.size());
+ Assertions.assertThrows(java.util.concurrent.TimeoutException.class,
+ () -> finished.get(100, java.util.concurrent.TimeUnit.MILLISECONDS));
+ Mockito.verifyNoInteractions(wrapper);
+ reply(runtimeCall.callback(), ResponseCode.SUCCESS, runtimeBody(Map.of("dispatchBehindBytes", "17")));
+ reply(stats.get(0).callback(), ResponseCode.SUCCESS, "{\"statsMinute\":{\"sum\":0,\"tps\":0}}".getBytes(StandardCharsets.UTF_8));
+ stats.get(0).callback().operationFail(new IllegalStateException("duplicate stats completion"));
+ Assertions.assertThrows(java.util.concurrent.TimeoutException.class,
+ () -> finished.get(100, java.util.concurrent.TimeUnit.MILLISECONDS));
+ Mockito.verifyNoInteractions(wrapper);
+ reply(stats.get(1).callback(), ResponseCode.SUCCESS, "{\"statsMinute\":{\"sum\":2,\"tps\":0.5}}".getBytes(StandardCharsets.UTF_8));
+ finished.get(2, java.util.concurrent.TimeUnit.SECONDS);
+ var capture = org.mockito.ArgumentCaptor.forClass(org.apache.eventmesh.dashboard.console.function.report.collect.RestoreData.class);
+ Mockito.verify(wrapper).sync(capture.capture());
+ var models = capture.getValue().getDataMap();
+ Assertions.assertEquals(Set.of(RocketmqBrokerMessagesIn.class, RocketmqBrokerMessagesOut.class,
+ RocketmqStorageDispatchBehindBytes.class), models.keySet());
+ Assertions.assertEquals(17L, rows(models, RocketmqStorageDispatchBehindBytes.class).get(0).getValue());
+ Assertions.assertEquals(1, rows(models, RocketmqBrokerMessagesIn.class).size());
+ Assertions.assertEquals(1, rows(models, RocketmqBrokerMessagesOut.class).size());
+ } finally {
+ executor.shutdownNow();
+ }
+ }
+
+ /** 验证 Broker 统计使用原生集群名查询,保留零值窗口和精确累计条数。 */
+ @Test
+ @DisplayName("Broker 原生集群键与窗口统计")
+ public void brokerStatsUseNativeClusterKeyAndKeepZeroWindowsAndExactCounts() throws Exception {
+ Set<String> names = new java.util.HashSet<>();
+ var models = collectMock((request, callback) -> {
+ switch (request.getCode()) {
+ case RequestCode.GET_BROKER_CONFIG:
+ reply(callback, ResponseCode.SUCCESS,
+ "brokerClusterName=集群-A\nbrokerName=not-the-cluster\n".getBytes(StandardCharsets.UTF_8));
+ break;
+ case RequestCode.VIEW_BROKER_STATS_DATA:
+ var header = (ViewBrokerStatsDataRequestHeader) request.readCustomHeader();
+ Assertions.assertEquals("集群-A", header.getStatsKey());
+ Assertions.assertTrue(names.add(header.getStatsName()), "Only one request per Broker statistic");
+ String body = "BROKER_PUT_NUMS".equals(header.getStatsName())
+ ? "{\"statsMinute\":{\"sum\":9007199254740993,\"tps\":1.25},\"statsHour\":{\"sum\":0,\"tps\":0}}"
+ : "{\"statsDay\":{\"sum\":9223372036854775807,\"tps\":0.5}}";
+ reply(callback, ResponseCode.SUCCESS, body.getBytes(StandardCharsets.UTF_8));
+ break;
+ default:
+ callback.operationFail(new IllegalStateException("Other collection direction unavailable"));
+ }
+ });
+ Assertions.assertEquals(Set.of("BROKER_PUT_NUMS", "BROKER_GET_NUMS"), names);
+ Assertions.assertEquals(Set.of(RocketmqBrokerMessagesIn.class, RocketmqBrokerMessagesOut.class), models.keySet());
+ var incoming = rows(models, RocketmqBrokerMessagesIn.class);
+ Assertions.assertEquals(2, incoming.size());
+ var minute = incoming.stream().filter(row -> "minute".equals(row.getWindow())).findFirst().orElseThrow();
+ Assertions.assertEquals(9007199254740993L, minute.getValueWindowCount());
+ Assertions.assertEquals(Float.valueOf(1.25F), minute.getValue());
+ var hour = incoming.stream().filter(row -> "hour".equals(row.getWindow())).findFirst().orElseThrow();
+ Assertions.assertEquals(0L, hour.getValueWindowCount());
+ Assertions.assertEquals(Float.valueOf(0F), hour.getValue());
+ var outgoing = rows(models, RocketmqBrokerMessagesOut.class);
+ Assertions.assertEquals(1, outgoing.size());
+ Assertions.assertEquals("day", outgoing.get(0).getWindow());
+ Assertions.assertEquals(Long.MAX_VALUE, outgoing.get(0).getValueWindowCount());
+ Assertions.assertEquals(Float.valueOf(0.5F), outgoing.get(0).getValue());
+ }
+
+ /** 验证非法窗口不会抹掉有效窗口,缺失窗口不会被补成零值。 */
+ @Test
+ @DisplayName("非法 Broker 窗口隔离")
+ public void invalidBrokerWindowsDoNotEraseValidWindowsOrInventZeros() throws Exception {
+ List<String> invalid = List.of("null", "{}", "[]", "\"invalid\"", "{\"sum\":1}", "{\"tps\":1}",
+ "{\"sum\":-1,\"tps\":1}", "{\"sum\":1.5,\"tps\":1}", "{\"sum\":9223372036854775808,\"tps\":1}",
+ "{\"sum\":\"1\",\"tps\":1}", "{\"sum\":null,\"tps\":1}", "{\"sum\":true,\"tps\":1}",
+ "{\"sum\":1,\"tps\":-1}", "{\"sum\":1,\"tps\":3.5e38}", "{\"sum\":1,\"tps\":1e309}",
+ "{\"sum\":1,\"tps\":\"NaN\"}", "{\"sum\":1,\"tps\":\"Infinity\"}",
+ "{\"sum\":1,\"tps\":\"0\"}", "{\"sum\":1,\"tps\":null}");
+ for (String window : invalid) {
+ var models = collectStatsBody("{\"statsMinute\":" + window
+ + ",\"statsHour\":{\"sum\":5,\"tps\":2.5},\"statsDay\":{\"sum\":0,\"tps\":0}}");
+ Assertions.assertEquals(Set.of(RocketmqBrokerMessagesIn.class, RocketmqBrokerMessagesOut.class), models.keySet(), window);
+ for (Class<?> type : List.of(RocketmqBrokerMessagesIn.class, RocketmqBrokerMessagesOut.class)) {
+ List<Object> samples = models.get(type);
+ Assertions.assertEquals(2, samples.size(), window);
+ Map<String, Long> counts = new java.util.HashMap<>();
+ for (Object row : samples) {
+ String name = brokerWindow(row);
+ counts.put(name, brokerWindowCount(row));
+ Assertions.assertEquals(Float.valueOf("hour".equals(name) ? 2.5F : 0F), ((RuntimeFloatValue) row).getValue(), window);
+ }
+ Assertions.assertEquals(Map.of("hour", 5L, "day", 0L), counts, window);
+ }
+ }
+ Assertions.assertTrue(collectStatsBody("{}").isEmpty(), "Missing windows are not zero-valued observations");
+ }
+
+ /** 验证集群名或统计数据不可用时,不会用位点推算 Broker 消息速率。 */
+ @Test
+ @DisplayName("Broker 统计缺失时不推算速率")
+ public void unavailableBrokerStatsAndClusterNameNeverProduceOffsetDerivedRates() throws Exception {
+ for (String config : List.of("", "brokerName=not-a-cluster\n", "brokerClusterName= \n")) {
+ AtomicInteger statsRequests = new AtomicInteger();
+ var models = collectMock((request, callback) -> {
+ if (request.getCode() == RequestCode.GET_BROKER_CONFIG) {
+ reply(callback, ResponseCode.SUCCESS, config.getBytes(StandardCharsets.UTF_8));
+ } else if (request.getCode() == RequestCode.GET_BROKER_RUNTIME_INFO) {
+ reply(callback, ResponseCode.SUCCESS, runtimeBody(Map.of("dispatchBehindBytes", "3")));
+ } else if (request.getCode() == RequestCode.VIEW_BROKER_STATS_DATA) {
+ statsRequests.incrementAndGet();
+ callback.operationFail(new IllegalStateException("Must not guess a Broker stats key"));
+ } else {
+ callback.operationFail(new IllegalStateException("Topic unavailable"));
+ }
+ });
+ Assertions.assertEquals(0, statsRequests.get(), config);
+ Assertions.assertEquals(Set.of(RocketmqStorageDispatchBehindBytes.class), models.keySet(), config);
+ }
+ for (byte[] body : new byte[][] {null, "".getBytes(StandardCharsets.UTF_8), "bad-json".getBytes(StandardCharsets.UTF_8),
+ "[]".getBytes(StandardCharsets.UTF_8)}) {
+ Assertions.assertTrue(collectMock((request, callback) -> {
+ if (request.getCode() == RequestCode.GET_BROKER_CONFIG) {
+ reply(callback, ResponseCode.SUCCESS, "brokerClusterName=cluster-A\n".getBytes(StandardCharsets.UTF_8));
+ } else if (request.getCode() == RequestCode.VIEW_BROKER_STATS_DATA) {
+ reply(callback, ResponseCode.SUCCESS, body);
+ } else {
+ callback.operationFail(new IllegalStateException("Other root unavailable"));
+ }
+ }).isEmpty(), "Malformed Broker responses must not produce default windows");
+ }
+ var models = collectMock((request, callback) -> {
+ switch (request.getCode()) {
+ case RequestCode.GET_BROKER_CONFIG:
+ reply(callback, ResponseCode.SUCCESS, "brokerClusterName=cluster-A\n".getBytes(StandardCharsets.UTF_8));
+ break;
+ case RequestCode.VIEW_BROKER_STATS_DATA:
+ reply(callback, ResponseCode.SYSTEM_ERROR, "{\"statsMinute\":{\"sum\":99,\"tps\":99}}".getBytes(StandardCharsets.UTF_8));
+ break;
+ case RequestCode.GET_ALL_TOPIC_CONFIG:
+ reply(callback, ResponseCode.SUCCESS, "{\"topicConfigTable\":{\"orders\":{}}}".getBytes(StandardCharsets.UTF_8));
+ break;
+ case RequestCode.GET_TOPIC_STATS_INFO:
+ var stats = new org.apache.rocketmq.remoting.protocol.admin.TopicStatsTable();
+ var offset = new org.apache.rocketmq.remoting.protocol.admin.TopicOffset();
+ offset.setMinOffset(100);
+ offset.setMaxOffset(999);
+ stats.getOffsetTable().put(new org.apache.rocketmq.common.message.MessageQueue("orders", "broker", 0), offset);
+ reply(callback, ResponseCode.SUCCESS, RemotingSerializable.encode(stats));
+ break;
+ default:
+ callback.operationFail(new IllegalStateException("No runtime or consumers"));
+ }
+ });
+ Assertions.assertEquals(Set.of(org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.Rocketmq2ProducerOffset.class,
+ RocketmqTopicNumber.class), models.keySet(), "Topic positions must never become Broker message counts or rates");
+ var offsets = rows(models, org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.Rocketmq2ProducerOffset.class);
+ Assertions.assertEquals(2, offsets.size());
+ Assertions.assertTrue(offsets.stream().anyMatch(row -> Long.valueOf(999L).equals(row.getValueMaxOffsetSum())));
+ }
+
+ /** 验证运行状态原生字段、精确字节数及各线程池维度。 */
+ @Test
+ @DisplayName("Broker 运行状态原生字段解析")
+ public void runtimeUsesNativeKeysAndPreservesExactBytesAndAllPoolDimensions() throws Exception {
+ long earliest = System.currentTimeMillis() - 60000;
+ long before = System.currentTimeMillis();
+ var values = new java.util.HashMap<String, String>();
+ values.put("dispatchBehindBytes", "9007199254740993");
+ values.put("remainHowManyDataToFlush", " 1.5 KiB ");
+ values.put("remainHowManyDataToCommit", "0.5KiB");
+ values.put("earliestMessageTimeStamp", Long.toString(earliest));
+ values.put("sendThreadPoolQueueSize", "0");
+ values.put("pullThreadPoolQueueSize", "1");
+ values.put("litePullThreadPoolQueueSize", "2");
+ values.put("queryThreadPoolQueueSize", "3");
+ values.put("ackThreadPoolQueueSize", "4");
+ values.put("EndTransactionQueueSize", "9007199254740993");
+ values.put("putTps", "123 456 789");
+ values.put("getTps", "123 456 789");
+ values.put("putMessageTimesTotal", "1000");
+ var models = collectRuntime(values);
+ long after = System.currentTimeMillis();
+ Assertions.assertEquals(Set.of(RocketmqStorageDispatchBehindBytes.class, RocketmqStorageFlushBehindBytes.class,
+ RocketmqStorageMessageReserveTime.class, RocketmqThreadPoolWartermark.class), models.keySet());
+ Assertions.assertEquals(9007199254740993L, rows(models, RocketmqStorageDispatchBehindBytes.class).get(0).getValue());
+ Assertions.assertEquals(2048L, rows(models, RocketmqStorageFlushBehindBytes.class).get(0).getValue());
+ long age = rows(models, RocketmqStorageMessageReserveTime.class).get(0).getValue();
+ Assertions.assertTrue(age >= before - earliest && age <= after - earliest, "Reserve time is age in milliseconds");
+ Map<String, Long> pools = new java.util.HashMap<>();
+ rows(models, RocketmqThreadPoolWartermark.class).forEach(row -> {
+ Assertions.assertNull(pools.put(row.getPoolName(), row.getValue()), "One sample per native pool");
+ Assertions.assertEquals(7L, row.getOrganizationId());
+ Assertions.assertEquals(1L, row.getClustersId());
+ Assertions.assertNotNull(row.getRuntimeId());
+ Assertions.assertNotNull(row.getTime());
+ });
+ Assertions.assertEquals(Map.of("send", 0L, "pull", 1L, "litePull", 2L, "query", 3L, "ack", 4L,
+ "endTransaction", 9007199254740993L), pools);
+ }
+
+ /** 验证待刷盘字节的单位换算和取整,并拒绝溢出或格式错误。 */
+ @Test
+ @DisplayName("待刷盘字节单位与异常值")
+ public void flushBytesParseNativeUnitsRoundAndRejectOverflowOrMalformedCommit() throws Exception {
+ Map<String, Long> accepted = Map.ofEntries(Map.entry("0 B", 0L), Map.entry("0.5 B", 1L),
+ Map.entry("9007199254740993 B", 9007199254740993L), Map.entry("9223372036854775807 B", Long.MAX_VALUE),
+ Map.entry(" 1.5 KiB ", 1536L), Map.entry("1.5MiB", 1572864L), Map.entry("1 GiB", 1073741824L),
+ Map.entry("1 TiB", 1099511627776L), Map.entry("1 PiB", 1125899906842624L));
+ for (var entry : accepted.entrySet()) {
+ var models = collectRuntime(Map.of("remainHowManyDataToFlush", entry.getKey()));
+ Assertions.assertEquals(Set.of(RocketmqStorageFlushBehindBytes.class), models.keySet(), entry.getKey());
+ Assertions.assertEquals(entry.getValue(), rows(models, RocketmqStorageFlushBehindBytes.class).get(0).getValue(), entry.getKey());
+ }
+ var summed = collectRuntime(Map.of("remainHowManyDataToFlush", "9007199254740993 B", "remainHowManyDataToCommit", "2 B"));
+ Assertions.assertEquals(9007199254740995L, rows(summed, RocketmqStorageFlushBehindBytes.class).get(0).getValue());
+ List<Map<String, String>> invalid = List.of(
+ Map.of("remainHowManyDataToFlush", "9223372036854775807 B", "remainHowManyDataToCommit", "1 B"),
+ Map.of("remainHowManyDataToFlush", "9223372036854775808 B"), Map.of("remainHowManyDataToFlush", "8192 PiB"),
+ Map.of("remainHowManyDataToFlush", "-1 B"), Map.of("remainHowManyDataToFlush", "NaN B"),
+ Map.of("remainHowManyDataToFlush", ""), Map.of("remainHowManyDataToFlush", "nonsense"),
+ Map.of("remainHowManyDataToFlush", "1 B", "remainHowManyDataToCommit", "bad"),
+ Map.of("remainHowManyDataToFlush", "1 B", "remainHowManyDataToCommit", ""),
+ Map.of("remainHowManyDataToFlush", "1 B", "remainHowManyDataToCommit", "-1 B"),
+ Map.of("remainHowManyDataToCommit", "1 B"));
+ for (var fields : invalid) {
+ var values = new java.util.HashMap<>(fields);
+ values.put("dispatchBehindBytes", "5");
+ var models = collectRuntime(values);
+ Assertions.assertEquals(Set.of(RocketmqStorageDispatchBehindBytes.class), models.keySet(), fields.toString());
+ Assertions.assertEquals(5L, rows(models, RocketmqStorageDispatchBehindBytes.class).get(0).getValue());
+ }
+ }
+
+ /** 验证单个运行状态字段错误不会影响其他指标,也不会采用替代字段伪造结果。 */
+ @Test
+ @DisplayName("运行状态错误字段隔离")
+ public void malformedRuntimeFieldsDoNotEraseOtherMeasurementsOrUseFallbacks() throws Exception {
+ for (String invalid : List.of("-1", "1.5", "NaN", "9223372036854775808", "")) {
+ var models = collectRuntime(Map.of("dispatchBehindBytes", invalid, "sendThreadPoolQueueSize", invalid,
+ "remainHowManyDataToFlush", "2 B", "queryThreadPoolQueueSize", "7",
+ "earliestMessageTimeStamp", Long.toString(System.currentTimeMillis() + 3600000),
+ "putTps", "1 2 3", "getTps", "4 5 6"));
+ Assertions.assertEquals(Set.of(RocketmqStorageFlushBehindBytes.class, RocketmqThreadPoolWartermark.class),
+ models.keySet(), invalid);
+ Assertions.assertEquals(2L, rows(models, RocketmqStorageFlushBehindBytes.class).get(0).getValue());
+ var pools = rows(models, RocketmqThreadPoolWartermark.class);
+ Assertions.assertEquals(1, pools.size());
+ Assertions.assertEquals("query", pools.get(0).getPoolName());
+ Assertions.assertEquals(7L, pools.get(0).getValue());
+ }
+ for (String earliest : List.of("0", "-1", "bad", "1.5", "9223372036854775808")) {
+ Assertions.assertTrue(collectRuntime(Map.of("earliestMessageTimeStamp", earliest)).isEmpty(), earliest);
+ }
+ Assertions.assertTrue(collectRuntime(Map.of("putTps", "1 2 3", "getTps", "4 5 6",
+ "dispatchBehind", "9", "flushBehindBytes", "9", "endTransactionThreadPoolQueueSize", "9")).isEmpty(),
+ "Unknown keys and runtime throughput cannot substitute for native Broker statistics");
+ }
+
+ /** 验证消费积压时间过滤非法时间戳,并保留合法的零积压。 */
+ @Test
+ @DisplayName("消费积压时间与零积压")
+ public void filtersInvalidLagTimestampsAndRetainsZeroBacklog() throws Exception {
+ long now = System.currentTimeMillis();
+ for (long delay : new long[] {0, 100, -1, now + 1}) {
+ var models = collectMock((request, callback) -> {
+ switch (request.getCode()) {
+ case RequestCode.GET_ALL_TOPIC_CONFIG -> reply(callback, ResponseCode.SUCCESS,
+ "{\"topicConfigTable\":{\"orders\":{}}}".getBytes(StandardCharsets.UTF_8));
+ case RequestCode.QUERY_TOPIC_CONSUME_BY_WHO -> reply(callback, ResponseCode.SUCCESS,
+ "{\"groupList\":[\"buyers\"]}".getBytes(StandardCharsets.UTF_8));
+ case RequestCode.GET_CONSUME_STATS -> {
+ var stats = new org.apache.rocketmq.remoting.protocol.admin.ConsumeStats();
+ var offset = new org.apache.rocketmq.remoting.protocol.admin.OffsetWrapper();
+ offset.setBrokerOffset(3L);
+ offset.setConsumerOffset(2L);
+ stats.getOffsetTable().put(new org.apache.rocketmq.common.message.MessageQueue("orders", "broker", 0), offset);
+ reply(callback, ResponseCode.SUCCESS, RemotingSerializable.encode(stats));
+ }
+ case RequestCode.QUERY_CONSUME_TIME_SPAN -> {
+ byte[] body = RemotingSerializable.encode(Map.of("consumeTimeSpanSet", List.of(
+ Map.of("messageQueue", Map.of("topic", "orders", "queueId", 0), "delayTime", delay, "minTimeStamp", now - 1000),
+ Map.of("messageQueue", Map.of("topic", "orders", "queueId", 1), "delayTime", 0, "minTimeStamp", -1),
+ Map.of("messageQueue", Map.of("topic", "another", "queueId", 0), "delayTime", 0, "minTimeStamp", now - 1000))));
+ reply(callback, ResponseCode.SUCCESS, body);
+ reply(callback, ResponseCode.SUCCESS, body);
+ }
+ default -> callback.operationFail(new IllegalStateException("Other requests unavailable"));
+ }
+ });
+ if (delay >= 0 && delay <= 100) {
+ var rows = rows(models, RocketmqConsumerLagLatency.class);
+ Assertions.assertEquals(1, rows.size(), "Duplicate callbacks must not duplicate samples");
+ Assertions.assertEquals(delay, rows.get(0).getValue());
+ Assertions.assertEquals("buyers", rows.get(0).getGroupName());
+ Assertions.assertEquals("0", rows.get(0).getQueueId());
+ } else {
+ Assertions.assertFalse(models.containsKey(RocketmqConsumerLagLatency.class));
+ }
+ }
+ }
+
+ /** 验证磁盘使用率和近似剩余字节采集,缺失或非法值保持缺失。 */
+ @Test
+ @DisplayName("磁盘使用率与剩余空间采集")
+ public void collectsDiskRatiosAndApproximateFreeBytesWithoutInventingMissingValues() throws Exception {
+ var models = collectRuntime(Map.of("commitLogDiskRatio", "0.75", "commitLogDiskRatio_/store/a", "0.75",
+ "consumeQueueDiskRatio", "0", "commitLogDirCapacity", "Total : 10 GiB, Free : 2.5 GiB."));
+ Assertions.assertEquals(3, rows(models, RocketmqStorageDiskUsage.class).size());
+ Assertions.assertEquals(2684354560L, rows(models, RocketmqStorageDiskFreeBytes.class).get(0).getValue());
+ var path = rows(models, RocketmqStorageDiskUsage.class).stream().filter(row -> row.getStoreType().equals("commitlog"))
+ .findFirst().orElseThrow();
+ Assertions.assertEquals("/store/a", path.getPath());
+ Assertions.assertEquals(0.75f, path.getValue());
+ for (String value : List.of("-1", "NaN", "Infinity", "1.5", "missing")) {
+ Assertions.assertTrue(collectRuntime(Map.of("commitLogDiskRatio", value)).isEmpty());
+ }
+ for (String value : List.of("Total : 1 GiB, Free : 2 GiB.", "Total : 0 B, Free : 0 B.", "unknown")) {
+ Assertions.assertTrue(collectRuntime(Map.of("commitLogDirCapacity", value)).isEmpty());
+ }
+ Assertions.assertEquals(0L, rows(collectRuntime(Map.of("commitLogDirCapacity", "Total : 1 GiB, Free : 0 B.")),
+ RocketmqStorageDiskFreeBytes.class).get(0).getValue());
+ }
+
+ /** 验证逐客户端采集成功 TPS、失败 TPS 和处理耗时,并将缺失或非法字段补零。 */
+ @Test
+ @DisplayName("逐客户端消费指标采集")
+ public void collectsClientMetricsPerClientAndDefaultsUnavailableFieldsToZero() throws Exception {
+ for (boolean available : List.of(true, false)) {
+ AtomicInteger requests = new AtomicInteger();
+ var models = collectMock((request, callback) -> {
+ switch (request.getCode()) {
+ case RequestCode.GET_ALL_TOPIC_CONFIG -> reply(callback, ResponseCode.SUCCESS,
+ "{\"topicConfigTable\":{\"orders\":{},\"payments\":{}}}".getBytes(StandardCharsets.UTF_8));
+ case RequestCode.QUERY_TOPIC_CONSUME_BY_WHO -> reply(callback, ResponseCode.SUCCESS,
+ "{\"groupList\":[\"buyers\"]}".getBytes(StandardCharsets.UTF_8));
+ case RequestCode.GET_CONSUMER_CONNECTION_LIST -> reply(callback, ResponseCode.SUCCESS,
+ "{\"connectionSet\":[{\"clientId\":\"a\"},{\"clientId\":\"b\"}]}".getBytes(StandardCharsets.UTF_8));
+ case RequestCode.GET_CONSUMER_RUNNING_INFO -> {
+ requests.incrementAndGet();
+ if (available) {
+ reply(callback, ResponseCode.SUCCESS, RemotingSerializable.encode(Map.of("statusTable", Map.of(
+ "orders", Map.of("consumeOKTPS", 2.5, "consumeFailedTPS", 0, "consumeRT", 12.25),
+ "payments", Map.of("consumeOKTPS", -1, "consumeRT", "NaN")))));
+ } else {
+ callback.operationFail(new IllegalStateException("Client disconnected"));
+ callback.operationFail(new IllegalStateException("Duplicate failure notification"));
+ }
+ }
+ default -> callback.operationFail(new IllegalStateException("Not needed for this case"));
+ }
+ });
+ Assertions.assertEquals(2, requests.get(), "Shared group is queried once; each client has one request");
+ if (available) {
+ var success = rows(models, RocketmqConsumerSuccessTps.class);
+ Assertions.assertEquals(Set.of("a", "b"), success.stream().map(RocketmqConsumerSuccessTps::getClientId)
+ .collect(java.util.stream.Collectors.toSet()));
+ Assertions.assertEquals(4, success.size());
+ Assertions.assertTrue(success.stream().allMatch(row ->
+ row.getValue() == (row.getTopicName().equals("orders") ? 2.5f : 0f)));
+ var failed = rows(models, RocketmqConsumerFailedTps.class);
+ Assertions.assertEquals(4, failed.size());
+ Assertions.assertTrue(failed.stream().allMatch(row -> row.getValue() == 0f));
+ var elapsed = rows(models, RocketmqConsumerProcessTime.class);
+ Assertions.assertEquals(4, elapsed.size());
+ Assertions.assertTrue(elapsed.stream().allMatch(row ->
+ row.getValue() == (row.getTopicName().equals("orders") ? 12.25f : 0f)));
+ } else {
+ Assertions.assertFalse(models.containsKey(RocketmqConsumerSuccessTps.class));
+ }
+ }
+ }
+
+ private Map<Class<?>, List<Object>> collectStatsBody(String body) throws Exception {
+ return collectMock((request, callback) -> {
+ if (request.getCode() == RequestCode.GET_BROKER_CONFIG) {
+ reply(callback, ResponseCode.SUCCESS, "brokerClusterName=cluster-A\n".getBytes(StandardCharsets.UTF_8));
+ } else if (request.getCode() == RequestCode.VIEW_BROKER_STATS_DATA) {
+ reply(callback, ResponseCode.SUCCESS, body.getBytes(StandardCharsets.UTF_8));
+ } else {
+ callback.operationFail(new IllegalStateException("Other root unavailable"));
+ }
+ });
+ }
+
+ private Map<Class<?>, List<Object>> collectRuntime(Map<String, String> values) throws Exception {
+ return collectMock((request, callback) -> {
+ if (request.getCode() == RequestCode.GET_BROKER_RUNTIME_INFO) {
+ reply(callback, ResponseCode.SUCCESS, runtimeBody(values));
+ } else {
+ callback.operationFail(new IllegalStateException("Other root unavailable"));
+ }
+ });
+ }
+
+ private byte[] runtimeBody(Map<String, String> values) {
+ KVTable table = new KVTable();
+ table.getTable().putAll(values);
+ return RemotingSerializable.encode(table);
+ }
+
+ private Map<Class<?>, List<Object>> collectMock(java.util.function.BiConsumer<RemotingCommand, InvokeCallback> answer) throws Exception {
+ DefaultRemotingClient client = Mockito.mock(DefaultRemotingClient.class);
+ SDKManage sdk = Mockito.mock(SDKManage.class);
+ RuntimeMetadata runtime = runtime();
+ Mockito.when(sdk.getClient(SDKTypeEnum.ADMIN, runtime.getUnique())).thenReturn(client);
+ Mockito.doAnswer(invocation -> {
+ answer.accept(invocation.getArgument(0), invocation.getArgument(2));
+ return null;
+ }).when(client).invokeAsync(Mockito.any(), Mockito.anyLong(), Mockito.any());
+ var wrapper = Mockito.mock(org.apache.eventmesh.dashboard.console.function.report.collect.DataSyncHandler.DataSyncHandlerWrapper.class);
+ try (var mocked = Mockito.mockStatic(SDKManage.class)) {
+ mocked.when(SDKManage::getInstance).thenReturn(sdk);
+ collector(runtime).collect(0, wrapper);
+ }
+ var capture = org.mockito.ArgumentCaptor.forClass(org.apache.eventmesh.dashboard.console.function.report.collect.RestoreData.class);
+ Mockito.verify(wrapper).sync(capture.capture());
+ return capture.getValue().getDataMap();
+ }
+
+ private <T> List<T> rows(Map<Class<?>, List<Object>> models, Class<T> type) {
+ Assertions.assertTrue(models.containsKey(type), "Missing collected model " + type.getSimpleName());
+ return models.get(type).stream().map(type::cast).toList();
+ }
+
+ private String brokerWindow(Object row) {
+ return row instanceof RocketmqBrokerMessagesIn
+ ? ((RocketmqBrokerMessagesIn) row).getWindow() : ((RocketmqBrokerMessagesOut) row).getWindow();
+ }
+
+ private Long brokerWindowCount(Object row) {
+ return row instanceof RocketmqBrokerMessagesIn
+ ? ((RocketmqBrokerMessagesIn) row).getValueWindowCount() : ((RocketmqBrokerMessagesOut) row).getValueWindowCount();
+ }
+
+ /** 连接真实 Broker,验证原生请求、异步回调和采集模型;需开启 rocketmq.collect.live。 */
+ @Test
+ @DisplayName("真实 Broker 请求与回调采集")
+ public void collectsFromRealBrokerThroughCallbacks() throws Exception {
+ Assumptions.assumeTrue(Boolean.getBoolean("rocketmq.collect.live"));
+ collectFromRealBroker(false);
+ }
+
+ /** 连接真实 Broker 采集指标,写入独立测试库并从 IoTDB 回读;需开启 rocketmq.collect.iotdb.live。 */
+ @Test
+ @DisplayName("真实 Broker 采集及 IoTDB 回读")
+ public void collectsFromRealBrokerAndReadsBackIotdbTables() throws Exception {
+ Assumptions.assumeTrue(Boolean.getBoolean("rocketmq.collect.iotdb.live"));
+ collectFromRealBroker(true);
+ }
+
+ private void collectFromRealBroker(boolean verifyIotdb) throws Exception {
+ RuntimeMetadata runtime = runtime();
+ AbstractSimpleCreateSDKConfig config = ConfigManage.getInstance()
+ .getSimpleCreateSdkConfig(runtime.getClusterType(), SDKTypeEnum.ADMIN);
+ NetAddress address = new NetAddress();
+ address.setAddress("127.0.0.1");
+ address.setPort(Integer.getInteger("rocketmq.collect.port", 21911));
+ config.setNetAddress(address);
+ DefaultRemotingClient real = SDKManage.getInstance().createClient(SDKTypeEnum.ADMIN, runtime, config, runtime.getClusterType());
+ DefaultRemotingClient observed = Mockito.mock(DefaultRemotingClient.class);
+ AtomicInteger outstanding = new AtomicInteger();
+ Set<Integer> successful = ConcurrentHashMap.newKeySet();
+ Map<String, byte[]> brokerResponses = new ConcurrentHashMap<>();
+ var runtimeBefore = new java.util.concurrent.atomic.AtomicLong();
+ var runtimeAfter = new java.util.concurrent.atomic.AtomicLong();
+ Mockito.doAnswer(call -> {
+ RemotingCommand request = call.getArgument(0);
+ InvokeCallback callback = call.getArgument(2);
+ outstanding.incrementAndGet();
+ try {
+ real.invokeAsync(request, call.getArgument(1), new InvokeCallback() {
+ @Override
+ public void operationComplete(ResponseFuture future) {
+ try {
+ if (future.getResponseCommand() != null && future.getResponseCommand().getCode() == ResponseCode.SUCCESS) {
+ successful.add(request.getCode());
+ if (request.getCode() == RequestCode.VIEW_BROKER_STATS_DATA) {
+ var header = (ViewBrokerStatsDataRequestHeader) request.readCustomHeader();
+ brokerResponses.put(header.getStatsName() + ":" + header.getStatsKey(), future.getResponseCommand().getBody());
+ if (header.getStatsName().startsWith("BROKER_")) {
+ brokerResponses.put(header.getStatsName(), future.getResponseCommand().getBody());
+ }
+ } else if (request.getCode() == RequestCode.GET_BROKER_CONFIG
+ || request.getCode() == RequestCode.GET_BROKER_RUNTIME_INFO
+ || request.getCode() == RequestCode.GET_ALL_TOPIC_CONFIG
+ || request.getCode() == RequestCode.GET_ALL_SUBSCRIPTIONGROUP_CONFIG) {
+ brokerResponses.put(Integer.toString(request.getCode()), future.getResponseCommand().getBody());
+ }
+ }
+ if (request.getCode() == RequestCode.GET_BROKER_RUNTIME_INFO) {
+ runtimeBefore.set(System.currentTimeMillis());
+ }
+ callback.operationComplete(future);
+ } finally {
+ if (request.getCode() == RequestCode.GET_BROKER_RUNTIME_INFO) {
+ runtimeAfter.set(System.currentTimeMillis());
+ }
+ outstanding.decrementAndGet();
+ }
+ }
+
+ @Override
+ public void operationFail(Throwable error) {
+ callback.operationFail(error);
+ }
+ });
+ } catch (Exception e) {
+ outstanding.decrementAndGet();
+ throw e;
+ }
+ return null;
+ }).when(observed).invokeAsync(Mockito.any(), Mockito.anyLong(), Mockito.any());
+ String topic = "callback_collect_" + java.util.UUID.randomUUID().toString().replace("-", "");
+ SDKManage sdk = Mockito.mock(SDKManage.class);
+ Mockito.when(sdk.getClient(SDKTypeEnum.ADMIN, runtime.getUnique())).thenReturn(observed);
+ try (var mocked = Mockito.mockStatic(SDKManage.class)) {
+ mocked.when(SDKManage::getInstance).thenReturn(sdk);
+ prepareBroker(real, topic);
+ RocketMQCollect collector = collector(runtime);
+ var wrapper = Mockito.mock(org.apache.eventmesh.dashboard.console.function.report.collect.DataSyncHandler.DataSyncHandlerWrapper.class);
+ collector.collect(0, wrapper);
+ var capture = org.mockito.ArgumentCaptor.forClass(org.apache.eventmesh.dashboard.console.function.report.collect.RestoreData.class);
+ Mockito.verify(wrapper).sync(capture.capture());
+ var models = capture.getValue().getDataMap();
+ Assertions.assertEquals(Set.of(
+ org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.Rocketmq2ProducerOffset.class,
+ RocketmqConsumerOffset.class, RocketmqConsumerConnectionNumber.class, RocketmqBrokerMessagesIn.class,
+ RocketmqBrokerMessagesOut.class, RocketmqStorageDispatchBehindBytes.class, RocketmqStorageFlushBehindBytes.class,
+ RocketmqStorageMessageReserveTime.class, RocketmqThreadPoolWartermark.class, RocketmqTopicNumber.class,
+ RocketmqConsumerGroupNumber.class, RocketmqTopicMessagesIn.class, RocketmqGroupMessagesOut.class, RocketmqConsumerLagLatency.class, RocketmqConsumerSuccessTps.class, RocketmqConsumerFailedTps.class, RocketmqConsumerProcessTime.class, RocketmqStorageDiskUsage.class, RocketmqStorageDiskFreeBytes.class), models.keySet(),
+ "Only existing Topic/consumer measurements and approved Broker stats/runtime measurements are collected");
+ var requests = org.mockito.ArgumentCaptor.forClass(RemotingCommand.class);
+ Mockito.verify(observed, Mockito.atLeastOnce()).invokeAsync(requests.capture(), Mockito.anyLong(), Mockito.any());
+ Assertions.assertEquals(1L, requests.getAllValues().stream()
+ .filter(request -> request.getCode() == RequestCode.GET_ALL_TOPIC_CONFIG).count(), "Topic list is shared");
+ for (RemotingCommand request : requests.getAllValues()) {
+ Assertions.assertTrue(Set.of(RequestCode.GET_ALL_TOPIC_CONFIG, RequestCode.GET_TOPIC_STATS_INFO,
+ RequestCode.QUERY_TOPIC_CONSUME_BY_WHO, RequestCode.GET_CONSUMER_CONNECTION_LIST,
+ RequestCode.GET_CONSUME_STATS, RequestCode.GET_BROKER_CONFIG, RequestCode.VIEW_BROKER_STATS_DATA,
+ RequestCode.GET_BROKER_RUNTIME_INFO, RequestCode.GET_ALL_SUBSCRIPTIONGROUP_CONFIG,
+ RequestCode.QUERY_CONSUME_TIME_SPAN, RequestCode.GET_CONSUMER_RUNNING_INFO).contains(request.getCode()), "Unexpected collection family");
+ if (request.getCode() == RequestCode.GET_CONSUME_STATS) {
+ var header = (org.apache.rocketmq.remoting.protocol.header.GetConsumeStatsRequestHeader) request.readCustomHeader();
+ Assertions.assertNotNull(header.getTopic(), "Consumer query must be scoped to its Topic");
+ }
+ }
+ var positions = models.get(org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.Rocketmq2ProducerOffset.class);
+ Assertions.assertTrue(positions.stream().map(row ->
+ (org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.Rocketmq2ProducerOffset) row)
+ .anyMatch(row -> topic.equals(row.getTopicName()) && Long.valueOf(3).equals(row.getValue())));
+ Assertions.assertTrue(models.containsKey(RocketmqConsumerConnectionNumber.class));
+ var connectionRows = models.get(RocketmqConsumerConnectionNumber.class);
+ Assertions.assertTrue(connectionRows.stream().map(RocketmqConsumerConnectionNumber.class::cast)
+ .anyMatch(row -> topic.equals(row.getGroupName()) && Long.valueOf(1L).equals(row.getValueConnectionCount())));
+ var consumers = models.get(org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.RocketmqConsumerOffset.class);
+ Assertions.assertTrue(consumers.stream().map(row ->
+ (org.apache.eventmesh.dashboard.console.function.report.model.rocketmq.RocketmqConsumerOffset) row)
+ .anyMatch(row -> topic.equals(row.getTopicName()) && topic.equals(row.getGroupName())
+ && Long.valueOf(2).equals(row.getValueConsumerOffset()) && Long.valueOf(1).equals(row.getValueOffsetLag())));
+ consumers.stream().map(RocketmqConsumerOffset.class::cast).filter(row -> topic.equals(row.getTopicName()))
+ .forEach(row -> LoggerFactory.getLogger(RocketMQCollectTest.class).info(
+ "消费进度验证:Topic={} Group={} Queue={} consumerOffset={} brokerOffset={} lag={}",
+ row.getTopicName(), row.getGroupName(), row.getQueueId(), row.getValueConsumerOffset(),
+ row.getValueBrokerOffset(), row.getValueOffsetLag()));
+ long deadline = System.currentTimeMillis() + 15000;
+ while (outstanding.get() != 0 && System.currentTimeMillis() < deadline) {
+ Thread.sleep(20);
+ }
+ Assertions.assertEquals(0, outstanding.get(), "All callbacks must finish");
+ for (int code : List.of(RequestCode.GET_ALL_TOPIC_CONFIG, RequestCode.GET_TOPIC_STATS_INFO,
+ RequestCode.QUERY_TOPIC_CONSUME_BY_WHO, RequestCode.GET_CONSUME_STATS, RequestCode.GET_CONSUMER_CONNECTION_LIST,
+ RequestCode.GET_BROKER_CONFIG, RequestCode.GET_BROKER_RUNTIME_INFO, RequestCode.VIEW_BROKER_STATS_DATA,
+ RequestCode.GET_ALL_SUBSCRIPTIONGROUP_CONFIG)) {
+ Assertions.assertTrue(successful.contains(code), "Missing successful request " + code);
+ }
+ assertLiveBrokerModels(models, runtime, brokerResponses, runtimeBefore.get(), runtimeAfter.get());
+ assertScopeModels(models, brokerResponses, topic);
+ Assertions.assertTrue(successful.contains(RequestCode.GET_CONSUMER_RUNNING_INFO));
+ Assertions.assertTrue(successful.contains(RequestCode.QUERY_CONSUME_TIME_SPAN));
+ var successfulClient = rows(models, RocketmqConsumerSuccessTps.class).stream()
+ .filter(row -> row.getTopicName().equals(topic) && row.getClientId().equals(topic)).findFirst().orElseThrow();
+ Assertions.assertEquals(topic, successfulClient.getGroupName());
+ Assertions.assertEquals(2.5f, successfulClient.getValue());
+ Assertions.assertEquals(0.5f, rows(models, RocketmqConsumerFailedTps.class).stream()
+ .filter(row -> row.getTopicName().equals(topic)).findFirst().orElseThrow().getValue());
+ Assertions.assertEquals(12.25f, rows(models, RocketmqConsumerProcessTime.class).stream()
+ .filter(row -> row.getTopicName().equals(topic)).findFirst().orElseThrow().getValue());
+ var lag = rows(models, RocketmqConsumerLagLatency.class).stream()
+ .filter(row -> row.getTopicName().equals(topic)).findFirst().orElseThrow();
+ Assertions.assertEquals("0", lag.getQueueId());
+ Assertions.assertTrue(lag.getValue() >= 0 && lag.getValue() < 30000);
+ var nativeRuntime = (Map<?, ?>) RemotingSerializable.decode(
+ brokerResponses.get(Integer.toString(RequestCode.GET_BROKER_RUNTIME_INFO)), Map.class).get("table");
+ for (var disk : rows(models, RocketmqStorageDiskUsage.class)) {
+ String key = disk.getStoreType().equals("commitlog") ? "commitLogDiskRatio_" + disk.getPath()
+ : disk.getStoreType().equals("commitlog_min") ? "commitLogDiskRatio" : "consumeQueueDiskRatio";
+ Assertions.assertEquals(Float.parseFloat(nativeRuntime.get(key).toString()), disk.getValue());
+ }
+
+ var nativeConfig = new java.util.Properties();
+ nativeConfig.load(new java.io.StringReader(new String(
+ brokerResponses.get(Integer.toString(RequestCode.GET_BROKER_CONFIG)), StandardCharsets.UTF_8)));
+ for (int code : List.of(RequestCode.GET_BROKER_CONFIG, RequestCode.GET_BROKER_RUNTIME_INFO)) {
+ Assertions.assertEquals(1L, requests.getAllValues().stream().filter(request -> request.getCode() == code).count());
+ }
+ var statsRequests = requests.getAllValues().stream()
+ .filter(request -> request.getCode() == RequestCode.VIEW_BROKER_STATS_DATA)
+ .filter(request -> ((ViewBrokerStatsDataRequestHeader) request.readCustomHeader()).getStatsName().startsWith("BROKER_"))
+ .toList();
+ Assertions.assertEquals(2, statsRequests.size());
+ Set<String> names = new java.util.HashSet<>();
+ for (RemotingCommand request : statsRequests) {
+ var header = (ViewBrokerStatsDataRequestHeader) request.readCustomHeader();
+ Assertions.assertEquals(nativeConfig.getProperty("brokerClusterName"), header.getStatsKey());
+ names.add(header.getStatsName());
+ }
+ Assertions.assertEquals(Set.of("BROKER_PUT_NUMS", "BROKER_GET_NUMS"), names);
+ if (verifyIotdb) {
+ verifyIotdbTables(models, runtime, topic);
+ }
+ } finally {
+ try {
+ DeleteTopicRequestHeader delete = new DeleteTopicRequestHeader();
+ delete.setTopic(topic);
+ assertSuccess(real.invokeSync(RemotingCommand.createRequestCommand(RequestCode.DELETE_TOPIC_IN_BROKER, delete), 3000));
+ DeleteSubscriptionGroupRequestHeader deleteGroup = new DeleteSubscriptionGroupRequestHeader();
+ deleteGroup.setGroupName(topic);
+ assertSuccess(real.invokeSync(RemotingCommand.createRequestCommand(RequestCode.DELETE_SUBSCRIPTIONGROUP, deleteGroup), 3000));
+ } finally {
+ real.shutdown();
+ SDKManage.getInstance().deleteClient(null, runtime.getUnique());
+ }
+ }
+ }
+
+ /** 验证配置中的 Topic、消费组总数,以及按 Topic 和消费组区分的消息窗口统计。 */
+ @Test
+ @DisplayName("资源总数与分范围消息统计")
+ public void countsConfiguredResourcesAndScopesMessageStats() throws Exception {
+ Set<String> statsKeys = new java.util.HashSet<>();
+ AtomicInteger groupRequests = new AtomicInteger();
+ var models = collectMock((request, callback) -> {
+ switch (request.getCode()) {
+ case RequestCode.GET_ALL_TOPIC_CONFIG:
+ reply(callback, ResponseCode.SUCCESS,
+ "{\"topicConfigTable\":{\"orders\":{},\"payments\":{},\"%DLQ%buyers\":{}}}".getBytes(StandardCharsets.UTF_8));
+ break;
+ case RequestCode.GET_ALL_SUBSCRIPTIONGROUP_CONFIG:
+ groupRequests.incrementAndGet();
+ reply(callback, ResponseCode.SUCCESS,
+ "{\"subscriptionGroupTable\":{\"buyers\":{},\"offline\":{},\"system\":{}}}".getBytes(StandardCharsets.UTF_8));
+ break;
+ case RequestCode.QUERY_TOPIC_CONSUME_BY_WHO:
+ reply(callback, ResponseCode.SUCCESS, "{\"groupList\":[\"buyers\"]}".getBytes(StandardCharsets.UTF_8));
+ break;
+ case RequestCode.VIEW_BROKER_STATS_DATA:
+ var header = (ViewBrokerStatsDataRequestHeader) request.readCustomHeader();
+ Assertions.assertTrue(statsKeys.add(header.getStatsName() + ":" + header.getStatsKey()));
+ reply(callback, ResponseCode.SUCCESS,
+ ("{\"statsMinute\":{\"sum\":9007199254740993,\"tps\":1.5},"
+ + "\"statsHour\":{\"sum\":0,\"tps\":0},\"statsDay\":{\"sum\":-1,\"tps\":1}}")
+ .getBytes(StandardCharsets.UTF_8));
+ callback.operationFail(new IllegalStateException("duplicate"));
+ break;
+ default:
+ callback.operationFail(new IllegalStateException("Unrelated data unavailable"));
+ }
+ });
+ Assertions.assertEquals(3L, rows(models, RocketmqTopicNumber.class).get(0).getValue());
+ Assertions.assertEquals(3L, rows(models, RocketmqConsumerGroupNumber.class).get(0).getValue());
+ Assertions.assertEquals(1, groupRequests.get());
+ Assertions.assertEquals(Set.of("TOPIC_PUT_NUMS:orders", "TOPIC_PUT_NUMS:payments", "TOPIC_PUT_NUMS:%DLQ%buyers",
+ "GROUP_GET_NUMS:orders@buyers", "GROUP_GET_NUMS:payments@buyers"), statsKeys);
+ Assertions.assertEquals(6, rows(models, RocketmqTopicMessagesIn.class).size());
+ Assertions.assertEquals(4, rows(models, RocketmqGroupMessagesOut.class).size());
+ rows(models, RocketmqGroupMessagesOut.class).forEach(row -> {
+ Assertions.assertEquals("buyers", row.getGroupName());
+ Assertions.assertEquals("minute".equals(row.getWindow()) ? 9007199254740993L : 0L, row.getValueWindowCount());
+ Assertions.assertEquals("minute".equals(row.getWindow()) ? 1.5F : 0F, row.getValue());
+ Assertions.assertNotNull(row.getRuntimeId());
+ });
+ }
+
+ /** 验证配置查询失败不生成零值,成功返回空配置时生成零值。 */
+ @Test
+ @DisplayName("缺失配置与空配置的零值语义")
+ public void missingConfigurationIsNotZeroButEmptyConfigurationIsZero() throws Exception {
+ for (String body : List.of("{}", "{\"topicConfigTable\":null,\"subscriptionGroupTable\":null}", "not-json")) {
+ var models = collectMock((request, callback) -> {
+ if (request.getCode() == RequestCode.GET_ALL_TOPIC_CONFIG || request.getCode() == RequestCode.GET_ALL_SUBSCRIPTIONGROUP_CONFIG) {
+ reply(callback, ResponseCode.SUCCESS, body.getBytes(StandardCharsets.UTF_8));
+ } else {
+ callback.operationFail(new IllegalStateException("Unavailable"));
+ }
+ });
+ Assertions.assertTrue(models.isEmpty(), body);
+ }
+ var empty = collectMock((request, callback) -> {
+ if (request.getCode() == RequestCode.GET_ALL_TOPIC_CONFIG || request.getCode() == RequestCode.GET_ALL_SUBSCRIPTIONGROUP_CONFIG) {
+ reply(callback, ResponseCode.SUCCESS,
+ "{\"topicConfigTable\":{},\"subscriptionGroupTable\":{}}".getBytes(StandardCharsets.UTF_8));
+ } else {
+ callback.operationFail(new IllegalStateException("Unavailable"));
+ }
+ });
+ Assertions.assertEquals(0L, rows(empty, RocketmqTopicNumber.class).get(0).getValue());
+ Assertions.assertEquals(0L, rows(empty, RocketmqConsumerGroupNumber.class).get(0).getValue());
+ }
+
+ /** 验证 Topic 和消费组指标使用名称与窗口标签,并保持 GAUGE 语义。 */
+ @Test
+ @DisplayName("Topic 与消费组指标表结构")
+ public void scopedModelsHaveNameAndWindowTagsAndGaugeSemantics() {
+ assertReportTable(RocketmqTopicNumber.class, "rocketmq_topic_number", List.of("runtime_id"), List.of("value"));
+ assertReportTable(RocketmqConsumerGroupNumber.class, "rocketmq_consumer_group_number", List.of("runtime_id"), List.of("value"));
+ assertReportTable(RocketmqTopicMessagesIn.class, "rocketmq_topic_messages_in",
+ List.of("topic_name", "window"), List.of("value_window_count"), List.of("value"));
+ assertReportTable(RocketmqGroupMessagesOut.class, "rocketmq_group_messages_out",
+ List.of("topic_name", "group_name", "window"), List.of("value_window_count"), List.of("value"));
+ var row = RocketMQCollectMapper.INSTANCE.groupMessages("orders", "buyers", "minute", 9007199254740993L, 1.5F);
+ Assertions.assertEquals("orders", row.getTopicName());
+ Assertions.assertEquals("buyers", row.getGroupName());
+ Assertions.assertEquals(9007199254740993L, row.getValueWindowCount());
+ Assertions.assertEquals(1.5F, row.getValue());
+ }
+
+ private void assertScopeModels(Map<Class<?>, List<Object>> models, Map<String, byte[]> responses, String topic) {
+ Map<?, ?> topics = RemotingSerializable.decode(responses.get(Integer.toString(RequestCode.GET_ALL_TOPIC_CONFIG)), Map.class);
+ Map<?, ?> groups = RemotingSerializable.decode(responses.get(Integer.toString(RequestCode.GET_ALL_SUBSCRIPTIONGROUP_CONFIG)), Map.class);
+ Assertions.assertEquals((long) ((Map<?, ?>) topics.get("topicConfigTable")).size(), rows(models, RocketmqTopicNumber.class).get(0).getValue());
+ Assertions.assertEquals((long) ((Map<?, ?>) groups.get("subscriptionGroupTable")).size(),
+ rows(models, RocketmqConsumerGroupNumber.class).get(0).getValue());
+ var incoming = rows(models, RocketmqTopicMessagesIn.class).stream().filter(row -> topic.equals(row.getTopicName())).toList();
+ var outgoing = rows(models, RocketmqGroupMessagesOut.class).stream().filter(row -> topic.equals(row.getTopicName())
+ && topic.equals(row.getGroupName())).toList();
+ Assertions.assertEquals(3, incoming.size());
+ Assertions.assertEquals(3, outgoing.size());
+ for (Object row : java.util.stream.Stream.concat(incoming.stream(), outgoing.stream()).toList()) {
+ boolean in = row instanceof RocketmqTopicMessagesIn;
+ String window = in ? ((RocketmqTopicMessagesIn) row).getWindow() : ((RocketmqGroupMessagesOut) row).getWindow();
+ long count = in ? ((RocketmqTopicMessagesIn) row).getValueWindowCount() : ((RocketmqGroupMessagesOut) row).getValueWindowCount();
+ String key = in ? "TOPIC_PUT_NUMS:" + topic : "GROUP_GET_NUMS:" + topic + "@" + topic;
+ Map<?, ?> wire = RemotingSerializable.decode(responses.get(key), Map.class);
+ Map<?, ?> sample = (Map<?, ?>) wire.get(Map.of("minute", "statsMinute", "hour", "statsHour", "day", "statsDay").get(window));
+ Assertions.assertEquals(Long.parseLong(sample.get("sum").toString()), count);
+ Assertions.assertEquals(((Number) sample.get("tps")).floatValue(), ((RuntimeFloatValue) row).getValue());
+ }
+ }
+
+ private void assertLiveBrokerModels(Map<Class<?>, List<Object>> models, RuntimeMetadata runtime,
+ Map<String, byte[]> responses, long before, long after) {
+ Map<String, String> windows = Map.of("minute", "statsMinute", "hour", "statsHour", "day", "statsDay");
+ Map<Class<?>, String> stats = Map.of(RocketmqBrokerMessagesIn.class, "BROKER_PUT_NUMS",
+ RocketmqBrokerMessagesOut.class, "BROKER_GET_NUMS");
+ for (var entry : stats.entrySet()) {
+ byte[] body = responses.get(entry.getValue());
+ Assertions.assertNotNull(body, entry.getValue());
+ var wire = RemotingSerializable.decode(body, java.util.HashMap.class);
+ List<Object> samples = models.get(entry.getKey());
+ Assertions.assertEquals(3, samples.size(), "Native Broker responses expose all three windows, including valid zero windows");
+ Set<String> observedWindows = new java.util.HashSet<>();
+ for (Object sample : samples) {
+ String name = brokerWindow(sample);
+ Assertions.assertTrue(observedWindows.add(name));
+ Assertions.assertInstanceOf(Map.class, wire.get(windows.get(name)));
+ var nativeWindow = (Map<?, ?>) wire.get(windows.get(name));
+ Assertions.assertInstanceOf(Number.class, nativeWindow.get("sum"));
+ Assertions.assertInstanceOf(Number.class, nativeWindow.get("tps"));
+ long count = new java.math.BigDecimal(nativeWindow.get("sum").toString()).longValueExact();
+ float rate = ((Number) nativeWindow.get("tps")).floatValue();
+ Assertions.assertTrue(count >= 0 && rate >= 0 && Float.isFinite(rate));
+ Assertions.assertEquals(count, brokerWindowCount(sample));
+ Assertions.assertEquals(Float.valueOf(rate), ((RuntimeFloatValue) sample).getValue());
+ Assertions.assertEquals(runtime.getId(), ((RuntimeFloatValue) sample).getRuntimeId());
+ LoggerFactory.getLogger(RocketMQCollectTest.class).info(
+ "Broker 窗口验证:stats={} window={} count={} messagesPerSecond={}", entry.getValue(), name, count, rate);
+ }
+ Assertions.assertEquals(windows.keySet(), observedWindows);
+ }
+ byte[] body = responses.get(Integer.toString(RequestCode.GET_BROKER_RUNTIME_INFO));
+ Assertions.assertNotNull(body);
+ var values = RemotingSerializable.decode(body, KVTable.class).getTable();
+ long dispatch = Long.parseLong(values.get("dispatchBehindBytes"));
+ Assertions.assertEquals(1, rows(models, RocketmqStorageDispatchBehindBytes.class).size());
+ Assertions.assertEquals(dispatch, rows(models, RocketmqStorageDispatchBehindBytes.class).get(0).getValue());
+ long flush = expectedNativeBytes(values.get("remainHowManyDataToFlush"));
+ if (values.containsKey("remainHowManyDataToCommit")) {
+ flush = Math.addExact(flush, expectedNativeBytes(values.get("remainHowManyDataToCommit")));
+ }
+ Assertions.assertEquals(1, rows(models, RocketmqStorageFlushBehindBytes.class).size());
+ Assertions.assertEquals(flush, rows(models, RocketmqStorageFlushBehindBytes.class).get(0).getValue());
+ long earliest = Long.parseLong(values.get("earliestMessageTimeStamp"));
+ Assertions.assertTrue(earliest > 0 && earliest <= before, "Fixture must expose a real earliest stored message");
+ Assertions.assertEquals(1, rows(models, RocketmqStorageMessageReserveTime.class).size());
+ long age = rows(models, RocketmqStorageMessageReserveTime.class).get(0).getValue();
+ Assertions.assertTrue(age >= before - earliest && age <= after - earliest, "Native timestamp is converted to age in ms");
+ Map<String, String> poolKeys = Map.of("send", "sendThreadPoolQueueSize", "pull", "pullThreadPoolQueueSize",
+ "litePull", "litePullThreadPoolQueueSize", "query", "queryThreadPoolQueueSize", "ack", "ackThreadPoolQueueSize",
+ "endTransaction", "EndTransactionQueueSize");
+ Map<String, Long> expectedPools = new java.util.HashMap<>();
+ poolKeys.forEach((pool, key) -> {
+ if (values.containsKey(key)) {
+ expectedPools.put(pool, Long.parseLong(values.get(key)));
+ }
+ });
+ Assertions.assertFalse(expectedPools.isEmpty(), "Fixture must expose native thread pool queues");
+ Map<String, Long> actualPools = new java.util.HashMap<>();
+ rows(models, RocketmqThreadPoolWartermark.class).forEach(row ->
+ Assertions.assertNull(actualPools.put(row.getPoolName(), row.getValue()), "Duplicate pool sample"));
+ Assertions.assertEquals(expectedPools, actualPools);
+ LoggerFactory.getLogger(RocketMQCollectTest.class).info(
+ "Broker 运行时验证:dispatchBytes={} flushAndCommitBytes={} reserveAgeMs={} poolQueues={}", dispatch, flush, age, actualPools);
+ }
+
+ private long expectedNativeBytes(String value) {
+ Assertions.assertNotNull(value, "Native byte value must be present");
+ var matcher = java.util.regex.Pattern.compile("([0-9]+(?:\\.[0-9]+)?)\\s*(B|KiB|MiB|GiB|TiB|PiB|EiB)?")
+ .matcher(value.trim());
+ Assertions.assertTrue(matcher.matches(), value);
+ String unit = matcher.group(2);
+ int power = unit == null || "B".equals(unit) ? 0 : List.of("KiB", "MiB", "GiB", "TiB", "PiB", "EiB").indexOf(unit) + 1;
+ return new java.math.BigDecimal(matcher.group(1)).multiply(java.math.BigDecimal.valueOf(1024).pow(power))
+ .setScale(0, java.math.RoundingMode.HALF_UP).longValueExact();
+ }
+
+ /** 使用真实报表引擎建表和写入,再通过独立 JDBC 连接读回本次采集的数据。 */
+ private void verifyIotdbTables(java.util.Map<Class<?>, List<Object>> models, RuntimeMetadata runtime, String topic) throws Exception {
+ var engine = new org.apache.eventmesh.dashboard.console.function.report.iotdb.IotDBReportEngine();
+ Class.forName("org.apache.iotdb.jdbc.IoTDBDriver");
+ String address = System.getProperty("rocketmq.collect.iotdb.address", "127.0.0.1:6667");
+ String configuredDatabase = System.getProperty("rocketmq.collect.iotdb.database");
+ String database = configuredDatabase == null
+ ? "collect_test_" + java.util.UUID.randomUUID().toString().replace("-", "") : configuredDatabase;
+ Assertions.assertTrue(database.matches("collect_test_[a-z0-9_]+"), "Use a dedicated collection test database");
+ String url = "jdbc:iotdb://" + address + "/" + database + "?sql_dialect=table";
+ var tables = new java.util.HashMap<Class<?>, String>(java.util.Map.<Class<?>, String>of(
+ RocketmqConsumerOffset.class, "rocketmq_consumer_offset",
+ RocketmqConsumerConnectionNumber.class, "rocketmq_consumer_connection_number",
+ RocketmqBrokerMessagesIn.class, "rocketmq_broker_messages_in",
+ RocketmqBrokerMessagesOut.class, "rocketmq_broker_messages_out",
+ RocketmqStorageDispatchBehindBytes.class, "rocketmq_storage_dispatch_behind_bytes",
+ RocketmqStorageFlushBehindBytes.class, "rocketmq_storage_flush_behind_bytes",
+ RocketmqStorageMessageReserveTime.class, "rocketmq_storage_message_reserve_time",
+ RocketmqThreadPoolWartermark.class, "rocketmq_thread_pool_wartermark"));
+ tables.put(RocketmqTopicNumber.class, "rocketmq_topic_number");
+ tables.put(RocketmqConsumerGroupNumber.class, "rocketmq_consumer_group_number");
+ tables.put(RocketmqTopicMessagesIn.class, "rocketmq_topic_messages_in");
+ tables.put(RocketmqGroupMessagesOut.class, "rocketmq_group_messages_out");
+ tables.put(RocketmqConsumerLagLatency.class, "rocketmq_consumer_lag_latency");
+ tables.put(RocketmqConsumerSuccessTps.class, "rocketmq_consumer_success_tps");
+ tables.put(RocketmqConsumerFailedTps.class, "rocketmq_consumer_failed_tps");
+ tables.put(RocketmqConsumerProcessTime.class, "rocketmq_consumer_process_time");
+ tables.put(RocketmqStorageDiskUsage.class, "rocketmq_storage_disk_usage");
+ tables.put(RocketmqStorageDiskFreeBytes.class, "rocketmq_storage_disk_free_bytes");
+ engine.setClazzToTableName(tables);
+ try {
+ // 使用独立测试库,避免同名历史表结构影响测试;建表及写入仍调用现有报表引擎。
+ try (var connection = java.sql.DriverManager.getConnection(
+ "jdbc:iotdb://" + address + "/?sql_dialect=table", "root", "root");
+ var statement = connection.createStatement()) {
+ statement.execute("create database if not exists " + database);
+ }
+ var source = new com.alibaba.druid.pool.DruidDataSource();
+ org.apache.commons.lang3.reflect.FieldUtils.writeField(engine, "dataSource", source, true);
+ source.setUrl(url);
+ source.setDriverClassName("org.apache.iotdb.jdbc.IoTDBDriver");
+ source.setUsername("root");
+ source.setPassword("root");
+ source.setMaxActive(2);
+ source.setMaxWait(5000);
+ source.init();
+ for (var entry : tables.entrySet()) {
+ Class<?> model = entry.getKey();
+ ReportMeta annotation = model.getAnnotation(ReportMeta.class);
+ var metadata = new ReportMetaData();
+ metadata.setClazz(model);
+ metadata.setTableName(annotation.tableName());
+ metadata.setReportName(annotation.reportName());
+ metadata.setComment(annotation.comment());
+ var fields = org.apache.commons.lang3.reflect.FieldUtils.getAllFieldsList(model);
+ engine.createReportHandler(metadata, fields);
+ engine.createReport(entry.getValue());
+ }
+ var rows = new java.util.HashMap<Class<?>, List<Object>>();
+ rows.put(RocketmqConsumerOffset.class, models.get(RocketmqConsumerOffset.class).stream()
+ .filter(row -> topic.equals(((RocketmqConsumerOffset) row).getTopicName())).toList());
+ rows.put(RocketmqConsumerConnectionNumber.class, models.get(RocketmqConsumerConnectionNumber.class).stream()
+ .filter(row -> topic.equals(((RocketmqConsumerConnectionNumber) row).getGroupName())).toList());
+ Assertions.assertFalse(rows.get(RocketmqConsumerOffset.class).isEmpty());
+ Assertions.assertEquals(1, rows.get(RocketmqConsumerConnectionNumber.class).size());
+ for (Class<?> model : tables.keySet()) {
+ if (model != RocketmqConsumerOffset.class && model != RocketmqConsumerConnectionNumber.class) {
+ Assertions.assertTrue(models.containsKey(model), "Missing live observations for " + model.getSimpleName());
+ Assertions.assertFalse(models.get(model).isEmpty());
+ rows.put(model, models.get(model));
+ }
+ }
+ // Producer aggregate rows have nullable numeric fields; the unchanged formatter is not exercised for those rows.
+ engine.batchInsertByClass(rows);
+ try (var connection = java.sql.DriverManager.getConnection(
+ url, "root", "root");
+ var statement = connection.createStatement()) {
+ for (var entry : tables.entrySet()) {
+ String sql = "select * from " + entry.getValue() + " where runtime_id = '" + runtime.getId() + "'";
+ try (var result = statement.executeQuery(sql)) {
+ int count = 0;
+ boolean expectedSample = false;
+ Set<String> dimensions = new java.util.HashSet<>();
+ while (result.next()) {
+ count++;
+ Assertions.assertEquals("7", result.getString("organization_id"));
+ Assertions.assertEquals(runtime.getClusterId().toString(), result.getString("clusters_id"));
+ Assertions.assertEquals(runtime.getId().toString(), result.getString("runtime_id"));
+ Assertions.assertNotNull(result.getObject("time"));
+ if (entry.getKey() == RocketmqConsumerOffset.class) {
+ Assertions.assertEquals(topic, result.getString("group_name"));
+ Assertions.assertEquals(topic, result.getString("topic_name"));
+ Assertions.assertEquals("0", result.getString("queue_id"));
+ expectedSample |= result.getLong("value_consumer_offset") == 2L
+ && result.getLong("value_broker_offset") == 3L && result.getLong("value_offset_lag") == 1L;
+ } else if (entry.getKey() == RocketmqConsumerConnectionNumber.class) {
+ Assertions.assertEquals(topic, result.getString("group_name"));
+ long expected = ((RocketmqConsumerConnectionNumber) rows.get(entry.getKey()).get(0)).getValueConnectionCount();
+ Assertions.assertEquals(expected, result.getLong("value_connection_count"));
+ expectedSample = expected == 1;
+ } else if (entry.getKey() == RocketmqTopicMessagesIn.class || entry.getKey() == RocketmqGroupMessagesOut.class) {
+ String observedTopic = result.getString("topic_name");
+ String window = result.getString("window");
+ String group = entry.getKey() == RocketmqGroupMessagesOut.class ? result.getString("group_name") : "";
+ Object expected = rows.get(entry.getKey()).stream().filter(row -> {
+ if (row instanceof RocketmqTopicMessagesIn incoming) {
+ return observedTopic.equals(incoming.getTopicName()) && window.equals(incoming.getWindow());
+ }
+ RocketmqGroupMessagesOut outgoing = (RocketmqGroupMessagesOut) row;
+ return observedTopic.equals(outgoing.getTopicName()) && window.equals(outgoing.getWindow())
+ && group.equals(outgoing.getGroupName());
+ }).findFirst().orElseThrow();
+ long countValue = expected instanceof RocketmqTopicMessagesIn incoming
+ ? incoming.getValueWindowCount() : ((RocketmqGroupMessagesOut) expected).getValueWindowCount();
+ Assertions.assertEquals(countValue, result.getLong("value_window_count"));
+ Assertions.assertEquals(((RuntimeFloatValue) expected).getValue(), result.getFloat("value"));
+ Assertions.assertTrue(dimensions.add(observedTopic + ":" + group + ":" + window));
+ expectedSample = true;
+ } else {
+ String dimension = assertBrokerReadback(result, entry.getKey(), rows.get(entry.getKey()));
+ Assertions.assertTrue(dimensions.add(dimension), "Duplicate persisted dimension " + dimension);
+ expectedSample = true;
+ }
+ }
+ Assertions.assertEquals(rows.get(entry.getKey()).size(), count, entry.getValue());
+ Assertions.assertTrue(expectedSample, "Missing expected sample in " + entry.getValue());
+ LoggerFactory.getLogger(RocketMQCollectTest.class).info(
+ "IoTDB readback verified database={} table={} runtime={} topic={} rows={}",
+ database, entry.getValue(), runtime.getId(), topic, count);
+ }
+ }
+ }
+ } finally {
+ var source = (com.alibaba.druid.pool.DruidDataSource)
+ org.apache.commons.lang3.reflect.FieldUtils.readField(engine, "dataSource", true);
+ if (source != null) {
+ source.close();
+ }
+ if (configuredDatabase == null) {
+ try (var connection = java.sql.DriverManager.getConnection(
+ "jdbc:iotdb://" + address + "/?sql_dialect=table", "root", "root");
+ var statement = connection.createStatement()) {
+ statement.execute("drop database if exists " + database);
+ }
+ }
+ }
+ }
+
+ private String assertBrokerReadback(java.sql.ResultSet result, Class<?> model, List<Object> expectedRows) throws Exception {
+ if (Set.of(RocketmqConsumerLagLatency.class, RocketmqConsumerSuccessTps.class, RocketmqConsumerFailedTps.class, RocketmqConsumerProcessTime.class, RocketmqStorageDiskUsage.class, RocketmqStorageDiskFreeBytes.class).contains(model)) {
+ var tags = org.apache.commons.lang3.reflect.FieldUtils.getAllFieldsList(model).stream()
+ .filter(field -> field.isAnnotationPresent(ReportTag.class)).toList();
+ List<Object> matches = new java.util.ArrayList<>();
+ for (Object row : expectedRows) {
+ boolean match = true;
+ for (var tag : tags) {
+ String column = tag.getName().replaceAll("([a-z])([A-Z])", "$1_$2").toLowerCase(java.util.Locale.ROOT);
+ Object value = org.apache.commons.lang3.reflect.FieldUtils.readField(tag, row, true);
+ match &= java.util.Objects.equals(value == null ? null : value.toString(), result.getString(column));
+ }
+ if (match) {
+ matches.add(row);
+ }
+ }
+ Assertions.assertEquals(1, matches.size());
+ Object row = matches.get(0);
+ if (row instanceof RuntimeFloatValue numeric) {
+ Assertions.assertEquals(numeric.getValue(), result.getFloat("value"));
+ } else {
+ Assertions.assertEquals(((RuntimeLongValue) row).getValue(), result.getLong("value"));
+ }
+ Assertions.assertFalse(result.wasNull());
+ return row.toString();
+ }
+ String dimension = "broker";
+ Object expected;
+ if (model == RocketmqBrokerMessagesIn.class || model == RocketmqBrokerMessagesOut.class) {
+ String window = result.getString("window");
+ expected = expectedRows.stream().filter(row -> window.equals(brokerWindow(row))).findFirst().orElseThrow();
+ Assertions.assertEquals(brokerWindowCount(expected).longValue(), result.getLong("value_window_count"));
+ Assertions.assertFalse(result.wasNull());
+ Assertions.assertEquals(((RuntimeFloatValue) expected).getValue().floatValue(), result.getFloat("value"));
+ Assertions.assertFalse(result.wasNull());
+ dimension = window;
+ } else {
+ if (model == RocketmqThreadPoolWartermark.class) {
+ String pool = result.getString("pool_name");
+ expected = expectedRows.stream().map(RocketmqThreadPoolWartermark.class::cast)
+ .filter(row -> pool.equals(row.getPoolName())).findFirst().orElseThrow();
+ dimension = pool;
+ } else {
+ Assertions.assertEquals(1, expectedRows.size());
+ expected = expectedRows.get(0);
+ }
+ Assertions.assertEquals(((RuntimeLongValue) expected).getValue().longValue(), result.getLong("value"));
+ Assertions.assertFalse(result.wasNull());
+ }
+ LoggerFactory.getLogger(RocketMQCollectTest.class).info("IoTDB Broker sample verified model={} dimension={} value={}",
+ model.getSimpleName(), dimension, result.getObject("value"));
+ return dimension;
+ }
+
+ private void assertSuccess(RemotingCommand response) {
+ Assertions.assertEquals(ResponseCode.SUCCESS, response.getCode(), response.getRemark());
+ }
+
+ private void prepareBroker(DefaultRemotingClient client, String topic) throws Exception {
+ // 真实 Broker 转发请求到已注册连接;客户端响应采用固定测试样本,不代表业务压力测试。
+ client.registerProcessor(RequestCode.GET_CONSUMER_RUNNING_INFO, new org.apache.rocketmq.remoting.netty.NettyRequestProcessor() {
+ @Override
+ public RemotingCommand processRequest(io.netty.channel.ChannelHandlerContext context, RemotingCommand request) {
+ RemotingCommand response = RemotingCommand.createResponseCommand(ResponseCode.SUCCESS, null);
+ response.setBody(RemotingSerializable.encode(Map.of("statusTable", Map.of(topic,
+ Map.of("consumeOKTPS", 2.5, "consumeFailedTPS", 0.5, "consumeRT", 12.25)))));
+ return response;
+ }
+
+ @Override
+ public boolean rejectRequest() {
+ return false;
+ }
+ }, null);
+ CreateTopicRequestHeader create = new CreateTopicRequestHeader();
+ create.setTopic(topic);
+ create.setDefaultTopic("TBW102");
+ create.setReadQueueNums(1);
+ create.setWriteQueueNums(1);
+ create.setPerm(6);
+ create.setTopicFilterType("SINGLE_TAG");
+ assertSuccess(client.invokeSync(RemotingCommand.createRequestCommand(RequestCode.UPDATE_AND_CREATE_TOPIC, create), 3000));
+ HeartbeatData heartbeat = new HeartbeatData();
+ heartbeat.setClientID(topic);
+ ProducerData producer = new ProducerData();
+ producer.setGroupName(topic);
+ heartbeat.getProducerDataSet().add(producer);
+ RemotingCommand beat = RemotingCommand.createRequestCommand(RequestCode.HEART_BEAT, null);
+ beat.setBody(RemotingSerializable.encode(heartbeat));
+ assertSuccess(client.invokeSync(beat, 3000));
+ for (int i = 0; i < 3; i++) {
+ SendMessageRequestHeader header = new SendMessageRequestHeader();
+ header.setProducerGroup(topic);
+ header.setTopic(topic);
+ header.setDefaultTopic(topic);
+ header.setDefaultTopicQueueNums(1);
+ header.setQueueId(0);
+ header.setSysFlag(0);
+ header.setBornTimestamp(System.currentTimeMillis());
+ header.setFlag(0);
+ header.setReconsumeTimes(0);
+ header.setUnitMode(false);
+ header.setBatch(false);
+ RemotingCommand send = RemotingCommand.createRequestCommand(RequestCode.SEND_MESSAGE, header);
+ send.setBody(("iotdb-metric-" + i).getBytes(StandardCharsets.UTF_8));
+ assertSuccess(client.invokeSync(send, 3000));
+ }
+ SubscriptionGroupConfig group = new SubscriptionGroupConfig();
+ group.setGroupName(topic);
+ RemotingCommand createGroup = RemotingCommand.createRequestCommand(RequestCode.UPDATE_AND_CREATE_SUBSCRIPTIONGROUP, null);
+ createGroup.setBody(RemotingSerializable.encode(group));
+ assertSuccess(client.invokeSync(createGroup, 3000));
+ org.apache.rocketmq.remoting.protocol.heartbeat.ConsumerData consumer =
+ new org.apache.rocketmq.remoting.protocol.heartbeat.ConsumerData();
+ consumer.setGroupName(topic);
+ consumer.setConsumeType(org.apache.rocketmq.remoting.protocol.heartbeat.ConsumeType.CONSUME_ACTIVELY);
+ consumer.setMessageModel(org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel.CLUSTERING);
+ consumer.setConsumeFromWhere(org.apache.rocketmq.common.consumer.ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
+ consumer.getSubscriptionDataSet().add(org.apache.rocketmq.remoting.protocol.filter.FilterAPI.buildSubscriptionData(topic, "*"));
+ heartbeat.getConsumerDataSet().add(consumer);
+ RemotingCommand consumerBeat = RemotingCommand.createRequestCommand(RequestCode.HEART_BEAT, null);
+ consumerBeat.setVersion(org.apache.rocketmq.common.MQVersion.CURRENT_VERSION);
+ consumerBeat.setBody(RemotingSerializable.encode(heartbeat));
+ assertSuccess(client.invokeSync(consumerBeat, 3000));
+ org.apache.rocketmq.remoting.protocol.header.PullMessageRequestHeader pull =
+ new org.apache.rocketmq.remoting.protocol.header.PullMessageRequestHeader();
+ pull.setConsumerGroup(topic);
+ pull.setTopic(topic);
+ pull.setQueueId(0);
+ pull.setQueueOffset(0L);
+ pull.setMaxMsgNums(2);
+ pull.setSysFlag(org.apache.rocketmq.common.sysflag.PullSysFlag.buildSysFlag(false, false, true, false));
+ pull.setCommitOffset(0L);
+ pull.setSuspendTimeoutMillis(0L);
+ pull.setSubscription("*");
+ pull.setSubVersion(System.currentTimeMillis());
+ pull.setExpressionType("TAG");
+ assertSuccess(client.invokeSync(RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, pull), 3000));
+ UpdateConsumerOffsetRequestHeader committed = new UpdateConsumerOffsetRequestHeader();
+ committed.setConsumerGroup(topic);
+ committed.setTopic(topic);
+ committed.setQueueId(0);
+ committed.setCommitOffset(2L);
+ assertSuccess(client.invokeSync(RemotingCommand.createRequestCommand(RequestCode.UPDATE_CONSUMER_OFFSET, committed), 3000));
+ }
+
+ private RuntimeMetadata runtime() {
+ RuntimeMetadata runtime = new RuntimeMetadata();
+ runtime.setId(System.nanoTime());
+ runtime.setClusterId(1L);
+ runtime.setClusterType(ClusterType.STORAGE_ROCKETMQ_BROKER_MAIN_SLAVE);
+ return runtime;
+ }
+
+
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@eventmesh.apache.org
For additional commands, e-mail: commits-help@eventmesh.apache.org