This is an automated email from the ASF dual-hosted git repository.

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git


The following commit(s) were added to refs/heads/rocketmq-studio by this push:
     new 64bae563 feat: read cluster, broker, client and dashboard data from a 
live cluster (#797)
64bae563 is described below

commit 64bae563b532054764cec4d6646032fa8dc04d98
Author: lizhimins <[email protected]>
AuthorDate: Mon Aug 3 14:32:29 2026 +0800

    feat: read cluster, broker, client and dashboard data from a live cluster 
(#797)
---
 .../studio/cluster/broker/ClusterService.java      |  76 ++++
 .../studio/cluster/client/ClientProviderStub.java  |   2 -
 .../ops/dashboard/DashboardProviderStub.java       |   7 +-
 .../studio/rocketmq/RocketMQAdminClientImpl.java   | 486 +++++++++++++++++++++
 .../studio/rocketmq/RocketMQAdminConfig.java       |  44 ++
 .../rocketmq/RocketMQBrokerConfigService.java      | 113 +++++
 .../studio/rocketmq/RocketMQClientProvider.java    | 264 +++++++++++
 .../studio/rocketmq/RocketMQClusterProvider.java   | 250 +++++++++++
 .../studio/rocketmq/RocketMQDashboardProvider.java | 280 ++++++++++++
 .../RocketMQProperties.java}                       |  21 +-
 10 files changed, 1525 insertions(+), 18 deletions(-)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterService.java
 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterService.java
index 45393dc1..a2f0eec3 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterService.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterService.java
@@ -29,11 +29,13 @@ import 
org.apache.rocketmq.studio.cluster.proxy.RestartProxyDTO;
 import org.apache.rocketmq.studio.common.domain.enums.ClusterStatus;
 import org.apache.rocketmq.studio.common.domain.enums.FlushDiskType;
 import org.apache.rocketmq.studio.common.exception.BusinessException;
+import org.apache.rocketmq.studio.rocketmq.RocketMQBrokerConfigService;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 
 import java.util.List;
+import java.util.Properties;
 
 @Slf4j
 @Service
@@ -42,18 +44,53 @@ public class ClusterService {
 
     private final ClusterRepository clusterRepository;
     private final ClusterProvider clusterProvider;
+    private final RocketMQBrokerConfigService brokerConfigService;
 
     public List<ClusterVO> listClusters() {
         log.info("Listing all clusters");
+        List<ClusterVO> discovered = clusterProvider.discoverClusters();
+        if (discovered != null && !discovered.isEmpty()) {
+            discovered.forEach(this::enrichWithLiveConfig);
+            return discovered;
+        }
         return clusterRepository.findAll();
     }
 
     public ClusterVO getCluster(String id) {
         log.info("Getting cluster detail: {}", id);
+        ClusterVO live = clusterProvider.refreshClusterDetail(id);
+        if (live != null) {
+            enrichWithLiveConfig(live);
+            return live;
+        }
         return clusterRepository.findById(id)
                 .orElseThrow(() -> new BusinessException(404, "Cluster not 
found: " + id));
     }
 
+    /**
+     * Attach live broker configuration (read from the first reachable master 
broker via the
+     * admin API) to a discovered cluster. Falls back to the persisted config, 
if any, when the
+     * live read is unavailable.
+     */
+    private void enrichWithLiveConfig(ClusterVO cluster) {
+        if (cluster.getBrokers() != null) {
+            for (BrokerVO broker : cluster.getBrokers()) {
+                if (broker.getAddr() != null && !broker.getAddr().isEmpty()) {
+                    try {
+                        
cluster.setConfig(brokerConfigService.getBrokerConfig(broker.getAddr()));
+                        return;
+                    } catch (Exception e) {
+                        log.warn("Failed to read live config from broker {}: 
{}",
+                                broker.getAddr(), e.getMessage());
+                    }
+                }
+            }
+        }
+        if (cluster.getConfig() == null && cluster.getId() != null) {
+            clusterRepository.findById(cluster.getId()).ifPresent(stored -> 
cluster.setConfig(stored.getConfig()));
+        }
+    }
+
     public ClusterVO updateClusterConfig(UpdateConfigDTO command) {
         log.info("Updating cluster config for: {}", command.getId());
         ClusterVO cluster = clusterRepository.findById(command.getId())
@@ -86,6 +123,16 @@ public class ClusterService {
             config.setBrokerPermission(command.getBrokerPermission());
         }
 
+        // Push config to live brokers via admin API
+        if (cluster.getBrokers() != null && !cluster.getBrokers().isEmpty()) {
+            Properties brokerProps = buildBrokerProperties(command);
+            for (BrokerVO broker : cluster.getBrokers()) {
+                if (broker.getAddr() != null && !broker.getAddr().isEmpty()) {
+                    brokerConfigService.updateBrokerConfig(broker.getAddr(), 
command.getId(), brokerProps);
+                }
+            }
+        }
+
         clusterRepository.updateConfig(command.getId(), config);
         cluster.setConfig(config);
         log.info("Cluster config updated successfully for: {}", 
command.getId());
@@ -110,6 +157,35 @@ public class ClusterService {
                 .build();
     }
 
+    private Properties buildBrokerProperties(UpdateConfigDTO command) {
+        Properties props = new Properties();
+        if (command.getFlushDiskType() != null) {
+            props.setProperty("flushDiskType", command.getFlushDiskType());
+        }
+        if (command.getAutoCreateTopicEnable() != null) {
+            props.setProperty("autoCreateTopicEnable", 
command.getAutoCreateTopicEnable().toString());
+        }
+        if (command.getAutoCreateSubscriptionGroup() != null) {
+            props.setProperty("autoCreateSubscriptionGroup", 
command.getAutoCreateSubscriptionGroup().toString());
+        }
+        if (command.getMaxMessageSize() != null) {
+            props.setProperty("maxMessageSize", 
command.getMaxMessageSize().toString());
+        }
+        if (command.getFileReservedTime() != null) {
+            props.setProperty("fileReservedTime", 
command.getFileReservedTime().toString());
+        }
+        if (command.getWriteQueueNums() != null) {
+            props.setProperty("defaultTopicQueueNums", 
command.getWriteQueueNums().toString());
+        }
+        if (command.getReadQueueNums() != null) {
+            props.setProperty("defaultTopicQueueNums", 
command.getReadQueueNums().toString());
+        }
+        if (command.getBrokerPermission() != null) {
+            props.setProperty("brokerPermission", 
command.getBrokerPermission().toString());
+        }
+        return props;
+    }
+
     private FlushDiskType parseFlushDiskType(String value) {
         try {
             return FlushDiskType.valueOf(value);
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/client/ClientProviderStub.java
 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/client/ClientProviderStub.java
index f399226f..8ca03c03 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/client/ClientProviderStub.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/client/ClientProviderStub.java
@@ -18,12 +18,10 @@ package org.apache.rocketmq.studio.cluster.client;
 
 import org.apache.rocketmq.studio.common.exception.BusinessException;
 import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Component;
 
 import java.util.List;
 
 @Slf4j
-@Component
 public class ClientProviderStub implements ClientProvider {
 
     @Override
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/ops/dashboard/DashboardProviderStub.java
 
b/server/src/main/java/org/apache/rocketmq/studio/ops/dashboard/DashboardProviderStub.java
index 83c67f8a..1400f8ef 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/ops/dashboard/DashboardProviderStub.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/ops/dashboard/DashboardProviderStub.java
@@ -19,10 +19,13 @@ package org.apache.rocketmq.studio.ops.dashboard;
 
 import org.apache.rocketmq.studio.common.exception.BusinessException;
 import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Component;
 
+/**
+ * Fails explicitly when no real dashboard provider is configured (trunk #699).
+ *
+ * <p>Not registered as a Spring bean: {@code RocketMQDashboardProvider} 
serves live data.
+ */
 @Slf4j
-@Component
 public class DashboardProviderStub implements DashboardProvider {
 
     @Override
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/rocketmq/RocketMQAdminClientImpl.java
 
b/server/src/main/java/org/apache/rocketmq/studio/rocketmq/RocketMQAdminClientImpl.java
new file mode 100644
index 00000000..69a0d039
--- /dev/null
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/rocketmq/RocketMQAdminClientImpl.java
@@ -0,0 +1,486 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.rocketmq;
+
+import org.apache.rocketmq.client.producer.DefaultMQProducer;
+import org.apache.rocketmq.client.producer.SendResult;
+import org.apache.rocketmq.common.TopicConfig;
+import org.apache.rocketmq.common.message.Message;
+import org.apache.rocketmq.remoting.protocol.body.ClusterInfo;
+import org.apache.rocketmq.remoting.protocol.route.BrokerData;
+import 
org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig;
+import org.apache.rocketmq.studio.common.exception.BusinessException;
+import org.apache.rocketmq.studio.instance.group.ConsumerGroupVO;
+import org.apache.rocketmq.studio.instance.topic.AdminClient;
+import org.apache.rocketmq.studio.instance.topic.SendMessageDTO;
+import org.apache.rocketmq.studio.instance.topic.SendMessageVO;
+import org.apache.rocketmq.studio.instance.topic.TopicVO;
+import org.apache.rocketmq.studio.ops.audit.AuditService;
+import org.apache.rocketmq.studio.persistence.entity.RmqGroup;
+import org.apache.rocketmq.studio.persistence.entity.RmqTopic;
+import org.apache.rocketmq.studio.persistence.mapper.RmqGroupMapper;
+import org.apache.rocketmq.studio.persistence.mapper.RmqTopicMapper;
+import org.apache.rocketmq.tools.admin.DefaultMQAdminExt;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Primary;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+import java.nio.charset.StandardCharsets;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Real AdminClient implementation backed by DefaultMQAdminExt.
+ * Provides topic CRUD, message sending, consumer group CRUD, and offset reset.
+ */
+@Service
+@Primary
+public class RocketMQAdminClientImpl implements AdminClient {
+
+    private static final Logger log = 
LoggerFactory.getLogger(RocketMQAdminClientImpl.class);
+
+    private final DefaultMQAdminExt adminExt;
+    private final RocketMQProperties properties;
+    private final RmqTopicMapper topicMapper;
+    private final RmqGroupMapper groupMapper;
+    private final AuditService auditService;
+
+    @Autowired
+    public RocketMQAdminClientImpl(
+            @Autowired(required = false) DefaultMQAdminExt adminExt,
+            RocketMQProperties properties,
+            RmqTopicMapper topicMapper,
+            RmqGroupMapper groupMapper,
+            AuditService auditService) {
+        this.adminExt = adminExt;
+        this.properties = properties;
+        this.topicMapper = topicMapper;
+        this.groupMapper = groupMapper;
+        this.auditService = auditService;
+    }
+
+    @Override
+    public TopicVO getTopic(String name) {
+        if (adminExt == null) {
+            throw new BusinessException(503, "RocketMQ admin not connected");
+        }
+        try {
+            var routeData = adminExt.examineTopicRouteInfo(name);
+            if (routeData == null || routeData.getQueueDatas() == null || 
routeData.getQueueDatas().isEmpty()) {
+                throw new BusinessException(404, "Topic not found: " + name);
+            }
+            var qd = routeData.getQueueDatas().get(0);
+            TopicVO vo = new TopicVO();
+            vo.setId(name);
+            vo.setName(name);
+            vo.setWriteQueues(qd.getWriteQueueNums());
+            vo.setReadQueues(qd.getReadQueueNums());
+            return vo;
+        } catch (BusinessException e) {
+            throw e;
+        } catch (Exception e) {
+            throw new BusinessException(500, "Failed to get topic: " + 
e.getMessage());
+        }
+    }
+
+    @Override
+    public ConsumerGroupVO getConsumerGroup(String name) {
+        if (adminExt == null) {
+            throw new BusinessException(503, "RocketMQ admin not connected");
+        }
+        ConsumerGroupVO vo = new ConsumerGroupVO();
+        vo.setId(name);
+        vo.setName(name);
+        try {
+            var conn = adminExt.examineConsumerConnectionInfo(name);
+            if (conn != null) {
+                if (conn.getConnectionSet() != null) {
+                    vo.setOnlineInstances(conn.getConnectionSet().size());
+                }
+                if (conn.getSubscriptionTable() != null) {
+                    vo.setSubscribedTopics(new 
ArrayList<>(conn.getSubscriptionTable().keySet()));
+                }
+            }
+        } catch (Exception ignored) {
+            // Group may be offline
+        }
+        return vo;
+    }
+
+    @Override
+    public TopicVO createTopic(TopicVO topic) {
+        if (adminExt == null) {
+            throw new BusinessException(503, "RocketMQ admin not connected");
+        }
+
+        String topicName = topic.getName();
+        int writeQueues = topic.getWriteQueues() > 0 ? topic.getWriteQueues() 
: 8;
+        int readQueues = topic.getReadQueues() > 0 ? topic.getReadQueues() : 8;
+
+        try {
+            Set<String> brokerAddrs = getAllMasterBrokerAddrs();
+            if (brokerAddrs.isEmpty()) {
+                throw new BusinessException(500, "No broker available to 
create topic");
+            }
+
+            TopicConfig topicConfig = new TopicConfig();
+            topicConfig.setTopicName(topicName);
+            topicConfig.setWriteQueueNums(writeQueues);
+            topicConfig.setReadQueueNums(readQueues);
+            topicConfig.setPerm(6); // RW
+
+            for (String addr : brokerAddrs) {
+                adminExt.createAndUpdateTopicConfig(addr, topicConfig);
+            }
+
+            // Persist to DB. Re-creating a topic that already has a record 
(for example when
+            // rebuilding a broker route from the console) must update it 
instead of failing on
+            // the unique (cluster_id, name) key.
+            String clusterName = getClusterName();
+            RmqTopic entity = topicMapper.selectOne(new 
LambdaQueryWrapper<RmqTopic>()
+                    .eq(RmqTopic::getClusterId, clusterName)
+                    .eq(RmqTopic::getName, topicName));
+            boolean isNew = entity == null;
+            if (isNew) {
+                entity = new RmqTopic();
+                entity.setName(topicName);
+                entity.setClusterId(clusterName);
+                entity.setCreatedAt(LocalDateTime.now());
+            }
+            entity.setTopicType(topic.getType() != null ? 
topic.getType().name() : "NORMAL");
+            entity.setReadQueueNums(readQueues);
+            entity.setWriteQueueNums(writeQueues);
+            entity.setPerm(6);
+            if (StringUtils.hasText(topic.getRemark())) {
+                entity.setRemark(topic.getRemark());
+            }
+            entity.setStatus("ACTIVE");
+            entity.setUpdatedAt(LocalDateTime.now());
+            if (isNew) {
+                topicMapper.insert(entity);
+            } else {
+                topicMapper.updateById(entity);
+            }
+
+            auditService.record("CREATE_TOPIC", topicName,
+                    "queues=" + writeQueues + "/" + readQueues, "SUCCESS");
+
+            topic.setId(topicName);
+            topic.setWriteQueues(writeQueues);
+            topic.setReadQueues(readQueues);
+            return topic;
+        } catch (BusinessException e) {
+            auditService.record("CREATE_TOPIC", topicName, e.getMessage(), 
"FAILED");
+            throw e;
+        } catch (Exception e) {
+            auditService.record("CREATE_TOPIC", topicName, e.getMessage(), 
"FAILED");
+            throw new BusinessException(500, "Failed to create topic: " + 
e.getMessage());
+        }
+    }
+
+    @Override
+    public TopicVO updateTopic(TopicVO topic) {
+        if (adminExt == null) {
+            throw new BusinessException(503, "RocketMQ admin not connected");
+        }
+
+        String topicName = topic.getName();
+        int writeQueues = topic.getWriteQueues() > 0 ? topic.getWriteQueues() 
: 8;
+        int readQueues = topic.getReadQueues() > 0 ? topic.getReadQueues() : 8;
+
+        try {
+            Set<String> brokerAddrs = getAllMasterBrokerAddrs();
+            if (brokerAddrs.isEmpty()) {
+                throw new BusinessException(500, "No broker available to 
update topic");
+            }
+
+            TopicConfig topicConfig = new TopicConfig();
+            topicConfig.setTopicName(topicName);
+            topicConfig.setWriteQueueNums(writeQueues);
+            topicConfig.setReadQueueNums(readQueues);
+            topicConfig.setPerm(6); // RW
+
+            for (String addr : brokerAddrs) {
+                adminExt.createAndUpdateTopicConfig(addr, topicConfig);
+            }
+
+            // Update DB record
+            RmqTopic existing = topicMapper.selectOne(
+                    new LambdaQueryWrapper<RmqTopic>().eq(RmqTopic::getName, 
topicName));
+            if (existing != null) {
+                existing.setWriteQueueNums(writeQueues);
+                existing.setReadQueueNums(readQueues);
+                existing.setUpdatedAt(LocalDateTime.now());
+                topicMapper.updateById(existing);
+            }
+
+            auditService.record("UPDATE_TOPIC", topicName,
+                    "queues=" + writeQueues + "/" + readQueues, "SUCCESS");
+
+            topic.setId(topicName);
+            topic.setWriteQueues(writeQueues);
+            topic.setReadQueues(readQueues);
+            return topic;
+        } catch (BusinessException e) {
+            auditService.record("UPDATE_TOPIC", topicName, e.getMessage(), 
"FAILED");
+            throw e;
+        } catch (Exception e) {
+            auditService.record("UPDATE_TOPIC", topicName, e.getMessage(), 
"FAILED");
+            throw new BusinessException(500, "Failed to update topic: " + 
e.getMessage());
+        }
+    }
+
+    @Override
+    public void deleteTopic(String name) {
+        if (adminExt == null) {
+            throw new BusinessException(503, "RocketMQ admin not connected");
+        }
+
+        try {
+            Set<String> brokerAddrs = getAllMasterBrokerAddrs();
+
+            // Delete from brokers
+            if (!brokerAddrs.isEmpty()) {
+                adminExt.deleteTopicInBroker(brokerAddrs, name);
+            }
+
+            // Delete from nameserver
+            String namesrvAddr = properties.getNamesrvAddr();
+            if (namesrvAddr != null && !namesrvAddr.isEmpty()) {
+                Set<String> nsAddrs = new HashSet<>();
+                for (String addr : namesrvAddr.split("[;,]")) {
+                    String trimmed = addr.trim();
+                    if (!trimmed.isEmpty()) {
+                        nsAddrs.add(trimmed);
+                    }
+                }
+                adminExt.deleteTopicInNameServer(nsAddrs, getClusterName(), 
name);
+            }
+
+            // Delete from DB
+            topicMapper.delete(new 
LambdaQueryWrapper<RmqTopic>().eq(RmqTopic::getName, name));
+
+            auditService.record("DELETE_TOPIC", name, "", "SUCCESS");
+        } catch (BusinessException e) {
+            auditService.record("DELETE_TOPIC", name, e.getMessage(), 
"FAILED");
+            throw e;
+        } catch (Exception e) {
+            auditService.record("DELETE_TOPIC", name, e.getMessage(), 
"FAILED");
+            throw new BusinessException(500, "Failed to delete topic: " + 
e.getMessage());
+        }
+    }
+
+    @Override
+    public SendMessageVO sendMessage(SendMessageDTO request) {
+        if (adminExt == null) {
+            throw new BusinessException(503, "RocketMQ admin not connected");
+        }
+
+        String namesrvAddr = properties.getNamesrvAddr();
+        if (namesrvAddr == null || namesrvAddr.isEmpty()) {
+            throw new BusinessException(500, "Nameserver address not 
configured");
+        }
+
+        DefaultMQProducer producer = new 
DefaultMQProducer("studio_msg_sender_" + System.currentTimeMillis());
+        producer.setNamesrvAddr(namesrvAddr);
+        producer.setSendMsgTimeout(5000);
+
+        try {
+            producer.start();
+
+            String topic = request.getTopic();
+            String tag = request.getTag() != null ? request.getTag() : "";
+            String key = request.getKey() != null ? request.getKey() : "";
+            String body = request.getBody() != null ? request.getBody() : "";
+
+            String fullTopic = tag.isEmpty() ? topic : topic + ":" + tag;
+            Message msg = new Message(topic, tag, key, 
body.getBytes(StandardCharsets.UTF_8));
+
+            // Add custom properties
+            if (request.getProperties() != null) {
+                for (Map.Entry<String, String> entry : 
request.getProperties().entrySet()) {
+                    msg.putUserProperty(entry.getKey(), entry.getValue());
+                }
+            }
+
+            SendResult sendResult = producer.send(msg);
+
+            auditService.record("SEND_MESSAGE", topic,
+                    "tag=" + tag + ", key=" + key + ", msgId=" + 
sendResult.getMsgId(), "SUCCESS");
+
+            return SendMessageVO.builder()
+                    .msgId(sendResult.getMsgId())
+                    .sendTime(System.currentTimeMillis())
+                    .offsetMsgId(sendResult.getOffsetMsgId())
+                    .build();
+        } catch (Exception e) {
+            auditService.record("SEND_MESSAGE", request.getTopic(), 
e.getMessage(), "FAILED");
+            throw new BusinessException(500, "Failed to send message: " + 
e.getMessage());
+        } finally {
+            producer.shutdown();
+        }
+    }
+
+    @Override
+    public ConsumerGroupVO createConsumerGroup(ConsumerGroupVO group) {
+        if (adminExt == null) {
+            throw new BusinessException(503, "RocketMQ admin not connected");
+        }
+
+        String groupName = group.getName();
+
+        try {
+            Set<String> brokerAddrs = getAllMasterBrokerAddrs();
+            if (brokerAddrs.isEmpty()) {
+                throw new BusinessException(500, "No broker available to 
create consumer group");
+            }
+
+            SubscriptionGroupConfig config = new SubscriptionGroupConfig();
+            config.setGroupName(groupName);
+            config.setConsumeEnable(true);
+            config.setConsumeBroadcastEnable(true);
+            config.setRetryQueueNums(1);
+            config.setRetryMaxTimes(group.getRetryMaxTimes() > 0 ? 
group.getRetryMaxTimes() : 16);
+
+            for (String addr : brokerAddrs) {
+                adminExt.createAndUpdateSubscriptionGroupConfig(addr, config);
+            }
+
+            // Persist to DB, upserting so re-creating an existing group does 
not violate the
+            // unique (cluster_id, name) key.
+            String groupClusterName = getClusterName();
+            RmqGroup entity = groupMapper.selectOne(new 
LambdaQueryWrapper<RmqGroup>()
+                    .eq(RmqGroup::getClusterId, groupClusterName)
+                    .eq(RmqGroup::getName, groupName));
+            boolean isNewGroup = entity == null;
+            if (isNewGroup) {
+                entity = new RmqGroup();
+                entity.setName(groupName);
+                entity.setClusterId(groupClusterName);
+                entity.setCreatedAt(LocalDateTime.now());
+            }
+            entity.setConsumeType(group.getConsumeType() != null ? 
group.getConsumeType().name() : "CLUSTERING");
+            entity.setMessageModel(group.getSubscriptionMode() != null ? 
group.getSubscriptionMode().name() : "Push");
+            entity.setMaxRetry(config.getRetryMaxTimes());
+            entity.setStatus("ACTIVE");
+            entity.setUpdatedAt(LocalDateTime.now());
+            if (isNewGroup) {
+                groupMapper.insert(entity);
+            } else {
+                groupMapper.updateById(entity);
+            }
+
+            auditService.record("CREATE_GROUP", groupName,
+                    "retryMaxTimes=" + config.getRetryMaxTimes(), "SUCCESS");
+
+            group.setId(groupName);
+            return group;
+        } catch (BusinessException e) {
+            auditService.record("CREATE_GROUP", groupName, e.getMessage(), 
"FAILED");
+            throw e;
+        } catch (Exception e) {
+            auditService.record("CREATE_GROUP", groupName, e.getMessage(), 
"FAILED");
+            throw new BusinessException(500, "Failed to create consumer group: 
" + e.getMessage());
+        }
+    }
+
+    @Override
+    public void deleteConsumerGroup(String name) {
+        if (adminExt == null) {
+            throw new BusinessException(503, "RocketMQ admin not connected");
+        }
+
+        try {
+            Set<String> brokerAddrs = getAllMasterBrokerAddrs();
+
+            for (String addr : brokerAddrs) {
+                adminExt.deleteSubscriptionGroup(addr, name, true);
+            }
+
+            // Delete from DB
+            groupMapper.delete(new 
LambdaQueryWrapper<RmqGroup>().eq(RmqGroup::getName, name));
+
+            auditService.record("DELETE_GROUP", name, "", "SUCCESS");
+        } catch (BusinessException e) {
+            auditService.record("DELETE_GROUP", name, e.getMessage(), 
"FAILED");
+            throw e;
+        } catch (Exception e) {
+            auditService.record("DELETE_GROUP", name, e.getMessage(), 
"FAILED");
+            throw new BusinessException(500, "Failed to delete consumer group: 
" + e.getMessage());
+        }
+    }
+
+    @Override
+    public void resetOffset(String name, long timestamp, String topic) {
+        if (adminExt == null) {
+            throw new BusinessException(503, "RocketMQ admin not connected");
+        }
+
+        try {
+            adminExt.resetOffsetByTimestamp(getClusterName(), topic, name, 
timestamp, false);
+            auditService.record("RESET_OFFSET", name,
+                    "topic=" + topic + ", timestamp=" + timestamp, "SUCCESS");
+        } catch (Exception e) {
+            auditService.record("RESET_OFFSET", name, e.getMessage(), 
"FAILED");
+            throw new BusinessException(500, "Failed to reset offset: " + 
e.getMessage());
+        }
+    }
+
+    // ── Helper methods ──────────────────────────────────────────────────
+
+    private Set<String> getAllMasterBrokerAddrs() throws Exception {
+        Set<String> addrs = new HashSet<>();
+        ClusterInfo clusterInfo = adminExt.examineBrokerClusterInfo();
+        if (clusterInfo == null || clusterInfo.getBrokerAddrTable() == null) {
+            return addrs;
+        }
+
+        for (BrokerData brokerData : 
clusterInfo.getBrokerAddrTable().values()) {
+            if (brokerData.getBrokerAddrs() == null) {
+                continue;
+            }
+            // Use master address (brokerId = 0) preferentially
+            String masterAddr = brokerData.getBrokerAddrs().get(0L);
+            if (masterAddr == null && !brokerData.getBrokerAddrs().isEmpty()) {
+                masterAddr = 
brokerData.getBrokerAddrs().values().iterator().next();
+            }
+            if (masterAddr != null) {
+                addrs.add(masterAddr);
+            }
+        }
+        return addrs;
+    }
+
+    private String getClusterName() {
+        try {
+            ClusterInfo clusterInfo = adminExt.examineBrokerClusterInfo();
+            if (clusterInfo != null && clusterInfo.getClusterAddrTable() != 
null
+                    && !clusterInfo.getClusterAddrTable().isEmpty()) {
+                return 
clusterInfo.getClusterAddrTable().keySet().iterator().next();
+            }
+        } catch (Exception ignored) {
+        }
+        return "DefaultCluster";
+    }
+}
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/rocketmq/RocketMQAdminConfig.java
 
b/server/src/main/java/org/apache/rocketmq/studio/rocketmq/RocketMQAdminConfig.java
new file mode 100644
index 00000000..8cf4e7f5
--- /dev/null
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/rocketmq/RocketMQAdminConfig.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.rocketmq;
+
+import org.apache.rocketmq.tools.admin.DefaultMQAdminExt;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import 
org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@EnableConfigurationProperties(RocketMQProperties.class)
+public class RocketMQAdminConfig {
+
+    private static final Logger log = 
LoggerFactory.getLogger(RocketMQAdminConfig.class);
+
+    @Bean(initMethod = "start", destroyMethod = "shutdown")
+    @ConditionalOnProperty(prefix = "studio.rocketmq", name = "namesrv-addr")
+    public DefaultMQAdminExt defaultMQAdminExt(
+            @Value("${studio.rocketmq.namesrv-addr:}") String namesrvAddr) {
+        log.info("Initializing DefaultMQAdminExt with namesrvAddr=[{}]", 
namesrvAddr);
+        DefaultMQAdminExt adminExt = new DefaultMQAdminExt();
+        adminExt.setNamesrvAddr(namesrvAddr);
+        adminExt.setInstanceName("studio-admin-" + System.currentTimeMillis());
+        return adminExt;
+    }
+}
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/rocketmq/RocketMQBrokerConfigService.java
 
b/server/src/main/java/org/apache/rocketmq/studio/rocketmq/RocketMQBrokerConfigService.java
new file mode 100644
index 00000000..67415674
--- /dev/null
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/rocketmq/RocketMQBrokerConfigService.java
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.rocketmq;
+
+import org.apache.rocketmq.studio.cluster.config.ClusterConfigVO;
+import org.apache.rocketmq.studio.common.domain.enums.FlushDiskType;
+import org.apache.rocketmq.studio.common.exception.BusinessException;
+import org.apache.rocketmq.studio.ops.audit.AuditService;
+import org.apache.rocketmq.tools.admin.DefaultMQAdminExt;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.lang.Nullable;
+import org.springframework.stereotype.Service;
+
+import java.util.Properties;
+
+@Slf4j
+@Service
+public class RocketMQBrokerConfigService {
+
+    private final DefaultMQAdminExt adminExt;
+    private final AuditService auditService;
+
+    public RocketMQBrokerConfigService(@Nullable DefaultMQAdminExt adminExt, 
AuditService auditService) {
+        this.adminExt = adminExt;
+        this.auditService = auditService;
+    }
+
+    /**
+     * Read broker config from the live broker via admin API.
+     */
+    public ClusterConfigVO getBrokerConfig(String brokerAddr) {
+        requireAdmin();
+        try {
+            Properties props = adminExt.getBrokerConfig(brokerAddr);
+            return mapToClusterConfigVO(props);
+        } catch (Exception e) {
+            log.error("Failed to get broker config from {}", brokerAddr, e);
+            throw new BusinessException(500, "Failed to get broker config: " + 
e.getMessage());
+        }
+    }
+
+    /**
+     * Update broker config on the live broker via admin API, then record 
audit.
+     */
+    public void updateBrokerConfig(String brokerAddr, String clusterId, 
Properties newConfig) {
+        requireAdmin();
+        try {
+            adminExt.updateBrokerConfig(brokerAddr, newConfig);
+            String detail = "brokerAddr=" + brokerAddr + ", config=" + 
newConfig;
+            auditService.record("UPDATE_BROKER_CONFIG", "CLUSTER:" + 
clusterId, detail, "SUCCESS");
+            log.info("Broker config updated successfully: {}", brokerAddr);
+        } catch (Exception e) {
+            log.error("Failed to update broker config at {}", brokerAddr, e);
+            String detail = "brokerAddr=" + brokerAddr + ", error=" + 
e.getMessage();
+            auditService.record("UPDATE_BROKER_CONFIG", "CLUSTER:" + 
clusterId, detail, "FAILURE");
+            throw new BusinessException(500, "Failed to update broker config: 
" + e.getMessage());
+        }
+    }
+
+    private ClusterConfigVO mapToClusterConfigVO(Properties props) {
+        ClusterConfigVO vo = new ClusterConfigVO();
+        
vo.setFlushDiskType(parseFlushDiskType(props.getProperty("flushDiskType", 
"ASYNC_FLUSH")));
+        
vo.setAutoCreateTopicEnable(Boolean.parseBoolean(props.getProperty("autoCreateTopicEnable",
 "true")));
+        
vo.setAutoCreateSubscriptionGroup(Boolean.parseBoolean(props.getProperty("autoCreateSubscriptionGroup",
 "true")));
+        vo.setMaxMessageSize(parseIntSafe(props.getProperty("maxMessageSize"), 
4194304));
+        
vo.setWriteQueueNums(parseIntSafe(props.getProperty("defaultTopicQueueNums"), 
8));
+        
vo.setReadQueueNums(parseIntSafe(props.getProperty("defaultTopicQueueNums"), 
8));
+        
vo.setFileReservedTime(parseIntSafe(props.getProperty("fileReservedTime"), 72));
+        
vo.setBrokerPermission(parseIntSafe(props.getProperty("brokerPermission"), 6));
+        vo.setDeleteWhen(props.getProperty("deleteWhen", "04"));
+        vo.setMsgTraceTopicName(props.getProperty("msgTraceTopicName", 
"RMQ_SYS_TRACE_TOPIC"));
+        return vo;
+    }
+
+    private FlushDiskType parseFlushDiskType(String value) {
+        try {
+            return FlushDiskType.valueOf(value);
+        } catch (IllegalArgumentException e) {
+            return FlushDiskType.ASYNC_FLUSH;
+        }
+    }
+
+    private int parseIntSafe(String value, int defaultValue) {
+        if (value == null || value.isEmpty()) {
+            return defaultValue;
+        }
+        try {
+            return Integer.parseInt(value.trim());
+        } catch (NumberFormatException e) {
+            return defaultValue;
+        }
+    }
+
+    private void requireAdmin() {
+        if (adminExt == null) {
+            throw new BusinessException(503, "RocketMQ admin is not 
configured. Set studio.rocketmq.namesrv-addr.");
+        }
+    }
+}
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/rocketmq/RocketMQClientProvider.java
 
b/server/src/main/java/org/apache/rocketmq/studio/rocketmq/RocketMQClientProvider.java
new file mode 100644
index 00000000..c2d3d389
--- /dev/null
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/rocketmq/RocketMQClientProvider.java
@@ -0,0 +1,264 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.rocketmq;
+
+import org.apache.rocketmq.remoting.protocol.LanguageCode;
+import org.apache.rocketmq.remoting.protocol.body.ClusterInfo;
+import org.apache.rocketmq.remoting.protocol.body.Connection;
+import org.apache.rocketmq.remoting.protocol.body.ConsumerConnection;
+import org.apache.rocketmq.remoting.protocol.body.ProducerConnection;
+import org.apache.rocketmq.remoting.protocol.body.SubscriptionGroupWrapper;
+import org.apache.rocketmq.remoting.protocol.body.TopicList;
+import org.apache.rocketmq.remoting.protocol.route.BrokerData;
+import org.apache.rocketmq.studio.cluster.client.ClientConnectionVO;
+import org.apache.rocketmq.studio.cluster.client.ClientProvider;
+import org.apache.rocketmq.studio.common.domain.enums.ClientLanguage;
+import org.apache.rocketmq.studio.common.domain.enums.ClientType;
+import org.apache.rocketmq.studio.common.domain.enums.Protocol;
+import org.apache.rocketmq.tools.admin.DefaultMQAdminExt;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.context.annotation.Primary;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
+/**
+ * Live {@link ClientProvider} backed by the RocketMQ admin API. It discovers 
producer
+ * connections by scanning non-system topics and consumer connections by 
scanning
+ * subscription groups across all brokers in the cluster.
+ */
+@Slf4j
+@Service
+@Primary
+public class RocketMQClientProvider implements ClientProvider {
+
+    /**
+     * Upper bound on the number of non-system topics scanned for producer 
connections,
+     * to avoid issuing an admin call per topic on clusters with a large topic 
count.
+     */
+    private static final int MAX_PRODUCER_TOPIC_SCAN = 50;
+
+    private static final long SUBSCRIPTION_GROUP_TIMEOUT_MILLIS = 5000L;
+
+    private final ObjectProvider<DefaultMQAdminExt> adminExtProvider;
+
+    public RocketMQClientProvider(ObjectProvider<DefaultMQAdminExt> 
adminExtProvider) {
+        this.adminExtProvider = adminExtProvider;
+    }
+
+    @Override
+    public List<ClientConnectionVO> findConnections(String clusterId, String 
type) {
+        DefaultMQAdminExt adminExt = adminExtProvider.getIfAvailable();
+        if (adminExt == null) {
+            log.warn("DefaultMQAdminExt is not configured, returning empty 
client connection list");
+            return List.of();
+        }
+
+        ClientType clientType = parseType(type);
+        List<ClientConnectionVO> connections = new ArrayList<>();
+        if (clientType == null || clientType == ClientType.Producer) {
+            connections.addAll(findProducerConnections(adminExt, clusterId));
+        }
+        if (clientType == null || clientType == ClientType.Consumer) {
+            connections.addAll(findConsumerConnections(adminExt, clusterId));
+        }
+        return connections;
+    }
+
+    private List<ClientConnectionVO> findProducerConnections(DefaultMQAdminExt 
adminExt, String clusterId) {
+        List<ClientConnectionVO> result = new ArrayList<>();
+        Set<String> topics;
+        try {
+            TopicList topicList = adminExt.fetchAllTopicList();
+            topics = topicList == null ? Set.of() : topicList.getTopicList();
+        } catch (Exception e) {
+            log.warn("Failed to fetch topic list for producer connection 
scan", e);
+            return result;
+        }
+
+        int scanned = 0;
+        for (String topic : topics) {
+            if (isSystemTopic(topic)) {
+                continue;
+            }
+            if (scanned >= MAX_PRODUCER_TOPIC_SCAN) {
+                log.info("Producer connection scan capped at {} non-system 
topics", MAX_PRODUCER_TOPIC_SCAN);
+                break;
+            }
+            scanned++;
+            try {
+                ProducerConnection producerConnection = 
adminExt.examineProducerConnectionInfo(null, topic);
+                if (producerConnection == null || 
producerConnection.getConnectionSet() == null) {
+                    continue;
+                }
+                for (Connection connection : 
producerConnection.getConnectionSet()) {
+                    result.add(toConnectionVO(connection, ClientType.Producer, 
topic, topic, clusterId));
+                }
+            } catch (Exception e) {
+                log.warn("Failed to examine producer connection for topic={}, 
skipping", topic, e);
+            }
+        }
+        return result;
+    }
+
+    private List<ClientConnectionVO> findConsumerConnections(DefaultMQAdminExt 
adminExt, String clusterId) {
+        List<ClientConnectionVO> result = new ArrayList<>();
+        Set<String> groups = collectSubscriptionGroups(adminExt);
+        for (String group : groups) {
+            if (isSystemGroup(group)) {
+                continue;
+            }
+            try {
+                ConsumerConnection consumerConnection = 
adminExt.examineConsumerConnectionInfo(group);
+                if (consumerConnection == null || 
consumerConnection.getConnectionSet() == null) {
+                    continue;
+                }
+                for (Connection connection : 
consumerConnection.getConnectionSet()) {
+                    result.add(toConnectionVO(connection, ClientType.Consumer, 
group, null, clusterId));
+                }
+            } catch (Exception e) {
+                log.warn("Failed to examine consumer connection for group={}, 
skipping", group, e);
+            }
+        }
+        return result;
+    }
+
+    private Set<String> collectSubscriptionGroups(DefaultMQAdminExt adminExt) {
+        Set<String> groups = new LinkedHashSet<>();
+        ClusterInfo clusterInfo;
+        try {
+            clusterInfo = adminExt.examineBrokerClusterInfo();
+        } catch (Exception e) {
+            log.warn("Failed to fetch cluster info for consumer connection 
scan", e);
+            return groups;
+        }
+        if (clusterInfo == null || clusterInfo.getBrokerAddrTable() == null) {
+            return groups;
+        }
+        for (BrokerData brokerData : 
clusterInfo.getBrokerAddrTable().values()) {
+            String brokerAddr = brokerData.selectBrokerAddr();
+            if (brokerAddr == null) {
+                continue;
+            }
+            try {
+                SubscriptionGroupWrapper wrapper =
+                        adminExt.getAllSubscriptionGroup(brokerAddr, 
SUBSCRIPTION_GROUP_TIMEOUT_MILLIS);
+                if (wrapper != null && wrapper.getSubscriptionGroupTable() != 
null) {
+                    
groups.addAll(wrapper.getSubscriptionGroupTable().keySet());
+                }
+            } catch (Exception e) {
+                log.warn("Failed to fetch subscription groups from broker={}, 
skipping", brokerAddr, e);
+            }
+        }
+        return groups;
+    }
+
+    private ClientConnectionVO toConnectionVO(Connection connection, 
ClientType type, String groupOrTopic,
+                                              String producerGroup, String 
clusterId) {
+        return ClientConnectionVO.builder()
+                .clientId(connection.getClientId())
+                .type(type)
+                .groupOrTopic(groupOrTopic)
+                .producerGroup(producerGroup)
+                .protocol(Protocol.Remoting)
+                .address(connection.getClientAddr())
+                .language(mapLanguage(connection.getLanguage()))
+                .version(String.valueOf(connection.getVersion()))
+                .clusterName(clusterId)
+                .build();
+    }
+
+    private ClientType parseType(String type) {
+        if (type == null || type.isBlank()) {
+            return null;
+        }
+        try {
+            return ClientType.valueOf(type.trim());
+        } catch (IllegalArgumentException e) {
+            String normalized = type.trim().toLowerCase(Locale.ROOT);
+            if (normalized.startsWith("prod")) {
+                return ClientType.Producer;
+            }
+            if (normalized.startsWith("cons")) {
+                return ClientType.Consumer;
+            }
+            log.warn("Unknown client type filter: {}", type);
+            return null;
+        }
+    }
+
+    private ClientLanguage mapLanguage(LanguageCode languageCode) {
+        if (languageCode == null) {
+            return null;
+        }
+        switch (languageCode) {
+            case JAVA:
+                return ClientLanguage.Java;
+            case GO:
+                return ClientLanguage.Go;
+            case PYTHON:
+                return ClientLanguage.Python;
+            case RUST:
+                return ClientLanguage.Rust;
+            case CPP:
+                return ClientLanguage.Cpp;
+            case DOTNET:
+                return ClientLanguage.CSharp;
+            case PHP:
+                return ClientLanguage.PHP;
+            default:
+                return null;
+        }
+    }
+
+    private boolean isSystemTopic(String topic) {
+        if (topic == null) {
+            return true;
+        }
+        return topic.startsWith("RMQ_SYS_")
+                || topic.startsWith("SCHEDULE_TOPIC_")
+                || topic.startsWith("%RETRY%")
+                || topic.startsWith("%DLQ%")
+                || topic.startsWith("TBW102")
+                || topic.startsWith("SELF_TEST_")
+                || topic.startsWith("DefaultCluster")
+                || topic.startsWith("broker_")
+                || topic.startsWith("OFFSET_MOVED_")
+                || topic.startsWith("CID_RMQ_SYS_")
+                || topic.startsWith("TRANS_CHECK_")
+                || topic.startsWith("BenchmarkTest");
+    }
+
+    private boolean isSystemGroup(String group) {
+        if (group == null) {
+            return true;
+        }
+        return group.startsWith("CID_RMQ_SYS_")
+                || group.startsWith("CID_ONSAPI_")
+                || group.startsWith("TOOLS_CONSUMER")
+                || group.startsWith("FILTERSRV_CONSUMER")
+                || group.startsWith("CID_SYS_")
+                || group.startsWith("%RETRY%")
+                || group.startsWith("SELF_TEST_")
+                || group.startsWith("CID_HOUSEKEEPING");
+    }
+}
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/rocketmq/RocketMQClusterProvider.java
 
b/server/src/main/java/org/apache/rocketmq/studio/rocketmq/RocketMQClusterProvider.java
new file mode 100644
index 00000000..f85d874b
--- /dev/null
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/rocketmq/RocketMQClusterProvider.java
@@ -0,0 +1,250 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.rocketmq;
+
+import org.apache.rocketmq.remoting.protocol.body.ClusterInfo;
+import org.apache.rocketmq.remoting.protocol.body.KVTable;
+import org.apache.rocketmq.remoting.protocol.route.BrokerData;
+import org.apache.rocketmq.studio.cluster.broker.BrokerVO;
+import org.apache.rocketmq.studio.cluster.broker.ClusterProvider;
+import org.apache.rocketmq.studio.cluster.broker.ClusterVO;
+import org.apache.rocketmq.studio.cluster.nameserver.NameServerVO;
+import org.apache.rocketmq.studio.common.domain.enums.BrokerStatus;
+import org.apache.rocketmq.studio.common.domain.enums.ClusterStatus;
+import org.apache.rocketmq.tools.admin.DefaultMQAdminExt;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Primary;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Real cluster discovery implementation using DefaultMQAdminExt.
+ * Falls back to returning empty results when adminExt is not configured or 
connection fails.
+ */
+@Service
+@Primary
+public class RocketMQClusterProvider implements ClusterProvider {
+
+    private static final Logger log = 
LoggerFactory.getLogger(RocketMQClusterProvider.class);
+
+    private final DefaultMQAdminExt adminExt;
+    private final RocketMQProperties properties;
+
+    @Autowired
+    public RocketMQClusterProvider(
+            @Autowired(required = false) DefaultMQAdminExt adminExt,
+            RocketMQProperties properties) {
+        this.adminExt = adminExt;
+        this.properties = properties;
+    }
+
+    @Override
+    public List<ClusterVO> discoverClusters() {
+        if (adminExt == null) {
+            log.debug("DefaultMQAdminExt not configured, returning empty 
cluster list");
+            return Collections.emptyList();
+        }
+
+        try {
+            ClusterInfo clusterInfo = adminExt.examineBrokerClusterInfo();
+            if (clusterInfo == null || clusterInfo.getClusterAddrTable() == 
null) {
+                return Collections.emptyList();
+            }
+
+            Map<String, Set<String>> clusterAddrTable = 
clusterInfo.getClusterAddrTable();
+            Map<String, BrokerData> brokerAddrTable = 
clusterInfo.getBrokerAddrTable();
+
+            List<ClusterVO> clusters = new ArrayList<>();
+            for (Map.Entry<String, Set<String>> entry : 
clusterAddrTable.entrySet()) {
+                String clusterName = entry.getKey();
+                Set<String> brokerNames = entry.getValue();
+
+                List<BrokerVO> brokers = buildBrokerList(brokerNames, 
brokerAddrTable);
+                List<NameServerVO> nameServers = buildNameServerList();
+
+                ClusterVO cluster = ClusterVO.builder()
+                        .name(clusterName)
+                        .status(ClusterStatus.healthy)
+                        .brokers(brokers)
+                        .nameServers(nameServers)
+                        .build();
+                cluster.setId(clusterName);
+                clusters.add(cluster);
+            }
+            return clusters;
+        } catch (Exception e) {
+            log.warn("Failed to discover clusters via NameServer: {}", 
e.getMessage());
+            return Collections.emptyList();
+        }
+    }
+
+    @Override
+    public ClusterVO refreshClusterDetail(String clusterId) {
+        if (adminExt == null) {
+            log.debug("DefaultMQAdminExt not configured, cannot refresh 
cluster detail");
+            return null;
+        }
+
+        try {
+            ClusterInfo clusterInfo = adminExt.examineBrokerClusterInfo();
+            if (clusterInfo == null || clusterInfo.getClusterAddrTable() == 
null) {
+                return null;
+            }
+
+            Map<String, Set<String>> clusterAddrTable = 
clusterInfo.getClusterAddrTable();
+            Map<String, BrokerData> brokerAddrTable = 
clusterInfo.getBrokerAddrTable();
+
+            Set<String> brokerNames = clusterAddrTable.get(clusterId);
+            if (brokerNames == null) {
+                return null;
+            }
+
+            List<BrokerVO> brokers = buildBrokerList(brokerNames, 
brokerAddrTable);
+            List<NameServerVO> nameServers = buildNameServerList();
+
+            ClusterVO cluster = ClusterVO.builder()
+                    .name(clusterId)
+                    .status(ClusterStatus.healthy)
+                    .brokers(brokers)
+                    .nameServers(nameServers)
+                    .build();
+            cluster.setId(clusterId);
+            return cluster;
+        } catch (Exception e) {
+            log.warn("Failed to refresh cluster detail for {}: {}", clusterId, 
e.getMessage());
+            return null;
+        }
+    }
+
+    private List<BrokerVO> buildBrokerList(Set<String> brokerNames,
+                                           Map<String, BrokerData> 
brokerAddrTable) {
+        List<BrokerVO> brokers = new ArrayList<>();
+        if (brokerNames == null || brokerAddrTable == null) {
+            return brokers;
+        }
+
+        for (String brokerName : brokerNames) {
+            BrokerData brokerData = brokerAddrTable.get(brokerName);
+            if (brokerData == null || brokerData.getBrokerAddrs() == null) {
+                continue;
+            }
+
+            // Use master address (brokerId = 0) preferentially
+            String masterAddr = brokerData.getBrokerAddrs().get(0L);
+            if (masterAddr == null && !brokerData.getBrokerAddrs().isEmpty()) {
+                masterAddr = 
brokerData.getBrokerAddrs().values().iterator().next();
+            }
+            if (masterAddr == null) {
+                continue;
+            }
+
+            BrokerVO.BrokerVOBuilder builder = BrokerVO.builder()
+                    .name(brokerName)
+                    .addr(masterAddr)
+                    .status(BrokerStatus.running);
+
+            // Try to get runtime info for version and TPS
+            enrichBrokerWithRuntimeInfo(builder, masterAddr);
+
+            brokers.add(builder.build());
+        }
+        return brokers;
+    }
+
+    private void enrichBrokerWithRuntimeInfo(BrokerVO.BrokerVOBuilder builder, 
String brokerAddr) {
+        try {
+            KVTable runtimeInfo = adminExt.fetchBrokerRuntimeStats(brokerAddr);
+            if (runtimeInfo == null || runtimeInfo.getTable() == null) {
+                return;
+            }
+
+            Map<String, String> table = runtimeInfo.getTable();
+
+            String version = table.getOrDefault("brokerVersionDesc",
+                    table.getOrDefault("rocketmqVersion", ""));
+            if (!version.isEmpty()) {
+                builder.version(version);
+            }
+
+            // Parse TPS from runtime stats
+            String putTps = table.get("putTps");
+            if (putTps != null && !putTps.isEmpty()) {
+                builder.tpsIn(parseFirstTpsValue(putTps));
+            }
+
+            String getTransferredTps = table.get("getTransferedTps");
+            if (getTransferredTps != null && !getTransferredTps.isEmpty()) {
+                builder.tpsOut(parseFirstTpsValue(getTransferredTps));
+            }
+
+            // Disk usage
+            String diskRatio = table.get("commitLogDiskRatio");
+            if (diskRatio != null && !diskRatio.isEmpty()) {
+                try {
+                    builder.diskUsage(Double.parseDouble(diskRatio));
+                } catch (NumberFormatException ignored) {
+                    // keep default
+                }
+            }
+        } catch (Exception e) {
+            log.debug("Failed to get runtime info for broker at {}: {}", 
brokerAddr, e.getMessage());
+        }
+    }
+
+    /**
+     * TPS properties are space-separated values representing 10s/1min/10min 
averages.
+     * Parse the first value (10s average).
+     */
+    private int parseFirstTpsValue(String tpsStr) {
+        try {
+            String[] parts = tpsStr.split(" ");
+            if (parts.length > 0) {
+                return (int) Double.parseDouble(parts[0]);
+            }
+        } catch (NumberFormatException ignored) {
+            // fall through
+        }
+        return 0;
+    }
+
+    private List<NameServerVO> buildNameServerList() {
+        List<NameServerVO> nameServers = new ArrayList<>();
+        String namesrvAddr = properties.getNamesrvAddr();
+        if (namesrvAddr == null || namesrvAddr.isEmpty()) {
+            return nameServers;
+        }
+
+        String[] addrs = namesrvAddr.split("[;,]");
+        for (String addr : addrs) {
+            String trimmed = addr.trim();
+            if (!trimmed.isEmpty()) {
+                nameServers.add(NameServerVO.builder()
+                        .addr(trimmed)
+                        .status(ClusterStatus.healthy)
+                        .build());
+            }
+        }
+        return nameServers;
+    }
+}
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/rocketmq/RocketMQDashboardProvider.java
 
b/server/src/main/java/org/apache/rocketmq/studio/rocketmq/RocketMQDashboardProvider.java
new file mode 100644
index 00000000..c6365f5d
--- /dev/null
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/rocketmq/RocketMQDashboardProvider.java
@@ -0,0 +1,280 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.rocketmq;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.rocketmq.remoting.protocol.body.ClusterInfo;
+import org.apache.rocketmq.remoting.protocol.body.KVTable;
+import org.apache.rocketmq.remoting.protocol.body.SubscriptionGroupWrapper;
+import org.apache.rocketmq.remoting.protocol.body.TopicList;
+import org.apache.rocketmq.remoting.protocol.route.BrokerData;
+import 
org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig;
+import org.apache.rocketmq.studio.common.domain.enums.ClusterStatus;
+import org.apache.rocketmq.studio.common.domain.enums.ClusterType;
+import org.apache.rocketmq.studio.ops.dashboard.ClusterOverviewVO;
+import org.apache.rocketmq.studio.ops.dashboard.DashboardDataVO;
+import org.apache.rocketmq.studio.ops.dashboard.DashboardProvider;
+import org.apache.rocketmq.studio.ops.dashboard.DashboardStatsVO;
+import org.apache.rocketmq.tools.admin.DefaultMQAdminExt;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.annotation.Primary;
+import org.springframework.lang.Nullable;
+import org.springframework.stereotype.Service;
+
+@Service
+@Primary
+public class RocketMQDashboardProvider implements DashboardProvider {
+
+    private static final Logger log = 
LoggerFactory.getLogger(RocketMQDashboardProvider.class);
+
+    private static final Set<String> SYSTEM_TOPIC_PREFIXES = Set.of(
+            "rmq_sys_", "SCHEDULE_TOPIC_", "RMQ_SYS_", "broker_", "%RETRY%", 
"%DLQ%",
+            "TBW102", "SELF_TEST_TOPIC", "BenchmarkTest", "OFFSET_MOVED_EVENT",
+            "DefaultCluster", "broker-a", "broker-b"
+    );
+
+    private final DefaultMQAdminExt adminExt;
+
+    public RocketMQDashboardProvider(@Nullable DefaultMQAdminExt adminExt) {
+        this.adminExt = adminExt;
+    }
+
+    @Override
+    public DashboardDataVO getDashboardData() {
+        if (adminExt == null) {
+            log.warn("DefaultMQAdminExt not available, returning empty 
dashboard");
+            return emptyDashboard();
+        }
+
+        int totalClusters = 0;
+        int totalBrokers = 0;
+        int totalTopics = 0;
+        int totalGroups = 0;
+        long tpsIn = 0;
+        long tpsOut = 0;
+        long messagesToday = 0;
+        List<ClusterOverviewVO> clusters = new ArrayList<>();
+
+        try {
+            ClusterInfo clusterInfo = adminExt.examineBrokerClusterInfo();
+            Map<String, Set<String>> clusterAddrTable = 
clusterInfo.getClusterAddrTable();
+            Map<String, BrokerData> brokerAddrTable = 
clusterInfo.getBrokerAddrTable();
+
+            totalClusters = clusterAddrTable.size();
+            totalBrokers = brokerAddrTable.size();
+
+            // Collect all unique broker addresses (master only, brokerId=0)
+            Set<String> masterAddrs = new HashSet<>();
+            for (BrokerData brokerData : brokerAddrTable.values()) {
+                String masterAddr = brokerData.getBrokerAddrs().get(0L);
+                if (masterAddr != null) {
+                    masterAddrs.add(masterAddr);
+                }
+            }
+
+            // Count topics
+            try {
+                TopicList topicList = adminExt.fetchAllTopicList();
+                Set<String> topics = topicList.getTopicList();
+                totalTopics = (int) topics.stream()
+                        .filter(t -> !isSystemTopic(t))
+                        .count();
+            } catch (Exception e) {
+                log.warn("Failed to fetch topic list: {}", e.getMessage());
+            }
+
+            // Count subscription groups and collect TPS from each master 
broker
+            Set<String> allGroups = new HashSet<>();
+            for (String brokerAddr : masterAddrs) {
+                try {
+                    SubscriptionGroupWrapper subscriptionGroupWrapper = 
adminExt.getAllSubscriptionGroup(brokerAddr, 5000);
+                    if (subscriptionGroupWrapper != null && 
subscriptionGroupWrapper.getSubscriptionGroupTable() != null) {
+                        for (Map.Entry<String, SubscriptionGroupConfig> entry :
+                                
subscriptionGroupWrapper.getSubscriptionGroupTable().entrySet()) {
+                            String groupName = entry.getKey();
+                            if (!isSystemGroup(groupName)) {
+                                allGroups.add(groupName);
+                            }
+                        }
+                    }
+                } catch (Exception e) {
+                    log.warn("Failed to get subscription groups from broker 
{}: {}", brokerAddr, e.getMessage());
+                }
+
+                // Get runtime stats for TPS
+                try {
+                    KVTable runtimeInfo = 
adminExt.fetchBrokerRuntimeStats(brokerAddr);
+                    if (runtimeInfo != null && runtimeInfo.getTable() != null) 
{
+                        Map<String, String> table = runtimeInfo.getTable();
+                        tpsIn += parseTps(table.get("putTps"));
+                        tpsOut += parseTps(table.get("getTransferedTps"));
+
+                        String msgPutToday = 
table.get("msgPutTotalTodayMorning");
+                        if (msgPutToday != null) {
+                            try {
+                                messagesToday += 
Long.parseLong(msgPutToday.trim());
+                            } catch (NumberFormatException ignored) {
+                            }
+                        }
+                    }
+                } catch (Exception e) {
+                    log.warn("Failed to get runtime info from broker {}: {}", 
brokerAddr, e.getMessage());
+                }
+            }
+            totalGroups = allGroups.size();
+
+            // Build per-cluster overview
+            for (Map.Entry<String, Set<String>> clusterEntry : 
clusterAddrTable.entrySet()) {
+                String clusterName = clusterEntry.getKey();
+                Set<String> brokerNames = clusterEntry.getValue();
+                int clusterBrokers = 0;
+                int clusterTpsIn = 0;
+                int clusterTpsOut = 0;
+                String version = "unknown";
+
+                for (String brokerName : brokerNames) {
+                    BrokerData brokerData = brokerAddrTable.get(brokerName);
+                    if (brokerData != null) {
+                        clusterBrokers++;
+                        String masterAddr = 
brokerData.getBrokerAddrs().get(0L);
+                        if (masterAddr != null) {
+                            try {
+                                KVTable rt = 
adminExt.fetchBrokerRuntimeStats(masterAddr);
+                                if (rt != null && rt.getTable() != null) {
+                                    clusterTpsIn += (int) 
parseTps(rt.getTable().get("putTps"));
+                                    clusterTpsOut += (int) 
parseTps(rt.getTable().get("getTransferedTps"));
+                                    String v = 
rt.getTable().get("brokerVersionDesc");
+                                    if (v != null && 
!"unknown".equals(version)) {
+                                        version = v;
+                                    }
+                                }
+                            } catch (Exception ignored) {
+                            }
+                        }
+                    }
+                }
+
+                clusters.add(ClusterOverviewVO.builder()
+                        .id(clusterName)
+                        .name(clusterName)
+                        .type(ClusterType.V5_PROXY_CLUSTER)
+                        .status(ClusterStatus.healthy)
+                        .brokers(clusterBrokers)
+                        .proxies(0)
+                        .topics(0)
+                        .groups(0)
+                        .tpsIn(clusterTpsIn)
+                        .tpsOut(clusterTpsOut)
+                        .version(version)
+                        .throughput(List.of())
+                        .build());
+            }
+
+        } catch (Exception e) {
+            log.error("Failed to collect dashboard data from RocketMQ 
cluster", e);
+            return emptyDashboard();
+        }
+
+        long messagesPerSecond = tpsIn + tpsOut;
+
+        DashboardStatsVO stats = DashboardStatsVO.builder()
+                .totalClusters(totalClusters)
+                .healthyClusters(totalClusters)
+                .totalBrokers(totalBrokers)
+                .totalProxies(0)
+                .totalNameServers(0)
+                .totalTopics(totalTopics)
+                .totalConsumerGroups(totalGroups)
+                .totalMessagesToday(messagesToday)
+                .messagesPerSecond(messagesPerSecond)
+                .tpsIn(tpsIn)
+                .tpsOut(tpsOut)
+                .build();
+
+        return DashboardDataVO.builder()
+                .stats(stats)
+                .clusters(clusters)
+                .build();
+    }
+
+    private DashboardDataVO emptyDashboard() {
+        DashboardStatsVO stats = DashboardStatsVO.builder()
+                .totalClusters(0)
+                .healthyClusters(0)
+                .totalBrokers(0)
+                .totalProxies(0)
+                .totalNameServers(0)
+                .totalTopics(0)
+                .totalConsumerGroups(0)
+                .totalMessagesToday(0)
+                .messagesPerSecond(0)
+                .tpsIn(0)
+                .tpsOut(0)
+                .build();
+        return DashboardDataVO.builder()
+                .stats(stats)
+                .clusters(List.of())
+                .build();
+    }
+
+    /**
+     * Parse TPS value from RocketMQ runtime stats format: "10minAvg 1minAvg 
10secAvg"
+     * Returns the 1-minute average (second value) as a long.
+     */
+    private long parseTps(String tpsStr) {
+        if (tpsStr == null || tpsStr.isBlank()) {
+            return 0;
+        }
+        try {
+            String[] parts = tpsStr.trim().split("\\s+");
+            if (parts.length >= 2) {
+                return (long) Double.parseDouble(parts[1]);
+            } else if (parts.length == 1) {
+                return (long) Double.parseDouble(parts[0]);
+            }
+        } catch (NumberFormatException e) {
+            log.debug("Failed to parse TPS value: {}", tpsStr);
+        }
+        return 0;
+    }
+
+    private boolean isSystemTopic(String topic) {
+        for (String prefix : SYSTEM_TOPIC_PREFIXES) {
+            if (topic.startsWith(prefix) || topic.equals(prefix)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean isSystemGroup(String group) {
+        return group.startsWith("CID_RMQ_SYS_")
+                || group.startsWith("rmq_sys_")
+                || group.equals("TOOLS_CONSUMER")
+                || group.equals("FILTERSRV_CONSUMER")
+                || group.equals("CID_ONSAPI_OWNER")
+                || group.equals("CID_ONSAPI_PERMISSION")
+                || group.equals("CID_ONSAPI_PULL")
+                || group.startsWith("SELF_TEST_");
+    }
+}
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/ops/dashboard/DashboardProviderStub.java
 
b/server/src/main/java/org/apache/rocketmq/studio/rocketmq/RocketMQProperties.java
similarity index 59%
copy from 
server/src/main/java/org/apache/rocketmq/studio/ops/dashboard/DashboardProviderStub.java
copy to 
server/src/main/java/org/apache/rocketmq/studio/rocketmq/RocketMQProperties.java
index 83c67f8a..5c4bbaac 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/ops/dashboard/DashboardProviderStub.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/rocketmq/RocketMQProperties.java
@@ -14,20 +14,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package org.apache.rocketmq.studio.rocketmq;
 
-package org.apache.rocketmq.studio.ops.dashboard;
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
 
-import org.apache.rocketmq.studio.common.exception.BusinessException;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Component;
-
-@Slf4j
-@Component
-public class DashboardProviderStub implements DashboardProvider {
-
-    @Override
-    public DashboardDataVO getDashboardData() {
-        log.warn("DashboardProviderStub.getDashboardData called without a real 
dashboard provider");
-        throw new BusinessException(501, "Dashboard provider is not 
configured");
-    }
+@Data
+@ConfigurationProperties(prefix = "studio.rocketmq")
+public class RocketMQProperties {
+    private String namesrvAddr;
 }

Reply via email to