消费详情
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
package com.xuxd.kafka.console.beans.vo;
|
||||
|
||||
import kafka.console.ConsumerConsole;
|
||||
import lombok.Data;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
/**
|
||||
* kafka-console-ui.
|
||||
*
|
||||
* @author xuxd
|
||||
* @date 2021-10-12 17:41:44
|
||||
**/
|
||||
@Data
|
||||
public class ConsumerDetailVO implements Comparable {
|
||||
|
||||
private String topic = "";
|
||||
|
||||
private int partition;
|
||||
|
||||
private String groupId = "";
|
||||
|
||||
private long consumerOffset = 0L;
|
||||
|
||||
private long logEndOffset = 0L;
|
||||
|
||||
private long lag = 0L;
|
||||
|
||||
private String consumerId = "";
|
||||
|
||||
private String clientId = "";
|
||||
|
||||
private String host = "";
|
||||
|
||||
public static ConsumerDetailVO from(ConsumerConsole.TopicPartitionConsumeInfo info) {
|
||||
ConsumerDetailVO vo = new ConsumerDetailVO();
|
||||
vo.topic = info.topicPartition().topic();
|
||||
vo.partition = info.topicPartition().partition();
|
||||
if (StringUtils.isNotEmpty(info.getGroupId())) {
|
||||
vo.groupId = info.getGroupId();
|
||||
}
|
||||
|
||||
if (StringUtils.isNotEmpty(info.consumerId())) {
|
||||
vo.consumerId = info.consumerId();
|
||||
}
|
||||
|
||||
if (StringUtils.isNotEmpty(info.clientId())) {
|
||||
vo.clientId = info.clientId();
|
||||
}
|
||||
|
||||
if (StringUtils.isNotEmpty(info.host())) {
|
||||
vo.host = info.host();
|
||||
}
|
||||
vo.consumerOffset = info.consumerOffset();
|
||||
vo.logEndOffset = info.logEndOffset();
|
||||
vo.lag = info.lag();
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Override public int compareTo(Object o) {
|
||||
|
||||
ConsumerDetailVO that = (ConsumerDetailVO) o;
|
||||
|
||||
if (!this.groupId.equals(that.groupId)) {
|
||||
return this.groupId.compareTo(that.groupId);
|
||||
}
|
||||
if (!this.topic.equals(that.topic)) {
|
||||
return this.topic.compareTo(that.topic);
|
||||
}
|
||||
|
||||
return this.partition - that.partition;
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,17 @@ package com.xuxd.kafka.console.service.impl;
|
||||
|
||||
import com.xuxd.kafka.console.beans.CounterList;
|
||||
import com.xuxd.kafka.console.beans.ResponseData;
|
||||
import com.xuxd.kafka.console.beans.vo.ConsumerDetailVO;
|
||||
import com.xuxd.kafka.console.beans.vo.ConsumerGroupVO;
|
||||
import com.xuxd.kafka.console.beans.vo.ConsumerMemberVO;
|
||||
import com.xuxd.kafka.console.service.ConsumerService;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import kafka.console.ConsumerConsole;
|
||||
@@ -80,6 +83,19 @@ public class ConsumerServiceImpl implements ConsumerService {
|
||||
}
|
||||
|
||||
@Override public ResponseData getConsumerDetail(String groupId) {
|
||||
return ResponseData.create().data(consumerConsole.getConsumerDetail(Collections.singleton(groupId))).success();
|
||||
Collection<ConsumerConsole.TopicPartitionConsumeInfo> consumerDetail = consumerConsole.getConsumerDetail(Collections.singleton(groupId));
|
||||
|
||||
List<ConsumerDetailVO> collect = consumerDetail.stream().map(ConsumerDetailVO::from).collect(Collectors.toList());
|
||||
Map<String, List<ConsumerDetailVO>> map = collect.stream().collect(Collectors.groupingBy(ConsumerDetailVO::getTopic));
|
||||
|
||||
Map<String, Object> res = new HashMap<>();
|
||||
map.forEach((topic, list) -> {
|
||||
Map<String, Object> sorting = new HashMap<>();
|
||||
Collections.sort(list);
|
||||
sorting.put("data", list);
|
||||
sorting.put("lag", list.stream().map(ConsumerDetailVO::getLag).reduce(Long::sum));
|
||||
res.put(topic, sorting);
|
||||
});
|
||||
return ResponseData.create().data(res).success();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import org.apache.kafka.clients.consumer.OffsetAndMetadata
|
||||
import org.apache.kafka.common.{ConsumerGroupState, TopicPartition}
|
||||
|
||||
import scala.beans.BeanProperty
|
||||
import scala.collection.{Map, Seq, mutable}
|
||||
import scala.collection.{Map, mutable}
|
||||
import scala.jdk.CollectionConverters._
|
||||
|
||||
/**
|
||||
@@ -86,6 +86,7 @@ class ConsumerConsole(config: KafkaConfig) extends KafkaConsole(config: KafkaCon
|
||||
t.logEndOffset = endOffsets.get(t.topicPartition).get.offset()
|
||||
t.consumerOffset = getPartitionOffset(t.topicPartition).get
|
||||
t.lag = t.logEndOffset - t.consumerOffset
|
||||
t.groupId = consumerGroup.groupId()
|
||||
(topicPartition, t)
|
||||
}).toMap
|
||||
|
||||
@@ -94,12 +95,16 @@ class ConsumerConsole(config: KafkaConfig) extends KafkaConsole(config: KafkaCon
|
||||
val t = topicPartitionConsumeInfoMap.get(topicPartition).get
|
||||
t.clientId = m.clientId()
|
||||
t.consumerId = m.consumerId()
|
||||
t.host = m.host()
|
||||
})
|
||||
})
|
||||
|
||||
topicPartitionConsumeInfoMap
|
||||
topicPartitionConsumeInfoMap.map(_._2).asInstanceOf[List[TopicPartitionConsumeInfo]]
|
||||
}
|
||||
groupOffsets.asJava.asInstanceOf[ util.Collection[TopicPartitionConsumeInfo]]
|
||||
val res = new util.ArrayList[TopicPartitionConsumeInfo]()
|
||||
groupOffsets.flatMap(_.toList).foreach(res.add(_))
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
private def describeConsumerGroups(groupIds: util.Set[String]): mutable.Map[String, ConsumerGroupDescription] = {
|
||||
|
||||
@@ -84,6 +84,10 @@ export const KafkaConsumerApi = {
|
||||
url: "/consumer/member",
|
||||
method: "get",
|
||||
},
|
||||
getConsumerDetail: {
|
||||
url: "/consumer/detail",
|
||||
method: "get",
|
||||
},
|
||||
};
|
||||
|
||||
export const KafkaClusterApi = {
|
||||
|
||||
121
ui/src/views/group/ConsumerDetail.vue
Normal file
121
ui/src/views/group/ConsumerDetail.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:title="'消费组: ' + group"
|
||||
:visible="show"
|
||||
:width="1800"
|
||||
:mask="false"
|
||||
:destroyOnClose="true"
|
||||
:footer="null"
|
||||
:maskClosable="false"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<div>
|
||||
<a-spin :spinning="loading">
|
||||
<div v-for="(v, k) in data" :key="k">
|
||||
<h4>Topic: {{ k }} | 积压: {{ v.lag }}</h4>
|
||||
<hr />
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="v.data"
|
||||
bordered
|
||||
:rowKey="(record) => record.topic + record.partition"
|
||||
>
|
||||
<span slot="clientId" slot-scope="text, record">
|
||||
<span v-if="text"> {{ text }}@{{ record.host }} </span>
|
||||
</span>
|
||||
</a-table>
|
||||
</div>
|
||||
</a-spin>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import request from "@/utils/request";
|
||||
import { KafkaConsumerApi } from "@/utils/api";
|
||||
import notification from "ant-design-vue/es/notification";
|
||||
|
||||
export default {
|
||||
name: "ConsumerDetail",
|
||||
props: {
|
||||
group: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
columns: columns,
|
||||
show: this.visible,
|
||||
data: [],
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
visible(v) {
|
||||
this.show = v;
|
||||
if (this.show) {
|
||||
this.getConsumerDetail();
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getConsumerDetail() {
|
||||
this.loading = true;
|
||||
request({
|
||||
url: KafkaConsumerApi.getConsumerDetail.url + "?groupId=" + this.group,
|
||||
method: KafkaConsumerApi.getConsumerDetail.method,
|
||||
}).then((res) => {
|
||||
this.loading = false;
|
||||
if (res.code != 0) {
|
||||
notification.error({
|
||||
message: "error",
|
||||
description: res.msg,
|
||||
});
|
||||
} else {
|
||||
this.data = res.data;
|
||||
}
|
||||
});
|
||||
},
|
||||
handleCancel() {
|
||||
this.data = [];
|
||||
this.$emit("closeConsumerDetailDialog", {});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: "分区",
|
||||
dataIndex: "partition",
|
||||
key: "partition",
|
||||
},
|
||||
{
|
||||
title: "客户端",
|
||||
dataIndex: "clientId",
|
||||
key: "clientId",
|
||||
scopedSlots: { customRender: "clientId" },
|
||||
},
|
||||
{
|
||||
title: "日志位点",
|
||||
dataIndex: "logEndOffset",
|
||||
key: "logEndOffset",
|
||||
},
|
||||
{
|
||||
title: "消费位点",
|
||||
dataIndex: "consumerOffset",
|
||||
key: "consumerOffset",
|
||||
},
|
||||
{
|
||||
title: "积压",
|
||||
dataIndex: "lag",
|
||||
key: "lag",
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -61,7 +61,7 @@
|
||||
</a-form>
|
||||
</div>
|
||||
<div class="operation-row-button">
|
||||
<a-button type="primary" @click="handleReset">新增/更新</a-button>
|
||||
<!-- <a-button type="primary" @click="handleReset">新增/更新</a-button>-->
|
||||
</div>
|
||||
<a-table
|
||||
:columns="columns"
|
||||
@@ -91,6 +91,13 @@
|
||||
>删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
<a-button
|
||||
size="small"
|
||||
href="javascript:;"
|
||||
class="operation-btn"
|
||||
@click="openConsumerDetailDialog(record.groupId)"
|
||||
>消费详情
|
||||
</a-button>
|
||||
</div>
|
||||
</a-table>
|
||||
<Member
|
||||
@@ -98,6 +105,12 @@
|
||||
:group="selectDetail.resourceName"
|
||||
@closeConsumerMemberDialog="closeConsumerDialog"
|
||||
></Member>
|
||||
<ConsumerDetail
|
||||
:visible="showConsumerDetailDialog"
|
||||
:group="selectDetail.resourceName"
|
||||
@closeConsumerDetailDialog="closeConsumerDetailDialog"
|
||||
>
|
||||
</ConsumerDetail>
|
||||
</div>
|
||||
</a-spin>
|
||||
</div>
|
||||
@@ -108,10 +121,11 @@ import request from "@/utils/request";
|
||||
import { KafkaConsumerApi } from "@/utils/api";
|
||||
import notification from "ant-design-vue/es/notification";
|
||||
import Member from "@/views/group/Member";
|
||||
import ConsumerDetail from "@/views/group/ConsumerDetail";
|
||||
|
||||
export default {
|
||||
name: "ConsumerGroup",
|
||||
components: { Member },
|
||||
components: { Member, ConsumerDetail },
|
||||
data() {
|
||||
return {
|
||||
queryParam: {},
|
||||
@@ -130,6 +144,7 @@ export default {
|
||||
},
|
||||
loading: false,
|
||||
showConsumerGroupDialog: false,
|
||||
showConsumerDetailDialog: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
@@ -179,6 +194,13 @@ export default {
|
||||
closeConsumerDialog() {
|
||||
this.showConsumerGroupDialog = false;
|
||||
},
|
||||
openConsumerDetailDialog(groupId) {
|
||||
this.showConsumerDetailDialog = true;
|
||||
this.selectDetail.resourceName = groupId;
|
||||
},
|
||||
closeConsumerDetailDialog() {
|
||||
this.showConsumerDetailDialog = false;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.getConsumerGroupList();
|
||||
|
||||
Reference in New Issue
Block a user