You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@rocketmq.apache.org by yu...@apache.org on 2017/06/15 02:53:17 UTC

[47/51] [partial] incubator-rocketmq-externals git commit: Release rocketmq-console 1.0.0 version

http://git-wip-us.apache.org/repos/asf/incubator-rocketmq-externals/blob/e218eef9/rocketmq-console/src/main/java/org/apache/rocketmq/console/support/GlobalExceptionHandler.java
----------------------------------------------------------------------
diff --git a/rocketmq-console/src/main/java/org/apache/rocketmq/console/support/GlobalExceptionHandler.java b/rocketmq-console/src/main/java/org/apache/rocketmq/console/support/GlobalExceptionHandler.java
new file mode 100644
index 0000000..37a8d64
--- /dev/null
+++ b/rocketmq-console/src/main/java/org/apache/rocketmq/console/support/GlobalExceptionHandler.java
@@ -0,0 +1,47 @@
+/*
+ * 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.console.support;
+
+import javax.servlet.http.HttpServletRequest;
+import org.apache.rocketmq.console.exception.ServiceException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+@ControllerAdvice(basePackages = "org.apache.rocketmq.console")
+public class GlobalExceptionHandler {
+    private Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
+
+    @ExceptionHandler(value = Exception.class)
+    @ResponseBody
+    public JsonResult<Object> jsonErrorHandler(HttpServletRequest req, Exception ex) throws Exception {
+        JsonResult<Object> value = null;
+        if (ex != null) {
+            logger.error("op=global_exception_handler_print_error", ex);
+            if (ex instanceof ServiceException) {
+                value = new JsonResult<Object>(((ServiceException) ex).getCode(), ex.getMessage());
+            }
+            else {
+                value = new JsonResult<Object>(-1, ex.getMessage());
+            }
+        }
+        return value;
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-rocketmq-externals/blob/e218eef9/rocketmq-console/src/main/java/org/apache/rocketmq/console/support/GlobalRestfulResponseBodyAdvice.java
----------------------------------------------------------------------
diff --git a/rocketmq-console/src/main/java/org/apache/rocketmq/console/support/GlobalRestfulResponseBodyAdvice.java b/rocketmq-console/src/main/java/org/apache/rocketmq/console/support/GlobalRestfulResponseBodyAdvice.java
new file mode 100644
index 0000000..e67fa33
--- /dev/null
+++ b/rocketmq-console/src/main/java/org/apache/rocketmq/console/support/GlobalRestfulResponseBodyAdvice.java
@@ -0,0 +1,62 @@
+/*
+ * 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.console.support;
+
+import java.lang.annotation.Annotation;
+import org.apache.rocketmq.console.aspect.admin.annotation.OriginalControllerReturnValue;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.MethodParameter;
+import org.springframework.http.MediaType;
+import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.http.server.ServerHttpRequest;
+import org.springframework.http.server.ServerHttpResponse;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
+
+@ControllerAdvice(basePackages = "org.apache.rocketmq.console")
+public class GlobalRestfulResponseBodyAdvice implements ResponseBodyAdvice<Object> {
+
+    private Logger logger = LoggerFactory.getLogger(GlobalRestfulResponseBodyAdvice.class);
+
+    @Override
+    public Object beforeBodyWrite(
+        Object obj, MethodParameter methodParameter, MediaType mediaType,
+        Class<? extends HttpMessageConverter<?>> converterType,
+        ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
+        Annotation originalControllerReturnValue = methodParameter.getMethodAnnotation(OriginalControllerReturnValue.class);
+        if (originalControllerReturnValue != null) {
+            return obj;
+        }
+        JsonResult value;
+        if (obj instanceof JsonResult) {
+            value = (JsonResult)obj;
+        }
+        else {
+            value = new JsonResult(obj);
+        }
+        return value;
+    }
+
+    @Override
+    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
+
+        return true;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-rocketmq-externals/blob/e218eef9/rocketmq-console/src/main/java/org/apache/rocketmq/console/support/JsonResult.java
----------------------------------------------------------------------
diff --git a/rocketmq-console/src/main/java/org/apache/rocketmq/console/support/JsonResult.java b/rocketmq-console/src/main/java/org/apache/rocketmq/console/support/JsonResult.java
new file mode 100644
index 0000000..f5e20dd
--- /dev/null
+++ b/rocketmq-console/src/main/java/org/apache/rocketmq/console/support/JsonResult.java
@@ -0,0 +1,63 @@
+/*
+ * 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.console.support;
+
+public class JsonResult<T> {
+    private int status = 0;
+    private T data;
+    private String errMsg;
+
+    public JsonResult(T data) {
+        this.data = data;
+    }
+
+    public JsonResult(int status, String errMsg) {
+        this.status = status;
+        this.errMsg = errMsg;
+    }
+
+    public JsonResult(int status, T data, String errMsg) {
+        this.status = status;
+        this.data = data;
+        this.errMsg = errMsg;
+    }
+
+    public int getStatus() {
+        return status;
+    }
+
+    public void setStatus(int status) {
+        this.status = status;
+    }
+
+    public T getData() {
+        return data;
+    }
+
+    public void setData(T data) {
+        this.data = data;
+    }
+
+    public String getErrMsg() {
+        return errMsg;
+    }
+
+    public void setErrMsg(String errMsg) {
+        this.errMsg = errMsg;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-rocketmq-externals/blob/e218eef9/rocketmq-console/src/main/java/org/apache/rocketmq/console/task/DashboardCollectTask.java
----------------------------------------------------------------------
diff --git a/rocketmq-console/src/main/java/org/apache/rocketmq/console/task/DashboardCollectTask.java b/rocketmq-console/src/main/java/org/apache/rocketmq/console/task/DashboardCollectTask.java
new file mode 100644
index 0000000..db1fbc4
--- /dev/null
+++ b/rocketmq-console/src/main/java/org/apache/rocketmq/console/task/DashboardCollectTask.java
@@ -0,0 +1,320 @@
+/*
+ * 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.console.task;
+
+import com.google.common.base.Stopwatch;
+import org.apache.rocketmq.common.protocol.body.ClusterInfo;
+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.route.BrokerData;
+import org.apache.rocketmq.common.protocol.route.TopicRouteData;
+import org.apache.rocketmq.console.aspect.admin.annotation.MultiMQAdminCmdMethod;
+import org.apache.rocketmq.store.stats.BrokerStatsManager;
+import org.apache.rocketmq.tools.admin.MQAdminExt;
+import org.apache.rocketmq.tools.command.stats.StatsAllSubCommand;
+import com.google.common.base.Throwables;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.io.Files;
+import java.io.File;
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import javax.annotation.Resource;
+import org.apache.rocketmq.common.MixAll;
+import org.apache.rocketmq.common.protocol.body.BrokerStatsData;
+import org.apache.rocketmq.console.config.RMQConfigure;
+import org.apache.rocketmq.console.service.DashboardCollectService;
+import org.apache.rocketmq.console.util.JsonUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+@Component
+public class DashboardCollectTask {
+    private Date currentDate = new Date();
+    @Resource
+    private MQAdminExt mqAdminExt;
+    @Resource
+    private RMQConfigure rmqConfigure;
+
+    @Resource
+    private DashboardCollectService dashboardCollectService;
+
+    private final static Logger log = LoggerFactory.getLogger(DashboardCollectTask.class);
+
+    @Scheduled(cron = "30 0/1 * * * ?")
+    @MultiMQAdminCmdMethod(timeoutMillis = 5000)
+    public void collectTopic() {
+        if (!rmqConfigure.isEnableDashBoardCollect()) {
+            return;
+        }
+        Date date = new Date();
+        Stopwatch stopwatch = Stopwatch.createStarted();
+        try {
+            TopicList topicList = mqAdminExt.fetchAllTopicList();
+            Set<String> topicSet = topicList.getTopicList();
+            for (String topic : topicSet) {
+                if (topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX) || topic.startsWith(MixAll.DLQ_GROUP_TOPIC_PREFIX)) {
+                    continue;
+                }
+
+                TopicRouteData topicRouteData = mqAdminExt.examineTopicRouteInfo(topic);
+
+                GroupList groupList = mqAdminExt.queryTopicConsumeByWho(topic);
+
+                double inTPS = 0;
+
+                long inMsgCntToday = 0;
+
+                double outTPS = 0;
+
+                long outMsgCntToday = 0;
+
+                for (BrokerData bd : topicRouteData.getBrokerDatas()) {
+                    String masterAddr = bd.getBrokerAddrs().get(MixAll.MASTER_ID);
+                    if (masterAddr != null) {
+                        try {
+                            stopwatch.start();
+                            log.info("start time: {}", stopwatch.toString());
+                            BrokerStatsData bsd = mqAdminExt.viewBrokerStatsData(masterAddr, BrokerStatsManager.TOPIC_PUT_NUMS, topic);
+                            stopwatch.stop();
+                            log.info("stop time : {}", stopwatch.toString());
+                            stopwatch.reset();
+                            inTPS += bsd.getStatsMinute().getTps();
+                            inMsgCntToday += StatsAllSubCommand.compute24HourSum(bsd);
+                        }
+                        catch (Exception e) {
+//                            throw Throwables.propagate(e);
+                        }
+                    }
+                }
+
+                if (groupList != null && !groupList.getGroupList().isEmpty()) {
+
+                    for (String group : groupList.getGroupList()) {
+                        for (BrokerData bd : topicRouteData.getBrokerDatas()) {
+                            String masterAddr = bd.getBrokerAddrs().get(MixAll.MASTER_ID);
+                            if (masterAddr != null) {
+                                try {
+                                    String statsKey = String.format("%s@%s", topic, group);
+                                    BrokerStatsData bsd = mqAdminExt.viewBrokerStatsData(masterAddr, BrokerStatsManager.GROUP_GET_NUMS, statsKey);
+                                    outTPS += bsd.getStatsMinute().getTps();
+                                    outMsgCntToday += StatsAllSubCommand.compute24HourSum(bsd);
+                                }
+                                catch (Exception e) {
+//                                    throw Throwables.propagate(e);
+                                }
+                            }
+                        }
+                    }
+                }
+
+                List<String> list;
+                try {
+                    list = dashboardCollectService.getTopicMap().get(topic);
+                }
+                catch (ExecutionException e) {
+                    throw Throwables.propagate(e);
+                }
+                if (null == list) {
+                    list = Lists.newArrayList();
+                }
+
+                list.add(date.getTime() + "," + new BigDecimal(inTPS).setScale(5, BigDecimal.ROUND_HALF_UP) + "," + inMsgCntToday + "," + new BigDecimal(outTPS).setScale(5, BigDecimal.ROUND_HALF_UP) + "," + outMsgCntToday);
+                dashboardCollectService.getTopicMap().put(topic, list);
+
+            }
+
+            log.debug("Topic Collected Data in memory = {}" + JsonUtil.obj2String(dashboardCollectService.getTopicMap().asMap()));
+        }
+        catch (Exception err) {
+            throw Throwables.propagate(err);
+        }
+    }
+
+    @Scheduled(cron = "0 0/1 * * * ?")
+    public void collectBroker() {
+        if (!rmqConfigure.isEnableDashBoardCollect()) {
+            return;
+        }
+        try {
+            Date date = new Date();
+            ClusterInfo clusterInfo = mqAdminExt.examineBrokerClusterInfo();
+            Set<Map.Entry<String, BrokerData>> clusterEntries = clusterInfo.getBrokerAddrTable().entrySet();
+
+            Map<String, String> addresses = Maps.newHashMap();
+            for (Map.Entry<String, BrokerData> clusterEntry : clusterEntries) {
+                HashMap<Long, String> addrs = clusterEntry.getValue().getBrokerAddrs();
+                Set<Map.Entry<Long, String>> addrsEntries = addrs.entrySet();
+                for (Map.Entry<Long, String> addrEntry : addrsEntries) {
+                    addresses.put(addrEntry.getValue(), clusterEntry.getKey() + ":" + addrEntry.getKey());
+                }
+            }
+            Set<Map.Entry<String, String>> entries = addresses.entrySet();
+            for (Map.Entry<String, String> entry : entries) {
+                List<String> list = dashboardCollectService.getBrokerMap().get(entry.getValue());
+                if (null == list) {
+                    list = Lists.newArrayList();
+                }
+                KVTable kvTable = fetchBrokerRuntimeStats(entry.getKey(), 3);
+                if (kvTable == null) {
+                    continue;
+                }
+                String[] tpsArray = kvTable.getTable().get("getTotalTps").split(" ");
+                BigDecimal totalTps = new BigDecimal(0);
+                for (String tps : tpsArray) {
+                    totalTps = totalTps.add(new BigDecimal(tps));
+                }
+                BigDecimal averageTps = totalTps.divide(new BigDecimal(tpsArray.length), 5, BigDecimal.ROUND_HALF_UP);
+                list.add(date.getTime() + "," + averageTps.toString());
+                dashboardCollectService.getBrokerMap().put(entry.getValue(), list);
+            }
+            log.debug("Broker Collected Data in memory = {}" + JsonUtil.obj2String(dashboardCollectService.getBrokerMap().asMap()));
+        }
+        catch (Exception e) {
+            throw Throwables.propagate(e);
+        }
+    }
+
+    private KVTable fetchBrokerRuntimeStats(String brokerAddr, Integer retryTime) {
+        if (retryTime == 0) {
+            return null;
+        }
+        try {
+            return mqAdminExt.fetchBrokerRuntimeStats(brokerAddr);
+        }
+        catch (Exception e) {
+            try {
+                Thread.sleep(1000);
+            }
+            catch (InterruptedException e1) {
+                throw Throwables.propagate(e1);
+            }
+            fetchBrokerRuntimeStats(brokerAddr, retryTime - 1);
+            throw Throwables.propagate(e);
+        }
+    }
+
+    @Scheduled(cron = "0/5 * * * * ?")
+    public void saveData() {
+        if (!rmqConfigure.isEnableDashBoardCollect()) {
+            return;
+        }
+        //one day refresh cache one time
+        String dataLocationPath = rmqConfigure.getConsoleCollectData();
+        DateFormat format = new SimpleDateFormat("yyyy-MM-dd");
+        String nowDateStr = format.format(new Date());
+        String currentDateStr = format.format(currentDate);
+        if (!currentDateStr.equals(nowDateStr)) {
+            dashboardCollectService.getBrokerMap().invalidateAll();
+            dashboardCollectService.getTopicMap().invalidateAll();
+            currentDate = new Date();
+        }
+        File brokerFile = new File(dataLocationPath + nowDateStr + ".json");
+        File topicFile = new File(dataLocationPath + nowDateStr + "_topic" + ".json");
+        try {
+            Map<String, List<String>> brokerFileMap;
+            Map<String, List<String>> topicFileMap;
+            if (brokerFile.exists()) {
+                brokerFileMap = dashboardCollectService.jsonDataFile2map(brokerFile);
+            }
+            else {
+                brokerFileMap = Maps.newHashMap();
+                Files.createParentDirs(brokerFile);
+            }
+
+            if (topicFile.exists()) {
+                topicFileMap = dashboardCollectService.jsonDataFile2map(topicFile);
+            }
+            else {
+                topicFileMap = Maps.newHashMap();
+                Files.createParentDirs(topicFile);
+            }
+
+            brokerFile.createNewFile();
+            topicFile.createNewFile();
+
+            writeFile(dashboardCollectService.getBrokerMap(), brokerFileMap, brokerFile);
+            writeFile(dashboardCollectService.getTopicMap(), topicFileMap, topicFile);
+            log.debug("Broker Collected Data in memory = {}" + JsonUtil.obj2String(dashboardCollectService.getBrokerMap().asMap()));
+            log.debug("Topic Collected Data in memory = {}" + JsonUtil.obj2String(dashboardCollectService.getTopicMap().asMap()));
+
+        }
+        catch (IOException e) {
+            throw Throwables.propagate(e);
+        }
+    }
+
+    private void writeFile(LoadingCache<String, List<String>> map, Map<String, List<String>> fileMap,
+        File file) throws IOException {
+        Map<String, List<String>> newMap = map.asMap();
+        Map<String, List<String>> resultMap = Maps.newHashMap();
+        if (fileMap.size() == 0) {
+            resultMap = newMap;
+        }
+        else {
+            for (Map.Entry<String, List<String>> entry : fileMap.entrySet()) {
+                List<String> oldList = entry.getValue();
+                List<String> newList = newMap.get(entry.getKey());
+                resultMap.put(entry.getKey(), appendData(newList, oldList));
+                if (newList == null || newList.size() == 0) {
+                    map.put(entry.getKey(), appendData(newList, oldList));
+                }
+            }
+
+            for (Map.Entry<String, List<String>> entry : newMap.entrySet()) {
+                List<String> oldList = fileMap.get(entry.getKey());
+                if (oldList == null || oldList.size() == 0) {
+                    resultMap.put(entry.getKey(), entry.getValue());
+                }
+            }
+        }
+        Files.write(JsonUtil.obj2String(resultMap).getBytes(), file);
+    }
+
+    private List<String> appendData(List<String> newTpsList, List<String> oldTpsList) {
+        List<String> result = Lists.newArrayList();
+        if (newTpsList == null || newTpsList.size() == 0) {
+            return oldTpsList;
+        }
+        if (oldTpsList == null || oldTpsList.size() == 0) {
+            return newTpsList;
+        }
+        String oldLastTps = oldTpsList.get(oldTpsList.size() - 1);
+        Long oldLastTimestamp = Long.parseLong(oldLastTps.split(",")[0]);
+        String newFirstTps = newTpsList.get(0);
+        Long newFirstTimestamp = Long.parseLong(newFirstTps.split(",")[0]);
+        if (oldLastTimestamp.longValue() < newFirstTimestamp.longValue()) {
+            result.addAll(oldTpsList);
+            result.addAll(newTpsList);
+            return result;
+        }
+        return newTpsList;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-rocketmq-externals/blob/e218eef9/rocketmq-console/src/main/java/org/apache/rocketmq/console/task/MonitorTask.java
----------------------------------------------------------------------
diff --git a/rocketmq-console/src/main/java/org/apache/rocketmq/console/task/MonitorTask.java b/rocketmq-console/src/main/java/org/apache/rocketmq/console/task/MonitorTask.java
new file mode 100644
index 0000000..0db07be
--- /dev/null
+++ b/rocketmq-console/src/main/java/org/apache/rocketmq/console/task/MonitorTask.java
@@ -0,0 +1,50 @@
+/*
+ * 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.console.task;
+
+import java.util.Map;
+import javax.annotation.Resource;
+import org.apache.rocketmq.console.model.ConsumerMonitorConfig;
+import org.apache.rocketmq.console.model.GroupConsumeInfo;
+import org.apache.rocketmq.console.service.ConsumerService;
+import org.apache.rocketmq.console.service.MonitorService;
+import org.apache.rocketmq.console.util.JsonUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+@Component
+public class MonitorTask {
+    private Logger logger = LoggerFactory.getLogger(MonitorTask.class);
+
+    @Resource
+    private MonitorService monitorService;
+
+    @Resource
+    private ConsumerService consumerService;
+
+//    @Scheduled(cron = "* * * * * ?")
+    public void scanProblemConsumeGroup() {
+        for (Map.Entry<String, ConsumerMonitorConfig> configEntry : monitorService.queryConsumerMonitorConfig().entrySet()) {
+            GroupConsumeInfo consumeInfo = consumerService.queryGroup(configEntry.getKey());
+            if (consumeInfo.getCount() < configEntry.getValue().getMinCount() || consumeInfo.getDiffTotal() > configEntry.getValue().getMaxDiffTotal()) {
+                logger.info("op=look consumeInfo {}", JsonUtil.obj2String(consumeInfo)); // notify the alert system
+            }
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-rocketmq-externals/blob/e218eef9/rocketmq-console/src/main/java/org/apache/rocketmq/console/util/JsonUtil.java
----------------------------------------------------------------------
diff --git a/rocketmq-console/src/main/java/org/apache/rocketmq/console/util/JsonUtil.java b/rocketmq-console/src/main/java/org/apache/rocketmq/console/util/JsonUtil.java
new file mode 100644
index 0000000..857a7fa
--- /dev/null
+++ b/rocketmq-console/src/main/java/org/apache/rocketmq/console/util/JsonUtil.java
@@ -0,0 +1,156 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.rocketmq.console.util;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
+import com.google.common.base.Strings;
+import com.google.common.base.Throwables;
+import java.io.IOException;
+import java.io.Writer;
+import java.text.SimpleDateFormat;
+import java.util.Map;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@SuppressWarnings("unchecked")
+public class JsonUtil {
+
+    private static Logger logger = LoggerFactory.getLogger(JsonUtil.class);
+    private static ObjectMapper objectMapper = new ObjectMapper();
+
+    private JsonUtil() {
+    }
+
+    static {
+        objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
+        objectMapper.configure(SerializationFeature.WRITE_ENUMS_USING_TO_STRING, true);
+        objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
+        objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
+        objectMapper.setFilters(new SimpleFilterProvider().setFailOnUnknownId(false));
+        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
+        objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
+    }
+
+    public static void writeValue(Writer writer, Object obj) {
+        try {
+            objectMapper.writeValue(writer, obj);
+        }
+        catch (IOException e) {
+            Throwables.propagateIfPossible(e);
+        }
+    }
+
+    public static <T> String obj2String(T src) {
+        if (src == null) {
+            return null;
+        }
+
+        try {
+            return src instanceof String ? (String)src : objectMapper.writeValueAsString(src);
+        }
+        catch (Exception e) {
+            logger.error("Parse Object to String error src=" + src, e);
+            return null;
+        }
+    }
+
+    public static <T> byte[] obj2Byte(T src) {
+        if (src == null) {
+            return null;
+        }
+
+        try {
+            return src instanceof byte[] ? (byte[])src : objectMapper.writeValueAsBytes(src);
+        }
+        catch (Exception e) {
+            logger.error("Parse Object to byte[] error", e);
+            return null;
+        }
+    }
+
+    public static <T> T string2Obj(String str, Class<T> clazz) {
+        if (Strings.isNullOrEmpty(str) || clazz == null) {
+            return null;
+        }
+        str = escapesSpecialChar(str);
+        try {
+            return clazz.equals(String.class) ? (T)str : objectMapper.readValue(str, clazz);
+        }
+        catch (Exception e) {
+            logger.error("Parse String to Object error\nString: {}\nClass<T>: {}\nError: {}", str, clazz.getName(), e);
+            return null;
+        }
+    }
+
+    public static <T> T byte2Obj(byte[] bytes, Class<T> clazz) {
+        if (bytes == null || clazz == null) {
+            return null;
+        }
+        try {
+            return clazz.equals(byte[].class) ? (T)bytes : objectMapper.readValue(bytes, clazz);
+        }
+        catch (Exception e) {
+            logger.error("Parse byte[] to Object error\nbyte[]: {}\nClass<T>: {}\nError: {}", bytes, clazz.getName(), e);
+            return null;
+        }
+    }
+
+    public static <T> T string2Obj(String str, TypeReference<T> typeReference) {
+        if (Strings.isNullOrEmpty(str) || typeReference == null) {
+            return null;
+        }
+        str = escapesSpecialChar(str);
+        try {
+            return (T)(typeReference.getType().equals(String.class) ? str : objectMapper.readValue(str, typeReference));
+        }
+        catch (Exception e) {
+            logger.error("Parse String to Object error\nString: {}\nTypeReference<T>: {}\nError: {}", str,
+                typeReference.getType(), e);
+            return null;
+        }
+    }
+
+    public static <T> T byte2Obj(byte[] bytes, TypeReference<T> typeReference) {
+        if (bytes == null || typeReference == null) {
+            return null;
+        }
+        try {
+            return (T)(typeReference.getType().equals(byte[].class) ? bytes : objectMapper.readValue(bytes,
+                typeReference));
+        }
+        catch (Exception e) {
+            logger.error("Parse byte[] to Object error\nbyte[]: {}\nTypeReference<T>: {}\nError: {}", bytes,
+                typeReference.getType(), e);
+            return null;
+        }
+    }
+
+    public static <T> T map2Obj(Map<String, String> map, Class<T> clazz) {
+        String str = obj2String(map);
+        return string2Obj(str, clazz);
+    }
+
+    private static String escapesSpecialChar(String str) {
+        return str.replace("\n", "\\n").replace("\r", "\\r");
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-rocketmq-externals/blob/e218eef9/rocketmq-console/src/main/resources/application.properties
----------------------------------------------------------------------
diff --git a/rocketmq-console/src/main/resources/application.properties b/rocketmq-console/src/main/resources/application.properties
new file mode 100644
index 0000000..de8eb1b
--- /dev/null
+++ b/rocketmq-console/src/main/resources/application.properties
@@ -0,0 +1,16 @@
+server.contextPath=
+server.port=8080
+#spring.application.index=true
+spring.application.name=rocketmq-console
+spring.http.encoding.charset=UTF-8
+spring.http.encoding.enabled=true
+spring.http.encoding.force=true
+logging.config=classpath:logback.xml
+#if this value is empty,use env value rocketmq.config.namesrvAddr  NAMESRV_ADDR | now, you can set it in ops page.default localhost:9876
+rocketmq.config.namesrvAddr=
+#if you use rocketmq version < 3.5.8, rocketmq.config.isVIPChannel should be false.default true
+rocketmq.config.isVIPChannel=
+#rocketmq-console's data path:dashboard/monitor
+rocketmq.config.dataPath=/tmp/rocketmq-console/data
+#set it false if you don't want use dashboard.default true
+rocketmq.config.enableDashBoardCollect=true
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-rocketmq-externals/blob/e218eef9/rocketmq-console/src/main/resources/logback.xml
----------------------------------------------------------------------
diff --git a/rocketmq-console/src/main/resources/logback.xml b/rocketmq-console/src/main/resources/logback.xml
new file mode 100644
index 0000000..c1fc79a
--- /dev/null
+++ b/rocketmq-console/src/main/resources/logback.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+	<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+		<encoder charset="UTF-8">
+			<pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] %5p %m%n</pattern>
+		</encoder>
+	</appender>
+
+	<appender name="FILE"
+		class="ch.qos.logback.core.rolling.RollingFileAppender">
+		<file>${user.home}/logs/consolelogs/rocketmq-console.log</file>
+		<append>true</append>
+		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+			<fileNamePattern>${user.home}/logs/consolelogs/rocketmq-console-%d{yyyy-MM-dd}.%i.log
+			</fileNamePattern>
+			<timeBasedFileNamingAndTriggeringPolicy
+				class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+				<maxFileSize>104857600</maxFileSize>
+			</timeBasedFileNamingAndTriggeringPolicy>
+			<MaxHistory>10</MaxHistory>
+		</rollingPolicy>
+		<encoder>
+			<pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] %5p %m%n</pattern>
+			<charset class="java.nio.charset.Charset">UTF-8</charset>
+		</encoder>
+	</appender>
+
+	<root level="INFO">
+		<appender-ref ref="STDOUT" />
+		<appender-ref ref="FILE" />
+	</root>
+
+</configuration> 
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-rocketmq-externals/blob/e218eef9/rocketmq-console/src/main/resources/static/index.html
----------------------------------------------------------------------
diff --git a/rocketmq-console/src/main/resources/static/index.html b/rocketmq-console/src/main/resources/static/index.html
new file mode 100644
index 0000000..6e3e6aa
--- /dev/null
+++ b/rocketmq-console/src/main/resources/static/index.html
@@ -0,0 +1,117 @@
+<!DOCTYPE html>
+<!--
+  ~ 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.
+  -->
+<html lang="en" ng-app="app">
+<head>
+    <meta charset="UTF-8">
+    <title>RocketMq-console-ng</title>
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name='description' content=''>
+    <meta name='keywords' content=''>
+    <!--iOS -->
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+
+
+    <!-- preLoading -->
+    <link rel="stylesheet" href="style/preLoading/normalize.css">
+    <link rel="stylesheet" href="style/preLoading/main.css">
+    <script src="vendor/preLoading/modernizr-2.6.2.min.js"></script>
+    <!-- preLoading -->
+
+    <link rel="stylesheet" href="vendor/bootstrap/css/bootstrap.min.css">
+    <link rel="stylesheet" href="vendor/bootstrap-material-design/css/bootstrap-material-design.css">
+    <link rel="stylesheet" href="vendor/bootstrap-material-design/css/ripples.css">
+    <link rel="stylesheet" href="vendor/angular/notification/angular-ui-notification.css">
+    <link rel="stylesheet" href="vendor/ng-dialog/ngDialog.min.css">
+    <link rel="stylesheet" href="vendor/ng-dialog/ngDialog-theme-default.css">
+    <link rel="stylesheet" href="vendor/dropdown/jquery.dropdown.css">
+    <link rel="stylesheet" href="vendor/datatimepicker/bootstrap-datetimepicker.min.css">
+    <link rel="stylesheet" href="vendor/font-awesome-4.7.0/css/font-awesome.min.css">
+    <link rel="stylesheet" href="vendor/font-awesome-4.7.0/fonts/fontawesome-webfont.svg">
+    <link rel="stylesheet" type="text/css" href="vendor/chosen/chosen.css"/>
+    <link rel="stylesheet" type="text/css" href="vendor/chosen/chosen-spinner.css"/>
+    <link rel="stylesheet" type="text/css" href="vendor/angular-material/angular-material.min.css"/>
+    <link rel="stylesheet" type="text/css" href="vendor/md-tab/docs.css"/>
+    <link rel="stylesheet" href="style/animate.css">
+    <link rel="stylesheet" href="style/theme.css">
+    <link rel="stylesheet" href="style/app.css">
+
+</head>
+<body ng-controller="AppCtrl">
+<!--[if lte IE 9]>
+<script type="text/javascript">
+    location.href = 'view/pages/un_support_browser.html';
+</script>
+<![endif]-->
+
+<div id="loader-wrapper">
+    <div id="loader"></div>
+
+    <div class="loader-section section-left"></div>
+    <div class="loader-section section-right"></div>
+
+</div>
+<div ng-include="'view/layout/_header.html'"></div>
+<section ng-view></section>
+<div ng-include="'view/layout/_footer.html'"></div>
+<script type="text/javascript" src="vendor/jquery/jquery1.11.3.min.js"></script>
+<script type="text/javascript" src="vendor/bootstrap/js/bootstrap.min.js"></script>
+<script type="text/javascript" src="vendor/bootstrap-material-design/js/material.min.js"></script>
+<script type="text/javascript" src="vendor/bootstrap-material-design/js/ripples.min.js"></script>
+<script type="text/javascript" src="vendor/angular/angular.min.js"></script>
+<script type="text/javascript" src="vendor/angular/angular-translate.min.js"></script>
+<script type="text/javascript"
+        src="vendor/angular/angular-translate-storage-cookie/angular-translate-storage-cookie.min.js"></script>
+<script type="text/javascript" src="vendor/angular/i18n/angular-locale_zh-cn.js"></script>
+<script type="text/javascript" src="src/i18n/en.js"></script>
+<script type="text/javascript" src="src/i18n/zh.js"></script>
+<script type="text/javascript" src="vendor/angular/angular-cookies.min.js"></script>
+<script type="text/javascript" src="vendor/angular/angular-animate.min.js"></script>
+<script type="text/javascript" src="vendor/angular/angular-route.min.js"></script>
+<script type="text/javascript" src="vendor/angular/angular-ui-router.min.js"></script>
+<script type="text/javascript" src="vendor/angular/angular-sanitize.min.js"></script>
+<script type="text/javascript" src="vendor/angular/angular-aria.min.js"></script>
+<script type="text/javascript" src="vendor/angular/angular-messages.min.js"></script>
+<script type="text/javascript" src="vendor/angular/angular-sanitize.min.js"></script>
+<script type="text/javascript" src="vendor/angular/notification/angular-ui-notification.js"></script>
+<script type="text/javascript" src="vendor/pagination/tm.pagination.js"></script>
+<script type="text/javascript" src="vendor/ng-dialog/ngDialog.min.js"></script>
+<script type="text/javascript" src="vendor/json-bigint/json-bigint.js"></script>
+<script type="text/javascript" src="vendor/dropdown/jquery.dropdown.js"></script>
+<script type="text/javascript" src="vendor/datatimepicker/moment.min.js"></script>
+<script type="text/javascript" src="vendor/datatimepicker/bootstrap-datetimepicker.min.js"></script>
+<script type="text/javascript" src="vendor/datatimepicker/angular-eonasdan-datetimepicker.min.js"></script>
+<script type="text/javascript" src="vendor/chosen/angular-chosen.js"></script>
+<script type="text/javascript" src="vendor/chosen/chosen.jquery.min.js"></script>
+<script type="text/javascript" src='vendor/md-tab/svg-assets-cache.js'></script>
+<script type="text/javascript" src='vendor/angular-material/angular-material.min.js'></script>
+<script type="text/javascript" src="vendor/echarts/echarts.min.js"></script>
+<script type="text/javascript" src="src/app.js"></script>
+<script type="text/javascript" src="src/controller.js?v=201702250025"></script>
+<script type="text/javascript" src="src/tools/tools.js?v=201703171710"></script>
+<script type="text/javascript" src="src/cluster.js?timestamp=4"></script>
+<script type="text/javascript" src="src/topic.js"></script>
+<script type="text/javascript" src="src/consumer.js?timestamp=6"></script>
+<script type="text/javascript" src="src/producer.js"></script>
+<script type="text/javascript" src="src/message.js"></script>
+<script type="text/javascript" src="src/ops.js?timestamp=7"></script>
+<script type="text/javascript" src="src/remoteApi/remoteApi.js"></script>
+<script type="text/javascript" src="vendor/preLoading/main.js"></script>
+
+</body>
+</html>

http://git-wip-us.apache.org/repos/asf/incubator-rocketmq-externals/blob/e218eef9/rocketmq-console/src/main/resources/static/src/app.js
----------------------------------------------------------------------
diff --git a/rocketmq-console/src/main/resources/static/src/app.js b/rocketmq-console/src/main/resources/static/src/app.js
new file mode 100644
index 0000000..b241010
--- /dev/null
+++ b/rocketmq-console/src/main/resources/static/src/app.js
@@ -0,0 +1,208 @@
+/*
+ * 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.
+ */
+'use strict';
+var app = angular.module('app', [
+    'ngAnimate',
+    'ngCookies',
+    'ngRoute',
+    'ngDialog',
+    'ngMaterial',
+    'ngSanitize',
+    'material.svgAssetsCache',
+    'ui-notification',
+    'tm.pagination',
+    'ae-datetimepicker',
+    'localytics.directives',
+    'pascalprecht.translate'
+]).run(
+        ['$rootScope','$location','$cookies',
+            function ($rootScope,$location,$cookies) {
+                // var filter = function(url){
+                //     var outFilterArrs = []
+                //     outFilterArrs.push("/login");
+                //     outFilterArrs.push("/reg");
+                //     outFilterArrs.push("/logout");
+                //     outFilterArrs.push("/404");
+                //     var flag = false;
+                //     $.each(outFilterArrs,function(i,value){
+                //         if(url.indexOf(value) > -1){
+                //             flag = true;
+                //             return false;
+                //         }
+                //     });
+                //     return flag;
+                // }
+
+                // if(angular.isDefined($cookies.get("isLogin")) && $cookies.get("isLogin") == 'true'){
+                //     chatApi.login();
+                // }
+
+
+                $rootScope.$on('$routeChangeSuccess', function() {
+                    var pathArray = $location.url().split("/");
+                    var index = pathArray.indexOf("");
+                    if(index >= 0){
+                        pathArray.remove(index);
+                    }
+                    $rootScope.path = pathArray[0];
+
+                    //初始化material UI控件
+                    $.material.init();
+                });
+
+                $rootScope.$on('$routeChangeStart',function (evt, next,current) {
+                    window.clearInterval($rootScope._thread);
+                })
+            }
+        ]
+    ).animation('.view', function () {
+        return {
+            animate: function (element, className, from, to, done) {
+                //styles
+            }
+        }
+    });
+
+app.provider('getDictName', function () {
+
+    var dictList = [];
+
+    this.init = function () {
+        var url = "src/data/dict.json";//无法使用common服务类,地址只能写死
+        var params = {};
+        $.get(url, params, function (ret) {
+            dictList = ret;
+        })
+    }
+
+    this.$get = function () {
+        return function (dictType, value) {
+            for (var i = 0; i < dictList.length; i++) {
+                var dict = dictList[i];
+                if (dict.TYPE == dictType && dict.DICT_VALUE == value) {
+                    return dict.DICT_NAME;
+                }
+            }
+        }
+    }
+})
+
+app.config(['$routeProvider', '$httpProvider','$cookiesProvider','getDictNameProvider','$sceProvider','$translateProvider','$mdThemingProvider',
+    function ($routeProvider, $httpProvider ,$cookiesProvider,getDictNameProvider,$sceProvider,$translateProvider,$mdThemingProvider) {
+        //关闭html校验,存在安全隐患,但目前没问题,使用ng-bind-html需要注意,防止跨站攻击
+        $sceProvider.enabled(false);
+        //前端字典项目初始化
+        getDictNameProvider.init();
+
+        //init angular
+        $mdThemingProvider.theme('default')
+            .primaryPalette('pink')
+            .accentPalette('light-blue');
+
+
+        //设置ajax默认配置
+        $.ajaxSetup({
+            type: "POST",
+            contentType: 'application/json',
+            cache:false,
+            timeout : 5000, //超时时间设置,单位毫秒
+            converters:{
+                "text json": JSONbig.parse
+            }
+        });
+
+        $httpProvider.defaults.cache = false;
+
+        $routeProvider.when('/', {
+            templateUrl: 'view/pages/index.html',
+            controller:'dashboardCtrl'
+        }).when('/cluster', {
+            templateUrl: 'view/pages/cluster.html',
+            controller:'clusterController'
+        }).when('/topic', {
+            templateUrl: 'view/pages/topic.html',
+            controller:'topicController'
+        }).when('/consumer', {
+            templateUrl: 'view/pages/consumer.html',
+            controller:'consumerController'
+        }).when('/producer', {
+            templateUrl: 'view/pages/producer.html',
+            controller:'producerController'
+        }).when('/message', {
+            templateUrl: 'view/pages/message.html',
+            controller:'messageController'
+        }).when('/ops', {
+            templateUrl: 'view/pages/ops.html',
+            controller:'opsController'
+        }).when('/404', {
+            templateUrl: '404'
+        }).otherwise('404');
+
+        $translateProvider.translations('en',en);
+        $translateProvider.translations('zh',zh);
+        $translateProvider.preferredLanguage('en');
+        $translateProvider.useCookieStorage();
+//        $translateProvider.useSanitizeValueStrategy('sanitize');
+
+    }]);
+
+app.filter('range', function() {
+    return function(input, range) {
+        var total = parseInt(range.totalPage) + 1;
+        var count = 5;
+        for (var i = range.start; i<total; i++) {
+            if(count > 0){
+                input.push(i);
+                count -- ;
+            }else {
+                break;
+            }
+        }
+        return input;
+    };
+});
+
+
+app.filter('dict',['getDictName',function(getDictName){
+    return function(value,type){
+        return getDictName(type,value);
+    }
+}])
+
+/**
+ * 数组扩展方法,移除数组中某一元素或某一段元素
+ * @param from 需要移除元素的索引开始值(只传一个参数表示单独移除该元素)
+ * @param to 需要移除元素的索引结束值
+ * @returns {*}
+ */
+Array.prototype.remove = function(from, to) {
+    var rest = this.slice((to || from) + 1 || this.length);
+    this.length = from < 0 ? this.length + from : from;
+    return this.push.apply(this, rest);
+};
+
+/**
+ * 根据元素值查询数组中元素的索引
+ * @param val
+ * @returns {number}
+ */
+Array.prototype.indexOf = function(val) {
+    for (var i = 0; i < this.length; i++) {
+        if (this[i] == val) return i;
+    }
+    return -1;
+};
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-rocketmq-externals/blob/e218eef9/rocketmq-console/src/main/resources/static/src/cluster.js
----------------------------------------------------------------------
diff --git a/rocketmq-console/src/main/resources/static/src/cluster.js b/rocketmq-console/src/main/resources/static/src/cluster.js
new file mode 100644
index 0000000..6f1baee
--- /dev/null
+++ b/rocketmq-console/src/main/resources/static/src/cluster.js
@@ -0,0 +1,72 @@
+/*
+ * 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.
+ */
+
+app.controller('clusterController', ['$scope','$location','$http','Notification','remoteApi','tools', function ($scope,$location,$http,Notification,remoteApi,tools) {
+    $scope.clusterMap = {};//cluster:brokerNameList
+    $scope.brokerMap = {};//brokerName:{id:addr}
+    $scope.brokerDetail = {};//{brokerName,id:detail}
+    $scope.clusterNames = [];
+    $scope.selectedCluster = "";
+    var callback = function (resp) {
+        if (resp.status == 0) {
+            $scope.clusterMap = resp.data.clusterInfo.clusterAddrTable;
+            $scope.brokerMap = resp.data.clusterInfo.brokerAddrTable;
+            $scope.brokerDetail = resp.data.brokerServer;
+            $.each($scope.clusterMap,function(clusterName,clusterBrokersNames){
+                $scope.clusterNames.push(clusterName);
+            });
+            if ($scope.clusterNames.length > 0) {
+                $scope.selectedCluster = $scope.clusterNames[0];
+            }
+            $scope.brokers = tools.generateBrokerMap($scope.brokerDetail,$scope.clusterMap,$scope.brokerMap);
+            $scope.switchCluster();
+        }else{
+            Notification.error({message: resp.errMsg, delay: 2000});
+        }
+    }
+
+    remoteApi.queryClusterList(callback);
+
+    $scope.switchCluster = function(){
+        $scope.instances = $scope.brokers[$scope.selectedCluster];
+    }
+
+    $scope.showDetail = function (brokerName,index) {
+        $scope.detail = $scope.brokerDetail[brokerName][index];
+        $scope.brokerName = brokerName;
+        $scope.index = index;
+        $(".brokerModal").modal();
+    }
+
+    $scope.showConfig = function (brokerAddr,brokerName,index) {
+        $scope.brokerAddr = brokerAddr;
+        $scope.brokerName = brokerName;
+        $scope.index = index;
+        $http({
+            method: "GET",
+            url: "cluster/brokerConfig.query",
+            params:{brokerAddr:brokerAddr}
+        }).success(function (resp) {
+            if (resp.status == 0) {
+                $scope.brokerConfig = resp.data;
+                $(".configModal").modal();
+            }else{
+                Notification.error({message: resp.errMsg, delay: 2000});
+            }
+        })
+    }
+}])

http://git-wip-us.apache.org/repos/asf/incubator-rocketmq-externals/blob/e218eef9/rocketmq-console/src/main/resources/static/src/consumer.js
----------------------------------------------------------------------
diff --git a/rocketmq-console/src/main/resources/static/src/consumer.js b/rocketmq-console/src/main/resources/static/src/consumer.js
new file mode 100644
index 0000000..cd093d4
--- /dev/null
+++ b/rocketmq-console/src/main/resources/static/src/consumer.js
@@ -0,0 +1,350 @@
+/*
+ * 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.
+ */
+
+var module = app;
+
+module.controller('consumerController', ['$scope', 'ngDialog', '$http','Notification',function ($scope, ngDialog, $http,Notification) {
+    $scope.paginationConf = {
+        currentPage: 1,
+        totalItems: 0,
+        itemsPerPage: 10,
+        pagesLength: 15,
+        perPageOptions: [10],
+        rememberPerPage: 'perPageItems',
+        onChange: function () {
+            $scope.showConsumerGroupList(this.currentPage,this.totalItems);
+        }
+    };
+    $scope.sortKey = null;
+    $scope.sortOrder=1;
+    $scope.intervalProcessSwitch = false;
+    $scope.intervalProcess = null;
+    $scope.allConsumerGrouopList = [];
+    $scope.consumerGroupShowList = [];
+    $scope.sortByKey = function (key) {
+        $scope.paginationConf.currentPage=1;
+        $scope.sortOrder = -$scope.sortOrder;
+        $scope.sortKey = key;
+        $scope.doSort();
+    };
+
+    $scope.doSort = function (){// todo  how to change this fe's code ? (it's dirty)
+        if($scope.sortKey == 'diffTotal'){
+            $scope.allConsumerGrouopList.sort(function(a,b) {return (a.diffTotal > b.diffTotal) ? $scope.sortOrder : ((b.diffTotal > a.diffTotal) ? -$scope.sortOrder : 0);} );
+        }
+        if($scope.sortKey == 'group'){
+            $scope.allConsumerGrouopList.sort(function(a,b) {return (a.group > b.group) ? $scope.sortOrder : ((b.group > a.group) ? -$scope.sortOrder : 0);} );
+        }
+        if($scope.sortKey == 'count'){
+            $scope.allConsumerGrouopList.sort(function(a,b) {return (a.count > b.count) ? $scope.sortOrder : ((b.count > a.count) ? -$scope.sortOrder : 0);} );
+        }
+        if($scope.sortKey == 'consumeTps'){
+            $scope.allConsumerGrouopList.sort(function(a,b) {return (a.consumeTps > b.consumeTps) ? $scope.sortOrder : ((b.consumeTps > a.consumeTps) ? -$scope.sortOrder : 0);} );
+        }
+        $scope.filterList($scope.paginationConf.currentPage)
+    };
+    $scope.refreshConsumerData = function () {
+        $http({
+            method: "GET",
+            url: "consumer/groupList.query"
+        }).success(function (resp) {
+            if(resp.status ==0){
+                $scope.allConsumerGrouopList = resp.data;
+                console.log($scope.allConsumerGrouopList);
+                console.log(JSON.stringify(resp));
+                $scope.showConsumerGroupList($scope.paginationConf.currentPage,$scope.allConsumerGrouopList.length);
+            }else {
+                Notification.error({message: resp.errMsg, delay: 2000});
+            }
+        });
+    };
+    $scope.monitor = function(consumerGroupName){
+        $http({
+            method: "GET",
+            url: "monitor/consumerMonitorConfigByGroupName.query",
+            params:{consumeGroupName:consumerGroupName}
+        }).success(function (resp) {
+            // if(resp.status ==0){
+                ngDialog.open({
+                    template: 'consumerMonitorDialog',
+                    controller: 'consumerMonitorDialogController',
+                    data:{consumerGroupName:consumerGroupName,data:resp.data}
+                });
+            // }else {
+            //     Notification.error({message: resp.errMsg, delay: 2000});
+            // }
+        });
+    };
+
+
+    $scope.$watch('intervalProcessSwitch', function () {
+        if ($scope.intervalProcess != null) {
+            clearInterval($scope.intervalProcess);
+            $scope.intervalProcess = null;
+        }
+        if ($scope.intervalProcessSwitch) {
+            $scope.intervalProcess = setInterval($scope.refreshConsumerData, 10000);
+        }
+    });
+
+
+    $scope.refreshConsumerData();
+    $scope.filterStr="";
+    $scope.$watch('filterStr', function() {
+        $scope.paginationConf.currentPage=1;
+        $scope.filterList(1)
+    });
+
+    $scope.filterList = function (currentPage) {
+        var lowExceptStr =  $scope.filterStr.toLowerCase();
+        var canShowList = [];
+        $scope.allConsumerGrouopList.forEach(function(element) {
+            console.log(element)
+            if (element.group.toLowerCase().indexOf(lowExceptStr) != -1){
+                canShowList.push(element);
+            }
+        });
+        $scope.paginationConf.totalItems =canShowList.length;
+        var perPage = $scope.paginationConf.itemsPerPage;
+        var from = (currentPage - 1) * perPage;
+        var to = (from + perPage)>canShowList.length?canShowList.length:from + perPage;
+        $scope.consumerGroupShowList = canShowList.slice(from, to);
+    };
+
+
+    $scope.showConsumerGroupList = function (currentPage,totalItem) {
+        var perPage = $scope.paginationConf.itemsPerPage;
+        var from = (currentPage - 1) * perPage;
+        var to = (from + perPage)>totalItem?totalItem:from + perPage;
+        $scope.consumerGroupShowList = $scope.allConsumerGrouopList.slice(from, to);
+        $scope.paginationConf.totalItems = totalItem ;
+        console.log($scope.consumerGroupShowList)
+        console.log($scope.paginationConf.totalItems)
+        $scope.doSort()
+    };
+    $scope.openAddDialog = function () {
+        $scope.openCreateOrUpdateDialog(null);
+    };
+    $scope.openCreateOrUpdateDialog = function(request){
+        var bIsUpdate = true;
+        if(request == null){
+            request = [{
+                brokerNameList: [],
+                subscriptionGroupConfig: {
+                    groupName: "",
+                    consumeEnable: true,
+                    consumeFromMinEnable: true,
+                    consumeBroadcastEnable: true,
+                    retryQueueNums: 1,
+                    retryMaxTimes: 16,
+                    brokerId: 0,
+                    whichBrokerWhenConsumeSlowly: 1
+                }
+            }];
+            bIsUpdate = false;
+        }
+        console.log(request);
+        $http({
+            method: "GET",
+            url: "cluster/list.query"
+        }).success(function (resp) {
+            if(resp.status ==0){
+                console.log(resp);
+                ngDialog.open({
+                    template: 'consumerModifyDialog',
+                    controller: 'consumerModifyDialogController',
+                    data:{
+                        consumerRequestList:request,
+                        allClusterNameList:Object.keys(resp.data.clusterInfo.clusterAddrTable),
+                        allBrokerNameList:Object.keys(resp.data.brokerServer),
+                        bIsUpdate:bIsUpdate
+                    }
+                });
+            }else {
+                Notification.error({message: resp.errMsg, delay: 2000});
+            }
+        });
+    };
+    $scope.detail = function(consumerGroupName){
+        $http({
+            method: "GET",
+            url: "consumer/queryTopicByConsumer.query",
+            params:{consumerGroup:consumerGroupName}
+        }).success(function (resp) {
+            if(resp.status ==0){
+                console.log(resp);
+                ngDialog.open({
+                    template: 'consumerTopicViewDialog',
+                    controller: 'consumerTopicViewDialogController',
+                    data:{consumerGroupName:consumerGroupName,data:resp.data}
+                });
+            }else {
+                Notification.error({message: resp.errMsg, delay: 2000});
+            }
+        });
+    };
+
+    $scope.client = function(consumerGroupName){
+        $http({
+            method: "GET",
+            url: "consumer/consumerConnection.query",
+            params:{consumerGroup:consumerGroupName}
+        }).success(function (resp) {
+            if(resp.status ==0){
+                console.log(resp);
+                ngDialog.open({
+                    template: 'clientInfoDialog',
+                    // controller: 'addTopicDialogController',
+                    data:{data:resp.data,consumerGroupName:consumerGroupName}
+                });
+            }else {
+                Notification.error({message: resp.errMsg, delay: 2000});
+            }
+        });
+    };
+    $scope.updateConfigDialog = function(consumerGroupName){
+        $http({
+            method: "GET",
+            url: "consumer/examineSubscriptionGroupConfig.query",
+            params:{consumerGroup:consumerGroupName}
+        }).success(function (resp) {
+            if(resp.status ==0){
+                console.log(resp);
+                $scope.openCreateOrUpdateDialog(resp.data);
+            }else {
+                Notification.error({message: resp.errMsg, delay: 2000});
+            }
+        });
+
+
+    };
+    $scope.delete = function(consumerGroupName){
+        $http({
+            method: "GET",
+            url: "consumer/fetchBrokerNameList.query",
+            params:{
+                consumerGroup:consumerGroupName
+            }
+        }).success(function (resp) {
+            if(resp.status ==0){
+                console.log(resp);
+
+                ngDialog.open({
+                    template: 'deleteConsumerDialog',
+                    controller: 'deleteConsumerDialogController',
+                    data:{
+                        // allClusterList:Object.keys(resp.data.clusterInfo.clusterAddrTable),
+                        allBrokerNameList:resp.data,
+                        consumerGroupName:consumerGroupName
+                    }
+                });
+            }else {
+                Notification.error({message: resp.errMsg, delay: 2000});
+            }
+        });
+    }
+
+}])
+module.controller('consumerMonitorDialogController', function ($scope, ngDialog, $http,Notification) {
+        $scope.createOrUpdateConsumerMonitor = function () {
+            $http({
+                method: "POST",
+                url: "monitor/createOrUpdateConsumerMonitor.do",
+                params:{consumeGroupName:$scope.ngDialogData.consumerGroupName,
+                    minCount:$scope.ngDialogData.data.minCount,
+                    maxDiffTotal:$scope.ngDialogData.data.maxDiffTotal}
+            }).success(function (resp) {
+                if(resp.status ==0){
+                    Notification.info({message: "delete success!", delay: 2000});
+                }else {
+                    Notification.error({message: resp.errMsg, delay: 2000});
+                }
+            });
+        }
+    }
+);
+
+
+module.controller('deleteConsumerDialogController', ['$scope', 'ngDialog', '$http','Notification',function ($scope, ngDialog, $http,Notification) {
+        $scope.selectedClusterList = [];
+        $scope.selectedBrokerNameList = [];
+        $scope.delete = function () {
+            console.log($scope.selectedClusterList);
+            console.log($scope.selectedBrokerNameList);
+            console.log($scope.ngDialogData.consumerGroupName);
+            $http({
+                method: "POST",
+                url: "consumer/deleteSubGroup.do",
+                data:{groupName:$scope.ngDialogData.consumerGroupName,
+                    brokerNameList:$scope.selectedBrokerNameList}
+            }).success(function (resp) {
+                if(resp.status ==0){
+                    Notification.info({message: "delete success!", delay: 2000});
+                }else {
+                    Notification.error({message: resp.errMsg, delay: 2000});
+                }
+            });
+        }
+    }]
+);
+
+module.controller('consumerModifyDialogController', ['$scope', 'ngDialog', '$http','Notification',function ($scope, ngDialog, $http,Notification) {
+        $scope.postConsumerRequest = function (consumerRequest) {
+            var request = JSON.parse(JSON.stringify(consumerRequest));
+            console.log(request);
+            $http({
+                method: "POST",
+                url: "consumer/createOrUpdate.do",
+                data:request
+            }).success(function (resp) {
+                if(resp.status ==0){
+                    Notification.info({message: "update success!", delay: 2000});
+                }else {
+                    Notification.error({message: resp.errMsg, delay: 2000});
+                }
+            });
+        }
+    }]
+);
+
+module.controller('consumerTopicViewDialogController', ['$scope', 'ngDialog', '$http', 'Notification', function ($scope, ngDialog, $http, Notification) {
+        $scope.consumerRunningInfo = function (consumerGroup, clientId, jstack) {
+            $http({
+                method: "GET",
+                url: "consumer/consumerRunningInfo.query",
+                params: {
+                    consumerGroup: consumerGroup,
+                    clientId: clientId,
+                    jstack: jstack
+                }
+            }).success(function (resp) {
+                if (resp.status == 0) {
+                    ngDialog.open({
+                        template: 'consumerClientDialog',
+                        data:{consumerClientInfo:resp.data,
+                        clientId:clientId}
+                    });
+                } else {
+                    Notification.error({message: resp.errMsg, delay: 2000});
+                }
+            });
+        };
+    }]
+);
+
+
+

http://git-wip-us.apache.org/repos/asf/incubator-rocketmq-externals/blob/e218eef9/rocketmq-console/src/main/resources/static/src/controller.js
----------------------------------------------------------------------
diff --git a/rocketmq-console/src/main/resources/static/src/controller.js b/rocketmq-console/src/main/resources/static/src/controller.js
new file mode 100644
index 0000000..8d5d5d2
--- /dev/null
+++ b/rocketmq-console/src/main/resources/static/src/controller.js
@@ -0,0 +1,557 @@
+/*
+ * 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.
+ */
+app.controller('AppCtrl', ['$scope','$rootScope','$cookies','$location','$translate', function ($scope,$rootScope,$cookies,$location,$translate) {
+    $scope.changeTranslate = function(langKey){
+        $translate.use(langKey);
+    }
+}]);
+
+app.controller('dashboardCtrl', ['$scope','$rootScope','$translate','$filter','Notification','remoteApi','tools', function ($scope,$rootScope,$translate,$filter,Notification,remoteApi,tools) {
+
+    $scope.barChart = echarts.init(document.getElementById('main'));
+    $scope.lineChart = echarts.init(document.getElementById('line'));
+    $scope.topicBarChart = echarts.init(document.getElementById('topicBar'));
+    $scope.topicLineChart = echarts.init(document.getElementById('topicLine'));
+    $scope.timepickerOptions ={format: 'YYYY-MM-DD', showClear: true};
+    $scope.topicNames = [];
+
+    $translate('BROKER').then(function (broker) {
+        $scope.BROKER_TITLE = broker;
+        initBrokerBarChart();
+        initBrokerLineChart();
+    }, function (translationId) {
+        $scope.BROKER_TITLE = translationId;
+    });
+
+    $translate('TOPIC').then(function (topic) {
+        $scope.TOPIC_TITLE = topic;
+        initTopicBarChart();
+        initTopicLineChart();
+    }, function (translationId) {
+        $scope.TOPIC_TITLE = translationId;
+    });
+
+    var initBrokerBarChart = function(){
+        $scope.barChart.setOption({
+            title: {
+                text:$scope.BROKER_TITLE + ' TOP 10'
+            },
+            tooltip: {},
+            legend: {
+                data:['TotalMsg']
+            },
+            axisPointer : {
+                type : 'shadow'
+            },
+            xAxis: {
+                data: [],
+                axisLabel: {
+                    inside: false,
+                    textStyle: {
+                        color: '#000000'
+                    },
+                    rotate: 0,
+                    interval:0
+                },
+                axisTick: {
+                    show: true
+                },
+                axisLine: {
+                    show: true
+                },
+                z: 10
+            },
+            yAxis: {
+                type: 'value',
+                boundaryGap: [0, '100%'],
+                axisLabel: {
+                    formatter: function(value){
+                        return value.toFixed(2);
+                    }
+                },
+                splitLine: {
+                    show: true
+                }
+            },
+            series: [{
+                name: 'TotalMsg',
+                type: 'bar',
+                data: []
+            }]
+        })
+    }
+
+    var initBrokerLineChart = function(){
+        $scope.lineChart.setOption({
+            title: {
+                text: $scope.BROKER_TITLE + ' 5min trend'
+            },
+            toolbox: {
+                feature: {
+                    dataZoom: {
+                        yAxisIndex: 'none'
+                    },
+                    restore: {},
+                    saveAsImage: {}
+                }
+            },
+            tooltip: {
+                trigger: 'axis',
+                axisPointer: {
+                    animation: false
+                }
+            },
+            yAxis: {
+                type: 'value',
+                boundaryGap: [0, '80%'],
+                axisLabel: {
+                    formatter: function(value){
+                        return value.toFixed(2);
+                    }
+                },
+                splitLine: {
+                    show: true
+                }
+            },
+            dataZoom: [{
+                type: 'inside',
+                start: 90,
+                end: 100
+            }, {
+                start: 0,
+                end: 10,
+                handleIcon: 'M10.7,11.9v-1.3H9.3v1.3c-4.9,0.3-8.8,4.4-8.8,9.4c0,5,3.9,9.1,8.8,9.4v1.3h1.3v-1.3c4.9-0.3,8.8-4.4,8.8-9.4C19.5,16.3,15.6,12.2,10.7,11.9z M13.3,24.4H6.7V23h6.6V24.4z M13.3,19.6H6.7v-1.4h6.6V19.6z',
+                handleSize: '80%',
+                handleStyle: {
+                    color: '#fff',
+                    shadowBlur: 3,
+                    shadowColor: 'rgba(0, 0, 0, 0.6)',
+                    shadowOffsetX: 2,
+                    shadowOffsetY: 2
+                }
+            }],
+            legend: {
+                data: [],
+                top:30
+            },
+            xAxis: {
+                type: 'time',
+                boundaryGap: false,
+                data: []
+            },
+            series: []
+        })
+
+    }
+
+    var initTopicBarChart = function(){
+        $scope.topicBarChart.setOption({
+            title: {
+                text:$scope.TOPIC_TITLE + ' TOP 10'
+            },
+            tooltip: {},
+            legend: {
+                data:['TotalMsg']
+            },
+            axisPointer : {
+                type : 'shadow'
+            },
+            xAxis: {
+                data: [],
+                axisLabel: {
+                    inside: false,
+                    textStyle: {
+                        color: '#000000'
+                    },
+                    rotate: 0,
+                    interval:0
+                },
+                axisTick: {
+                    show: true
+                },
+                axisLine: {
+                    show: true
+                },
+                z: 10
+            },
+            yAxis: {
+                type: 'value',
+                boundaryGap: [0, '100%'],
+                axisLabel: {
+                    formatter: function(value){
+                        return value.toFixed(2);
+                    }
+                },
+                splitLine: {
+                    show: true
+                }
+            },
+            series: [{
+                name: 'TotalMsg',
+                type: 'bar',
+                data: []
+            }]
+        })
+    }
+
+    var initTopicLineChart = function(){
+        var _option = {
+            baseOption:{
+                title: {
+                    text: $scope.TOPIC_TITLE + ' 5min trend'
+                },
+                toolbox: {
+                    feature: {
+                        dataZoom: {
+                            yAxisIndex: 'none'
+                        },
+                        restore: {},
+                        saveAsImage: {}
+                    }
+                },
+                grid:{
+                    top:100
+                },
+                tooltip: {
+                    trigger: 'axis',
+                    axisPointer: {
+                        animation: false
+                    }
+                },
+                yAxis: {
+                    type: 'value',
+                    boundaryGap: [0, '80%'],
+                    axisLabel: {
+                        formatter: function(value){
+                            return value.toFixed(2);
+                        }
+                    },
+                    splitLine: {
+                        show: true
+                    }
+                },
+                dataZoom: [{
+                    type: 'inside',
+                    start: 90,
+                    end: 100
+                }, {
+                    start: 0,
+                    end: 10,
+                    handleIcon: 'M10.7,11.9v-1.3H9.3v1.3c-4.9,0.3-8.8,4.4-8.8,9.4c0,5,3.9,9.1,8.8,9.4v1.3h1.3v-1.3c4.9-0.3,8.8-4.4,8.8-9.4C19.5,16.3,15.6,12.2,10.7,11.9z M13.3,24.4H6.7V23h6.6V24.4z M13.3,19.6H6.7v-1.4h6.6V19.6z',
+                    handleSize: '80%',
+                    handleStyle: {
+                        color: '#fff',
+                        shadowBlur: 3,
+                        shadowColor: 'rgba(0, 0, 0, 0.6)',
+                        shadowOffsetX: 2,
+                        shadowOffsetY: 2
+                    }
+                }],
+                legend:{
+                    data:[],
+                    top:30
+                },
+                xAxis: {
+                    type: 'time',
+                    boundaryGap: false,
+                    data: []
+                },
+                series: []
+            }
+        }
+        $scope.topicLineChart.setOption(_option)
+
+    }
+
+    var getBrokerBarChartOp = function(xAxisData,data){
+        // 指定图表的配置项和数据
+        var option = {
+            xAxis: {
+                data: xAxisData,
+                axisLabel: {
+                    inside: false,
+                    textStyle: {
+                        color: '#000000'
+                    },
+                    rotate: 0,
+                    interval:0
+                },
+                axisTick: {
+                    show: true
+                },
+                axisLine: {
+                    show: true
+                },
+                z: 10
+            },
+            series: [{
+                name: 'TotalMsg',
+                type: 'bar',
+                data: data
+            }]
+        };
+
+        $scope.barChart.setOption(option);
+    }
+
+    var callback = function (resp) {
+        $scope.barChart.hideLoading();
+        if (resp.status == 0) {
+            var clusterAddrTable = resp.data.clusterInfo.clusterAddrTable;
+            var brokerMap = resp.data.clusterInfo.brokerAddrTable;
+            var brokerDetail = resp.data.brokerServer;
+            var clusterMap = tools.generateBrokerMap(brokerDetail,clusterAddrTable,brokerMap);
+            $scope.brokerArray = [];
+            $.each(clusterMap,function(clusterName,brokers){
+                $.each(brokers,function(i,broker){
+                    $scope.brokerArray.push(broker)
+                })
+            });
+
+            //sort the brokerArray
+            $scope.brokerArray.sort(function(firstBroker,lastBroker){
+                var firstTotalMsg = parseFloat(firstBroker.msgGetTotalTodayNow);
+                var lastTotalMsg = parseFloat(lastBroker.msgGetTotalTodayNow);
+                return lastTotalMsg-firstTotalMsg;
+            });
+
+            var xAxisData = [],
+                data = [];
+
+            $.each($scope.brokerArray,function(i,broker){
+                if(i > 9){
+                    return false;
+                }
+                xAxisData.push(broker.brokerName + ":" + broker.index);
+                data.push(broker.msgGetTotalTodayNow);
+            })
+            getBrokerBarChartOp(xAxisData,data);
+        }else{
+            Notification.error({message: resp.errMsg, delay: 2000});
+        }
+    }
+
+    $scope.barChart.showLoading();
+    remoteApi.queryClusterList(callback);
+
+    $scope.topicBarChart.showLoading();
+    remoteApi.queryTopicCurrentData(function(resp){
+        $scope.topicBarChart.hideLoading();
+        if (resp.status == 0) {
+            var topicList = resp.data;
+            topicList.sort(function(first,last){
+                var firstTotalMsg = parseFloat(first.split(",")[1]);
+                var lastTotalMsg = parseFloat(last.split(",")[1]);
+                return lastTotalMsg-firstTotalMsg;
+            })
+
+            var xAxisData = [];
+            var data = [];
+            $.each(topicList,function (i,currentData) {
+                var currentArray = currentData.split(",");
+                $scope.topicNames.push(currentArray[0]);
+                if(!angular.isDefined($scope.selectedTopic)){
+                    $scope.selectedTopic = currentArray[0];
+                }
+            })
+            $.each(topicList,function (i, currentData) {
+                if(i > 9){
+                    return false;
+                }
+                var currentArray = currentData.split(",");
+                xAxisData.push(currentArray[0]);
+                data.push(currentArray[1]);
+            })
+            // 指定图表的配置项和数据
+            var option = {
+                xAxis: {
+                    data: xAxisData,
+                    axisLabel: {
+                        inside: false,
+                        textStyle: {
+                            color: '#000000'
+                        },
+                        rotate: 60,
+                        interval:0
+                    },
+                    axisTick: {
+                        show: true
+                    },
+                    axisLine: {
+                        show: true
+                    },
+                    z: 10
+                },
+                series: [{
+                    name: 'TotalMsg',
+                    type: 'bar',
+                    data: data
+                }]
+            };
+            $scope.topicBarChart.setOption(option);
+            queryLineData();
+        }else{
+            Notification.error({message: resp.errMsg, delay: 2000});
+        }
+    })
+
+
+    var getBrokerLineChart = function(legend,data){
+        var series = [];
+        var xAxisData = [];
+        var flag = true;
+        var i = 0;
+        $.each(data,function(key,value){
+            // if(i > 9 ){
+            //     return false;
+            // }
+            var _tps = [];
+            $.each(value,function(i,tpsValue){
+                var tpsArray = tpsValue.split(",");
+                if(flag){
+                    xAxisData.push($filter('date')(tpsArray[0], "HH:mm:ss"));
+                }
+                _tps.push(tpsArray[1]);
+            })
+            flag = false;
+            var _series = {
+                name:key,
+                type:'line',
+                smooth:true,
+                symbol: 'none',
+                sampling: 'average',
+                data: _tps
+            }
+            series.push(_series);
+            i++
+        })
+
+        var option = {
+            legend: {
+                data: legend
+            },
+            color: ["#FF0000", "#00BFFF", "#FF00FF", "#1ce322", "#000000", '#EE7942'],
+            xAxis: {
+                type: 'category',
+                boundaryGap: false,
+                data: xAxisData
+            },
+            series: series
+        };
+        return option;
+    }
+
+    var getTopicLineChart = function(legend,data){
+        var series = [];
+        var xAxisData = [];
+        var flag = true;
+        var i = 0;
+        $.each(data,function(key,value){
+            var _tps = [];
+            $.each(value,function(i,tpsValue){
+                var tpsArray = tpsValue.split(",");
+                if(flag){
+                    xAxisData.push($filter('date')(tpsArray[0], "HH:mm:ss"));
+                }
+                _tps.push(tpsArray[3]);
+            })
+            flag = false;
+            var _series = {
+                name:key,
+                type:'line',
+                smooth:true,
+                symbol: 'none',
+                sampling: 'average',
+                data: _tps
+            }
+            series.push(_series);
+            i++
+        })
+
+        var option = {
+            baseOption:{
+                legend: {
+                    data: legend
+                },
+                // color: ["#FF0000", "#00BFFF", "#FF00FF", "#1ce322", "#000000", '#EE7942'],
+                xAxis: {
+                    type: 'category',
+                    boundaryGap: false,
+                    data: xAxisData
+                },
+                series: series
+            },
+            media:[
+                {
+                    query:{},
+                    option:{
+
+                    }
+                }
+            ]
+
+        };
+        return option;
+    }
+
+
+    var queryLineData = function () {
+        var _date;
+        if($scope.date != null){
+            _date = $filter('date')($scope.date.valueOf(), "yyyy-MM-dd");
+        }else{
+            _date = $filter('date')(new Date(), "yyyy-MM-dd");
+        }
+        // $scope.lineChart.showLoading();
+        remoteApi.queryBrokerHisData(_date,function(resp){
+            // $scope.lineChart.hideLoading();
+            if (resp.status == 0) {
+                var _data = {}
+                var _xAxisData = [];
+                $.each(resp.data,function(address,values){
+                    _data[address] = values;
+                    _xAxisData.push(address);
+                })
+                $scope.lineChart.setOption(getBrokerLineChart(_xAxisData,_data));
+            }else{
+                Notification.error({message: "" + resp.errMsg, delay: 2000});
+            }
+        })
+
+        $scope.topicLineChart.showLoading();
+        remoteApi.queryTopicHisData(_date,$scope.selectedTopic,function (resp) {
+            $scope.topicLineChart.hideLoading();
+            if (resp.status == 0) {
+                var _data = {};
+                _data[$scope.selectedTopic] = resp.data;
+                var _xAxisData = $scope.selectedTopic;
+                $scope.topicLineChart.setOption(getTopicLineChart(_xAxisData,_data));
+            }else{
+                Notification.error({message: "" + resp.errMsg, delay: 2000});
+            }
+
+        })
+
+    }
+
+    //router after will clear this thread
+    $rootScope._thread = setInterval( queryLineData, tools.dashboardRefreshTime);
+
+
+}]);
+
+

http://git-wip-us.apache.org/repos/asf/incubator-rocketmq-externals/blob/e218eef9/rocketmq-console/src/main/resources/static/src/data/dict.json
----------------------------------------------------------------------
diff --git a/rocketmq-console/src/main/resources/static/src/data/dict.json b/rocketmq-console/src/main/resources/static/src/data/dict.json
new file mode 100644
index 0000000..defdab3
--- /dev/null
+++ b/rocketmq-console/src/main/resources/static/src/data/dict.json
@@ -0,0 +1,4 @@
+[
+  {"TYPE":"DEMO_TYPE","DICT_VALUE":"0","DICT_NAME":"test1"},
+  {"TYPE":"DEMO_TYPE","DICT_VALUE":"1","DICT_NAME":"test2"}
+]
\ No newline at end of file