[ISSUE #30]Added DLQ message management (#31)

* [ISSUE #30]Added DLQ message management

* remove the specific namesrvAddr in application.properties.

Co-authored-by: zhangjidi <zhangjidi@cmss.chinamobile.com>
This commit is contained in:
zhangjidi2016
2021-10-31 11:29:55 +08:00
committed by GitHub
parent d5fed12773
commit 4b2b61e394
16 changed files with 936 additions and 20 deletions

13
pom.xml
View File

@@ -100,6 +100,8 @@
<mockito-inline.version>3.3.3</mockito-inline.version>
<jaxb-api.version>2.3.1</jaxb-api.version>
<commons-pool2.version>2.4.3</commons-pool2.version>
<easyexcel.version>2.2.10</easyexcel.version>
<asm.version>4.2</asm.version>
</properties>
<dependencies>
@@ -234,6 +236,17 @@
<artifactId>commons-pool2</artifactId>
<version>${commons-pool2.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>${easyexcel.version}</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>${asm.version}</version>
</dependency>
</dependencies>
<build>
<plugins>

View File

@@ -0,0 +1,73 @@
/*
* 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.dashboard.controller;
import com.google.common.collect.Lists;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import org.apache.rocketmq.common.MixAll;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.dashboard.exception.ServiceException;
import org.apache.rocketmq.dashboard.model.DlqMessageExcelModel;
import org.apache.rocketmq.dashboard.model.request.MessageQuery;
import org.apache.rocketmq.dashboard.permisssion.Permission;
import org.apache.rocketmq.dashboard.service.DlqMessageService;
import org.apache.rocketmq.dashboard.util.ExcelUtil;
import org.apache.rocketmq.tools.admin.MQAdminExt;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@RequestMapping("/dlqMessage")
@Permission
public class DlqMessageController {
@Resource
private DlqMessageService dlqMessageService;
@Resource
private MQAdminExt mqAdminExt;
@RequestMapping(value = "/queryDlqMessageByConsumerGroup.query", method = RequestMethod.POST)
@ResponseBody
public Object queryDlqMessageByConsumerGroup(@RequestBody MessageQuery query) {
return dlqMessageService.queryDlqMessageByPage(query);
}
@GetMapping(value = "/exportDlqMessage.do")
public void exportDlqMessage(HttpServletResponse response, @RequestParam String consumerGroup,
@RequestParam String msgId) {
MessageExt messageExt = null;
try {
String topic = MixAll.DLQ_GROUP_TOPIC_PREFIX + consumerGroup;
messageExt = mqAdminExt.viewMessage(topic, msgId);
} catch (Exception e) {
throw new ServiceException(-1, String.format("Failed to query message by Id: %s", msgId));
}
DlqMessageExcelModel excelModel = new DlqMessageExcelModel(messageExt);
try {
ExcelUtil.writeExcel(response, Lists.newArrayList(excelModel), "dlq", "dlq", DlqMessageExcelModel.class);
} catch (Exception e) {
throw new ServiceException(-1, String.format("export dlq message failed!"));
}
}
}

View File

@@ -0,0 +1,81 @@
/*
* 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.dashboard.model;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.alibaba.excel.metadata.BaseRowModel;
import com.alibaba.excel.util.DateUtils;
import com.google.common.base.Charsets;
import java.io.Serializable;
import java.util.Date;
import lombok.Data;
import org.apache.rocketmq.common.message.MessageExt;
@Data
public class DlqMessageExcelModel extends BaseRowModel implements Serializable {
@ExcelProperty(value = "topic", index = 0)
@ColumnWidth(value = 15)
private String topic;
@ExcelProperty(value = "msgId", index = 1)
@ColumnWidth(value = 15)
private String msgId;
@ExcelProperty(value = "bornHost", index = 2)
@ColumnWidth(value = 15)
private String bornHost;
@ExcelProperty(value = "bornTimestamp", index = 3)
@ColumnWidth(value = 25)
private String bornTimestamp;
@ExcelProperty(value = "storeTimestamp", index = 4)
@ColumnWidth(value = 25)
private String storeTimestamp;
@ExcelProperty(value = "reconsumeTimes", index = 5)
@ColumnWidth(value = 25)
private int reconsumeTimes;
@ExcelProperty(value = "properties", index = 6)
@ColumnWidth(value = 20)
private String properties;
@ExcelProperty(value = "messageBody", index = 7)
@ColumnWidth(value = 20)
private String messageBody;
@ExcelProperty(value = "bodyCRC", index = 8)
@ColumnWidth(value = 15)
private int bodyCRC;
public DlqMessageExcelModel(MessageExt messageExt) {
this.topic = messageExt.getTopic();
this.msgId = messageExt.getMsgId();
this.bornHost = messageExt.getBornHostString();
this.bornTimestamp = DateUtils.format(new Date(messageExt.getBornTimestamp()), DateUtils.DATE_FORMAT_19);
this.storeTimestamp = DateUtils.format(new Date(messageExt.getStoreTimestamp()), DateUtils.DATE_FORMAT_19);
this.reconsumeTimes = messageExt.getReconsumeTimes();
this.properties = messageExt.getProperties().toString();
this.messageBody = new String(messageExt.getBody(), Charsets.UTF_8);
this.bodyCRC = messageExt.getBodyCRC();
}
}

View File

@@ -0,0 +1,26 @@
/*
* 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.dashboard.service;
import org.apache.rocketmq.dashboard.model.MessagePage;
import org.apache.rocketmq.dashboard.model.request.MessageQuery;
public interface DlqMessageService {
MessagePage queryDlqMessageByPage(MessageQuery query);
}

View File

@@ -0,0 +1,68 @@
/*
* 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.dashboard.service.impl;
import com.google.common.base.Throwables;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.MixAll;
import org.apache.rocketmq.common.protocol.ResponseCode;
import org.apache.rocketmq.dashboard.model.MessagePage;
import org.apache.rocketmq.dashboard.model.MessageView;
import org.apache.rocketmq.dashboard.model.request.MessageQuery;
import org.apache.rocketmq.dashboard.service.DlqMessageService;
import org.apache.rocketmq.dashboard.service.MessageService;
import org.apache.rocketmq.tools.admin.MQAdminExt;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class DlqMessageServiceImpl implements DlqMessageService {
@Resource
private MQAdminExt mqAdminExt;
@Resource
private MessageService messageService;
@Override
public MessagePage queryDlqMessageByPage(MessageQuery query) {
List<MessageView> messageViews = new ArrayList<>();
PageRequest page = PageRequest.of(query.getPageNum(), query.getPageSize());
String topic = query.getTopic();
try {
mqAdminExt.examineTopicRouteInfo(topic);
} catch (MQClientException e) {
// If the %DLQ%Group does not exist, the message returns null
if (topic.startsWith(MixAll.DLQ_GROUP_TOPIC_PREFIX)
&& e.getResponseCode() == ResponseCode.TOPIC_NOT_EXIST) {
return new MessagePage(new PageImpl<>(messageViews, page, 0), query.getTaskId());
} else {
throw Throwables.propagate(e);
}
} catch (Exception e) {
throw Throwables.propagate(e);
}
return messageService.queryMessageByPage(query);
}
}

View File

@@ -0,0 +1,56 @@
/*
* 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.dashboard.util;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.support.ExcelTypeEnum;
import com.alibaba.excel.write.metadata.style.WriteCellStyle;
import com.alibaba.excel.write.metadata.style.WriteFont;
import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
public class ExcelUtil {
public static void writeExcel(HttpServletResponse response, List<? extends Object> data, String fileName,
String sheetName, Class clazz) throws Exception {
WriteCellStyle headWriteCellStyle = new WriteCellStyle();
WriteFont writeFont = new WriteFont();
writeFont.setFontHeightInPoints((short)12);
writeFont.setFontName("Microsoft YaHei UI");
headWriteCellStyle.setWriteFont(writeFont);
headWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
contentWriteCellStyle.setWriteFont(writeFont);
contentWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
HorizontalCellStyleStrategy horizontalCellStyleStrategy = new HorizontalCellStyleStrategy(headWriteCellStyle, contentWriteCellStyle);
EasyExcel.write(getOutputStream(fileName, response), clazz)
.excelType(ExcelTypeEnum.XLSX).sheet(sheetName).registerWriteHandler(horizontalCellStyleStrategy).doWrite(data);
}
private static OutputStream getOutputStream(String fileName, HttpServletResponse response) throws Exception {
fileName = URLEncoder.encode(fileName, "UTF-8");
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding("utf8");
response.setHeader("Content-Disposition", "attachment;filename=" + fileName + ".xlsx");
return response.getOutputStream();
}
}

View File

@@ -107,6 +107,7 @@
<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/dlqMessage.js"></script>
<script type="text/javascript" src="src/messageTrace.js"></script>
<script type="text/javascript" src="src/ops.js?timestamp=7"></script>
<script type="text/javascript" src="src/remoteApi/remoteApi.js"></script>

View File

@@ -195,6 +195,9 @@ app.config(['$routeProvider', '$httpProvider','$cookiesProvider','getDictNamePro
}).when('/message', {
templateUrl: 'view/pages/message.html',
controller:'messageController'
}).when('/dlqMessage', {
templateUrl: 'view/pages/dlqMessage.html',
controller:'dlqMessageController'
}).when('/messageTrace', {
templateUrl: 'view/pages/messageTrace.html',
controller:'messageTraceController'

View File

@@ -0,0 +1,184 @@
/*
* 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;
const SYS_GROUP_TOPIC_PREFIX = "%SYS%";
module.controller('dlqMessageController', ['$scope', 'ngDialog', '$http', 'Notification', function ($scope, ngDialog, $http, Notification) {
$scope.allConsumerGroupList = [];
$scope.selectedConsumerGroup = [];
$scope.messageId = "";
$scope.queryDlqMessageByConsumerGroupResult = [];
$scope.queryDlqMessageByMessageIdResult = {};
$http({
method: "GET",
url: "consumer/groupList.query"
}).success(function (resp) {
if (resp.status == 0) {
for (const consumerGroup of resp.data) {
if (!consumerGroup.group.startsWith(SYS_GROUP_TOPIC_PREFIX)) {
$scope.allConsumerGroupList.push(consumerGroup.group);
}
}
$scope.allConsumerGroupList.sort();
} else {
Notification.error({message: resp.errMsg, delay: 2000});
}
});
$scope.timepickerBegin = moment().subtract(3, 'hour').format('YYYY-MM-DD HH:mm');
$scope.timepickerEnd = moment().format('YYYY-MM-DD HH:mm');
$scope.timepickerOptions = {format: 'YYYY-MM-DD HH:mm', showClear: true};
$scope.taskId = "";
$scope.paginationConf = {
currentPage: 1,
totalItems: 0,
itemsPerPage: 20,
pagesLength: 15,
perPageOptions: [10],
rememberPerPage: 'perPageItems',
onChange: function () {
$scope.queryDlqMessageByConsumerGroup()
}
};
$scope.queryDlqMessageByConsumerGroup = function () {
$("#noMsgTip").css("display", "none");
if ($scope.timepickerEnd < $scope.timepickerBegin) {
Notification.error({message: "endTime is later than beginTime!", delay: 2000});
return
}
if ($scope.selectedConsumerGroup === [] || (typeof $scope.selectedConsumerGroup) == "object") {
return
}
$http({
method: "POST",
url: "dlqMessage/queryDlqMessageByConsumerGroup.query",
data: {
topic: DLQ_GROUP_TOPIC_PREFIX + $scope.selectedConsumerGroup,
begin: $scope.timepickerBegin.valueOf(),
end: $scope.timepickerEnd.valueOf(),
pageNum: $scope.paginationConf.currentPage,
pageSize: $scope.paginationConf.itemsPerPage,
taskId: $scope.taskId
}
}).success(function (resp) {
if (resp.status === 0) {
$scope.messageShowList = resp.data.page.content;
if ($scope.messageShowList.length == 0){
$("#noMsgTip").removeAttr("style");
}
console.log($scope.messageShowList);
if (resp.data.page.first) {
$scope.paginationConf.currentPage = 1;
}
$scope.paginationConf.currentPage = resp.data.page.number + 1;
$scope.paginationConf.totalItems = resp.data.page.totalElements;
$scope.taskId = resp.data.taskId
} else {
Notification.error({message: resp.errMsg, delay: 2000});
}
});
}
$scope.queryDlqMessageDetail = function (messageId, consumerGroup) {
$http({
method: "GET",
url: "messageTrace/viewMessage.query",
params: {
msgId: messageId,
topic: DLQ_GROUP_TOPIC_PREFIX + consumerGroup
}
}).success(function (resp) {
if (resp.status == 0) {
console.log(resp);
ngDialog.open({
template: 'dlqMessageDetailViewDialog',
data: resp.data
});
} else {
Notification.error({message: resp.errMsg, delay: 2000});
}
});
};
$scope.queryDlqMessageByMessageId = function (messageId, consumerGroup) {
$http({
method: "GET",
url: "messageTrace/viewMessage.query",
params: {
msgId: messageId,
topic: DLQ_GROUP_TOPIC_PREFIX + consumerGroup
}
}).success(function (resp) {
if (resp.status == 0) {
$scope.queryDlqMessageByMessageIdResult = resp.data;
} else {
Notification.error({message: resp.errMsg, delay: 2000});
}
});
};
$scope.changeShowMessageList = function (currentPage, totalItem) {
var perPage = $scope.paginationConf.itemsPerPage;
var from = (currentPage - 1) * perPage;
var to = (from + perPage) > totalItem ? totalItem : from + perPage;
$scope.messageShowList = $scope.queryMessageByTopicResult.slice(from, to);
$scope.paginationConf.totalItems = totalItem;
};
$scope.onChangeQueryCondition = function () {
console.log("change")
$scope.taskId = "";
$scope.paginationConf.currentPage = 1;
$scope.paginationConf.totalItems = 0;
}
$scope.resendDlqMessage = function (messageView, consumerGroup) {
const topic = messageView.properties.RETRY_TOPIC;
const msgId = messageView.properties.ORIGIN_MESSAGE_ID;
$http({
method: "POST",
url: "message/consumeMessageDirectly.do",
params: {
msgId: msgId,
consumerGroup: consumerGroup,
topic: topic
}
}).success(function (resp) {
if (resp.status == 0) {
ngDialog.open({
template: 'operationResultDialog',
data: {
result: resp.data
}
});
} else {
ngDialog.open({
template: 'operationResultDialog',
data: {
result: resp.errMsg
}
});
}
});
};
$scope.exportDlqMessage = function (msgId, consumerGroup) {
window.location.href = "dlqMessage/exportDlqMessage.do?msgId=" + msgId + "&consumerGroup=" + consumerGroup;
}
}]);

View File

@@ -41,6 +41,7 @@ var en = {
"RESEND_MESSAGE":"Resend Message",
"VIEW_EXCEPTION":"View Exception",
"MESSAGETRACE":"MessageTrace",
"DLQ_MESSAGE":"DLQMessage",
"COMMIT": "Commit",
"OPERATION": "Operation",
"ADD": "Add",
@@ -108,5 +109,7 @@ var en = {
"ENABLE_MESSAGE_TRACE":"Enable Message Trace",
"MESSAGE_TRACE_DETAIL":"Message Trace Detail",
"TRACE_TOPIC":"TraceTopic",
"SELECT_TRACE_TOPIC":"selectTraceTopic"
"SELECT_TRACE_TOPIC":"selectTraceTopic",
"EXPORT": "export",
"NO_MATCH_RESULT": "no match result"
}

View File

@@ -39,8 +39,9 @@ var zh = {
"PRODUCER":"生产者",
"MESSAGE":"消息",
"MESSAGE_DETAIL":"消息详情",
"RESEND_MESSAGE":"重新消费",
"RESEND_MESSAGE":"重新发送",
"VIEW_EXCEPTION":"查看异常",
"DLQ_MESSAGE":"死信消息",
"MESSAGETRACE":"消息轨迹",
"OPERATION": "操作",
"ADD": "新增",
@@ -109,5 +110,7 @@ var zh = {
"ENABLE_MESSAGE_TRACE":"开启消息轨迹",
"MESSAGE_TRACE_DETAIL":"消息轨迹详情",
"TRACE_TOPIC":"消息轨迹主题",
"SELECT_TRACE_TOPIC":"选择消息轨迹主题"
"SELECT_TRACE_TOPIC":"选择消息轨迹主题",
"EXPORT": "导出",
"NO_MATCH_RESULT": "没有查到符合条件的结果"
}

View File

@@ -34,6 +34,7 @@
<li ng-class="path =='consumer' ? 'active':''"><a ng-href="#/consumer">{{'CONSUMER' | translate}}</a></li>
<li ng-class="path =='producer' ? 'active':''"><a ng-href="#/producer">{{'PRODUCER' | translate}}</a></li>
<li ng-class="path =='message' ? 'active':''"><a ng-href="#/message">{{'MESSAGE' | translate}}</a></li>
<li ng-class="path =='dlqMessage' ? 'active':''"><a ng-href="#/dlqMessage">{{'DLQ_MESSAGE' | translate}}</a></li>
<li ng-class="path =='messageTrace' ? 'active':''"><a ng-href="#/messageTrace">{{'MESSAGETRACE' | translate}}</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">

View File

@@ -209,8 +209,8 @@
<div class="modal-body " ng-repeat="item in ngDialogData.consumerRequestList">
<form id="addAppForm" name="addAppForm" class="form-horizontal" novalidate>
<div class="form-group" ng-hide="ngDialogData.bIsUpdate">
<label class="control-label col-sm-4">clusterName:</label>
<div class="col-sm-8">
<label class="control-label col-sm-3">clusterName:</label>
<div class="col-sm-9">
<select name="mySelectClusterNameList" multiple chosen
ng-model="item.clusterNameList"
ng-options="clusterNameItem for clusterNameItem in ngDialogData.allClusterNameList">
@@ -219,8 +219,8 @@
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-4">brokerName:</label>
<div class="col-sm-8">
<label class="control-label col-sm-3">brokerName:</label>
<div class="col-sm-9">
<select name="mySelectBrokerNameList" multiple chosen
ng-disabled="ngDialogData.bIsUpdate"
ng-model="item.brokerNameList"
@@ -230,16 +230,16 @@
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-4">groupName:</label>
<div class="col-sm-8">
<label class="control-label col-sm-3">groupName:</label>
<div class="col-sm-9">
<input class="form-control" ng-model="item.subscriptionGroupConfig.groupName" type="text"
ng-disabled="ngDialogData.bIsUpdate" required/>
<span class="text-danger" ng-show="addAppForm.name.$error.required">编号不能为空.</span>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-4">consumeEnable:</label>
<div class="col-sm-8">
<label class="control-label col-sm-3">consumeEnable:</label>
<div class="col-sm-9">
<md-switch class="md-primary" ng-disabled="{{!ngDialogData.writeOperationEnabled}}" md-no-ink
aria-label="Switch No Ink" ng-model="item.subscriptionGroupConfig.consumeEnable">
</md-switch>
@@ -257,8 +257,8 @@
<!--</div>-->
<!--</div>-->
<div class="form-group">
<label class="control-label col-sm-4">consumeBroadcastEnable:</label>
<div class="col-sm-8">
<label class="control-label col-sm-3">consumeBroadcastEnable:</label>
<div class="col-sm-9">
<md-switch class="md-primary" ng-disabled="{{!ngDialogData.writeOperationEnabled}}" md-no-ink
aria-label="Switch No Ink"
ng-model="item.subscriptionGroupConfig.consumeBroadcastEnable">
@@ -266,8 +266,8 @@
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-4">retryQueueNums:</label>
<div class="col-sm-8">
<label class="control-label col-sm-3">retryQueueNums:</label>
<div class="col-sm-9">
<input class="form-control" ng-model="item.subscriptionGroupConfig.retryQueueNums"
type="text" ng-disabled="{{!ngDialogData.writeOperationEnabled}}"
required/>
@@ -284,16 +284,16 @@
<!--</div>-->
<!--</div>-->
<div class="form-group">
<label class="control-label col-sm-2">brokerId:</label>
<div class="col-sm-8">
<label class="control-label col-sm-3">brokerId:</label>
<div class="col-sm-9">
<input class="form-control" ng-model="item.subscriptionGroupConfig.brokerId" type="text"
ng-disabled="{{!ngDialogData.writeOperationEnabled}}" required/>
<span class="text-danger" ng-show="addAppForm.name.$error.required">编号不能为空.</span>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2">whichBrokerWhenConsumeSlowly:</label>
<div class="col-sm-8">
<label class="control-label col-sm-3">whichBrokerWhenConsumeSlowly:</label>
<div class="col-sm-9">
<input class="form-control"
ng-model="item.subscriptionGroupConfig.whichBrokerWhenConsumeSlowly" type="text"
ng-disabled="{{!ngDialogData.writeOperationEnabled}}" required/>

View File

@@ -0,0 +1,266 @@
<!--
~ 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.
-->
<div class="container-fluid" id="deployHistoryList">
<div class="modal-body">
<div ng-cloak="" class="tabsdemoDynamicHeight">
<md-content>
<md-tabs md-dynamic-height="" md-border-bottom="">
<md-tab label="Consumer">
<h5 class="md-display-5">Total {{paginationConf.totalItems}} Messages</h5>
<md-content class="md-padding" style="min-height:600px">
<div class="row">
<form class="form-inline pull-left">
<div class="form-group">
<label>{{'CONSUMER' | translate}}:</label>
</div>
<div class="form-group ">
<div style="width: 300px">
<select name="mySelectGroup" chosen
ng-model="selectedConsumerGroup"
ng-options="item for item in allConsumerGroupList"
ng-change="onChangeQueryCondition()"
required>
<option value=""></option>
</select>
</div>
</div>
<div class="form-group ">
<label>{{'BEGIN' | translate}}:</label>
<div class="input-group">
<input class="form-control" datetimepicker
ng-change="onChangeQueryCondition()" ng-model="timepickerBegin"
options="timepickerOptions"/>
<span class="input-group-addon"><span
class="glyphicon glyphicon-calendar"></span></span>
</div>
</div>
<div class="form-group">
<label>{{'END' | translate}}:</label>
<div class="input-group">
<input class="form-control" datetimepicker
ng-change="onChangeQueryCondition()" ng-model="timepickerEnd"
options="timepickerOptions"/>
<span class="input-group-addon"><span
class="glyphicon glyphicon-calendar"></span></span>
</div>
</div>
<button id="searchAppsButton" type="button"
class="btn btn-raised btn-sm btn-primary"
data-toggle="modal"
ng-click="queryDlqMessageByConsumerGroup()">
<span class="glyphicon glyphicon-search"></span> {{ 'SEARCH' | translate}}
</button>
</form>
<table class="table table-bordered text-middle">
<tr>
<th class="text-center">Message ID</th>
<th class="text-center">Tag</th>
<th class="text-center">Key</th>
<th class="text-center">StoreTime</th>
<th class="text-center">Operation</th>
</tr>
<tr style="display: none" id="noMsgTip">
<td colspan="6" style="text-align: center">{{'NO_MATCH_RESULT' | translate}}</td>
</tr>
<tr ng-repeat="item in messageShowList">
<td class="text-center">{{item.msgId}}</td>
<td class="text-center">{{item.properties.TAGS}}</td>
<td class="text-center">{{item.properties.KEYS}}</td>
<td class="text-center">{{item.storeTimestamp | date:'yyyy-MM-dd HH:mm:ss'}}
</td>
<td class="text-center">
<button class="btn btn-raised btn-sm btn-primary" type="button"
ng-click="queryDlqMessageDetail(item.msgId, selectedConsumerGroup)">
{{'MESSAGE_DETAIL' | translate}}
</button>
<button class="btn btn-raised btn-sm btn-primary" type="button"
ng-click="resendDlqMessage(item, selectedConsumerGroup)">
{{'RESEND_MESSAGE' | translate}}
</button>
<button class="btn btn-raised btn-sm btn-primary" type="button"
ng-click="exportDlqMessage(item.msgId, selectedConsumerGroup)">
{{'EXPORT' | translate}}
</button>
</td>
</tr>
</table>
<tm-pagination conf="paginationConf"></tm-pagination>
</div>
</md-content>
</md-tab>
<md-tab label="Message ID">
<h5 class="md-display-5">topic can't be empty if you producer client version>=v3.5.8</h5>
<md-content class="md-padding" style="min-height:600px">
<div class="row">
<form class="form-inline pull-left">
<div class="form-group">
<label>{{'CONSUMER' | translate}}:</label>
</div>
<div class="form-group ">
<div style="width: 300px">
<select name="mySelectGroup" chosen
ng-model="selectedConsumerGroup"
ng-options="item for item in allConsumerGroupList"
ng-change="onChangeQueryCondition()"
required>
<option value=""></option>
</select>
</div>
</div>
<div class="form-group">
<label>MessageId:</label>
<input class="form-control" style="width: 450px" type="text" ng-model="messageId"
required/>
</div>
<button type="button" class="btn btn-raised btn-sm btn-primary" data-toggle="modal"
ng-click="queryDlqMessageByMessageId(messageId, selectedConsumerGroup)">
<span class="glyphicon glyphicon-search"></span> {{ 'SEARCH' | translate}}
</button>
</form>
<table class="table table-bordered text-middle">
<tr>
<th class="text-center">Message ID</th>
<th class="text-center">Tag</th>
<th class="text-center">Key</th>
<th class="text-center">StoreTime</th>
<th class="text-center">Operation</th>
</tr>
<tr ng-repeat="item in queryDlqMessageByMessageIdResult">
<td class="text-center">{{item.msgId}}</td>
<td class="text-center">{{item.properties.TAGS}}</td>
<td class="text-center">{{item.properties.KEYS}}</td>
<td class="text-center">{{item.storeTimestamp | date:'yyyy-MM-dd HH:mm:ss'}}
</td>
<td class="text-center">
<button class="btn btn-raised btn-sm btn-primary" type="button"
ng-click="queryDlqMessageDetail(item.msgId, selectedConsumerGroup)">
{{'MESSAGE_DETAIL' | translate}}
</button>
<button class="btn btn-raised btn-sm btn-primary" type="button"
ng-click="resendDlqMessage(item, selectedConsumerGroup)">
{{'RESEND_MESSAGE' | translate}}
</button>
<button class="btn btn-raised btn-sm btn-primary" type="button"
ng-click="exportDlqMessage(item.msgId, selectedConsumerGroup)">
{{'EXPORT' | translate}}
</button>
</td>
</tr>
</table>
</div>
</md-content>
</md-tab>
</md-tabs>
</md-content>
</div>
</div>
</div>
<script type="text/ng-template" id="dlqMessageDetailViewDialog">
<md-content class="md-padding">
<div>
<form id="addAppForm" name="addAppForm" class="form-horizontal" novalidate>
<div class="form-group">
<label class="control-label col-sm-2">Message ID:</label>
<div class="col-sm-10">
<label class="form-control">{{ngDialogData.messageView.msgId}}</label>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2">Topic:</label>
<div class="col-sm-10">
<label class="form-control">{{ngDialogData.messageView.topic}}</label>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2">Properties:</label>
<div class="col-sm-10">
<textarea class="form-control"
style="min-height:60px; resize: none"
readonly>{{ngDialogData.messageView.properties}}</textarea>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2">ReconsumeTimes:</label>
<div class="col-sm-10">
<label class="form-control">{{ngDialogData.messageView.reconsumeTimes}}</label>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2">Tag:</label>
<div class="col-sm-10">
<label class="form-control">{{ngDialogData.messageView.properties.TAGS}}</label>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2">Key:</label>
<div class="col-sm-10">
<label class="form-control">{{ngDialogData.messageView.properties.KEYS}}</label>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2">Storetime:</label>
<div class="col-sm-10">
<label class="form-control">{{ngDialogData.messageView.storeTimestamp | date:'yyyy-MM-dd HH:mm:ss'}}</label>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2">StoreHost:</label>
<div class="col-sm-10">
<label class="form-control">{{ngDialogData.messageView.storeHost}}</label>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2">Message body:</label>
<div class="col-sm-10">
<textarea class="form-control"
ng-model="ngDialogData.messageView.messageBody"
style="min-height:100px; resize: none"
readonly></textarea>
</div>
</div>
</form>
</div>
</md-content>
<div class="modal-footer">
<div class="ngdialog-buttons">
<button type="button" class="ngdialog-button ngdialog-button-secondary"
ng-click="closeThisDialog('Cancel')">{{ 'CLOSE' | translate }}
</button>
</div>
</div>
</script>
<script type="text/ng-template" id="operationResultDialog">
<div class="modal-header">
<h4 class="modal-title">Result</h4>
</div>
<div class="modal-body ">
<form class="form-horizontal" novalidate>
{{ngDialogData.result}}
</form>
</div>
<div class="modal-footer">
<div class="ngdialog-buttons">
<button type="button" class="ngdialog-button ngdialog-button-secondary"
ng-click="closeThisDialog('Cancel')">{{ 'CLOSE' | translate }}
</button>
</div>
</div>
</script>

View File

@@ -228,7 +228,7 @@
</form>
</div>
<p>messageTrackList:</p>
<table class="table-bordered table">
<table class="table-bordered table text-middle">
<tr>
<th class="text-center">consumerGroup</th>
<th class="text-center">trackType</th>

View File

@@ -0,0 +1,138 @@
/*
* 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.dashboard.controller;
import com.alibaba.fastjson.JSON;
import com.google.common.collect.Lists;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.MixAll;
import org.apache.rocketmq.common.protocol.ResponseCode;
import org.apache.rocketmq.common.protocol.route.TopicRouteData;
import org.apache.rocketmq.dashboard.model.MessagePage;
import org.apache.rocketmq.dashboard.model.MessageView;
import org.apache.rocketmq.dashboard.model.request.MessageQuery;
import org.apache.rocketmq.dashboard.service.impl.DlqMessageServiceImpl;
import org.apache.rocketmq.dashboard.service.impl.MessageServiceImpl;
import org.apache.rocketmq.dashboard.util.MockObjectUtil;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import static org.hamcrest.Matchers.hasSize;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
public class DlqMessageControllerTest extends BaseControllerTest {
@InjectMocks
private DlqMessageController dlqMessageController;
@Spy
private DlqMessageServiceImpl dlqMessageService;
@Mock
private MessageServiceImpl messageService;
@Test
public void testQueryDlqMessageByConsumerGroup() throws Exception {
final String url = "/dlqMessage/queryDlqMessageByConsumerGroup.query";
MessageQuery query = new MessageQuery();
query.setPageNum(1);
query.setPageSize(10);
query.setTopic(MixAll.DLQ_GROUP_TOPIC_PREFIX + "group_test");
query.setTaskId("");
query.setBegin(System.currentTimeMillis() - 3 * 24 * 60 * 60 * 1000);
query.setEnd(System.currentTimeMillis());
{
TopicRouteData topicRouteData = MockObjectUtil.createTopicRouteData();
when(mqAdminExt.examineTopicRouteInfo(any()))
.thenThrow(new MQClientException(ResponseCode.TOPIC_NOT_EXIST, "topic not exist"))
.thenThrow(new MQClientException(ResponseCode.NO_MESSAGE, "query no message"))
.thenThrow(new RuntimeException())
.thenReturn(topicRouteData);
MessageView messageView = MessageView.fromMessageExt(MockObjectUtil.createMessageExt());
PageRequest page = PageRequest.of(query.getPageNum(), query.getPageSize());
MessagePage messagePage = new MessagePage
(new PageImpl<>(Lists.newArrayList(messageView), page, 0), query.getTaskId());
when(messageService.queryMessageByPage(any())).thenReturn(messagePage);
}
requestBuilder = MockMvcRequestBuilders.post(url);
requestBuilder.contentType(MediaType.APPLICATION_JSON_UTF8);
requestBuilder.content(JSON.toJSONString(query));
// 1、%DLQ%group_test is not exist
perform = mockMvc.perform(requestBuilder);
perform.andExpect(status().isOk())
.andExpect(jsonPath("$.data.page.content", hasSize(0)));
// 2、Other MQClientException occur
perform = mockMvc.perform(requestBuilder);
perform.andExpect(status().isOk())
.andExpect(jsonPath("$.data").doesNotExist())
.andExpect(jsonPath("$.status").value(-1))
.andExpect(jsonPath("$.errMsg").isNotEmpty());
// 3、Other Exception occur
perform = mockMvc.perform(requestBuilder);
perform.andExpect(status().isOk())
.andExpect(jsonPath("$.data").doesNotExist())
.andExpect(jsonPath("$.status").value(-1))
.andExpect(jsonPath("$.errMsg").isNotEmpty());
// 4、query dlq message success
perform = mockMvc.perform(requestBuilder);
perform.andExpect(status().isOk())
.andExpect(jsonPath("$.data.page.content", hasSize(1)))
.andExpect(jsonPath("$.data.page.content[0].msgId").value("0A9A003F00002A9F0000000000000319"));
}
@Test
public void testExportDlqMessage() throws Exception {
final String url = "/dlqMessage/exportDlqMessage.do";
{
when(mqAdminExt.viewMessage(any(), any()))
.thenThrow(new RuntimeException())
.thenReturn(MockObjectUtil.createMessageExt());
}
requestBuilder = MockMvcRequestBuilders.get(url);
requestBuilder.param("consumerGroup", "group_test");
requestBuilder.param("msgId", "0A9A003F00002A9F0000000000000319");
// 1、viewMessage exception
perform = mockMvc.perform(requestBuilder);
perform.andExpect(status().isOk())
.andExpect(jsonPath("$.data").doesNotExist())
.andExpect(jsonPath("$.status").value(-1))
.andExpect(jsonPath("$.errMsg").isNotEmpty());
// 2、export dlqMessage success
perform = mockMvc.perform(requestBuilder);
perform.andExpect(status().is(200))
.andExpect(content().contentType("application/vnd.ms-excel"));
}
@Override protected Object getTestController() {
return dlqMessageController;
}
}