/*
 * 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.exporter.task;

import com.alibaba.fastjson.JSON;
import org.apache.commons.lang3.StringUtils;
import org.apache.rocketmq.client.consumer.PullResult;
import org.apache.rocketmq.client.consumer.PullStatus;
import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.MixAll;
import org.apache.rocketmq.common.admin.ConsumeStats;
import org.apache.rocketmq.common.admin.OffsetWrapper;
import org.apache.rocketmq.common.admin.TopicOffset;
import org.apache.rocketmq.common.admin.TopicStatsTable;
import org.apache.rocketmq.common.message.MessageQueue;
import org.apache.rocketmq.common.protocol.ResponseCode;
import org.apache.rocketmq.common.protocol.body.BrokerStatsData;
import org.apache.rocketmq.common.protocol.body.ClusterInfo;
import org.apache.rocketmq.common.protocol.body.Connection;
import org.apache.rocketmq.common.protocol.body.ConsumerConnection;
import org.apache.rocketmq.common.protocol.body.GroupList;
import org.apache.rocketmq.common.protocol.body.KVTable;
import org.apache.rocketmq.common.protocol.body.TopicList;
import org.apache.rocketmq.common.protocol.heartbeat.MessageModel;
import org.apache.rocketmq.common.protocol.route.BrokerData;
import org.apache.rocketmq.common.protocol.route.TopicRouteData;
import org.apache.rocketmq.exporter.config.CollectClientMetricExecutorConfig;
import org.apache.rocketmq.exporter.config.RMQConfigure;
import org.apache.rocketmq.exporter.model.BrokerRuntimeStats;
import org.apache.rocketmq.exporter.model.common.TwoTuple;
import org.apache.rocketmq.exporter.service.RMQMetricsService;
import org.apache.rocketmq.exporter.service.client.MQAdminExtImpl;
import org.apache.rocketmq.exporter.util.Utils;
import org.apache.rocketmq.remoting.exception.RemotingConnectException;
import org.apache.rocketmq.remoting.exception.RemotingException;
import org.apache.rocketmq.remoting.exception.RemotingSendRequestException;
import org.apache.rocketmq.remoting.exception.RemotingTimeoutException;
import org.apache.rocketmq.store.stats.BrokerStatsManager;
import org.apache.rocketmq.tools.admin.MQAdminExt;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

@Component
public class MetricsCollectTask {
    @Resource
    @Qualifier("mqAdminExtImpl")
    private MQAdminExt mqAdminExt;
    @Resource
    private RMQConfigure rmqConfigure;
    @Resource
    @Qualifier("collectClientMetricExecutor")
    private ExecutorService collectClientMetricExecutor;
    @Resource
    private RMQMetricsService metricsService;
    private static String clusterName = null;
    private final static Logger log = LoggerFactory.getLogger(MetricsCollectTask.class);

    private BlockingQueue<Runnable> collectClientTaskBlockQueue;

    @Bean(name = "collectClientMetricExecutor")
    private ExecutorService collectClientMetricExecutor(CollectClientMetricExecutorConfig collectClientMetricExecutorConfig) {
        collectClientTaskBlockQueue = new LinkedBlockingDeque<Runnable>(collectClientMetricExecutorConfig.getQueueSize());
        ExecutorService executorService = new ClientMetricCollectorFixedThreadPoolExecutor(
                collectClientMetricExecutorConfig.getCorePoolSize(),
                collectClientMetricExecutorConfig.getMaximumPoolSize(),
                collectClientMetricExecutorConfig.getKeepAliveTime(),
                TimeUnit.MILLISECONDS,
                this.collectClientTaskBlockQueue,
                new ThreadFactory() {
                    private final AtomicLong threadIndex = new AtomicLong(0);

                    @Override
                    public Thread newThread(Runnable r) {
                        return new Thread(r, "collectClientMetricThread_" + this.threadIndex.incrementAndGet());
                    }
                },
                new ThreadPoolExecutor.DiscardOldestPolicy()
        );
        return executorService;
    }

    @PostConstruct
    public void init() throws InterruptedException, RemotingConnectException, RemotingTimeoutException, RemotingSendRequestException, MQBrokerException {
        log.info("MetricsCollectTask init starting....");
        long start = System.currentTimeMillis();
        ClusterInfo clusterInfo = mqAdminExt.examineBrokerClusterInfo();
        StringBuilder infoOut = new StringBuilder();
        for (String clusterName : clusterInfo.getClusterAddrTable().keySet()) {
            infoOut.append(String.format("cluster name= %s, broker name = %s%n", clusterName, clusterInfo.getClusterAddrTable().get(clusterName)));
            if (clusterName != null && MetricsCollectTask.clusterName == null) {
                MetricsCollectTask.clusterName = clusterName;
            }
        }
        for (String brokerName : clusterInfo.getBrokerAddrTable().keySet()) {
            infoOut.append(String.format("broker name = %s, master broker address= %s%n", brokerName, clusterInfo.getBrokerAddrTable().get(brokerName).getBrokerAddrs().get(MixAll.MASTER_ID)));
        }
        log.info(infoOut.toString());
        if (clusterName == null) {
            log.error("get cluster info error" );
        }
        log.info(String.format("MetricsCollectTask init finished....cost:%d", System.currentTimeMillis() - start));
    }

    @Scheduled(cron = "${task.collectTopicOffset.cron}")
    public void collectTopicOffset() {
        if (!rmqConfigure.isEnableCollect()) {
            return;
        }
        log.info("topic offset collection task starting....");
        long start = System.currentTimeMillis();
        TopicList topicList = null;
        try {
            topicList = mqAdminExt.fetchAllTopicList();
        } catch (Exception ex) {
            log.error(String.format("collectTopicOffset-exception comes getting topic list from namesrv, address is %s",
                    JSON.toJSONString(mqAdminExt.getNameServerAddressList())));
            return;
        }
        Set<String> topicSet = topicList != null ? topicList.getTopicList() : null;
        if (topicSet == null || topicSet.isEmpty()) {
            log.error(String.format("collectTopicOffset-the topic list is empty. the namesrv address is %s",
                    JSON.toJSONString(mqAdminExt.getNameServerAddressList())));
            return;
        }

        for (String topic : topicSet) {
            TopicStatsTable topicStats = null;
            try {
                topicStats = mqAdminExt.examineTopicStats(topic);
            } catch (Exception ex) {
                log.error(String.format("collectTopicOffset-getting topic(%s) stats error. the namesrv address is %s",
                        topic,
                        JSON.toJSONString(mqAdminExt.getNameServerAddressList())));
                continue;
            }

            Set<Map.Entry<MessageQueue, TopicOffset>> topicStatusEntries = topicStats.getOffsetTable().entrySet();
            HashMap<String, Long> brokerOffsetMap = new HashMap<>();
            HashMap<String, Long> brokerUpdateTimestampMap = new HashMap<>();

            for (Map.Entry<MessageQueue, TopicOffset> topicStatusEntry : topicStatusEntries) {
                MessageQueue q = topicStatusEntry.getKey();
                TopicOffset offset = topicStatusEntry.getValue();

                if (brokerOffsetMap.containsKey(q.getBrokerName())) {
                    brokerOffsetMap.put(q.getBrokerName(), brokerOffsetMap.get(q.getBrokerName()) + offset.getMaxOffset());
                } else {
                    brokerOffsetMap.put(q.getBrokerName(), offset.getMaxOffset());
                }

                if (brokerUpdateTimestampMap.containsKey(q.getBrokerName())) {
                    if (offset.getLastUpdateTimestamp() > brokerUpdateTimestampMap.get(q.getBrokerName())) {
                        brokerUpdateTimestampMap.put(q.getBrokerName(), offset.getLastUpdateTimestamp());
                    }
                } else {
                    brokerUpdateTimestampMap.put(q.getBrokerName(), offset.getLastUpdateTimestamp());
                }
            }
            Set<Map.Entry<String, Long>> brokerOffsetEntries = brokerOffsetMap.entrySet();
            for (Map.Entry<String, Long> brokerOffsetEntry : brokerOffsetEntries) {
                metricsService.getCollector().addTopicOffsetMetric(clusterName, brokerOffsetEntry.getKey(), topic,
                        brokerUpdateTimestampMap.get(brokerOffsetEntry.getKey()), brokerOffsetEntry.getValue());
            }
        }
        log.info("topic offset collection task finished...." + (System.currentTimeMillis() - start));
    }

    @Scheduled(cron = "${task.collectConsumerOffset.cron}")
    public void collectConsumerOffset() {
        if (!rmqConfigure.isEnableCollect()) {
            return;
        }
        log.info("consumer offset collection task starting....");
        long start = System.currentTimeMillis();
        TopicList topicList = null;
        try {
            topicList = mqAdminExt.fetchAllTopicList();
        } catch (Exception ex) {
            log.error(String.format("collectConsumerOffset-fetch topic list from namesrv error, the address is %s",
                    JSON.toJSONString(mqAdminExt.getNameServerAddressList())), ex);
            return;
        }


        Set<String> topicSet = topicList.getTopicList();
        for (String topic : topicSet) {
            GroupList groupList = null;

            boolean isDLQTopic = topic.startsWith(MixAll.DLQ_GROUP_TOPIC_PREFIX);
            if (isDLQTopic) {
                continue;
            }
            try {
                groupList = mqAdminExt.queryTopicConsumeByWho(topic);
            } catch (Exception ex) {
                log.warn(String.format("collectConsumerOffset-topic's consumer is empty, %s", topic));
                continue;
            }

            if (groupList == null || groupList.getGroupList() == null || groupList.getGroupList().isEmpty()) {
                log.warn(String.format("no any consumer for topic(%s), ignore this topic", topic));
                continue;
            }


            for (String group : groupList.getGroupList()) {
                ConsumeStats consumeStats = null;
                ConsumerConnection onlineConsumers = null;
                long diff = 0L, totalConsumerOffset = 0L, totalBrokerOffset = 0L;
                int countOfOnlineConsumers = 0;

                double consumeTPS = 0F;
                MessageModel messageModel = MessageModel.CLUSTERING;
                try {
                    onlineConsumers = mqAdminExt.examineConsumerConnectionInfo(group);
                    messageModel = onlineConsumers.getMessageModel();
                } catch (InterruptedException | RemotingException ex) {
                    log.error(String.format("get topic's(%s) online consumers(%s) exception", topic, group), ex);
                } catch (MQClientException ex) {
                    handleTopicNotExistException(ex.getResponseCode(), ex, topic, group);
                } catch (MQBrokerException ex) {
                    handleTopicNotExistException(ex.getResponseCode(), ex, topic, group);
                }
                if (onlineConsumers == null || onlineConsumers.getConnectionSet() == null || onlineConsumers.getConnectionSet().isEmpty()) {
                    log.warn(String.format("no any consumer online. topic=%s, consumer group=%s. ignore this", topic, group));
                    countOfOnlineConsumers = 0;
                } else {
                    countOfOnlineConsumers = onlineConsumers.getConnectionSet().size();
                }
                {
                    String cAddrs = "", localAddrs = "";
                    if (countOfOnlineConsumers > 0) {
                        TwoTuple<String, String> addresses = buildClientAddresses(onlineConsumers.getConnectionSet());
                        cAddrs = addresses.getFirst();
                        localAddrs = addresses.getSecond();
                    }
                    metricsService.getCollector().addGroupCountMetric(group, cAddrs, localAddrs, countOfOnlineConsumers);
                }
                if (countOfOnlineConsumers > 0) {
                    collectClientMetricExecutor.submit(new ClientMetricTaskRunnable(
                            group,
                            onlineConsumers,
                            false,
                            this.mqAdminExt,
                            log,
                            this.metricsService
                    ));
                }
                try {
                    consumeStats = mqAdminExt.examineConsumeStats(group, topic);
                } catch (InterruptedException | RemotingException ex) {
                    log.error(String.format("get topic's(%s) consumer-stats(%s) exception", topic, group), ex);
                } catch (MQClientException ex) {
                    handleTopicNotExistException(ex.getResponseCode(), ex, topic, group);
                } catch (MQBrokerException ex) {
                    handleTopicNotExistException(ex.getResponseCode(), ex, topic, group);
                }
                if (consumeStats == null || consumeStats.getOffsetTable() == null || consumeStats.getOffsetTable().isEmpty()) {
                    log.warn(String.format("no any offset for consumer(%s), topic(%s), ignore this", group, topic));
                    continue;
                }
                {
                    diff = consumeStats.computeTotalDiff();
                    consumeTPS = consumeStats.getConsumeTps();
                    metricsService.getCollector().addGroupDiffMetric(
                            String.valueOf(countOfOnlineConsumers),
                            group,
                            topic,
                            String.valueOf(messageModel.ordinal()),
                            diff
                    );
                    //metricsService.getCollector().addGroupConsumeTPSMetric(topic, group, consumeTPS);
                }
                // get consumer broker offset
                try {
                    HashMap<String, Long> consumeOffsetMap = new HashMap<>();
                    for (Map.Entry<MessageQueue, OffsetWrapper> consumeStatusEntry : consumeStats.getOffsetTable().entrySet()) {
                        MessageQueue q = consumeStatusEntry.getKey();
                        OffsetWrapper offset = consumeStatusEntry.getValue();
                        if (consumeOffsetMap.containsKey(q.getBrokerName())) {
                            consumeOffsetMap.put(q.getBrokerName(), consumeOffsetMap.get(q.getBrokerName()) + offset.getConsumerOffset());
                        } else {
                            consumeOffsetMap.put(q.getBrokerName(), offset.getConsumerOffset());
                        }
                    }
                    for (Map.Entry<String, Long> consumeOffsetEntry : consumeOffsetMap.entrySet()) {
                        metricsService.getCollector().addGroupBrokerTotalOffsetMetric(clusterName,
                                consumeOffsetEntry.getKey(), topic, group, consumeOffsetEntry.getValue());
                    }
                } catch (Exception ex) {
                    log.warn("addGroupBrokerTotalOffsetMetric error", ex);
                }

                // get consumer latency
                try {
                    HashMap<String, Long> consumerLatencyMap = new HashMap<>();
                    for (Map.Entry<MessageQueue, OffsetWrapper> consumeStatusEntry : consumeStats.getOffsetTable().entrySet()) {
                        MessageQueue q = consumeStatusEntry.getKey();
                        OffsetWrapper offset = consumeStatusEntry.getValue();
                        PullResult consumePullResult = ((MQAdminExtImpl) mqAdminExt).queryMsgByOffset(q, offset.getConsumerOffset());
                        long lagTime = 0;
                        if (consumePullResult != null && consumePullResult.getPullStatus() == PullStatus.FOUND) {
                            lagTime = System.currentTimeMillis() - consumePullResult.getMsgFoundList().get(0).getStoreTimestamp();
                            if (offset.getBrokerOffset() == offset.getConsumerOffset()) {
                                lagTime = 0;
                            }
                        } else if (consumePullResult.getPullStatus() == PullStatus.OFFSET_ILLEGAL) {
                            PullResult pullResult = ((MQAdminExtImpl) mqAdminExt).queryMsgByOffset(q, consumePullResult.getMinOffset());
                            if (pullResult != null && pullResult.getPullStatus() == PullStatus.FOUND) {
                                lagTime = System.currentTimeMillis() - consumePullResult.getMsgFoundList().get(0).getStoreTimestamp();
                            }
                        }
                        if (!consumerLatencyMap.containsKey(q.getBrokerName())) {
                            consumerLatencyMap.put(q.getBrokerName(), lagTime > 0 ? lagTime : 0);
                        } else if (lagTime > consumerLatencyMap.get(q.getBrokerName())) {
                            consumerLatencyMap.put(q.getBrokerName(), lagTime);
                        }
                    }
                    for (Map.Entry<String, Long> consumeLatencyEntry : consumerLatencyMap.entrySet()) {
                        metricsService.getCollector().addGroupGetLatencyByStoreTimeMetric(clusterName,
                                consumeLatencyEntry.getKey(), topic, group, consumeLatencyEntry.getValue());
                    }

                } catch (Exception ex) {
                    log.warn("addGroupGetLatencyByStoreTimeMetric error", ex);
                }
            }
        }
        log.info("consumer offset collection task finished...." + (System.currentTimeMillis() - start));
    }

    @Scheduled(cron = "${task.collectBrokerStatsTopic.cron}")
    public void collectBrokerStatsTopic() {
        if (!rmqConfigure.isEnableCollect()) {
            return;
        }
        log.info("broker topic stats collection task starting....");
        long start = System.currentTimeMillis();
        Set<String> topicSet = null;
        try {
            TopicList topicList = mqAdminExt.fetchAllTopicList();
            topicSet = topicList.getTopicList();
        } catch (Exception ex) {
            log.error(String.format("collectBrokerStatsTopic-fetch topic list from namesrv error, the address is %s",
                    JSON.toJSONString(mqAdminExt.getNameServerAddressList())), ex);
            return;
        }
        if (topicSet == null || topicSet.isEmpty()) {
            return;
        }
        ClusterInfo clusterInfo = null;
        try {
            clusterInfo = mqAdminExt.examineBrokerClusterInfo();
        } catch (Exception ex) {
            log.error(String.format("collectBrokerStatsTopic-fetch cluster info exception, the address is %s",
                    JSON.toJSONString(mqAdminExt.getNameServerAddressList())), ex);
            return;
        }

        for (String topic : topicSet) {
            if (topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX) || topic.startsWith(MixAll.DLQ_GROUP_TOPIC_PREFIX)) {
                continue;
            }
            TopicRouteData topicRouteData = null;

            try {
                topicRouteData = mqAdminExt.examineTopicRouteInfo(topic);
            } catch (Exception ex) {
                log.error(String.format("fetch topic route error. ignore %s", topic), ex);
                continue;
            }
            for (BrokerData bd : topicRouteData.getBrokerDatas()) {
                String masterAddr = bd.getBrokerAddrs().get(MixAll.MASTER_ID);
                if (!StringUtils.isBlank(masterAddr)) {
                    BrokerStatsData bsd = null;
                    try {
                        //how many messages has sent for the topic
                        bsd = mqAdminExt.viewBrokerStatsData(masterAddr, BrokerStatsManager.TOPIC_PUT_NUMS, topic);
                        String brokerIP = clusterInfo.getBrokerAddrTable().get(bd.getBrokerName()).getBrokerAddrs().get(MixAll.MASTER_ID);
                        metricsService.getCollector().addTopicPutNumsMetric(
                                bd.getCluster(),
                                bd.getBrokerName(),
                                brokerIP,
                                topic,
                                Utils.getFixedDouble(bsd.getStatsMinute().getTps())
                        );
                    } catch (MQClientException ex) {
                        if (ex.getResponseCode() == ResponseCode.SYSTEM_ERROR) {
                            log.error(String.format("TOPIC_PUT_NUMS-error, topic=%s, master broker=%s, %s", topic, masterAddr, ex.getErrorMessage()));
                        } else {
                            log.error(String.format("TOPIC_PUT_NUMS-error, topic=%s, master broker=%s", topic, masterAddr), ex);
                        }
                    } catch (RemotingTimeoutException | InterruptedException | RemotingSendRequestException | RemotingConnectException ex1) {
                        log.error(String.format("TOPIC_PUT_NUMS-error, topic=%s, master broker=%s", topic, masterAddr), ex1);
                    }
                    try {
                        //how many bytes has sent for the topic
                        bsd = mqAdminExt.viewBrokerStatsData(masterAddr, BrokerStatsManager.TOPIC_PUT_SIZE, topic);
                        String brokerIP = clusterInfo.getBrokerAddrTable().get(bd.getBrokerName()).getBrokerAddrs().get(MixAll.MASTER_ID);
                        metricsService.getCollector().addTopicPutSizeMetric(
                                bd.getCluster(),
                                bd.getBrokerName(),
                                brokerIP,
                                topic,
                                Utils.getFixedDouble(bsd.getStatsMinute().getTps())
                        );
                    } catch (MQClientException ex) {
                        if (ex.getResponseCode() == ResponseCode.SYSTEM_ERROR) {
                            log.error(String.format("TOPIC_PUT_SIZE-error, topic=%s, master broker=%s, %s", topic, masterAddr, ex.getErrorMessage()));
                        } else {
                            log.error(String.format("TOPIC_PUT_SIZE-error, topic=%s, master broker=%s", topic, masterAddr), ex);
                        }
                    } catch (InterruptedException | RemotingConnectException | RemotingTimeoutException | RemotingSendRequestException ex) {
                        log.error(String.format("TOPIC_PUT_SIZE-error, topic=%s, master broker=%s", topic, masterAddr), ex);
                    }
                }
            }

            GroupList groupList = null;
            try {
                groupList = mqAdminExt.queryTopicConsumeByWho(topic);
            } catch (Exception ex) {
                log.error(String.format("collectBrokerStatsTopic-fetch consumers for topic(%s) error, ignore this topic", topic), ex);
                continue;
            }
            if (groupList.getGroupList() == null || groupList.getGroupList().isEmpty()) {
                log.warn(String.format("collectBrokerStatsTopic-topic's consumer is empty, %s", topic));
                continue;
            }
            for (String group : groupList.getGroupList()) {
                for (BrokerData bd : topicRouteData.getBrokerDatas()) {
                    String masterAddr = bd.getBrokerAddrs().get(MixAll.MASTER_ID);
                    if (masterAddr != null) {
                        String statsKey = String.format("%s@%s", topic, group);
                        BrokerStatsData bsd = null;
                        try {
                            //how many messages the consumer has get for the topic
                            bsd = mqAdminExt.viewBrokerStatsData(masterAddr, BrokerStatsManager.GROUP_GET_NUMS, statsKey);
                            metricsService.getCollector().addGroupGetNumsMetric(
                                    bd.getCluster(),
                                    bd.getBrokerName(),
                                    topic,
                                    group,
                                    Utils.getFixedDouble(bsd.getStatsMinute().getTps()));
                        } catch (MQClientException ex) {
                            if (ex.getResponseCode() == ResponseCode.SYSTEM_ERROR) {
                                log.error(String.format("GROUP_GET_NUMS-error, topic=%s, group=%s,master broker=%s, %s", topic, group, masterAddr, ex.getErrorMessage()));
                            } else {
                                log.error(String.format("GROUP_GET_NUMS-error, topic=%s, group=%s,master broker=%s", topic, group, masterAddr), ex);
                            }
                        } catch (InterruptedException | RemotingConnectException | RemotingTimeoutException | RemotingSendRequestException ex) {
                            log.error(String.format("GROUP_GET_NUMS-error, topic=%s, group=%s,master broker=%s", topic, group, masterAddr), ex);
                        }
                        try {
                            //how many bytes the consumer has get for the topic
                            bsd = mqAdminExt.viewBrokerStatsData(masterAddr, BrokerStatsManager.GROUP_GET_SIZE, statsKey);
                            metricsService.getCollector().addGroupGetSizeMetric(
                                    bd.getCluster(),
                                    bd.getBrokerName(),
                                    topic,
                                    group,
                                    Utils.getFixedDouble(bsd.getStatsMinute().getTps()));
                        } catch (MQClientException ex) {
                            if (ex.getResponseCode() == ResponseCode.SYSTEM_ERROR) {
                                log.error(String.format("GROUP_GET_SIZE-error, topic=%s, group=%s, master broker=%s, %s", topic, group, masterAddr, ex.getErrorMessage()));
                            } else {
                                log.error(String.format("GROUP_GET_SIZE-error, topic=%s, group=%s, master broker=%s", topic, group, masterAddr), ex);
                            }
                        } catch (InterruptedException | RemotingConnectException | RemotingTimeoutException | RemotingSendRequestException ex) {
                            log.error(String.format("GROUP_GET_SIZE-error, topic=%s, group=%s, master broker=%s", topic, group, masterAddr), ex);
                        }
                        try {
                            ////how many re-send times the consumer did for the topic
                            bsd = mqAdminExt.viewBrokerStatsData(masterAddr, BrokerStatsManager.SNDBCK_PUT_NUMS, statsKey);
                            metricsService.getCollector().addSendBackNumsMetric(
                                    bd.getCluster(),
                                    bd.getBrokerName(),
                                    topic,
                                    group,
                                    Utils.getFixedDouble(bsd.getStatsMinute().getTps()));
                        } catch (MQClientException ex) {
                            if (ex.getResponseCode() == ResponseCode.SYSTEM_ERROR) {
                                log.error(String.format("SNDBCK_PUT_NUMS-error, topic=%s, group=%s, master broker=%s, %s", topic, group, masterAddr, ex.getErrorMessage()));
                            } else {
                                log.error(String.format("SNDBCK_PUT_NUMS-error, topic=%s, group=%s, master broker=%s", topic, group, masterAddr), ex);
                            }
                        } catch (InterruptedException | RemotingConnectException | RemotingTimeoutException | RemotingSendRequestException ex) {
                            log.error(String.format("SNDBCK_PUT_NUMS-error, topic=%s, group=%s, master broker=%s", topic, group, masterAddr), ex);
                        }
                    }
                }
            }
        }
        log.info("broker topic stats collection task finished...." + (System.currentTimeMillis() - start));
    }

    @Scheduled(cron = "${task.collectBrokerStats.cron}")
    public void collectBrokerStats() {
        if (!rmqConfigure.isEnableCollect()) {
            return;
        }
        log.info("broker stats collection task starting....");
        long start = System.currentTimeMillis();
        ClusterInfo clusterInfo = null;
        try {
            clusterInfo = mqAdminExt.examineBrokerClusterInfo();
        } catch (Exception ex) {
            log.error(String.format("collectBrokerStats-get cluster info from namesrv error. address is %s", JSON.toJSONString(mqAdminExt.getNameServerAddressList())), ex);
            return;
        }

        Set<Map.Entry<String, BrokerData>> clusterEntries = clusterInfo.getBrokerAddrTable().entrySet();
        for (Map.Entry<String, BrokerData> clusterEntry : clusterEntries) {
            String masterAddr = clusterEntry.getValue().getBrokerAddrs().get(MixAll.MASTER_ID);
            if (StringUtils.isBlank(masterAddr)) {
                continue;
            }
            BrokerStatsData bsd = null;
            try {
                bsd = mqAdminExt.viewBrokerStatsData(masterAddr, BrokerStatsManager.BROKER_PUT_NUMS, clusterEntry.getValue().getCluster());
                String brokerIP = clusterEntry.getValue().getBrokerAddrs().get(MixAll.MASTER_ID);
                metricsService.getCollector().addBrokerPutNumsMetric(
                        clusterEntry.getValue().getCluster(),
                        brokerIP,
                        clusterEntry.getValue().getBrokerName(),
                        Utils.getFixedDouble(bsd.getStatsMinute().getTps()));
            } catch (Exception ex) {
                log.error(String.format("BROKER_PUT_NUMS-error, master broker=%s", masterAddr), ex);
            }
            try {
                bsd = mqAdminExt.viewBrokerStatsData(masterAddr, BrokerStatsManager.BROKER_GET_NUMS, clusterEntry.getValue().getCluster());
                String brokerIP = clusterEntry.getValue().getBrokerAddrs().get(MixAll.MASTER_ID);
                metricsService.getCollector().addBrokerGetNumsMetric(
                        clusterEntry.getValue().getCluster(),
                        brokerIP,
                        clusterEntry.getValue().getBrokerName(),
                        Utils.getFixedDouble(bsd.getStatsMinute().getTps()));
            } catch (Exception ex) {
                log.error(String.format("BROKER_GET_NUMS-error, master broker=%s", masterAddr), ex);
            }
        }
        log.info("broker stats collection task finished...." + (System.currentTimeMillis() - start));
    }

    @Scheduled(cron = "${task.collectBrokerRuntimeStats.cron}")
    public void collectBrokerRuntimeStats() {
        if (!rmqConfigure.isEnableCollect()) {
            return;
        }
        log.info("broker runtime stats collection task starting....");
        long start = System.currentTimeMillis();
        ClusterInfo clusterInfo = null;
        try {
            clusterInfo = mqAdminExt.examineBrokerClusterInfo();
        } catch (Exception ex) {
            log.error(String.format("collectBrokerRuntimeStats-get cluster info from namesrv error. address is %s", JSON.toJSONString(mqAdminExt.getNameServerAddressList())), ex);
            return;
        }

        Set<Map.Entry<String, BrokerData>> clusterEntries = clusterInfo.getBrokerAddrTable().entrySet();
        for (Map.Entry<String, BrokerData> clusterEntry : clusterEntries) {
            String masterAddr = clusterEntry.getValue().getBrokerAddrs().get(MixAll.MASTER_ID);
            String clusterName = clusterEntry.getValue().getCluster();

            KVTable kvTable = null;
            if (!StringUtils.isBlank(masterAddr)) {
                try {
                    kvTable = mqAdminExt.fetchBrokerRuntimeStats(masterAddr);
                } catch (RemotingConnectException | RemotingSendRequestException | RemotingTimeoutException | InterruptedException ex) {
                    log.error(String.format("collectBrokerRuntimeStats-get fetch broker runtime stats error, address=%s", masterAddr), ex);
                } catch (MQBrokerException ex) {
                    if (ex.getResponseCode() == ResponseCode.SYSTEM_ERROR) {
                        log.error(String.format("collectBrokerRuntimeStats-get fetch broker runtime stats error, address=%s, error=%s", masterAddr, ex.getErrorMessage()));
                    } else {
                        log.error(String.format("collectBrokerRuntimeStats-get fetch broker runtime stats error, address=%s", masterAddr), ex);
                    }
                }
            }
            if (kvTable == null || kvTable.getTable() == null || kvTable.getTable().isEmpty()) {
                continue;
            }
            try {
                BrokerRuntimeStats brokerRuntimeStats = new BrokerRuntimeStats(kvTable);
                metricsService.getCollector().addBrokerRuntimeStatsMetric(brokerRuntimeStats, clusterName, masterAddr, "");
            } catch (Exception ex) {
                log.error(String.format("collectBrokerRuntimeStats-parse or report broker runtime stats error, %s", JSON.toJSONString(kvTable)), ex);
            }

        }

        log.info("broker runtime stats collection task finished...." + (System.currentTimeMillis() - start));
    }

    private static TwoTuple<String, String> buildClientAddresses(HashSet<Connection> connectionSet) {
        if (connectionSet == null || connectionSet.isEmpty()) {
            return new TwoTuple<>("", "");
        }
        List<String> clientAddresses = new ArrayList<>();
        List<String> clientIdAddresses = new ArrayList<>();

        for (Connection connection : connectionSet) {
            clientAddresses.add(connection.getClientAddr());//tcp connect address
            clientIdAddresses.add(connection.getClientId());//local ip
        }
        String str1 = String.join(",", clientAddresses);
        String str2 = String.join(",", clientIdAddresses);
        return new TwoTuple<>(str1, str2);
    }

    private void handleTopicNotExistException(int responseCode, Exception ex, String topic, String group) {
        if (responseCode == ResponseCode.TOPIC_NOT_EXIST || responseCode == ResponseCode.CONSUMER_NOT_ONLINE) {
            log.error(String.format("get topic's(%s) consumer-stats(%s) exception, detail: %s", topic, group, ex.getMessage()));
        } else {
            log.error(String.format("get topic's(%s) consumer-stats(%s) exception", topic, group), ex);
        }
    }
}