Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5930e44fdf | ||
|
|
98f33bb2cc | ||
|
|
0ec3bac6c2 | ||
|
|
bd814d550d | ||
|
|
b9548d1640 | ||
|
|
57a41e087f | ||
|
|
54cd402810 | ||
|
|
c17b0aa4b9 | ||
|
|
8169ddb019 | ||
|
|
5f24c62855 | ||
|
|
3b21fc4cd8 | ||
|
|
d15ec4a2db | ||
|
|
12431db525 | ||
|
|
20535027bf | ||
|
|
222ba34702 | ||
|
|
39e50a6589 | ||
|
|
e881c58a8f | ||
|
|
34c87997d1 | ||
|
|
4639335a9d | ||
|
|
73fed3face | ||
|
|
1b028fcb4f | ||
|
|
62569c4454 | ||
|
|
a219551802 | ||
|
|
7a98eb479f | ||
|
|
405f272fb7 |
18
README.md
@@ -1,10 +1,11 @@
|
|||||||
# kafka可视化管理平台
|
# kafka可视化管理平台
|
||||||
一款轻量级的kafka可视化管理平台,安装配置快捷、简单易用。
|
一款轻量级的kafka可视化管理平台,安装配置快捷、简单易用。
|
||||||
为了开发的省事,没有多语言支持,只支持中文展示。
|
为了开发的省事,没有国际化支持,只支持中文展示。
|
||||||
用过rocketmq-console吧,对,前端展示风格跟那个有点类似。
|
用过rocketmq-console吧,对,前端展示风格跟那个有点类似。
|
||||||
## 安装包下载
|
## 安装包下载
|
||||||
* 点击下载:[kafka-console-ui.tar.gz](http://43.128.31.53/kafka-console-ui.tar.gz) 或 [kafka-console-ui.zip](http://43.128.31.53/kafka-console-ui.zip)
|
以下两种方式2选一,直接下载安装包或下载源码,手动打包
|
||||||
* 参考下面的打包部署,下载源码重新打包
|
* 点击下载(v1.0.1版本):[kafka-console-ui.tar.gz](https://github.com/xxd763795151/kafka-console-ui/releases/download/v1.0.1/kafka-console-ui.tar.gz) 或 [kafka-console-ui.zip](https://github.com/xxd763795151/kafka-console-ui/releases/download/v1.0.1/kafka-console-ui.zip)
|
||||||
|
* 参考下面的打包部署,下载源码重新打包(提交的最新功能特性)
|
||||||
## 功能支持
|
## 功能支持
|
||||||
* 集群信息
|
* 集群信息
|
||||||
* Topic管理
|
* Topic管理
|
||||||
@@ -76,4 +77,13 @@ sh bin/shutdown.sh
|
|||||||
## 前端
|
## 前端
|
||||||
前端代码在工程的ui目录下,找个前端开发的ide打开进行开发即可。
|
前端代码在工程的ui目录下,找个前端开发的ide打开进行开发即可。
|
||||||
## 注意
|
## 注意
|
||||||
前后分离,直接启动后端如果未编译前端代码是没有前端页面的,可以先打包进行编译`sh package.sh`,然后再用idea启动,或者前端部分单独启动
|
前后分离,直接启动后端如果未编译前端代码是没有前端页面的,可以先打包进行编译`sh package.sh`,然后再用idea启动,或者前端部分单独启动
|
||||||
|
# 页面示例
|
||||||
|
如果未启用ACL配置,不会显示ACL的菜单页面,所以导航栏上没有Acl这一项
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
增加消息检索页面
|
||||||
|

|
||||||
BIN
document/Topic.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 354 KiB After Width: | Height: | Size: 492 KiB |
BIN
document/消息.png
Normal file
|
After Width: | Height: | Size: 400 KiB |
BIN
document/消费组.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
document/运维.png
Normal file
|
After Width: | Height: | Size: 126 KiB |
BIN
document/集群.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
2
pom.xml
@@ -10,7 +10,7 @@
|
|||||||
</parent>
|
</parent>
|
||||||
<groupId>com.xuxd</groupId>
|
<groupId>com.xuxd</groupId>
|
||||||
<artifactId>kafka-console-ui</artifactId>
|
<artifactId>kafka-console-ui</artifactId>
|
||||||
<version>1.0.0</version>
|
<version>1.0.2</version>
|
||||||
<name>kafka-console-ui</name>
|
<name>kafka-console-ui</name>
|
||||||
<description>Kafka console manage ui</description>
|
<description>Kafka console manage ui</description>
|
||||||
<properties>
|
<properties>
|
||||||
|
|||||||
27
src/main/java/com/xuxd/kafka/console/beans/QueryMessage.java
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package com.xuxd.kafka.console.beans;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* kafka-console-ui.
|
||||||
|
*
|
||||||
|
* @author xuxd
|
||||||
|
* @date 2021-12-11 09:45:49
|
||||||
|
**/
|
||||||
|
@Data
|
||||||
|
public class QueryMessage {
|
||||||
|
|
||||||
|
private String topic;
|
||||||
|
|
||||||
|
private int partition;
|
||||||
|
|
||||||
|
private long startTime;
|
||||||
|
|
||||||
|
private long endTime;
|
||||||
|
|
||||||
|
private long offset;
|
||||||
|
|
||||||
|
private String keyDeserializer;
|
||||||
|
|
||||||
|
private String valueDeserializer;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.xuxd.kafka.console.beans;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* kafka-console-ui.
|
||||||
|
*
|
||||||
|
* @author xuxd
|
||||||
|
* @date 2021-11-19 17:07:50
|
||||||
|
**/
|
||||||
|
@Data
|
||||||
|
public class ReplicaAssignment {
|
||||||
|
|
||||||
|
private long version = 1L;
|
||||||
|
|
||||||
|
private List<Partition> partitions;
|
||||||
|
|
||||||
|
private long interBrokerThrottle = -1;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
static class Partition {
|
||||||
|
private String topic;
|
||||||
|
|
||||||
|
private int partition;
|
||||||
|
|
||||||
|
private List<Integer> replicas;
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/main/java/com/xuxd/kafka/console/beans/SendMessage.java
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package com.xuxd.kafka.console.beans;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* kafka-console-ui.
|
||||||
|
*
|
||||||
|
* @author xuxd
|
||||||
|
* @date 2021-12-19 23:28:31
|
||||||
|
**/
|
||||||
|
@Data
|
||||||
|
public class SendMessage {
|
||||||
|
|
||||||
|
private String topic;
|
||||||
|
|
||||||
|
private int partition;
|
||||||
|
|
||||||
|
private String key;
|
||||||
|
|
||||||
|
private String body;
|
||||||
|
|
||||||
|
private int num;
|
||||||
|
|
||||||
|
private long offset;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.xuxd.kafka.console.beans.dto;
|
||||||
|
|
||||||
|
import com.xuxd.kafka.console.beans.enums.ThrottleUnit;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* kafka-console-ui.
|
||||||
|
*
|
||||||
|
* @author xuxd
|
||||||
|
* @date 2021-11-24 19:37:10
|
||||||
|
**/
|
||||||
|
@Data
|
||||||
|
public class BrokerThrottleDTO {
|
||||||
|
|
||||||
|
private List<Integer> brokerList = new ArrayList<>();
|
||||||
|
|
||||||
|
private long throttle;
|
||||||
|
|
||||||
|
private ThrottleUnit unit;
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package com.xuxd.kafka.console.beans.dto;
|
||||||
|
|
||||||
|
import com.xuxd.kafka.console.beans.QueryMessage;
|
||||||
|
import java.util.Date;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* kafka-console-ui.
|
||||||
|
*
|
||||||
|
* @author xuxd
|
||||||
|
* @date 2021-12-11 09:17:59
|
||||||
|
**/
|
||||||
|
@Data
|
||||||
|
public class QueryMessageDTO {
|
||||||
|
|
||||||
|
private String topic;
|
||||||
|
|
||||||
|
private int partition;
|
||||||
|
|
||||||
|
private Date startTime;
|
||||||
|
|
||||||
|
private Date endTime;
|
||||||
|
|
||||||
|
private Long offset;
|
||||||
|
|
||||||
|
private String keyDeserializer;
|
||||||
|
|
||||||
|
private String valueDeserializer;
|
||||||
|
|
||||||
|
public QueryMessage toQueryMessage() {
|
||||||
|
QueryMessage queryMessage = new QueryMessage();
|
||||||
|
queryMessage.setTopic(topic);
|
||||||
|
queryMessage.setPartition(partition);
|
||||||
|
if (startTime != null) {
|
||||||
|
queryMessage.setStartTime(startTime.getTime());
|
||||||
|
}
|
||||||
|
if (endTime != null) {
|
||||||
|
queryMessage.setEndTime(endTime.getTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offset != null) {
|
||||||
|
queryMessage.setOffset(offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
queryMessage.setKeyDeserializer(keyDeserializer);
|
||||||
|
queryMessage.setValueDeserializer(valueDeserializer);
|
||||||
|
|
||||||
|
return queryMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.xuxd.kafka.console.beans.dto;
|
||||||
|
|
||||||
|
import com.xuxd.kafka.console.beans.enums.TopicThrottleSwitch;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* kafka-console-ui.
|
||||||
|
*
|
||||||
|
* @author xuxd
|
||||||
|
* @date 2021-11-26 15:33:37
|
||||||
|
**/
|
||||||
|
@Data
|
||||||
|
public class TopicThrottleDTO {
|
||||||
|
|
||||||
|
private String topic;
|
||||||
|
|
||||||
|
private List<Integer> partitions;
|
||||||
|
|
||||||
|
private TopicThrottleSwitch operation;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.xuxd.kafka.console.beans.enums;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* kafka-console-ui.
|
||||||
|
*
|
||||||
|
* @author xuxd
|
||||||
|
* @date 2021-11-24 19:38:00
|
||||||
|
**/
|
||||||
|
public enum ThrottleUnit {
|
||||||
|
KB, MB;
|
||||||
|
|
||||||
|
public long toKb(long size) {
|
||||||
|
if (this == MB) {
|
||||||
|
return 1024 * size;
|
||||||
|
}
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.xuxd.kafka.console.beans.enums;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* kafka-console-ui.
|
||||||
|
*
|
||||||
|
* @author xuxd
|
||||||
|
* @date 2021-11-26 15:33:07
|
||||||
|
**/
|
||||||
|
public enum TopicThrottleSwitch {
|
||||||
|
ON,OFF;
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.xuxd.kafka.console.beans.vo;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.apache.kafka.clients.consumer.ConsumerRecord;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* kafka-console-ui.
|
||||||
|
*
|
||||||
|
* @author xuxd
|
||||||
|
* @date 2021-12-11 14:19:35
|
||||||
|
**/
|
||||||
|
@Data
|
||||||
|
public class ConsumerRecordVO {
|
||||||
|
|
||||||
|
private String topic;
|
||||||
|
|
||||||
|
private int partition;
|
||||||
|
|
||||||
|
private long offset;
|
||||||
|
|
||||||
|
private long timestamp;
|
||||||
|
|
||||||
|
public static ConsumerRecordVO fromConsumerRecord(ConsumerRecord record) {
|
||||||
|
ConsumerRecordVO vo = new ConsumerRecordVO();
|
||||||
|
vo.setTopic(record.topic());
|
||||||
|
vo.setPartition(record.partition());
|
||||||
|
vo.setOffset(record.offset());
|
||||||
|
vo.setTimestamp(record.timestamp());
|
||||||
|
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.xuxd.kafka.console.beans.vo;
|
||||||
|
|
||||||
|
import com.xuxd.kafka.console.beans.TopicPartition;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* kafka-console-ui.
|
||||||
|
*
|
||||||
|
* @author xuxd
|
||||||
|
* @date 2021-11-30 16:03:41
|
||||||
|
**/
|
||||||
|
@Data
|
||||||
|
public class CurrentReassignmentVO {
|
||||||
|
|
||||||
|
private final String topic;
|
||||||
|
|
||||||
|
private final int partition;
|
||||||
|
|
||||||
|
private final List<Integer> replicas;
|
||||||
|
|
||||||
|
private final List<Integer> addingReplicas;
|
||||||
|
|
||||||
|
private final List<Integer> removingReplicas;
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package com.xuxd.kafka.console.beans.vo;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* kafka-console-ui.
|
||||||
|
*
|
||||||
|
* @author xuxd
|
||||||
|
* @date 2021-12-12 12:45:23
|
||||||
|
**/
|
||||||
|
@Data
|
||||||
|
public class MessageDetailVO {
|
||||||
|
|
||||||
|
private String topic;
|
||||||
|
|
||||||
|
private int partition;
|
||||||
|
|
||||||
|
private long offset;
|
||||||
|
|
||||||
|
private long timestamp;
|
||||||
|
|
||||||
|
private String timestampType;
|
||||||
|
|
||||||
|
private List<HeaderVO> headers = new ArrayList<>();
|
||||||
|
|
||||||
|
private Object key;
|
||||||
|
|
||||||
|
private Object value;
|
||||||
|
|
||||||
|
private List<ConsumerVO> consumers;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class HeaderVO {
|
||||||
|
String key;
|
||||||
|
|
||||||
|
String value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class ConsumerVO {
|
||||||
|
String groupId;
|
||||||
|
|
||||||
|
String status;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,12 +29,16 @@ public class TopicPartitionVO {
|
|||||||
|
|
||||||
private long diff;
|
private long diff;
|
||||||
|
|
||||||
|
private long beginTime;
|
||||||
|
|
||||||
|
private long endTime;
|
||||||
|
|
||||||
public static TopicPartitionVO from(TopicPartitionInfo partitionInfo) {
|
public static TopicPartitionVO from(TopicPartitionInfo partitionInfo) {
|
||||||
TopicPartitionVO partitionVO = new TopicPartitionVO();
|
TopicPartitionVO partitionVO = new TopicPartitionVO();
|
||||||
partitionVO.setPartition(partitionInfo.partition());
|
partitionVO.setPartition(partitionInfo.partition());
|
||||||
partitionVO.setLeader(partitionInfo.leader().toString());
|
partitionVO.setLeader(partitionInfo.leader().toString());
|
||||||
partitionVO.setReplicas(partitionInfo.replicas().stream().map(Node::toString).collect(Collectors.toList()));
|
partitionVO.setReplicas(partitionInfo.replicas().stream().map(node -> node.host() + ":" + node.port() + " (id: " + node.idString() + ")").collect(Collectors.toList()));
|
||||||
partitionVO.setIsr(partitionInfo.isr().stream().map(Node::toString).collect(Collectors.toList()));
|
partitionVO.setIsr(partitionInfo.isr().stream().map(Node::idString).collect(Collectors.toList()));
|
||||||
return partitionVO;
|
return partitionVO;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import kafka.console.ConfigConsole;
|
|||||||
import kafka.console.ConsumerConsole;
|
import kafka.console.ConsumerConsole;
|
||||||
import kafka.console.KafkaAclConsole;
|
import kafka.console.KafkaAclConsole;
|
||||||
import kafka.console.KafkaConfigConsole;
|
import kafka.console.KafkaConfigConsole;
|
||||||
|
import kafka.console.MessageConsole;
|
||||||
import kafka.console.OperationConsole;
|
import kafka.console.OperationConsole;
|
||||||
import kafka.console.TopicConsole;
|
import kafka.console.TopicConsole;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
@@ -54,4 +55,9 @@ public class KafkaConfiguration {
|
|||||||
ConsumerConsole consumerConsole) {
|
ConsumerConsole consumerConsole) {
|
||||||
return new OperationConsole(config, topicConsole, consumerConsole);
|
return new OperationConsole(config, topicConsole, consumerConsole);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public MessageConsole messageConsole(KafkaConfig config) {
|
||||||
|
return new MessageConsole(config);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package com.xuxd.kafka.console.controller;
|
||||||
|
|
||||||
|
import com.xuxd.kafka.console.beans.SendMessage;
|
||||||
|
import com.xuxd.kafka.console.beans.dto.QueryMessageDTO;
|
||||||
|
import com.xuxd.kafka.console.service.MessageService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* kafka-console-ui.
|
||||||
|
*
|
||||||
|
* @author xuxd
|
||||||
|
* @date 2021-12-11 09:22:19
|
||||||
|
**/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/message")
|
||||||
|
public class MessageController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MessageService messageService;
|
||||||
|
|
||||||
|
@PostMapping("/search/time")
|
||||||
|
public Object searchByTime(@RequestBody QueryMessageDTO dto) {
|
||||||
|
return messageService.searchByTime(dto.toQueryMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/search/offset")
|
||||||
|
public Object searchByOffset(@RequestBody QueryMessageDTO dto) {
|
||||||
|
return messageService.searchByOffset(dto.toQueryMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/search/detail")
|
||||||
|
public Object searchDetail(@RequestBody QueryMessageDTO dto) {
|
||||||
|
return messageService.searchDetail(dto.toQueryMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/deserializer/list")
|
||||||
|
public Object deserializerList() {
|
||||||
|
return messageService.deserializerList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/send")
|
||||||
|
public Object send(@RequestBody SendMessage message) {
|
||||||
|
return messageService.send(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/resend")
|
||||||
|
public Object resend(@RequestBody SendMessage message) {
|
||||||
|
return messageService.resend(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.xuxd.kafka.console.controller;
|
package com.xuxd.kafka.console.controller;
|
||||||
|
|
||||||
|
import com.xuxd.kafka.console.beans.TopicPartition;
|
||||||
|
import com.xuxd.kafka.console.beans.dto.BrokerThrottleDTO;
|
||||||
import com.xuxd.kafka.console.beans.dto.ReplicationDTO;
|
import com.xuxd.kafka.console.beans.dto.ReplicationDTO;
|
||||||
import com.xuxd.kafka.console.beans.dto.SyncDataDTO;
|
import com.xuxd.kafka.console.beans.dto.SyncDataDTO;
|
||||||
import com.xuxd.kafka.console.service.OperationService;
|
import com.xuxd.kafka.console.service.OperationService;
|
||||||
@@ -52,4 +54,24 @@ public class OperationController {
|
|||||||
public Object electPreferredLeader(@RequestBody ReplicationDTO dto) {
|
public Object electPreferredLeader(@RequestBody ReplicationDTO dto) {
|
||||||
return operationService.electPreferredLeader(dto.getTopic(), dto.getPartition());
|
return operationService.electPreferredLeader(dto.getTopic(), dto.getPartition());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/broker/throttle")
|
||||||
|
public Object configThrottle(@RequestBody BrokerThrottleDTO dto) {
|
||||||
|
return operationService.configThrottle(dto.getBrokerList(), dto.getUnit().toKb(dto.getThrottle()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/broker/throttle")
|
||||||
|
public Object removeThrottle(@RequestBody BrokerThrottleDTO dto) {
|
||||||
|
return operationService.removeThrottle(dto.getBrokerList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/replication/reassignments")
|
||||||
|
public Object currentReassignments() {
|
||||||
|
return operationService.currentReassignments();
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/replication/reassignments")
|
||||||
|
public Object cancelReassignment(@RequestBody TopicPartition partition) {
|
||||||
|
return operationService.cancelReassignment(new org.apache.kafka.common.TopicPartition(partition.getTopic(), partition.getPartition()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package com.xuxd.kafka.console.controller;
|
package com.xuxd.kafka.console.controller;
|
||||||
|
|
||||||
|
import com.xuxd.kafka.console.beans.ReplicaAssignment;
|
||||||
import com.xuxd.kafka.console.beans.dto.AddPartitionDTO;
|
import com.xuxd.kafka.console.beans.dto.AddPartitionDTO;
|
||||||
import com.xuxd.kafka.console.beans.dto.NewTopicDTO;
|
import com.xuxd.kafka.console.beans.dto.NewTopicDTO;
|
||||||
|
import com.xuxd.kafka.console.beans.dto.TopicThrottleDTO;
|
||||||
import com.xuxd.kafka.console.beans.enums.TopicType;
|
import com.xuxd.kafka.console.beans.enums.TopicType;
|
||||||
import com.xuxd.kafka.console.service.TopicService;
|
import com.xuxd.kafka.console.service.TopicService;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -71,4 +73,24 @@ public class TopicController {
|
|||||||
|
|
||||||
return topicService.addPartitions(topic, addNum, assignment);
|
return topicService.addPartitions(topic, addNum, assignment);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/replica/assignment")
|
||||||
|
public Object getCurrentReplicaAssignment(@RequestParam String topic) {
|
||||||
|
return topicService.getCurrentReplicaAssignment(topic);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/replica/assignment")
|
||||||
|
public Object updateReplicaAssignment(@RequestBody ReplicaAssignment assignment) {
|
||||||
|
return topicService.updateReplicaAssignment(assignment);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/replica/throttle")
|
||||||
|
public Object configThrottle(@RequestBody TopicThrottleDTO dto) {
|
||||||
|
return topicService.configThrottle(dto.getTopic(), dto.getPartitions(), dto.getOperation());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/send/stats")
|
||||||
|
public Object sendStats(@RequestParam String topic) {
|
||||||
|
return topicService.sendStats(topic);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,4 +38,6 @@ public interface ConsumerService {
|
|||||||
ResponseData getTopicSubscribedByGroups(String topic);
|
ResponseData getTopicSubscribedByGroups(String topic);
|
||||||
|
|
||||||
ResponseData getOffsetPartition(String groupId);
|
ResponseData getOffsetPartition(String groupId);
|
||||||
|
|
||||||
|
ResponseData<Set<String>> getSubscribedGroups(String topic);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.xuxd.kafka.console.service;
|
||||||
|
|
||||||
|
import com.xuxd.kafka.console.beans.QueryMessage;
|
||||||
|
import com.xuxd.kafka.console.beans.ResponseData;
|
||||||
|
import com.xuxd.kafka.console.beans.SendMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* kafka-console-ui.
|
||||||
|
*
|
||||||
|
* @author xuxd
|
||||||
|
* @date 2021-12-11 09:43:26
|
||||||
|
**/
|
||||||
|
public interface MessageService {
|
||||||
|
|
||||||
|
ResponseData searchByTime(QueryMessage queryMessage);
|
||||||
|
|
||||||
|
ResponseData searchByOffset(QueryMessage queryMessage);
|
||||||
|
|
||||||
|
ResponseData searchDetail(QueryMessage queryMessage);
|
||||||
|
|
||||||
|
ResponseData deserializerList();
|
||||||
|
|
||||||
|
ResponseData send(SendMessage message);
|
||||||
|
|
||||||
|
ResponseData resend(SendMessage message);
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
package com.xuxd.kafka.console.service;
|
package com.xuxd.kafka.console.service;
|
||||||
|
|
||||||
import com.xuxd.kafka.console.beans.ResponseData;
|
import com.xuxd.kafka.console.beans.ResponseData;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
|
import org.apache.kafka.common.TopicPartition;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* kafka-console-ui.
|
* kafka-console-ui.
|
||||||
@@ -20,4 +22,12 @@ public interface OperationService {
|
|||||||
ResponseData deleteAlignmentById(Long id);
|
ResponseData deleteAlignmentById(Long id);
|
||||||
|
|
||||||
ResponseData electPreferredLeader(String topic, int partition);
|
ResponseData electPreferredLeader(String topic, int partition);
|
||||||
|
|
||||||
|
ResponseData configThrottle(List<Integer> brokerList, long size);
|
||||||
|
|
||||||
|
ResponseData removeThrottle(List<Integer> brokerList);
|
||||||
|
|
||||||
|
ResponseData currentReassignments();
|
||||||
|
|
||||||
|
ResponseData cancelReassignment(TopicPartition partition);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
package com.xuxd.kafka.console.service;
|
package com.xuxd.kafka.console.service;
|
||||||
|
|
||||||
|
import com.xuxd.kafka.console.beans.ReplicaAssignment;
|
||||||
import com.xuxd.kafka.console.beans.ResponseData;
|
import com.xuxd.kafka.console.beans.ResponseData;
|
||||||
|
import com.xuxd.kafka.console.beans.enums.TopicThrottleSwitch;
|
||||||
import com.xuxd.kafka.console.beans.enums.TopicType;
|
import com.xuxd.kafka.console.beans.enums.TopicType;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Properties;
|
|
||||||
import org.apache.kafka.clients.admin.NewTopic;
|
import org.apache.kafka.clients.admin.NewTopic;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -26,4 +26,12 @@ public interface TopicService {
|
|||||||
ResponseData createTopic(NewTopic topic);
|
ResponseData createTopic(NewTopic topic);
|
||||||
|
|
||||||
ResponseData addPartitions(String topic, int addNum, List<List<Integer>> newAssignmentst);
|
ResponseData addPartitions(String topic, int addNum, List<List<Integer>> newAssignmentst);
|
||||||
|
|
||||||
|
ResponseData getCurrentReplicaAssignment(String topic);
|
||||||
|
|
||||||
|
ResponseData updateReplicaAssignment(ReplicaAssignment assignment);
|
||||||
|
|
||||||
|
ResponseData configThrottle(String topic, List<Integer> partitions, TopicThrottleSwitch throttleSwitch);
|
||||||
|
|
||||||
|
ResponseData sendStats(String topic);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import java.util.HashSet;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import kafka.console.ConsumerConsole;
|
import kafka.console.ConsumerConsole;
|
||||||
import kafka.console.TopicConsole;
|
import kafka.console.TopicConsole;
|
||||||
@@ -48,6 +49,8 @@ public class ConsumerServiceImpl implements ConsumerService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private TopicConsole topicConsole;
|
private TopicConsole topicConsole;
|
||||||
|
|
||||||
|
private ReentrantLock lock = new ReentrantLock();
|
||||||
|
|
||||||
@Override public ResponseData getConsumerGroupList(List<String> groupIds, Set<ConsumerGroupState> states) {
|
@Override public ResponseData getConsumerGroupList(List<String> groupIds, Set<ConsumerGroupState> states) {
|
||||||
String simulateGroup = "inner_xxx_not_exit_group_###" + System.currentTimeMillis();
|
String simulateGroup = "inner_xxx_not_exit_group_###" + System.currentTimeMillis();
|
||||||
Set<String> groupList = new HashSet<>();
|
Set<String> groupList = new HashSet<>();
|
||||||
@@ -142,7 +145,7 @@ public class ConsumerServiceImpl implements ConsumerService {
|
|||||||
@Override public ResponseData resetOffsetByDate(String groupId, String topic, String dateStr) {
|
@Override public ResponseData resetOffsetByDate(String groupId, String topic, String dateStr) {
|
||||||
long timestamp = -1L;
|
long timestamp = -1L;
|
||||||
try {
|
try {
|
||||||
StringBuilder sb = new StringBuilder(dateStr.replace(" ", "T")).append(".000");
|
StringBuilder sb = new StringBuilder(dateStr.replace(" ", "T")).append(".000+08:00");//固定为utc+08:00东8区来计算
|
||||||
timestamp = Utils.getDateTime(sb.toString());
|
timestamp = Utils.getDateTime(sb.toString());
|
||||||
} catch (ParseException e) {
|
} catch (ParseException e) {
|
||||||
throw new IllegalArgumentException(e);
|
throw new IllegalArgumentException(e);
|
||||||
@@ -167,25 +170,7 @@ public class ConsumerServiceImpl implements ConsumerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override public ResponseData getTopicSubscribedByGroups(String topic) {
|
@Override public ResponseData getTopicSubscribedByGroups(String topic) {
|
||||||
if (topicSubscribedInfo.isNeedRefresh(topic)) {
|
Set<String> groups = this.getSubscribedGroups(topic).getData();
|
||||||
Set<String> groupIdList = consumerConsole.getConsumerGroupIdList(Collections.emptySet());
|
|
||||||
Map<String, Set<String>> cache = new HashMap<>();
|
|
||||||
Map<String, List<TopicPartition>> subscribeTopics = consumerConsole.listSubscribeTopics(groupIdList);
|
|
||||||
|
|
||||||
subscribeTopics.forEach((groupId, tl) -> {
|
|
||||||
tl.forEach(topicPartition -> {
|
|
||||||
String t = topicPartition.topic();
|
|
||||||
if (!cache.containsKey(t)) {
|
|
||||||
cache.put(t, new HashSet<>());
|
|
||||||
}
|
|
||||||
cache.get(t).add(groupId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
topicSubscribedInfo.refresh(cache);
|
|
||||||
}
|
|
||||||
|
|
||||||
Set<String> groups = topicSubscribedInfo.getSubscribedGroups(topic);
|
|
||||||
|
|
||||||
Map<String, Object> res = new HashMap<>();
|
Map<String, Object> res = new HashMap<>();
|
||||||
Collection<ConsumerConsole.TopicPartitionConsumeInfo> consumerDetail = consumerConsole.getConsumerDetail(groups);
|
Collection<ConsumerConsole.TopicPartitionConsumeInfo> consumerDetail = consumerConsole.getConsumerDetail(groups);
|
||||||
@@ -212,6 +197,34 @@ public class ConsumerServiceImpl implements ConsumerService {
|
|||||||
return ResponseData.create().data(Utils.abs(groupId.hashCode()) % size);
|
return ResponseData.create().data(Utils.abs(groupId.hashCode()) % size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override public ResponseData<Set<String>> getSubscribedGroups(String topic) {
|
||||||
|
if (topicSubscribedInfo.isNeedRefresh(topic) && !lock.isLocked()) {
|
||||||
|
try {
|
||||||
|
lock.lock();
|
||||||
|
Set<String> groupIdList = consumerConsole.getConsumerGroupIdList(Collections.emptySet());
|
||||||
|
Map<String, Set<String>> cache = new HashMap<>();
|
||||||
|
Map<String, List<TopicPartition>> subscribeTopics = consumerConsole.listSubscribeTopics(groupIdList);
|
||||||
|
|
||||||
|
subscribeTopics.forEach((groupId, tl) -> {
|
||||||
|
tl.forEach(topicPartition -> {
|
||||||
|
String t = topicPartition.topic();
|
||||||
|
if (!cache.containsKey(t)) {
|
||||||
|
cache.put(t, new HashSet<>());
|
||||||
|
}
|
||||||
|
cache.get(t).add(groupId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
topicSubscribedInfo.refresh(cache);
|
||||||
|
} finally {
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> groups = topicSubscribedInfo.getSubscribedGroups(topic);
|
||||||
|
return ResponseData.create(Set.class).data(groups).success();
|
||||||
|
}
|
||||||
|
|
||||||
class TopicSubscribedInfo {
|
class TopicSubscribedInfo {
|
||||||
long lastTime = System.currentTimeMillis();
|
long lastTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,212 @@
|
|||||||
|
package com.xuxd.kafka.console.service.impl;
|
||||||
|
|
||||||
|
import com.xuxd.kafka.console.beans.QueryMessage;
|
||||||
|
import com.xuxd.kafka.console.beans.ResponseData;
|
||||||
|
import com.xuxd.kafka.console.beans.SendMessage;
|
||||||
|
import com.xuxd.kafka.console.beans.vo.ConsumerRecordVO;
|
||||||
|
import com.xuxd.kafka.console.beans.vo.MessageDetailVO;
|
||||||
|
import com.xuxd.kafka.console.service.ConsumerService;
|
||||||
|
import com.xuxd.kafka.console.service.MessageService;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import kafka.console.ConsumerConsole;
|
||||||
|
import kafka.console.MessageConsole;
|
||||||
|
import kafka.console.TopicConsole;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.collections.CollectionUtils;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.kafka.clients.admin.TopicDescription;
|
||||||
|
import org.apache.kafka.clients.consumer.ConsumerRecord;
|
||||||
|
import org.apache.kafka.clients.producer.ProducerRecord;
|
||||||
|
import org.apache.kafka.common.TopicPartition;
|
||||||
|
import org.apache.kafka.common.serialization.ByteArrayDeserializer;
|
||||||
|
import org.apache.kafka.common.serialization.BytesDeserializer;
|
||||||
|
import org.apache.kafka.common.serialization.Deserializer;
|
||||||
|
import org.apache.kafka.common.serialization.DoubleDeserializer;
|
||||||
|
import org.apache.kafka.common.serialization.FloatDeserializer;
|
||||||
|
import org.apache.kafka.common.serialization.IntegerDeserializer;
|
||||||
|
import org.apache.kafka.common.serialization.StringDeserializer;
|
||||||
|
import org.springframework.beans.BeansException;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.context.ApplicationContextAware;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import scala.Tuple2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* kafka-console-ui.
|
||||||
|
*
|
||||||
|
* @author xuxd
|
||||||
|
* @date 2021-12-11 09:43:44
|
||||||
|
**/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class MessageServiceImpl implements MessageService, ApplicationContextAware {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MessageConsole messageConsole;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TopicConsole topicConsole;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ConsumerConsole consumerConsole;
|
||||||
|
|
||||||
|
private ApplicationContext applicationContext;
|
||||||
|
|
||||||
|
private Map<String, Deserializer> deserializerDict = new HashMap<>();
|
||||||
|
|
||||||
|
{
|
||||||
|
deserializerDict.put("ByteArray", new ByteArrayDeserializer());
|
||||||
|
deserializerDict.put("Integer", new IntegerDeserializer());
|
||||||
|
deserializerDict.put("String", new StringDeserializer());
|
||||||
|
deserializerDict.put("Float", new FloatDeserializer());
|
||||||
|
deserializerDict.put("Double", new DoubleDeserializer());
|
||||||
|
deserializerDict.put("Byte", new BytesDeserializer());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String defaultDeserializer = "String";
|
||||||
|
|
||||||
|
@Override public ResponseData searchByTime(QueryMessage queryMessage) {
|
||||||
|
int maxNums = 10000;
|
||||||
|
|
||||||
|
Set<TopicPartition> partitions = getPartitions(queryMessage);
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
List<ConsumerRecord<byte[], byte[]>> records = messageConsole.searchBy(partitions, queryMessage.getStartTime(), queryMessage.getEndTime(), maxNums);
|
||||||
|
log.info("search message by time, cost time: {}", (System.currentTimeMillis() - startTime));
|
||||||
|
List<ConsumerRecordVO> vos = records.stream().filter(record -> record.timestamp() <= queryMessage.getEndTime())
|
||||||
|
.map(ConsumerRecordVO::fromConsumerRecord).collect(Collectors.toList());
|
||||||
|
Map<String, Object> res = new HashMap<>();
|
||||||
|
res.put("maxNum", maxNums);
|
||||||
|
res.put("realNum", vos.size());
|
||||||
|
res.put("data", vos.subList(0, Math.min(maxNums, vos.size())));
|
||||||
|
return ResponseData.create().data(res).success();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public ResponseData searchByOffset(QueryMessage queryMessage) {
|
||||||
|
Map<TopicPartition, ConsumerRecord<byte[], byte[]>> recordMap = searchRecordByOffset(queryMessage);
|
||||||
|
|
||||||
|
return ResponseData.create().data(recordMap.values().stream().map(ConsumerRecordVO::fromConsumerRecord).collect(Collectors.toList())).success();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public ResponseData searchDetail(QueryMessage queryMessage) {
|
||||||
|
if (queryMessage.getPartition() == -1) {
|
||||||
|
throw new IllegalArgumentException();
|
||||||
|
}
|
||||||
|
if (StringUtils.isBlank(queryMessage.getKeyDeserializer())) {
|
||||||
|
queryMessage.setKeyDeserializer(defaultDeserializer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StringUtils.isBlank(queryMessage.getValueDeserializer())) {
|
||||||
|
queryMessage.setValueDeserializer(defaultDeserializer);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<TopicPartition, ConsumerRecord<byte[], byte[]>> recordMap = searchRecordByOffset(queryMessage);
|
||||||
|
ConsumerRecord<byte[], byte[]> record = recordMap.get(new TopicPartition(queryMessage.getTopic(), queryMessage.getPartition()));
|
||||||
|
if (record != null) {
|
||||||
|
MessageDetailVO vo = new MessageDetailVO();
|
||||||
|
vo.setTopic(record.topic());
|
||||||
|
vo.setPartition(record.partition());
|
||||||
|
vo.setOffset(record.offset());
|
||||||
|
vo.setTimestamp(record.timestamp());
|
||||||
|
vo.setTimestampType(record.timestampType().name());
|
||||||
|
try {
|
||||||
|
vo.setKey(deserializerDict.get(queryMessage.getKeyDeserializer()).deserialize(queryMessage.getTopic(), record.key()));
|
||||||
|
} catch (Exception e) {
|
||||||
|
vo.setKey("KeyDeserializer Error: " + e.getMessage());
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
vo.setValue(deserializerDict.get(queryMessage.getValueDeserializer()).deserialize(queryMessage.getTopic(), record.value()));
|
||||||
|
} catch (Exception e) {
|
||||||
|
vo.setValue("ValueDeserializer Error: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
record.headers().forEach(header -> {
|
||||||
|
MessageDetailVO.HeaderVO headerVO = new MessageDetailVO.HeaderVO();
|
||||||
|
headerVO.setKey(header.key());
|
||||||
|
headerVO.setValue(new String(header.value()));
|
||||||
|
vo.getHeaders().add(headerVO);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 为了尽量保持代码好看,不直接注入另一个service层的实现类了
|
||||||
|
Set<String> groupIds = applicationContext.getBean(ConsumerService.class).getSubscribedGroups(record.topic()).getData();
|
||||||
|
Collection<ConsumerConsole.TopicPartitionConsumeInfo> consumerDetail = consumerConsole.getConsumerDetail(groupIds);
|
||||||
|
|
||||||
|
List<MessageDetailVO.ConsumerVO> consumerVOS = new LinkedList<>();
|
||||||
|
consumerDetail.forEach(consumerInfo -> {
|
||||||
|
if (consumerInfo.topicPartition().equals(new TopicPartition(record.topic(), record.partition()))) {
|
||||||
|
MessageDetailVO.ConsumerVO consumerVO = new MessageDetailVO.ConsumerVO();
|
||||||
|
consumerVO.setGroupId(consumerInfo.getGroupId());
|
||||||
|
consumerVO.setStatus(consumerInfo.getConsumerOffset() <= record.offset() ? "unconsume" : "consumed");
|
||||||
|
consumerVOS.add(consumerVO);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
vo.setConsumers(consumerVOS);
|
||||||
|
return ResponseData.create().data(vo).success();
|
||||||
|
}
|
||||||
|
return ResponseData.create().failed("Not found message detail.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public ResponseData deserializerList() {
|
||||||
|
return ResponseData.create().data(deserializerDict.keySet()).success();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public ResponseData send(SendMessage message) {
|
||||||
|
messageConsole.send(message.getTopic(), message.getPartition(), message.getKey(), message.getBody(), message.getNum());
|
||||||
|
return ResponseData.create().success();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public ResponseData resend(SendMessage message) {
|
||||||
|
TopicPartition partition = new TopicPartition(message.getTopic(), message.getPartition());
|
||||||
|
Map<TopicPartition, Object> offsetTable = new HashMap<>(1, 1.0f);
|
||||||
|
offsetTable.put(partition, message.getOffset());
|
||||||
|
Map<TopicPartition, ConsumerRecord<byte[], byte[]>> recordMap = messageConsole.searchBy(offsetTable);
|
||||||
|
if (recordMap.isEmpty()) {
|
||||||
|
return ResponseData.create().failed("Get message failed.");
|
||||||
|
}
|
||||||
|
ConsumerRecord<byte[], byte[]> record = recordMap.get(partition);
|
||||||
|
ProducerRecord<byte[], byte[]> producerRecord = new ProducerRecord<>(record.topic(), record.partition(), record.key(), record.value(), record.headers());
|
||||||
|
Tuple2<Object, String> tuple2 = messageConsole.sendSync(producerRecord);
|
||||||
|
boolean success = (boolean) tuple2._1();
|
||||||
|
return success ? ResponseData.create().success("success: " + tuple2._2()) : ResponseData.create().failed(tuple2._2());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<TopicPartition, ConsumerRecord<byte[], byte[]>> searchRecordByOffset(QueryMessage queryMessage) {
|
||||||
|
Set<TopicPartition> partitions = getPartitions(queryMessage);
|
||||||
|
|
||||||
|
Map<TopicPartition, Object> offsetTable = new HashMap<>();
|
||||||
|
partitions.forEach(tp -> {
|
||||||
|
offsetTable.put(tp, queryMessage.getOffset());
|
||||||
|
});
|
||||||
|
Map<TopicPartition, ConsumerRecord<byte[], byte[]>> recordMap = messageConsole.searchBy(offsetTable);
|
||||||
|
return recordMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<TopicPartition> getPartitions(QueryMessage queryMessage) {
|
||||||
|
Set<TopicPartition> partitions = new HashSet<>();
|
||||||
|
if (queryMessage.getPartition() != -1) {
|
||||||
|
partitions.add(new TopicPartition(queryMessage.getTopic(), queryMessage.getPartition()));
|
||||||
|
} else {
|
||||||
|
List<TopicDescription> list = topicConsole.getTopicList(Collections.singleton(queryMessage.getTopic()));
|
||||||
|
if (CollectionUtils.isEmpty(list)) {
|
||||||
|
throw new IllegalArgumentException("Can not find topic info.");
|
||||||
|
}
|
||||||
|
Set<TopicPartition> set = list.get(0).partitions().stream()
|
||||||
|
.map(tp -> new TopicPartition(queryMessage.getTopic(), tp.partition())).collect(Collectors.toSet());
|
||||||
|
partitions.addAll(set);
|
||||||
|
}
|
||||||
|
return partitions;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void setApplicationContext(ApplicationContext context) throws BeansException {
|
||||||
|
this.applicationContext = context;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,16 +5,21 @@ import com.google.gson.Gson;
|
|||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
import com.xuxd.kafka.console.beans.ResponseData;
|
import com.xuxd.kafka.console.beans.ResponseData;
|
||||||
import com.xuxd.kafka.console.beans.dos.MinOffsetAlignmentDO;
|
import com.xuxd.kafka.console.beans.dos.MinOffsetAlignmentDO;
|
||||||
|
import com.xuxd.kafka.console.beans.vo.CurrentReassignmentVO;
|
||||||
import com.xuxd.kafka.console.beans.vo.OffsetAlignmentVO;
|
import com.xuxd.kafka.console.beans.vo.OffsetAlignmentVO;
|
||||||
import com.xuxd.kafka.console.dao.MinOffsetAlignmentMapper;
|
import com.xuxd.kafka.console.dao.MinOffsetAlignmentMapper;
|
||||||
import com.xuxd.kafka.console.service.OperationService;
|
import com.xuxd.kafka.console.service.OperationService;
|
||||||
|
import com.xuxd.kafka.console.utils.GsonUtil;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import kafka.console.OperationConsole;
|
import kafka.console.OperationConsole;
|
||||||
|
import org.apache.kafka.clients.admin.PartitionReassignment;
|
||||||
import org.apache.kafka.common.TopicPartition;
|
import org.apache.kafka.common.TopicPartition;
|
||||||
import org.springframework.beans.factory.ObjectProvider;
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@@ -30,7 +35,7 @@ import scala.Tuple2;
|
|||||||
@Service
|
@Service
|
||||||
public class OperationServiceImpl implements OperationService {
|
public class OperationServiceImpl implements OperationService {
|
||||||
|
|
||||||
private Gson gson = new Gson();
|
private Gson gson = GsonUtil.INSTANCE.get();
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private OperationConsole operationConsole;
|
private OperationConsole operationConsole;
|
||||||
@@ -122,4 +127,39 @@ public class OperationServiceImpl implements OperationService {
|
|||||||
|
|
||||||
return (boolean) tuple2._1() ? ResponseData.create().success() : ResponseData.create().failed(tuple2._2());
|
return (boolean) tuple2._1() ? ResponseData.create().success() : ResponseData.create().failed(tuple2._2());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override public ResponseData configThrottle(List<Integer> brokerList, long size) {
|
||||||
|
Tuple2<Object, String> tuple2 = operationConsole.modifyInterBrokerThrottle(new HashSet<>(brokerList), size);
|
||||||
|
|
||||||
|
return (boolean) tuple2._1() ? ResponseData.create().success() : ResponseData.create().failed(tuple2._2());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public ResponseData removeThrottle(List<Integer> brokerList) {
|
||||||
|
Tuple2<Object, String> tuple2 = operationConsole.clearBrokerLevelThrottles(new HashSet<>(brokerList));
|
||||||
|
|
||||||
|
return (boolean) tuple2._1() ? ResponseData.create().success() : ResponseData.create().failed(tuple2._2());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public ResponseData currentReassignments() {
|
||||||
|
Map<TopicPartition, PartitionReassignment> reassignmentMap = operationConsole.currentReassignments();
|
||||||
|
List<CurrentReassignmentVO> vos = reassignmentMap.entrySet().stream().map(entry -> {
|
||||||
|
TopicPartition partition = entry.getKey();
|
||||||
|
PartitionReassignment reassignment = entry.getValue();
|
||||||
|
return new CurrentReassignmentVO(partition.topic(),
|
||||||
|
partition.partition(), reassignment.replicas(), reassignment.addingReplicas(), reassignment.removingReplicas());
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
return ResponseData.create().data(vos).success();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public ResponseData cancelReassignment(TopicPartition partition) {
|
||||||
|
Map<TopicPartition, Throwable> res = operationConsole.cancelPartitionReassignments(Collections.singleton(partition));
|
||||||
|
if (!res.isEmpty()) {
|
||||||
|
StringBuilder sb = new StringBuilder("Failed: ");
|
||||||
|
res.forEach((p, t) -> {
|
||||||
|
sb.append(p.toString()).append(": ").append(t.getMessage()).append(System.lineSeparator());
|
||||||
|
});
|
||||||
|
return ResponseData.create().failed(sb.toString());
|
||||||
|
}
|
||||||
|
return ResponseData.create().success();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
package com.xuxd.kafka.console.service.impl;
|
package com.xuxd.kafka.console.service.impl;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.xuxd.kafka.console.beans.ReplicaAssignment;
|
||||||
import com.xuxd.kafka.console.beans.ResponseData;
|
import com.xuxd.kafka.console.beans.ResponseData;
|
||||||
|
import com.xuxd.kafka.console.beans.enums.TopicThrottleSwitch;
|
||||||
import com.xuxd.kafka.console.beans.enums.TopicType;
|
import com.xuxd.kafka.console.beans.enums.TopicType;
|
||||||
import com.xuxd.kafka.console.beans.vo.TopicDescriptionVO;
|
import com.xuxd.kafka.console.beans.vo.TopicDescriptionVO;
|
||||||
import com.xuxd.kafka.console.beans.vo.TopicPartitionVO;
|
import com.xuxd.kafka.console.beans.vo.TopicPartitionVO;
|
||||||
import com.xuxd.kafka.console.service.TopicService;
|
import com.xuxd.kafka.console.service.TopicService;
|
||||||
|
import com.xuxd.kafka.console.utils.GsonUtil;
|
||||||
|
import java.util.Calendar;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -12,13 +17,16 @@ import java.util.HashSet;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import kafka.console.MessageConsole;
|
||||||
import kafka.console.TopicConsole;
|
import kafka.console.TopicConsole;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.kafka.clients.admin.NewPartitions;
|
import org.apache.kafka.clients.admin.NewPartitions;
|
||||||
import org.apache.kafka.clients.admin.NewTopic;
|
import org.apache.kafka.clients.admin.NewTopic;
|
||||||
import org.apache.kafka.clients.admin.TopicDescription;
|
import org.apache.kafka.clients.admin.TopicDescription;
|
||||||
|
import org.apache.kafka.clients.consumer.ConsumerRecord;
|
||||||
import org.apache.kafka.common.TopicPartition;
|
import org.apache.kafka.common.TopicPartition;
|
||||||
import org.apache.kafka.common.TopicPartitionInfo;
|
import org.apache.kafka.common.TopicPartitionInfo;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@@ -38,6 +46,11 @@ public class TopicServiceImpl implements TopicService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private TopicConsole topicConsole;
|
private TopicConsole topicConsole;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MessageConsole messageConsole;
|
||||||
|
|
||||||
|
private Gson gson = GsonUtil.INSTANCE.get();
|
||||||
|
|
||||||
@Override public ResponseData getTopicNameList(boolean internal) {
|
@Override public ResponseData getTopicNameList(boolean internal) {
|
||||||
return ResponseData.create().data(topicConsole.getTopicNameList(internal)).success();
|
return ResponseData.create().data(topicConsole.getTopicNameList(internal)).success();
|
||||||
}
|
}
|
||||||
@@ -98,6 +111,10 @@ public class TopicServiceImpl implements TopicService {
|
|||||||
mapTuple2._2().forEach((k, v) -> {
|
mapTuple2._2().forEach((k, v) -> {
|
||||||
endTable.put(k.partition(), (Long) v);
|
endTable.put(k.partition(), (Long) v);
|
||||||
});
|
});
|
||||||
|
// computer the valid time range.
|
||||||
|
Map<TopicPartition, Object> beginOffsetTable = new HashMap<>();
|
||||||
|
Map<TopicPartition, Object> endOffsetTable = new HashMap<>();
|
||||||
|
Map<Integer, TopicPartition> partitionCache = new HashMap<>();
|
||||||
|
|
||||||
for (TopicPartitionVO partitionVO : voList) {
|
for (TopicPartitionVO partitionVO : voList) {
|
||||||
long begin = beginTable.get(partitionVO.getPartition());
|
long begin = beginTable.get(partitionVO.getPartition());
|
||||||
@@ -105,7 +122,29 @@ public class TopicServiceImpl implements TopicService {
|
|||||||
partitionVO.setBeginOffset(begin);
|
partitionVO.setBeginOffset(begin);
|
||||||
partitionVO.setEndOffset(end);
|
partitionVO.setEndOffset(end);
|
||||||
partitionVO.setDiff(end - begin);
|
partitionVO.setDiff(end - begin);
|
||||||
|
|
||||||
|
if (begin != end) {
|
||||||
|
TopicPartition partition = new TopicPartition(topic, partitionVO.getPartition());
|
||||||
|
partitionCache.put(partitionVO.getPartition(), partition);
|
||||||
|
beginOffsetTable.put(partition, begin);
|
||||||
|
endOffsetTable.put(partition, end - 1); // end must < endOff
|
||||||
|
} else {
|
||||||
|
partitionVO.setBeginTime(-1L);
|
||||||
|
partitionVO.setEndTime(-1L);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map<TopicPartition, ConsumerRecord<byte[], byte[]>> beginRecordMap = messageConsole.searchBy(beginOffsetTable);
|
||||||
|
Map<TopicPartition, ConsumerRecord<byte[], byte[]>> endRecordMap = messageConsole.searchBy(endOffsetTable);
|
||||||
|
|
||||||
|
for (TopicPartitionVO partitionVO : voList) {
|
||||||
|
if (partitionVO.getBeginTime() != -1L) {
|
||||||
|
TopicPartition partition = partitionCache.get(partitionVO.getPartition());
|
||||||
|
partitionVO.setBeginTime(beginRecordMap.containsKey(partition) ? beginRecordMap.get(partition).timestamp() : -1L);
|
||||||
|
partitionVO.setEndTime(endRecordMap.containsKey(partition) ? endRecordMap.get(partition).timestamp() : -1L);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return ResponseData.create().data(voList).success();
|
return ResponseData.create().data(voList).success();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,4 +169,92 @@ public class TopicServiceImpl implements TopicService {
|
|||||||
|
|
||||||
return success ? ResponseData.create().success() : ResponseData.create().failed(tuple2._2());
|
return success ? ResponseData.create().success() : ResponseData.create().failed(tuple2._2());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override public ResponseData getCurrentReplicaAssignment(String topic) {
|
||||||
|
Tuple2<Object, String> tuple2 = topicConsole.getCurrentReplicaAssignmentJson(topic);
|
||||||
|
boolean success = (boolean) tuple2._1();
|
||||||
|
|
||||||
|
return success ? ResponseData.create().data(gson.fromJson(tuple2._2(), ReplicaAssignment.class)).success() : ResponseData.create().failed(tuple2._2());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public ResponseData updateReplicaAssignment(ReplicaAssignment assignment) {
|
||||||
|
Tuple2<Object, String> tuple2 = topicConsole.updateReplicas(gson.toJson(assignment), assignment.getInterBrokerThrottle());
|
||||||
|
boolean success = (boolean) tuple2._1();
|
||||||
|
return success ? ResponseData.create().success() : ResponseData.create().failed(tuple2._2());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResponseData configThrottle(String topic, List<Integer> partitions, TopicThrottleSwitch throttleSwitch) {
|
||||||
|
Tuple2<Object, String> tuple2 = null;
|
||||||
|
switch (throttleSwitch) {
|
||||||
|
case ON:
|
||||||
|
tuple2 = topicConsole.configThrottle(topic, partitions);
|
||||||
|
break;
|
||||||
|
case OFF:
|
||||||
|
tuple2 = topicConsole.clearThrottle(topic);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("switch is unknown.");
|
||||||
|
}
|
||||||
|
boolean success = (boolean) tuple2._1();
|
||||||
|
return success ? ResponseData.create().success() : ResponseData.create().failed(tuple2._2());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public ResponseData sendStats(String topic) {
|
||||||
|
Calendar calendar = Calendar.getInstance();
|
||||||
|
long current = calendar.getTimeInMillis();
|
||||||
|
|
||||||
|
calendar.set(Calendar.HOUR_OF_DAY, 0);
|
||||||
|
calendar.set(Calendar.MINUTE, 0);
|
||||||
|
calendar.set(Calendar.SECOND, 0);
|
||||||
|
calendar.set(Calendar.MILLISECOND, 0);
|
||||||
|
long today = calendar.getTimeInMillis();
|
||||||
|
|
||||||
|
calendar.add(Calendar.DAY_OF_MONTH, -1);
|
||||||
|
long yesterday = calendar.getTimeInMillis();
|
||||||
|
|
||||||
|
Map<TopicPartition, Long> currentOffset = topicConsole.getOffsetForTimestamp(topic, current);
|
||||||
|
Map<TopicPartition, Long> todayOffset = topicConsole.getOffsetForTimestamp(topic, today);
|
||||||
|
Map<TopicPartition, Long> yesterdayOffset = topicConsole.getOffsetForTimestamp(topic, yesterday);
|
||||||
|
|
||||||
|
Map<String, Object> res = new HashMap<>();
|
||||||
|
|
||||||
|
// 昨天的消息数是今天减去昨天的
|
||||||
|
AtomicLong yesterdayTotal = new AtomicLong(0L), todayTotal = new AtomicLong(0L);
|
||||||
|
Map<Integer, Long> yesterdayDetail = new HashMap<>(), todayDetail = new HashMap<>();
|
||||||
|
todayOffset.forEach(((partition, aLong) -> {
|
||||||
|
Long last = yesterdayOffset.get(partition);
|
||||||
|
long diff = last == null ? aLong : aLong - last;
|
||||||
|
yesterdayDetail.put(partition.partition(), diff);
|
||||||
|
yesterdayTotal.addAndGet(diff);
|
||||||
|
}));
|
||||||
|
currentOffset.forEach(((partition, aLong) -> {
|
||||||
|
Long last = todayOffset.get(partition);
|
||||||
|
long diff = last == null ? aLong : aLong - last;
|
||||||
|
todayDetail.put(partition.partition(), diff);
|
||||||
|
todayTotal.addAndGet(diff);
|
||||||
|
}));
|
||||||
|
|
||||||
|
Map<String, Object> yes = new HashMap<>(), to = new HashMap<>();
|
||||||
|
yes.put("detail", convertList(yesterdayDetail));
|
||||||
|
yes.put("total", yesterdayTotal.get());
|
||||||
|
to.put("detail", convertList(todayDetail));
|
||||||
|
to.put("total", todayTotal.get());
|
||||||
|
|
||||||
|
res.put("yesterday", yes);
|
||||||
|
res.put("today", to);
|
||||||
|
// 今天的消息数是现在减去今天0时的
|
||||||
|
return ResponseData.create().data(res).success();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Map<String, Object>> convertList(Map<Integer, Long> source) {
|
||||||
|
List<Map<String, Object>> collect = source.entrySet().stream().map(entry -> {
|
||||||
|
Map<String, Object> map = new HashMap<>(3, 1.0f);
|
||||||
|
map.put("partition", entry.getKey());
|
||||||
|
map.put("num", entry.getValue());
|
||||||
|
return map;
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
|
||||||
|
return collect;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
19
src/main/java/com/xuxd/kafka/console/utils/GsonUtil.java
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package com.xuxd.kafka.console.utils;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* kafka-console-ui.
|
||||||
|
*
|
||||||
|
* @author xuxd
|
||||||
|
* @date 2021-11-19 17:01:01
|
||||||
|
**/
|
||||||
|
public enum GsonUtil {
|
||||||
|
INSTANCE;
|
||||||
|
|
||||||
|
private Gson gson = new Gson();
|
||||||
|
|
||||||
|
public Gson get() {
|
||||||
|
return gson;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,21 @@
|
|||||||
package kafka.console
|
package kafka.console
|
||||||
|
|
||||||
import java.util.Properties
|
|
||||||
|
|
||||||
import com.xuxd.kafka.console.config.KafkaConfig
|
import com.xuxd.kafka.console.config.KafkaConfig
|
||||||
import kafka.zk.{AdminZkClient, KafkaZkClient}
|
import kafka.zk.{AdminZkClient, KafkaZkClient}
|
||||||
import org.apache.kafka.clients.CommonClientConfigs
|
import org.apache.kafka.clients.CommonClientConfigs
|
||||||
import org.apache.kafka.clients.admin.{AbstractOptions, Admin, AdminClientConfig}
|
import org.apache.kafka.clients.admin._
|
||||||
import org.apache.kafka.clients.consumer.{ConsumerConfig, KafkaConsumer}
|
import org.apache.kafka.clients.consumer.{ConsumerConfig, KafkaConsumer, OffsetAndMetadata}
|
||||||
|
import org.apache.kafka.clients.producer.KafkaProducer
|
||||||
|
import org.apache.kafka.common.TopicPartition
|
||||||
import org.apache.kafka.common.config.SaslConfigs
|
import org.apache.kafka.common.config.SaslConfigs
|
||||||
import org.apache.kafka.common.serialization.ByteArrayDeserializer
|
import org.apache.kafka.common.requests.ListOffsetsResponse
|
||||||
|
import org.apache.kafka.common.serialization.{ByteArrayDeserializer, ByteArraySerializer, StringSerializer}
|
||||||
import org.apache.kafka.common.utils.Time
|
import org.apache.kafka.common.utils.Time
|
||||||
|
import org.slf4j.{Logger, LoggerFactory}
|
||||||
|
|
||||||
|
import java.util.Properties
|
||||||
|
import scala.collection.{Map, Seq}
|
||||||
|
import scala.jdk.CollectionConverters.{MapHasAsJava, MapHasAsScala}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* kafka-console-ui.
|
* kafka-console-ui.
|
||||||
@@ -55,6 +61,38 @@ class KafkaConsole(config: KafkaConfig) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected def withProducerAndCatchError(f: KafkaProducer[String, String] => Any, eh: Exception => Any,
|
||||||
|
extra: Properties = new Properties()): Any = {
|
||||||
|
val props = getProps()
|
||||||
|
props.putAll(extra)
|
||||||
|
props.put(ConsumerConfig.CLIENT_ID_CONFIG, String.valueOf(System.currentTimeMillis()))
|
||||||
|
val producer = new KafkaProducer[String, String](props, new StringSerializer, new StringSerializer)
|
||||||
|
try {
|
||||||
|
f(producer)
|
||||||
|
} catch {
|
||||||
|
case er: Exception => eh(er)
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
producer.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected def withByteProducerAndCatchError(f: KafkaProducer[Array[Byte], Array[Byte]] => Any, eh: Exception => Any,
|
||||||
|
extra: Properties = new Properties()): Any = {
|
||||||
|
val props = getProps()
|
||||||
|
props.putAll(extra)
|
||||||
|
props.put(ConsumerConfig.CLIENT_ID_CONFIG, String.valueOf(System.currentTimeMillis()))
|
||||||
|
val producer = new KafkaProducer[Array[Byte], Array[Byte]](props, new ByteArraySerializer, new ByteArraySerializer)
|
||||||
|
try {
|
||||||
|
f(producer)
|
||||||
|
} catch {
|
||||||
|
case er: Exception => eh(er)
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
producer.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected def withZKClient(f: AdminZkClient => Any): Any = {
|
protected def withZKClient(f: AdminZkClient => Any): Any = {
|
||||||
val zkClient = KafkaZkClient(config.getZookeeperAddr, false, 30000, 30000, Int.MaxValue, Time.SYSTEM)
|
val zkClient = KafkaZkClient(config.getZookeeperAddr, false, 30000, 30000, Int.MaxValue, Time.SYSTEM)
|
||||||
val adminZkClient = new AdminZkClient(zkClient)
|
val adminZkClient = new AdminZkClient(zkClient)
|
||||||
@@ -89,3 +127,57 @@ class KafkaConsole(config: KafkaConfig) {
|
|||||||
props
|
props
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
object KafkaConsole {
|
||||||
|
val log: Logger = LoggerFactory.getLogger(this.getClass)
|
||||||
|
|
||||||
|
def getCommittedOffsets(admin: Admin, groupId: String,
|
||||||
|
timeoutMs: Integer): Map[TopicPartition, OffsetAndMetadata] = {
|
||||||
|
admin.listConsumerGroupOffsets(
|
||||||
|
groupId, new ListConsumerGroupOffsetsOptions().timeoutMs(timeoutMs)
|
||||||
|
).partitionsToOffsetAndMetadata.get.asScala
|
||||||
|
}
|
||||||
|
|
||||||
|
def getLogTimestampOffsets(admin: Admin, topicPartitions: Seq[TopicPartition],
|
||||||
|
timestamp: java.lang.Long, timeoutMs: Integer): Map[TopicPartition, OffsetAndMetadata] = {
|
||||||
|
val timestampOffsets = topicPartitions.map { topicPartition =>
|
||||||
|
topicPartition -> OffsetSpec.forTimestamp(timestamp)
|
||||||
|
}.toMap
|
||||||
|
val offsets = admin.listOffsets(
|
||||||
|
timestampOffsets.asJava,
|
||||||
|
new ListOffsetsOptions().timeoutMs(timeoutMs)
|
||||||
|
).all.get
|
||||||
|
val (successfulOffsetsForTimes, unsuccessfulOffsetsForTimes) =
|
||||||
|
offsets.asScala.partition(_._2.offset != ListOffsetsResponse.UNKNOWN_OFFSET)
|
||||||
|
|
||||||
|
val successfulLogTimestampOffsets = successfulOffsetsForTimes.map {
|
||||||
|
case (topicPartition, listOffsetsResultInfo) => topicPartition -> new OffsetAndMetadata(listOffsetsResultInfo.offset)
|
||||||
|
}.toMap
|
||||||
|
|
||||||
|
unsuccessfulOffsetsForTimes.foreach { entry =>
|
||||||
|
log.warn(s"\nWarn: Partition " + entry._1.partition() + " from topic " + entry._1.topic() +
|
||||||
|
" is empty. Falling back to latest known offset.")
|
||||||
|
}
|
||||||
|
|
||||||
|
successfulLogTimestampOffsets ++ getLogEndOffsets(admin, unsuccessfulOffsetsForTimes.keySet.toSeq, timeoutMs)
|
||||||
|
}
|
||||||
|
|
||||||
|
def getLogEndOffsets(admin: Admin,
|
||||||
|
topicPartitions: Seq[TopicPartition], timeoutMs: Integer): Predef.Map[TopicPartition, OffsetAndMetadata] = {
|
||||||
|
val endOffsets = topicPartitions.map { topicPartition =>
|
||||||
|
topicPartition -> OffsetSpec.latest
|
||||||
|
}.toMap
|
||||||
|
val offsets = admin.listOffsets(
|
||||||
|
endOffsets.asJava,
|
||||||
|
new ListOffsetsOptions().timeoutMs(timeoutMs)
|
||||||
|
).all.get
|
||||||
|
val res = topicPartitions.map { topicPartition =>
|
||||||
|
Option(offsets.get(topicPartition)) match {
|
||||||
|
case Some(listOffsetsResultInfo) => topicPartition -> new OffsetAndMetadata(listOffsetsResultInfo.offset)
|
||||||
|
case _ =>
|
||||||
|
throw new IllegalArgumentException
|
||||||
|
}
|
||||||
|
}.toMap
|
||||||
|
res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
196
src/main/scala/kafka/console/MessageConsole.scala
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
package kafka.console
|
||||||
|
|
||||||
|
import com.xuxd.kafka.console.config.KafkaConfig
|
||||||
|
import org.apache.kafka.clients.consumer.{ConsumerConfig, ConsumerRecord}
|
||||||
|
import org.apache.kafka.clients.producer.ProducerRecord
|
||||||
|
import org.apache.kafka.common.TopicPartition
|
||||||
|
|
||||||
|
import java.time.Duration
|
||||||
|
import java.util
|
||||||
|
import java.util.Properties
|
||||||
|
import scala.collection.immutable
|
||||||
|
import scala.jdk.CollectionConverters.{CollectionHasAsScala, MapHasAsScala, SeqHasAsJava}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* kafka-console-ui.
|
||||||
|
*
|
||||||
|
* @author xuxd
|
||||||
|
* @date 2021-12-11 09:39:40
|
||||||
|
* */
|
||||||
|
class MessageConsole(config: KafkaConfig) extends KafkaConsole(config: KafkaConfig) with Logging {
|
||||||
|
|
||||||
|
def searchBy(partitions: util.Collection[TopicPartition], startTime: Long, endTime: Long,
|
||||||
|
maxNums: Int): util.List[ConsumerRecord[Array[Byte], Array[Byte]]] = {
|
||||||
|
var startOffTable: immutable.Map[TopicPartition, Long] = Map.empty
|
||||||
|
var endOffTable: immutable.Map[TopicPartition, Long] = Map.empty
|
||||||
|
withAdminClientAndCatchError(admin => {
|
||||||
|
val startTable = KafkaConsole.getLogTimestampOffsets(admin, partitions.asScala.toSeq, startTime, timeoutMs)
|
||||||
|
startOffTable = startTable.map(t2 => (t2._1, t2._2.offset())).toMap
|
||||||
|
|
||||||
|
endOffTable = KafkaConsole.getLogTimestampOffsets(admin, partitions.asScala.toSeq, endTime, timeoutMs)
|
||||||
|
.map(t2 => (t2._1, t2._2.offset())).toMap
|
||||||
|
}, e => {
|
||||||
|
log.error("getLogTimestampOffsets error.", e)
|
||||||
|
throw new RuntimeException("getLogTimestampOffsets error", e)
|
||||||
|
})
|
||||||
|
var terminate: Boolean = (startOffTable == endOffTable)
|
||||||
|
val res = new util.LinkedList[ConsumerRecord[Array[Byte], Array[Byte]]]()
|
||||||
|
// 如果最小和最大偏移一致,就结束
|
||||||
|
if (!terminate) {
|
||||||
|
|
||||||
|
val arrive = new util.HashSet[TopicPartition](partitions)
|
||||||
|
val props = new Properties()
|
||||||
|
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false")
|
||||||
|
withConsumerAndCatchError(consumer => {
|
||||||
|
consumer.assign(partitions)
|
||||||
|
for ((tp, off) <- startOffTable) {
|
||||||
|
consumer.seek(tp, off)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 终止条件
|
||||||
|
// 1.所有查询分区达都到最大偏移的时候
|
||||||
|
while (!terminate) {
|
||||||
|
// 达到查询的最大条数
|
||||||
|
if (res.size() >= maxNums) {
|
||||||
|
terminate = true
|
||||||
|
} else {
|
||||||
|
val records = consumer.poll(Duration.ofMillis(timeoutMs))
|
||||||
|
|
||||||
|
if (records.isEmpty) {
|
||||||
|
terminate = true
|
||||||
|
} else {
|
||||||
|
for ((tp, endOff) <- endOffTable) {
|
||||||
|
if (!terminate) {
|
||||||
|
var recordList = records.records(tp)
|
||||||
|
if (!recordList.isEmpty) {
|
||||||
|
val first = recordList.get(0)
|
||||||
|
if (first.offset() >= endOff) {
|
||||||
|
arrive.remove(tp)
|
||||||
|
} else {
|
||||||
|
//
|
||||||
|
// (String topic,
|
||||||
|
// int partition,
|
||||||
|
// long offset,
|
||||||
|
// long timestamp,
|
||||||
|
// TimestampType timestampType,
|
||||||
|
// Long checksum,
|
||||||
|
// int serializedKeySize,
|
||||||
|
// int serializedValueSize,
|
||||||
|
// K key,
|
||||||
|
// V value,
|
||||||
|
// Headers headers,
|
||||||
|
// Optional<Integer> leaderEpoch)
|
||||||
|
val nullVList = recordList.asScala.map(record => new ConsumerRecord[Array[Byte], Array[Byte]](record.topic(),
|
||||||
|
record.partition(),
|
||||||
|
record.offset(),
|
||||||
|
record.timestamp(),
|
||||||
|
record.timestampType(),
|
||||||
|
record.checksum(),
|
||||||
|
record.serializedKeySize(),
|
||||||
|
record.serializedValueSize(),
|
||||||
|
record.key(),
|
||||||
|
null,
|
||||||
|
record.headers(),
|
||||||
|
record.leaderEpoch())).toSeq.asJava
|
||||||
|
res.addAll(nullVList)
|
||||||
|
if (recordList.get(recordList.size() - 1).offset() >= endOff) {
|
||||||
|
arrive.remove(tp)
|
||||||
|
}
|
||||||
|
if (recordList != null) {
|
||||||
|
recordList = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (arrive.isEmpty) {
|
||||||
|
terminate = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, e => {
|
||||||
|
log.error("searchBy time error.", e)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
def searchBy(
|
||||||
|
tp2o: util.Map[TopicPartition, Long]): util.Map[TopicPartition, ConsumerRecord[Array[Byte], Array[Byte]]] = {
|
||||||
|
val props = new Properties()
|
||||||
|
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false")
|
||||||
|
val res = new util.HashMap[TopicPartition, ConsumerRecord[Array[Byte], Array[Byte]]]()
|
||||||
|
withConsumerAndCatchError(consumer => {
|
||||||
|
var tpSet = tp2o.keySet()
|
||||||
|
val tpSetCopy = new util.HashSet[TopicPartition](tpSet)
|
||||||
|
val endOffsets = consumer.endOffsets(tpSet)
|
||||||
|
val beginOffsets = consumer.beginningOffsets(tpSet)
|
||||||
|
for ((tp, off) <- tp2o.asScala) {
|
||||||
|
val endOff = endOffsets.get(tp)
|
||||||
|
// if (endOff <= off) {
|
||||||
|
// consumer.seek(tp, endOff)
|
||||||
|
// tpSetCopy.remove(tp)
|
||||||
|
// } else {
|
||||||
|
// consumer.seek(tp, off)
|
||||||
|
// }
|
||||||
|
val beginOff = beginOffsets.get(tp)
|
||||||
|
if (off < beginOff || off >= endOff) {
|
||||||
|
tpSetCopy.remove(tp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tpSet = tpSetCopy
|
||||||
|
consumer.assign(tpSet)
|
||||||
|
tpSet.asScala.foreach(tp => {
|
||||||
|
consumer.seek(tp, tp2o.get(tp))
|
||||||
|
})
|
||||||
|
|
||||||
|
var terminate = tpSet.isEmpty
|
||||||
|
while (!terminate) {
|
||||||
|
val records = consumer.poll(Duration.ofMillis(timeoutMs))
|
||||||
|
val tps = new util.HashSet(tpSet).asScala
|
||||||
|
for (tp <- tps) {
|
||||||
|
if (!res.containsKey(tp)) {
|
||||||
|
val recordList = records.records(tp)
|
||||||
|
if (!recordList.isEmpty) {
|
||||||
|
val record = recordList.get(0)
|
||||||
|
res.put(tp, record)
|
||||||
|
tpSet.remove(tp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tpSet.isEmpty) {
|
||||||
|
terminate = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}, e => {
|
||||||
|
log.error("searchBy offset error.", e)
|
||||||
|
})
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
def send(topic: String, partition: Int, key: String, value: String, num: Int): Unit = {
|
||||||
|
withProducerAndCatchError(producer => {
|
||||||
|
val nullKey = if (key != null && key.trim().length() == 0) null else key
|
||||||
|
for (a <- 1 to num) {
|
||||||
|
val record = if (partition != -1) new ProducerRecord[String, String](topic, partition, nullKey, value)
|
||||||
|
else new ProducerRecord[String, String](topic, nullKey, value)
|
||||||
|
producer.send(record)
|
||||||
|
}
|
||||||
|
}, e => log.error("send error.", e))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def sendSync(record: ProducerRecord[Array[Byte], Array[Byte]]): (Boolean, String) = {
|
||||||
|
withByteProducerAndCatchError(producer => {
|
||||||
|
val metadata = producer.send(record).get()
|
||||||
|
(true, metadata.toString())
|
||||||
|
}, e => {
|
||||||
|
log.error("send error.", e)
|
||||||
|
(false, e.getMessage)
|
||||||
|
}).asInstanceOf[(Boolean, String)]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
package kafka.console
|
package kafka.console
|
||||||
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import java.util.{Collections, Properties}
|
|
||||||
|
|
||||||
import com.xuxd.kafka.console.config.KafkaConfig
|
import com.xuxd.kafka.console.config.KafkaConfig
|
||||||
import org.apache.kafka.clients.admin.ElectLeadersOptions
|
import kafka.admin.ReassignPartitionsCommand
|
||||||
|
import org.apache.kafka.clients.admin.{ElectLeadersOptions, ListPartitionReassignmentsOptions, PartitionReassignment}
|
||||||
import org.apache.kafka.clients.consumer.KafkaConsumer
|
import org.apache.kafka.clients.consumer.KafkaConsumer
|
||||||
import org.apache.kafka.common.serialization.ByteArrayDeserializer
|
import org.apache.kafka.common.serialization.ByteArrayDeserializer
|
||||||
import org.apache.kafka.common.{ElectionType, TopicPartition}
|
import org.apache.kafka.common.{ElectionType, TopicPartition}
|
||||||
|
|
||||||
import scala.jdk.CollectionConverters.{CollectionHasAsScala, ListHasAsScala, MapHasAsScala, SeqHasAsJava, SetHasAsJava, SetHasAsScala}
|
import java.util.concurrent.TimeUnit
|
||||||
|
import java.util.{Collections, Properties}
|
||||||
|
import scala.jdk.CollectionConverters.{CollectionHasAsScala, ListHasAsScala, MapHasAsJava, MapHasAsScala, SeqHasAsJava, SetHasAsJava, SetHasAsScala}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* kafka-console-ui.
|
* kafka-console-ui.
|
||||||
@@ -210,4 +210,47 @@ class OperationConsole(config: KafkaConfig, topicConsole: TopicConsole,
|
|||||||
val topicList = topicConsole.getTopicList(Collections.singleton(topic))
|
val topicList = topicConsole.getTopicList(Collections.singleton(topic))
|
||||||
topicList.asScala.flatMap(_.partitions().asScala.map(t => new TopicPartition(topic, t.partition()))).toSet.asJava
|
topicList.asScala.flatMap(_.partitions().asScala.map(t => new TopicPartition(topic, t.partition()))).toSet.asJava
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
def modifyInterBrokerThrottle(reassigningBrokers: util.Set[Int],
|
||||||
|
interBrokerThrottle: Long): (Boolean, String) = {
|
||||||
|
withAdminClientAndCatchError(admin => {
|
||||||
|
ReassignPartitionsCommand.modifyInterBrokerThrottle(admin, reassigningBrokers.asScala.toSet, interBrokerThrottle)
|
||||||
|
(true, "")
|
||||||
|
}, e => {
|
||||||
|
log.error("modifyInterBrokerThrottle error.", e)
|
||||||
|
(false, e.getMessage)
|
||||||
|
}).asInstanceOf[(Boolean, String)]
|
||||||
|
}
|
||||||
|
|
||||||
|
def clearBrokerLevelThrottles(brokers: util.Set[Int]): (Boolean, String) = {
|
||||||
|
withAdminClientAndCatchError(admin => {
|
||||||
|
ReassignPartitionsCommand.clearBrokerLevelThrottles(admin, brokers.asScala.toSet)
|
||||||
|
(true, "")
|
||||||
|
}, e => {
|
||||||
|
log.error("clearBrokerLevelThrottles error.", e)
|
||||||
|
(false, e.getMessage)
|
||||||
|
}).asInstanceOf[(Boolean, String)]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* current reassigning is active.
|
||||||
|
*/
|
||||||
|
def currentReassignments(): util.Map[TopicPartition, PartitionReassignment] = {
|
||||||
|
withAdminClientAndCatchError(admin => {
|
||||||
|
admin.listPartitionReassignments(withTimeoutMs(new ListPartitionReassignmentsOptions)).reassignments().get()
|
||||||
|
}, e => {
|
||||||
|
Collections.emptyMap()
|
||||||
|
log.error("listPartitionReassignments error.", e)
|
||||||
|
}).asInstanceOf[util.Map[TopicPartition, PartitionReassignment]]
|
||||||
|
}
|
||||||
|
|
||||||
|
def cancelPartitionReassignments(reassignments: util.Set[TopicPartition]): util.Map[TopicPartition, Throwable] = {
|
||||||
|
withAdminClientAndCatchError(admin => {
|
||||||
|
val res = ReassignPartitionsCommand.cancelPartitionReassignments(admin, reassignments.asScala.toSet)
|
||||||
|
res.asJava
|
||||||
|
}, e => {
|
||||||
|
log.error("cancelPartitionReassignments error.", e)
|
||||||
|
throw e
|
||||||
|
}).asInstanceOf[util.Map[TopicPartition, Throwable]]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,18 @@
|
|||||||
package kafka.console
|
package kafka.console
|
||||||
|
|
||||||
import java.util
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import java.util.{Collections, List, Set}
|
|
||||||
|
|
||||||
import com.xuxd.kafka.console.config.KafkaConfig
|
import com.xuxd.kafka.console.config.KafkaConfig
|
||||||
|
import kafka.admin.ReassignPartitionsCommand._
|
||||||
|
import kafka.utils.Json
|
||||||
import org.apache.kafka.clients.admin._
|
import org.apache.kafka.clients.admin._
|
||||||
import org.apache.kafka.common.TopicPartition
|
import org.apache.kafka.common.errors.UnknownTopicOrPartitionException
|
||||||
|
import org.apache.kafka.common.utils.Time
|
||||||
|
import org.apache.kafka.common.{TopicPartition, TopicPartitionReplica}
|
||||||
|
|
||||||
import scala.jdk.CollectionConverters.{CollectionHasAsScala, SetHasAsJava}
|
import java.util
|
||||||
|
import java.util.concurrent.{ExecutionException, TimeUnit}
|
||||||
|
import java.util.{Collections, List, Set}
|
||||||
|
import scala.collection.{Map, Seq}
|
||||||
|
import scala.jdk.CollectionConverters.{CollectionHasAsScala, MapHasAsJava, MapHasAsScala, SeqHasAsJava, SetHasAsJava}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* kafka-console-ui.
|
* kafka-console-ui.
|
||||||
@@ -121,4 +125,180 @@ class TopicConsole(config: KafkaConfig) extends KafkaConsole(config: KafkaConfig
|
|||||||
(false, e.getMessage)
|
(false, e.getMessage)
|
||||||
}).asInstanceOf[(Boolean, String)]
|
}).asInstanceOf[(Boolean, String)]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def getCurrentReplicaAssignmentJson(topic: String): (Boolean, String) = {
|
||||||
|
withAdminClientAndCatchError(admin => {
|
||||||
|
val json = formatAsReassignmentJson(getReplicaAssignmentForTopics(admin, Seq(topic)), Map.empty)
|
||||||
|
(true, json)
|
||||||
|
}, e => {
|
||||||
|
log.error("getCurrentReplicaAssignmentJson error, ", e)
|
||||||
|
(false, e.getMessage)
|
||||||
|
}).asInstanceOf[(Boolean, String)]
|
||||||
|
}
|
||||||
|
|
||||||
|
def updateReplicas(reassignmentJson: String, interBrokerThrottle: Long = -1L): (Boolean, String) = {
|
||||||
|
withAdminClientAndCatchError(admin => {
|
||||||
|
executeAssignment(admin, reassignmentJson, interBrokerThrottle)
|
||||||
|
(true, "")
|
||||||
|
}, e => {
|
||||||
|
log.error("executeAssignment error, ", e)
|
||||||
|
(false, e.getMessage)
|
||||||
|
}).asInstanceOf[(Boolean, String)]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy and modify from @{link kafka.admin.ReassignPartitionsCommand#executeAssignment}.
|
||||||
|
*/
|
||||||
|
def executeAssignment(adminClient: Admin,
|
||||||
|
reassignmentJson: String,
|
||||||
|
interBrokerThrottle: Long = -1L,
|
||||||
|
logDirThrottle: Long = -1L,
|
||||||
|
timeoutMs: Long = 30000L,
|
||||||
|
time: Time = Time.SYSTEM): Unit = {
|
||||||
|
val (proposedParts, proposedReplicas) = parseExecuteAssignmentArgs(reassignmentJson)
|
||||||
|
val currentReassignments = adminClient.
|
||||||
|
listPartitionReassignments().reassignments().get().asScala
|
||||||
|
// If there is an existing assignment
|
||||||
|
// This helps avoid surprising users.
|
||||||
|
if (currentReassignments.nonEmpty) {
|
||||||
|
throw new TerseReassignmentFailureException("Cannot execute because there is an existing partition assignment.")
|
||||||
|
}
|
||||||
|
verifyBrokerIds(adminClient, proposedParts.values.flatten.toSet)
|
||||||
|
val currentParts = getReplicaAssignmentForPartitions(adminClient, proposedParts.keySet.toSet)
|
||||||
|
log.info("currentPartitionReplicaAssignment: " + currentPartitionReplicaAssignmentToString(proposedParts, currentParts))
|
||||||
|
log.info(s"newPartitionReplicaAssignment: $reassignmentJson")
|
||||||
|
|
||||||
|
if (interBrokerThrottle >= 0 || logDirThrottle >= 0) {
|
||||||
|
|
||||||
|
if (interBrokerThrottle >= 0) {
|
||||||
|
val moveMap = calculateProposedMoveMap(currentReassignments, proposedParts, currentParts)
|
||||||
|
modifyReassignmentThrottle(adminClient, moveMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logDirThrottle >= 0) {
|
||||||
|
val movingBrokers = calculateMovingBrokers(proposedReplicas.keySet.toSet)
|
||||||
|
modifyLogDirThrottle(adminClient, movingBrokers, logDirThrottle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the partition reassignments.
|
||||||
|
val errors = alterPartitionReassignments(adminClient, proposedParts)
|
||||||
|
if (errors.nonEmpty) {
|
||||||
|
throw new TerseReassignmentFailureException(
|
||||||
|
"Error reassigning partition(s):%n%s".format(
|
||||||
|
errors.keySet.toBuffer.sortWith(compareTopicPartitions).map { part =>
|
||||||
|
s"$part: ${errors(part).getMessage}"
|
||||||
|
}.mkString(System.lineSeparator())))
|
||||||
|
}
|
||||||
|
if (proposedReplicas.nonEmpty) {
|
||||||
|
executeMoves(adminClient, proposedReplicas, timeoutMs, time)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def configThrottle(topic: String, partitions: util.List[Integer]): (Boolean, String) = {
|
||||||
|
withAdminClientAndCatchError(admin => {
|
||||||
|
val throttles = {
|
||||||
|
if (partitions.get(0) == -1) {
|
||||||
|
Map(topic -> "*")
|
||||||
|
} else {
|
||||||
|
val topicDescription = admin.describeTopics(Collections.singleton(topic), withTimeoutMs(new DescribeTopicsOptions))
|
||||||
|
.all().get().values().asScala.toList
|
||||||
|
|
||||||
|
def convert(partition: Integer, replicas: scala.List[Int]): String = {
|
||||||
|
replicas.map("%d:%d".format(partition, _)).toSet.mkString(",")
|
||||||
|
}
|
||||||
|
|
||||||
|
val ptor = topicDescription.head.partitions().asScala.map(info => (info.partition(), info.replicas().asScala.map(_.id()))).toMap
|
||||||
|
val conf = partitions.asScala.map(partition => convert(partition, ptor.get(partition) match {
|
||||||
|
case Some(v) => v.toList
|
||||||
|
case None => throw new IllegalArgumentException
|
||||||
|
})).toList
|
||||||
|
Map(topic -> conf.mkString(","))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modifyTopicThrottles(admin, throttles, throttles)
|
||||||
|
(true, "")
|
||||||
|
}, e => {
|
||||||
|
log.error("configThrottle error, ", e)
|
||||||
|
(false, e.getMessage)
|
||||||
|
}).asInstanceOf[(Boolean, String)]
|
||||||
|
}
|
||||||
|
|
||||||
|
def clearThrottle(topic: String): (Boolean, String) = {
|
||||||
|
withAdminClientAndCatchError(admin => {
|
||||||
|
clearTopicLevelThrottles(admin, Collections.singleton(topic).asScala.toSet)
|
||||||
|
(true, "")
|
||||||
|
}, e => {
|
||||||
|
log.error("clearThrottle error, ", e)
|
||||||
|
(false, e.getMessage)
|
||||||
|
}).asInstanceOf[(Boolean, String)]
|
||||||
|
}
|
||||||
|
|
||||||
|
def getOffsetForTimestamp(topic: String, timestamp: java.lang.Long): util.Map[TopicPartition, java.lang.Long] = {
|
||||||
|
withAdminClientAndCatchError(admin => {
|
||||||
|
val partitions = describeTopics(admin, Collections.singleton(topic)).get(topic) match {
|
||||||
|
case Some(topicDescription: TopicDescription) => topicDescription.partitions()
|
||||||
|
.asScala.map(info => new TopicPartition(topic, info.partition())).toSeq
|
||||||
|
case None => throw new IllegalArgumentException("topic is not exist.")
|
||||||
|
}
|
||||||
|
val offsetMap = KafkaConsole.getLogTimestampOffsets(admin, partitions, timestamp, timeoutMs)
|
||||||
|
offsetMap.map(tuple2 => (tuple2._1, tuple2._2.offset())).toMap.asJava
|
||||||
|
}, e => {
|
||||||
|
log.error("clearThrottle error, ", e)
|
||||||
|
Collections.emptyMap()
|
||||||
|
}).asInstanceOf[util.Map[TopicPartition, java.lang.Long]]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current replica assignments for some topics.
|
||||||
|
*
|
||||||
|
* @param adminClient The AdminClient to use.
|
||||||
|
* @param topics The topics to get information about.
|
||||||
|
* @return A map from partitions to broker assignments.
|
||||||
|
* If any topic can't be found, an exception will be thrown.
|
||||||
|
*/
|
||||||
|
private def getReplicaAssignmentForTopics(adminClient: Admin,
|
||||||
|
topics: Seq[String])
|
||||||
|
: Map[TopicPartition, Seq[Int]] = {
|
||||||
|
describeTopics(adminClient, topics.toSet.asJava).flatMap {
|
||||||
|
case (topicName, topicDescription) => topicDescription.partitions.asScala.map { info =>
|
||||||
|
(new TopicPartition(topicName, info.partition), info.replicas.asScala.map(_.id).toSeq)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def formatAsReassignmentJson(partitionsToBeReassigned: Map[TopicPartition, Seq[Int]],
|
||||||
|
replicaLogDirAssignment: Map[TopicPartitionReplica, String]): String = {
|
||||||
|
Json.encodeAsString(Map(
|
||||||
|
"version" -> 1,
|
||||||
|
"partitions" -> partitionsToBeReassigned.keySet.toBuffer.sortWith(compareTopicPartitions).map {
|
||||||
|
tp =>
|
||||||
|
val replicas = partitionsToBeReassigned(tp)
|
||||||
|
Map(
|
||||||
|
"topic" -> tp.topic,
|
||||||
|
"partition" -> tp.partition,
|
||||||
|
"replicas" -> replicas.asJava
|
||||||
|
).asJava
|
||||||
|
}.asJava
|
||||||
|
).asJava)
|
||||||
|
}
|
||||||
|
|
||||||
|
private def describeTopics(adminClient: Admin,
|
||||||
|
topics: Set[String])
|
||||||
|
: Map[String, TopicDescription] = {
|
||||||
|
adminClient.describeTopics(topics).values.asScala.map { case (topicName, topicDescriptionFuture) =>
|
||||||
|
try topicName -> topicDescriptionFuture.get
|
||||||
|
catch {
|
||||||
|
case t: ExecutionException if t.getCause.isInstanceOf[UnknownTopicOrPartitionException] =>
|
||||||
|
throw new ExecutionException(
|
||||||
|
new UnknownTopicOrPartitionException(s"Topic $topicName not found."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def modifyReassignmentThrottle(admin: Admin, moveMap: MoveMap): Unit = {
|
||||||
|
val leaderThrottles = calculateLeaderThrottles(moveMap)
|
||||||
|
val followerThrottles = calculateFollowerThrottles(moveMap)
|
||||||
|
modifyTopicThrottles(admin, leaderThrottles, followerThrottles)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<div id="nav">
|
<div id="nav">
|
||||||
|
<h2 class="logo">Kafka 控制台</h2>
|
||||||
<router-link to="/" class="pad-l-r">主页</router-link>
|
<router-link to="/" class="pad-l-r">主页</router-link>
|
||||||
<span>|</span
|
<span>|</span
|
||||||
><router-link to="/cluster-page" class="pad-l-r">集群</router-link>
|
><router-link to="/cluster-page" class="pad-l-r">集群</router-link>
|
||||||
@@ -8,6 +9,8 @@
|
|||||||
><router-link to="/topic-page" class="pad-l-r">Topic</router-link>
|
><router-link to="/topic-page" class="pad-l-r">Topic</router-link>
|
||||||
<span>|</span
|
<span>|</span
|
||||||
><router-link to="/group-page" class="pad-l-r">消费组</router-link>
|
><router-link to="/group-page" class="pad-l-r">消费组</router-link>
|
||||||
|
<span>|</span
|
||||||
|
><router-link to="/message-page" class="pad-l-r">消息</router-link>
|
||||||
<span v-show="config.enableAcl">|</span
|
<span v-show="config.enableAcl">|</span
|
||||||
><router-link to="/acl-page" class="pad-l-r" v-show="config.enableAcl"
|
><router-link to="/acl-page" class="pad-l-r" v-show="config.enableAcl"
|
||||||
>Acl</router-link
|
>Acl</router-link
|
||||||
@@ -44,7 +47,6 @@ export default {
|
|||||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
text-align: center;
|
|
||||||
color: #2c3e50;
|
color: #2c3e50;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +61,7 @@ export default {
|
|||||||
padding-top: 1%;
|
padding-top: 1%;
|
||||||
padding-bottom: 1%;
|
padding-bottom: 1%;
|
||||||
margin-bottom: 1%;
|
margin-bottom: 1%;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#nav a {
|
#nav a {
|
||||||
@@ -81,4 +84,10 @@ export default {
|
|||||||
height: 90%;
|
height: 90%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
.logo {
|
||||||
|
float: left;
|
||||||
|
left: 1%;
|
||||||
|
top: 1%;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -43,6 +43,12 @@ const routes = [
|
|||||||
component: () =>
|
component: () =>
|
||||||
import(/* webpackChunkName: "cluster" */ "../views/cluster/Cluster.vue"),
|
import(/* webpackChunkName: "cluster" */ "../views/cluster/Cluster.vue"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/message-page",
|
||||||
|
name: "Message",
|
||||||
|
component: () =>
|
||||||
|
import(/* webpackChunkName: "cluster" */ "../views/message/Message.vue"),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const router = new VueRouter({
|
const router = new VueRouter({
|
||||||
|
|||||||
@@ -117,6 +117,22 @@ export const KafkaTopicApi = {
|
|||||||
url: "/topic/partition/new",
|
url: "/topic/partition/new",
|
||||||
method: "post",
|
method: "post",
|
||||||
},
|
},
|
||||||
|
getCurrentReplicaAssignment: {
|
||||||
|
url: "/topic/replica/assignment",
|
||||||
|
method: "get",
|
||||||
|
},
|
||||||
|
updateReplicaAssignment: {
|
||||||
|
url: "/topic/replica/assignment",
|
||||||
|
method: "post",
|
||||||
|
},
|
||||||
|
configThrottle: {
|
||||||
|
url: "/topic/replica/throttle",
|
||||||
|
method: "post",
|
||||||
|
},
|
||||||
|
sendStats: {
|
||||||
|
url: "/topic/send/stats",
|
||||||
|
method: "get",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const KafkaConsumerApi = {
|
export const KafkaConsumerApi = {
|
||||||
@@ -190,4 +206,46 @@ export const KafkaOpApi = {
|
|||||||
url: "/op/replication/preferred",
|
url: "/op/replication/preferred",
|
||||||
method: "post",
|
method: "post",
|
||||||
},
|
},
|
||||||
|
configThrottle: {
|
||||||
|
url: "/op/broker/throttle",
|
||||||
|
method: "post",
|
||||||
|
},
|
||||||
|
removeThrottle: {
|
||||||
|
url: "/op/broker/throttle",
|
||||||
|
method: "delete",
|
||||||
|
},
|
||||||
|
currentReassignments: {
|
||||||
|
url: "/op/replication/reassignments",
|
||||||
|
method: "get",
|
||||||
|
},
|
||||||
|
cancelReassignment: {
|
||||||
|
url: "/op/replication/reassignments",
|
||||||
|
method: "delete",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export const KafkaMessageApi = {
|
||||||
|
searchByTime: {
|
||||||
|
url: "/message/search/time",
|
||||||
|
method: "post",
|
||||||
|
},
|
||||||
|
searchByOffset: {
|
||||||
|
url: "/message/search/offset",
|
||||||
|
method: "post",
|
||||||
|
},
|
||||||
|
searchDetail: {
|
||||||
|
url: "/message/search/detail",
|
||||||
|
method: "post",
|
||||||
|
},
|
||||||
|
deserializerList: {
|
||||||
|
url: "/message/deserializer/list",
|
||||||
|
method: "get",
|
||||||
|
},
|
||||||
|
send: {
|
||||||
|
url: "/message/send",
|
||||||
|
method: "post",
|
||||||
|
},
|
||||||
|
resend: {
|
||||||
|
url: "/message/resend",
|
||||||
|
method: "post",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { VueAxios } from "./axios";
|
|||||||
const request = axios.create({
|
const request = axios.create({
|
||||||
// API 请求的默认前缀
|
// API 请求的默认前缀
|
||||||
baseURL: process.env.VUE_APP_API_BASE_URL,
|
baseURL: process.env.VUE_APP_API_BASE_URL,
|
||||||
timeout: 30000, // 请求超时时间
|
timeout: 120000, // 请求超时时间
|
||||||
});
|
});
|
||||||
|
|
||||||
// 异常拦截处理器
|
// 异常拦截处理器
|
||||||
|
|||||||
@@ -47,6 +47,15 @@
|
|||||||
@click="openResetOffsetByTimeDialog(k)"
|
@click="openResetOffsetByTimeDialog(k)"
|
||||||
>时间戳
|
>时间戳
|
||||||
</a-button>
|
</a-button>
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
icon="reload"
|
||||||
|
size="small"
|
||||||
|
style="float: right"
|
||||||
|
@click="getConsumerDetail"
|
||||||
|
>
|
||||||
|
刷新
|
||||||
|
</a-button>
|
||||||
<hr />
|
<hr />
|
||||||
<a-table
|
<a-table
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
@@ -70,6 +79,11 @@
|
|||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
</a-table>
|
</a-table>
|
||||||
|
<p>
|
||||||
|
<strong style="color: red"
|
||||||
|
>注意:重置位点时,要求当前没有正在运行的消费端,否则重置的时候会报错,返回失败信息</strong
|
||||||
|
>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a-modal
|
<a-modal
|
||||||
|
|||||||
@@ -32,6 +32,10 @@
|
|||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
|
<hr />
|
||||||
|
<p>
|
||||||
|
*注意:该时间为北京时间。这里固定为东8区的计算时间,如果所在地区不是采用北京时间(中国大部分地区都是采用的北京时间),请自行对照为当地时间重置。
|
||||||
|
</p>
|
||||||
</a-spin>
|
</a-spin>
|
||||||
</div>
|
</div>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
|
|||||||
58
ui/src/views/message/Message.vue
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<template>
|
||||||
|
<div class="content">
|
||||||
|
<a-spin :spinning="loading">
|
||||||
|
<a-tabs default-active-key="1" size="large" tabPosition="top">
|
||||||
|
<a-tab-pane key="1" tab="根据时间查询消息">
|
||||||
|
<SearchByTime :topic-list="topicList"></SearchByTime>
|
||||||
|
</a-tab-pane>
|
||||||
|
<a-tab-pane key="2" tab="根据偏移查询消息">
|
||||||
|
<SearchByOffset :topic-list="topicList"></SearchByOffset>
|
||||||
|
</a-tab-pane>
|
||||||
|
<a-tab-pane key="3" tab="在线发送">
|
||||||
|
<SendMessage :topic-list="topicList"></SendMessage>
|
||||||
|
</a-tab-pane>
|
||||||
|
</a-tabs>
|
||||||
|
</a-spin>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import SearchByTime from "@/views/message/SearchByTime";
|
||||||
|
import SearchByOffset from "@/views/message/SearchByOffset";
|
||||||
|
import request from "@/utils/request";
|
||||||
|
import { KafkaTopicApi } from "@/utils/api";
|
||||||
|
import notification from "ant-design-vue/lib/notification";
|
||||||
|
import SendMessage from "@/views/message/SendMessage";
|
||||||
|
export default {
|
||||||
|
name: "Message",
|
||||||
|
components: { SearchByTime, SearchByOffset, SendMessage },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
topicList: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getTopicNameList() {
|
||||||
|
request({
|
||||||
|
url: KafkaTopicApi.getTopicNameList.url,
|
||||||
|
method: KafkaTopicApi.getTopicNameList.method,
|
||||||
|
}).then((res) => {
|
||||||
|
if (res.code == 0) {
|
||||||
|
this.topicList = res.data;
|
||||||
|
} else {
|
||||||
|
notification.error({
|
||||||
|
message: "error",
|
||||||
|
description: res.msg,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.getTopicNameList();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
265
ui/src/views/message/MessageDetail.vue
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
<template>
|
||||||
|
<a-modal
|
||||||
|
title="消息详情"
|
||||||
|
:visible="show"
|
||||||
|
:width="800"
|
||||||
|
:mask="false"
|
||||||
|
:destroyOnClose="true"
|
||||||
|
:footer="null"
|
||||||
|
:maskClosable="false"
|
||||||
|
@cancel="handleCancel"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<a-spin :spinning="loading">
|
||||||
|
<div>
|
||||||
|
<h4>消息信息</h4>
|
||||||
|
<hr />
|
||||||
|
<div class="message-detail" id="message-detail">
|
||||||
|
<p>
|
||||||
|
<label class="title">Topic: </label>
|
||||||
|
<span class="m-info">{{ data.topic }}</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label class="title">分区: </label>
|
||||||
|
<span class="m-info">{{ data.partition }}</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label class="title">偏移: </label>
|
||||||
|
<span class="m-info">{{ data.offset }}</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label class="title">消息头: </label>
|
||||||
|
<span class="m-info">{{ data.headers }}</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label class="title">时间类型: </label>
|
||||||
|
<span class="m-info"
|
||||||
|
>{{
|
||||||
|
data.timestampType
|
||||||
|
}}(表示下面的时间是哪种类型:消息创建、写入日志亦或其它)</span
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label class="title">时间: </label>
|
||||||
|
<span class="m-info">{{ formatTime(data.timestamp) }}</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label class="title">Key反序列化: </label>
|
||||||
|
<a-select
|
||||||
|
style="width: 120px"
|
||||||
|
v-model="keyDeserializer"
|
||||||
|
@change="keyDeserializerChange"
|
||||||
|
>
|
||||||
|
<a-select-option
|
||||||
|
v-for="v in deserializerList"
|
||||||
|
:key="v"
|
||||||
|
:value="v"
|
||||||
|
>
|
||||||
|
{{ v }}
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
<span>选一个合适反序列化器,要不可能乱码了</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label class="title">Key: </label>
|
||||||
|
<span class="m-info">{{ data.key }}</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label class="title">消息体反序列化: </label>
|
||||||
|
<a-select
|
||||||
|
v-model="valueDeserializer"
|
||||||
|
style="width: 120px"
|
||||||
|
@change="valueDeserializerChange"
|
||||||
|
>
|
||||||
|
<a-select-option
|
||||||
|
v-for="v in deserializerList"
|
||||||
|
:key="v"
|
||||||
|
:value="v"
|
||||||
|
>
|
||||||
|
{{ v }}
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
<span>选一个合适反序列化器,要不可能乱码了</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label class="title">消息体: </label>
|
||||||
|
<a-textarea
|
||||||
|
type="textarea"
|
||||||
|
:value="data.value"
|
||||||
|
:rows="5"
|
||||||
|
:read-only="true"
|
||||||
|
></a-textarea>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4>消费信息</h4>
|
||||||
|
<hr />
|
||||||
|
<a-table
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="data.consumers"
|
||||||
|
bordered
|
||||||
|
row-key="groupId"
|
||||||
|
>
|
||||||
|
<div slot="status" slot-scope="text">
|
||||||
|
<span v-if="text == 'consumed'">已消费</span
|
||||||
|
><span v-else style="color: red">未消费</span>
|
||||||
|
</div>
|
||||||
|
</a-table>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4>操作</h4>
|
||||||
|
<hr />
|
||||||
|
<a-popconfirm
|
||||||
|
title="确定将当前这条消息重新发回broker?"
|
||||||
|
ok-text="确认"
|
||||||
|
cancel-text="取消"
|
||||||
|
@confirm="resend"
|
||||||
|
>
|
||||||
|
<a-button type="primary" icon="reload"> 重新发送 </a-button>
|
||||||
|
</a-popconfirm>
|
||||||
|
</div>
|
||||||
|
</a-spin>
|
||||||
|
</div>
|
||||||
|
</a-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import request from "@/utils/request";
|
||||||
|
import { KafkaMessageApi } from "@/utils/api";
|
||||||
|
import notification from "ant-design-vue/lib/notification";
|
||||||
|
import moment from "moment";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "MessageDetail",
|
||||||
|
props: {
|
||||||
|
record: {},
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
show: this.visible,
|
||||||
|
data: {},
|
||||||
|
loading: false,
|
||||||
|
deserializerList: [],
|
||||||
|
keyDeserializer: "String",
|
||||||
|
valueDeserializer: "String",
|
||||||
|
consumerDetail: [],
|
||||||
|
columns,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
visible(v) {
|
||||||
|
this.show = v;
|
||||||
|
if (this.show) {
|
||||||
|
this.getMessageDetail();
|
||||||
|
this.getDeserializerList();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getMessageDetail() {
|
||||||
|
this.loading = true;
|
||||||
|
const params = Object.assign({}, this.record, {
|
||||||
|
keyDeserializer: this.keyDeserializer,
|
||||||
|
valueDeserializer: this.valueDeserializer,
|
||||||
|
});
|
||||||
|
request({
|
||||||
|
url: KafkaMessageApi.searchDetail.url,
|
||||||
|
method: KafkaMessageApi.searchDetail.method,
|
||||||
|
data: params,
|
||||||
|
}).then((res) => {
|
||||||
|
this.loading = false;
|
||||||
|
if (res.code != 0) {
|
||||||
|
notification.error({
|
||||||
|
message: "error",
|
||||||
|
description: res.msg,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.data = res.data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getDeserializerList() {
|
||||||
|
request({
|
||||||
|
url: KafkaMessageApi.deserializerList.url,
|
||||||
|
method: KafkaMessageApi.deserializerList.method,
|
||||||
|
}).then((res) => {
|
||||||
|
if (res.code != 0) {
|
||||||
|
notification.error({
|
||||||
|
message: "error",
|
||||||
|
description: res.msg,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.deserializerList = res.data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleCancel() {
|
||||||
|
this.data = {};
|
||||||
|
this.$emit("closeDetailDialog", { refresh: false });
|
||||||
|
},
|
||||||
|
formatTime(time) {
|
||||||
|
return moment(time).format("YYYY-MM-DD HH:mm:ss:SSS");
|
||||||
|
},
|
||||||
|
keyDeserializerChange() {
|
||||||
|
this.getMessageDetail();
|
||||||
|
},
|
||||||
|
valueDeserializerChange() {
|
||||||
|
this.getMessageDetail();
|
||||||
|
},
|
||||||
|
resend() {
|
||||||
|
const params = Object.assign({}, this.data);
|
||||||
|
this.loading = true;
|
||||||
|
request({
|
||||||
|
url: KafkaMessageApi.resend.url,
|
||||||
|
method: KafkaMessageApi.resend.method,
|
||||||
|
data: params,
|
||||||
|
}).then((res) => {
|
||||||
|
this.loading = false;
|
||||||
|
if (res.code != 0) {
|
||||||
|
notification.error({
|
||||||
|
message: "error",
|
||||||
|
description: res.msg,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.$message.success(res.msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: "消费组",
|
||||||
|
dataIndex: "groupId",
|
||||||
|
key: "groupId",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "消费情况",
|
||||||
|
dataIndex: "status",
|
||||||
|
key: "status",
|
||||||
|
scopedSlots: { customRender: "status" },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.m-info {
|
||||||
|
/*text-decoration: underline;*/
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
width: 15%;
|
||||||
|
display: inline-block;
|
||||||
|
text-align: right;
|
||||||
|
margin-right: 2%;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.ant-spin-container #message-detail textarea {
|
||||||
|
max-width: 80% !important;
|
||||||
|
vertical-align: top !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
95
ui/src/views/message/MessageList.vue
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<a-table
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="data"
|
||||||
|
bordered
|
||||||
|
:row-key="
|
||||||
|
(record, index) => {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div slot="operation" slot-scope="record">
|
||||||
|
<a-button
|
||||||
|
size="small"
|
||||||
|
href="javascript:;"
|
||||||
|
class="operation-btn"
|
||||||
|
@click="openDetailDialog(record)"
|
||||||
|
>消息详情
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</a-table>
|
||||||
|
<MessageDetail
|
||||||
|
:visible="showDetailDialog"
|
||||||
|
:record="record"
|
||||||
|
@closeDetailDialog="closeDetailDialog"
|
||||||
|
></MessageDetail>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import moment from "moment";
|
||||||
|
import MessageDetail from "@/views/message/MessageDetail";
|
||||||
|
export default {
|
||||||
|
name: "MessageList",
|
||||||
|
components: { MessageDetail },
|
||||||
|
props: {
|
||||||
|
data: {
|
||||||
|
type: Array,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
columns: columns,
|
||||||
|
showDetailDialog: false,
|
||||||
|
record: {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
openDetailDialog(record) {
|
||||||
|
this.record = record;
|
||||||
|
this.showDetailDialog = true;
|
||||||
|
},
|
||||||
|
closeDetailDialog() {
|
||||||
|
this.showDetailDialog = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: "topic",
|
||||||
|
dataIndex: "topic",
|
||||||
|
key: "topic",
|
||||||
|
width: 300,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "分区",
|
||||||
|
dataIndex: "partition",
|
||||||
|
key: "partition",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "偏移",
|
||||||
|
dataIndex: "offset",
|
||||||
|
key: "offset",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "时间",
|
||||||
|
dataIndex: "timestamp",
|
||||||
|
key: "timestamp",
|
||||||
|
slots: { title: "timestamp" },
|
||||||
|
scopedSlots: { customRender: "timestamp" },
|
||||||
|
customRender: (text) => {
|
||||||
|
return moment(text).format("YYYY-MM-DD HH:mm:ss:SSS");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "操作",
|
||||||
|
key: "operation",
|
||||||
|
scopedSlots: { customRender: "operation" },
|
||||||
|
width: 200,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
195
ui/src/views/message/SearchByOffset.vue
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
<template>
|
||||||
|
<div class="tab-content">
|
||||||
|
<a-spin :spinning="loading">
|
||||||
|
<div id="search-offset-form-advanced-search">
|
||||||
|
<a-form
|
||||||
|
class="ant-advanced-search-form"
|
||||||
|
:form="form"
|
||||||
|
@submit="handleSearch"
|
||||||
|
>
|
||||||
|
<a-row :gutter="24">
|
||||||
|
<a-col :span="9">
|
||||||
|
<a-form-item label="topic">
|
||||||
|
<a-select
|
||||||
|
class="topic-select"
|
||||||
|
@change="handleTopicChange"
|
||||||
|
show-search
|
||||||
|
option-filter-prop="children"
|
||||||
|
v-decorator="[
|
||||||
|
'topic',
|
||||||
|
{
|
||||||
|
rules: [{ required: true, message: '请选择一个topic!' }],
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
placeholder="请选择一个topic"
|
||||||
|
>
|
||||||
|
<a-select-option v-for="v in topicList" :key="v" :value="v">
|
||||||
|
{{ v }}
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="6">
|
||||||
|
<a-form-item label="分区">
|
||||||
|
<a-select
|
||||||
|
class="type-select"
|
||||||
|
show-search
|
||||||
|
option-filter-prop="children"
|
||||||
|
v-model="selectPartition"
|
||||||
|
placeholder="请选择一个分区"
|
||||||
|
>
|
||||||
|
<a-select-option v-for="v in partitions" :key="v" :value="v">
|
||||||
|
<span v-if="v == -1">全部</span> <span v-else>{{ v }}</span>
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="7">
|
||||||
|
<a-form-item label="偏移">
|
||||||
|
<a-input
|
||||||
|
v-decorator="[
|
||||||
|
'offset',
|
||||||
|
{
|
||||||
|
rules: [{ required: true, message: '请输入消息偏移!' }],
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
placeholder="消息偏移"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="2" :style="{ textAlign: 'right' }">
|
||||||
|
<a-form-item>
|
||||||
|
<a-button type="primary" html-type="submit"> 搜索</a-button>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</a-form>
|
||||||
|
</div>
|
||||||
|
<MessageList :data="data"></MessageList>
|
||||||
|
</a-spin>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import request from "@/utils/request";
|
||||||
|
import { KafkaMessageApi, KafkaTopicApi } from "@/utils/api";
|
||||||
|
import notification from "ant-design-vue/lib/notification";
|
||||||
|
import MessageList from "@/views/message/MessageList";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "SearchByOffset",
|
||||||
|
components: { MessageList },
|
||||||
|
props: {
|
||||||
|
topicList: {
|
||||||
|
type: Array,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
form: this.$form.createForm(this, { name: "message_search_offset" }),
|
||||||
|
partitions: [],
|
||||||
|
selectPartition: undefined,
|
||||||
|
rangeConfig: {
|
||||||
|
rules: [{ type: "array", required: true, message: "请选择时间!" }],
|
||||||
|
},
|
||||||
|
data: defaultData,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleSearch(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.form.validateFields((err, values) => {
|
||||||
|
if (!err) {
|
||||||
|
const data = Object.assign({}, values, {
|
||||||
|
partition: this.selectPartition,
|
||||||
|
});
|
||||||
|
this.loading = true;
|
||||||
|
request({
|
||||||
|
url: KafkaMessageApi.searchByOffset.url,
|
||||||
|
method: KafkaMessageApi.searchByOffset.method,
|
||||||
|
data: data,
|
||||||
|
}).then((res) => {
|
||||||
|
this.loading = false;
|
||||||
|
if (res.code == 0) {
|
||||||
|
this.$message.success(res.msg);
|
||||||
|
this.data = res.data;
|
||||||
|
} else {
|
||||||
|
notification.error({
|
||||||
|
message: "error",
|
||||||
|
description: res.msg,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getPartitionInfo(topic) {
|
||||||
|
this.loading = true;
|
||||||
|
request({
|
||||||
|
url: KafkaTopicApi.getPartitionInfo.url + "?topic=" + topic,
|
||||||
|
method: KafkaTopicApi.getPartitionInfo.method,
|
||||||
|
}).then((res) => {
|
||||||
|
this.loading = false;
|
||||||
|
if (res.code != 0) {
|
||||||
|
notification.error({
|
||||||
|
message: "error",
|
||||||
|
description: res.msg,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.partitions = res.data.map((v) => v.partition);
|
||||||
|
this.partitions.splice(0, 0, -1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleTopicChange(topic) {
|
||||||
|
this.selectPartition = -1;
|
||||||
|
this.getPartitionInfo(topic);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const defaultData = [];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.tab-content {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-advanced-search-form {
|
||||||
|
padding: 24px;
|
||||||
|
background: #fbfbfb;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-advanced-search-form .ant-form-item {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-advanced-search-form .ant-form-item-control-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#components-form-topic-advanced-search .ant-form {
|
||||||
|
max-width: none;
|
||||||
|
margin-bottom: 1%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-offset-form-advanced-search .search-result-list {
|
||||||
|
margin-top: 16px;
|
||||||
|
border: 1px dashed #e9e9e9;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: #fafafa;
|
||||||
|
min-height: 200px;
|
||||||
|
text-align: center;
|
||||||
|
padding-top: 80px;
|
||||||
|
}
|
||||||
|
.topic-select {
|
||||||
|
width: 400px !important;
|
||||||
|
}
|
||||||
|
.type-select {
|
||||||
|
width: 200px !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
201
ui/src/views/message/SearchByTime.vue
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
<template>
|
||||||
|
<div class="tab-content">
|
||||||
|
<a-spin :spinning="loading">
|
||||||
|
<div id="search-time-form-advanced-search">
|
||||||
|
<a-form
|
||||||
|
class="ant-advanced-search-form"
|
||||||
|
:form="form"
|
||||||
|
@submit="handleSearch"
|
||||||
|
>
|
||||||
|
<a-row :gutter="24">
|
||||||
|
<a-col :span="9">
|
||||||
|
<a-form-item label="topic">
|
||||||
|
<a-select
|
||||||
|
class="topic-select"
|
||||||
|
@change="handleTopicChange"
|
||||||
|
show-search
|
||||||
|
option-filter-prop="children"
|
||||||
|
v-decorator="[
|
||||||
|
'topic',
|
||||||
|
{
|
||||||
|
rules: [{ required: true, message: '请选择一个topic!' }],
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
placeholder="请选择一个topic"
|
||||||
|
>
|
||||||
|
<a-select-option v-for="v in topicList" :key="v" :value="v">
|
||||||
|
{{ v }}
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="5">
|
||||||
|
<a-form-item label="分区">
|
||||||
|
<a-select
|
||||||
|
class="type-select"
|
||||||
|
show-search
|
||||||
|
option-filter-prop="children"
|
||||||
|
v-model="selectPartition"
|
||||||
|
placeholder="请选择一个分区"
|
||||||
|
>
|
||||||
|
<a-select-option v-for="v in partitions" :key="v" :value="v">
|
||||||
|
<span v-if="v == -1">全部</span> <span v-else>{{ v }}</span>
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="8">
|
||||||
|
<a-form-item label="时间">
|
||||||
|
<a-range-picker
|
||||||
|
v-decorator="['time', rangeConfig]"
|
||||||
|
show-time
|
||||||
|
format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="2" :style="{ textAlign: 'right' }">
|
||||||
|
<a-form-item>
|
||||||
|
<a-button type="primary" html-type="submit"> 搜索</a-button>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</a-form>
|
||||||
|
</div>
|
||||||
|
<p style="margin-top: 1%">
|
||||||
|
<strong
|
||||||
|
>检索条数:{{ data.realNum }},允许返回的最大条数:{{
|
||||||
|
data.maxNum
|
||||||
|
}}</strong
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<MessageList :data="data.data"></MessageList>
|
||||||
|
</a-spin>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import request from "@/utils/request";
|
||||||
|
import { KafkaMessageApi, KafkaTopicApi } from "@/utils/api";
|
||||||
|
import notification from "ant-design-vue/lib/notification";
|
||||||
|
import MessageList from "@/views/message/MessageList";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "SearchByTime",
|
||||||
|
components: { MessageList },
|
||||||
|
props: {
|
||||||
|
topicList: {
|
||||||
|
type: Array,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
form: this.$form.createForm(this, { name: "message_search_time" }),
|
||||||
|
partitions: [],
|
||||||
|
selectPartition: undefined,
|
||||||
|
rangeConfig: {
|
||||||
|
rules: [{ type: "array", required: true, message: "请选择时间!" }],
|
||||||
|
},
|
||||||
|
data: defaultData,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleSearch(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.form.validateFields((err, values) => {
|
||||||
|
if (!err) {
|
||||||
|
const data = Object.assign({}, values, {
|
||||||
|
partition: this.selectPartition,
|
||||||
|
});
|
||||||
|
data.startTime = values.time[0].valueOf();
|
||||||
|
data.endTime = values.time[1];
|
||||||
|
this.loading = true;
|
||||||
|
request({
|
||||||
|
url: KafkaMessageApi.searchByTime.url,
|
||||||
|
method: KafkaMessageApi.searchByTime.method,
|
||||||
|
data: data,
|
||||||
|
}).then((res) => {
|
||||||
|
this.loading = false;
|
||||||
|
if (res.code == 0) {
|
||||||
|
this.$message.success(res.msg);
|
||||||
|
this.data = res.data;
|
||||||
|
} else {
|
||||||
|
notification.error({
|
||||||
|
message: "error",
|
||||||
|
description: res.msg,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getPartitionInfo(topic) {
|
||||||
|
this.loading = true;
|
||||||
|
request({
|
||||||
|
url: KafkaTopicApi.getPartitionInfo.url + "?topic=" + topic,
|
||||||
|
method: KafkaTopicApi.getPartitionInfo.method,
|
||||||
|
}).then((res) => {
|
||||||
|
this.loading = false;
|
||||||
|
if (res.code != 0) {
|
||||||
|
notification.error({
|
||||||
|
message: "error",
|
||||||
|
description: res.msg,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.partitions = res.data.map((v) => v.partition);
|
||||||
|
this.partitions.splice(0, 0, -1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleTopicChange(topic) {
|
||||||
|
this.selectPartition = -1;
|
||||||
|
this.getPartitionInfo(topic);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const defaultData = { realNum: 0, maxNum: 0 };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.tab-content {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-advanced-search-form {
|
||||||
|
padding: 24px;
|
||||||
|
background: #fbfbfb;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-advanced-search-form .ant-form-item {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-advanced-search-form .ant-form-item-control-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#components-form-topic-advanced-search .ant-form {
|
||||||
|
max-width: none;
|
||||||
|
margin-bottom: 1%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-time-form-advanced-search .search-result-list {
|
||||||
|
margin-top: 16px;
|
||||||
|
border: 1px dashed #e9e9e9;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: #fafafa;
|
||||||
|
min-height: 200px;
|
||||||
|
text-align: center;
|
||||||
|
padding-top: 80px;
|
||||||
|
}
|
||||||
|
.topic-select {
|
||||||
|
width: 400px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-select {
|
||||||
|
width: 150px !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
173
ui/src/views/message/SendMessage.vue
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
<template>
|
||||||
|
<div class="content">
|
||||||
|
<a-spin :spinning="loading">
|
||||||
|
<a-form :form="form" @submit="handleSubmit">
|
||||||
|
<a-form-item label="Topic">
|
||||||
|
<a-select
|
||||||
|
class="topic-select"
|
||||||
|
@change="handleTopicChange"
|
||||||
|
show-search
|
||||||
|
option-filter-prop="children"
|
||||||
|
v-decorator="[
|
||||||
|
'topic',
|
||||||
|
{
|
||||||
|
rules: [{ required: true, message: '请选择一个topic!' }],
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
placeholder="请选择一个topic"
|
||||||
|
>
|
||||||
|
<a-select-option v-for="v in topicList" :key="v" :value="v">
|
||||||
|
{{ v }}
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="分区">
|
||||||
|
<a-select
|
||||||
|
class="type-select"
|
||||||
|
show-search
|
||||||
|
option-filter-prop="children"
|
||||||
|
v-model="selectPartition"
|
||||||
|
placeholder="请选择一个分区"
|
||||||
|
>
|
||||||
|
<a-select-option v-for="v in partitions" :key="v" :value="v">
|
||||||
|
<span v-if="v == -1">默认</span> <span v-else>{{ v }}</span>
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="消息Key">
|
||||||
|
<a-input v-decorator="['key', { initialValue: 'key' }]" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="消息体" has-feedback>
|
||||||
|
<a-textarea
|
||||||
|
v-decorator="[
|
||||||
|
'body',
|
||||||
|
{
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '输入消息体!',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
placeholder="输入消息体!"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="发送的消息数">
|
||||||
|
<a-input-number
|
||||||
|
v-decorator="[
|
||||||
|
'num',
|
||||||
|
{
|
||||||
|
initialValue: 1,
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '输入消息数!',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
:min="1"
|
||||||
|
:max="32"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item :wrapper-col="{ span: 12, offset: 5 }">
|
||||||
|
<a-button type="primary" html-type="submit"> 提交 </a-button>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</a-spin>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import request from "@/utils/request";
|
||||||
|
import { KafkaTopicApi, KafkaMessageApi } from "@/utils/api";
|
||||||
|
import notification from "ant-design-vue/lib/notification";
|
||||||
|
export default {
|
||||||
|
name: "SendMessage",
|
||||||
|
components: {},
|
||||||
|
props: {
|
||||||
|
topicList: {
|
||||||
|
type: Array,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
form: this.$form.createForm(this, { name: "message_send" }),
|
||||||
|
loading: false,
|
||||||
|
partitions: [],
|
||||||
|
selectPartition: undefined,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getTopicNameList() {
|
||||||
|
request({
|
||||||
|
url: KafkaTopicApi.getTopicNameList.url,
|
||||||
|
method: KafkaTopicApi.getTopicNameList.method,
|
||||||
|
}).then((res) => {
|
||||||
|
if (res.code == 0) {
|
||||||
|
this.topicList = res.data;
|
||||||
|
} else {
|
||||||
|
notification.error({
|
||||||
|
message: "error",
|
||||||
|
description: res.msg,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getPartitionInfo(topic) {
|
||||||
|
this.loading = true;
|
||||||
|
request({
|
||||||
|
url: KafkaTopicApi.getPartitionInfo.url + "?topic=" + topic,
|
||||||
|
method: KafkaTopicApi.getPartitionInfo.method,
|
||||||
|
}).then((res) => {
|
||||||
|
this.loading = false;
|
||||||
|
if (res.code != 0) {
|
||||||
|
notification.error({
|
||||||
|
message: "error",
|
||||||
|
description: res.msg,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.partitions = res.data.map((v) => v.partition);
|
||||||
|
this.partitions.splice(0, 0, -1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleTopicChange(topic) {
|
||||||
|
this.selectPartition = -1;
|
||||||
|
this.getPartitionInfo(topic);
|
||||||
|
},
|
||||||
|
handleSubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.form.validateFields((err, values) => {
|
||||||
|
if (!err) {
|
||||||
|
const param = Object.assign({}, values, {
|
||||||
|
partition: this.selectPartition,
|
||||||
|
});
|
||||||
|
this.loading = true;
|
||||||
|
request({
|
||||||
|
url: KafkaMessageApi.send.url,
|
||||||
|
method: KafkaMessageApi.send.method,
|
||||||
|
data: param,
|
||||||
|
}).then((res) => {
|
||||||
|
this.loading = false;
|
||||||
|
if (res.code == 0) {
|
||||||
|
this.$message.success(res.msg);
|
||||||
|
} else {
|
||||||
|
notification.error({
|
||||||
|
message: "error",
|
||||||
|
description: res.msg,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.getTopicNameList();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
157
ui/src/views/op/ConfigThrottle.vue
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
<template>
|
||||||
|
<a-modal
|
||||||
|
title="限流配置"
|
||||||
|
:visible="show"
|
||||||
|
:width="1000"
|
||||||
|
:mask="false"
|
||||||
|
:maskClosable="false"
|
||||||
|
okText="确认"
|
||||||
|
cancelText="取消"
|
||||||
|
:destroyOnClose="true"
|
||||||
|
@cancel="handleCancel"
|
||||||
|
@ok="ok"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<a-spin :spinning="loading">
|
||||||
|
<a-form
|
||||||
|
:form="form"
|
||||||
|
:label-col="{ span: 5 }"
|
||||||
|
:wrapper-col="{ span: 12 }"
|
||||||
|
>
|
||||||
|
<a-form-item label="Broker">
|
||||||
|
<a-select
|
||||||
|
mode="multiple"
|
||||||
|
option-filter-prop="children"
|
||||||
|
v-decorator="[
|
||||||
|
'brokerList',
|
||||||
|
{
|
||||||
|
initialValue: brokers,
|
||||||
|
rules: [{ required: true, message: '请选择一个broker!' }],
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
placeholder="请选择一个broker"
|
||||||
|
>
|
||||||
|
<a-select-option v-for="v in brokers" :key="v" :value="v">
|
||||||
|
<span v-if="v == -1">全部</span> <span v-else>{{ v }}</span>
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="带宽">
|
||||||
|
<a-input-number
|
||||||
|
:min="1"
|
||||||
|
:max="1024"
|
||||||
|
v-decorator="[
|
||||||
|
'throttle',
|
||||||
|
{
|
||||||
|
initialValue: 1,
|
||||||
|
rules: [{ required: true, message: '输入带宽!' }],
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
<a-select default-value="MB" v-model="unit" style="width: 100px">
|
||||||
|
<a-select-option value="MB"> MB/s </a-select-option>
|
||||||
|
<a-select-option value="KB"> KB/s </a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
<hr />
|
||||||
|
<div><h4>注意:</h4></div>
|
||||||
|
<ul>
|
||||||
|
<li>该限速带宽,指的是broker之间副本进行同步时占用的带宽</li>
|
||||||
|
<li>该配置是broker级别配置,是针对broker上topic的副本</li>
|
||||||
|
<li>
|
||||||
|
在当前页面对指定broker限流配置后,并不是说设置后该broker上的所有topic副本同步就被限制为当前流速了。这仅仅是速率设置,如果需要对某topic的副本同步进行限流,还需要去
|
||||||
|
Topic->限流 处操作,只有进行限流操作的topic,该限速才会对其生效
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
上面这句话的意思就是,这里只配置topic副本同步的速率,要使这个配置真正在某个topic上生效,还要开启这个topic的限流
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<h4>如何检查限流配置是否成功:</h4>
|
||||||
|
kafka的限流速率是通过下面这两项配置的:
|
||||||
|
<ul>
|
||||||
|
<li>leader.replication.throttled.rate</li>
|
||||||
|
<li>follower.replication.throttled.rate</li>
|
||||||
|
</ul>
|
||||||
|
只需通过
|
||||||
|
<strong>集群->属性配置</strong>
|
||||||
|
查看是否存在这两项配置,如果存在便是配置的有限流,值的大小就是速率,单位:kb/s
|
||||||
|
</a-spin>
|
||||||
|
</div>
|
||||||
|
</a-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import request from "@/utils/request";
|
||||||
|
import { KafkaClusterApi, KafkaOpApi } from "@/utils/api";
|
||||||
|
import notification from "ant-design-vue/lib/notification";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "ConfigThrottle",
|
||||||
|
props: {
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
show: this.visible,
|
||||||
|
loading: false,
|
||||||
|
form: this.$form.createForm(this, { name: "ConfigThrottleForm" }),
|
||||||
|
brokers: [],
|
||||||
|
unit: "MB",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
visible(v) {
|
||||||
|
this.show = v;
|
||||||
|
if (this.show) {
|
||||||
|
this.getClusterInfo();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleCancel() {
|
||||||
|
this.$emit("closeConfigThrottleDialog", { refresh: false });
|
||||||
|
},
|
||||||
|
getClusterInfo() {
|
||||||
|
this.loading = true;
|
||||||
|
request({
|
||||||
|
url: KafkaClusterApi.getClusterInfo.url,
|
||||||
|
method: KafkaClusterApi.getClusterInfo.method,
|
||||||
|
}).then((res) => {
|
||||||
|
this.loading = false;
|
||||||
|
this.brokers = [];
|
||||||
|
res.data.nodes.forEach((node) => this.brokers.push(node.id));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
ok() {
|
||||||
|
this.form.validateFields((err, values) => {
|
||||||
|
if (!err) {
|
||||||
|
const data = Object.assign({}, values, { unit: this.unit });
|
||||||
|
this.loading = true;
|
||||||
|
request({
|
||||||
|
url: KafkaOpApi.configThrottle.url,
|
||||||
|
method: KafkaOpApi.configThrottle.method,
|
||||||
|
data: data,
|
||||||
|
}).then((res) => {
|
||||||
|
this.loading = false;
|
||||||
|
if (res.code == 0) {
|
||||||
|
this.$message.success(res.msg);
|
||||||
|
this.$emit("closeConfigThrottleDialog", { refresh: false });
|
||||||
|
} else {
|
||||||
|
notification.error({
|
||||||
|
message: "error",
|
||||||
|
description: res.msg,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
166
ui/src/views/op/CurrentReassignments.vue
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
<template>
|
||||||
|
<a-modal
|
||||||
|
title="正在进行副本重分配的分区"
|
||||||
|
:visible="show"
|
||||||
|
:width="1200"
|
||||||
|
:mask="false"
|
||||||
|
:destroyOnClose="true"
|
||||||
|
:footer="null"
|
||||||
|
:maskClosable="false"
|
||||||
|
@cancel="handleCancel"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<a-spin :spinning="loading">
|
||||||
|
<a-table
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="data"
|
||||||
|
bordered
|
||||||
|
:rowKey="(record) => record.topic + record.partition"
|
||||||
|
>
|
||||||
|
<div slot="replicas" slot-scope="text">
|
||||||
|
<span v-for="i in text" :key="i">
|
||||||
|
{{ i }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div slot="addingReplicas" slot-scope="text">
|
||||||
|
<span v-for="i in text" :key="i">
|
||||||
|
{{ i }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div slot="removingReplicas" slot-scope="text">
|
||||||
|
<span v-for="i in text" :key="i">
|
||||||
|
{{ i }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div slot="operation" slot-scope="record">
|
||||||
|
<a-popconfirm
|
||||||
|
title="取消正在进行的副本重分配任务?"
|
||||||
|
ok-text="确认"
|
||||||
|
cancel-text="取消"
|
||||||
|
@confirm="cancelReassignment(record)"
|
||||||
|
>
|
||||||
|
<a-button size="small" href="javascript:;" class="operation-btn"
|
||||||
|
>取消
|
||||||
|
</a-button>
|
||||||
|
</a-popconfirm>
|
||||||
|
</div>
|
||||||
|
</a-table>
|
||||||
|
</a-spin>
|
||||||
|
</div>
|
||||||
|
</a-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import request from "@/utils/request";
|
||||||
|
import { KafkaOpApi } from "@/utils/api";
|
||||||
|
import notification from "ant-design-vue/es/notification";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "CurrentReassignments",
|
||||||
|
props: {
|
||||||
|
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.currentReassignments();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
currentReassignments() {
|
||||||
|
this.loading = true;
|
||||||
|
const api = KafkaOpApi.currentReassignments;
|
||||||
|
request({
|
||||||
|
url: api.url,
|
||||||
|
method: api.method,
|
||||||
|
}).then((res) => {
|
||||||
|
this.loading = false;
|
||||||
|
if (res.code != 0) {
|
||||||
|
notification.error({
|
||||||
|
message: "error",
|
||||||
|
description: res.msg,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.data = res.data;
|
||||||
|
this.yesterday = this.data.yesterday;
|
||||||
|
this.today = this.data.today;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleCancel() {
|
||||||
|
this.$emit("closeCurrentReassignmentsDialog", {});
|
||||||
|
},
|
||||||
|
cancelReassignment(record) {
|
||||||
|
const param = { topic: record.topic, partition: record.partition };
|
||||||
|
this.loading = true;
|
||||||
|
const api = KafkaOpApi.cancelReassignment;
|
||||||
|
request({
|
||||||
|
url: api.url,
|
||||||
|
method: api.method,
|
||||||
|
data: param,
|
||||||
|
}).then((res) => {
|
||||||
|
this.loading = false;
|
||||||
|
if (res.code != 0) {
|
||||||
|
notification.error({
|
||||||
|
message: "error",
|
||||||
|
description: res.msg,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.currentReassignments();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: "Topic",
|
||||||
|
dataIndex: "topic",
|
||||||
|
key: "topic",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "分区",
|
||||||
|
dataIndex: "partition",
|
||||||
|
key: "partition",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "副本",
|
||||||
|
dataIndex: "replicas",
|
||||||
|
key: "replicas",
|
||||||
|
scopedSlots: { customRender: "replicas" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "正在增加的副本",
|
||||||
|
dataIndex: "addingReplicas",
|
||||||
|
key: "addingReplicas",
|
||||||
|
scopedSlots: { customRender: "addingReplicas" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "正在移除的副本",
|
||||||
|
dataIndex: "removingReplicas",
|
||||||
|
key: "removingReplicas",
|
||||||
|
scopedSlots: { customRender: "removingReplicas" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "操作",
|
||||||
|
key: "operation",
|
||||||
|
scopedSlots: { customRender: "operation" },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -1,5 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
|
<div class="content-module">
|
||||||
|
<a-card title="Broker管理" style="width: 100%; text-align: left">
|
||||||
|
<p>
|
||||||
|
<a-button type="primary" @click="openConfigThrottleDialog">
|
||||||
|
配置限流
|
||||||
|
</a-button>
|
||||||
|
<label>说明:</label>
|
||||||
|
<span
|
||||||
|
>设置指定broker上的topic的副本之间数据同步占用的带宽,这个设置是broker级别的,但是设置后还要去对应的topic上进行限流配置,指定对这个topic的相关副本进行限制</span
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<a-button type="primary" @click="openRemoveThrottleDialog">
|
||||||
|
解除限流
|
||||||
|
</a-button>
|
||||||
|
<label>说明:</label>
|
||||||
|
<span>解除指定broker上的topic副本之间数据同步占用的带宽限制</span>
|
||||||
|
</p>
|
||||||
|
</a-card>
|
||||||
|
</div>
|
||||||
<div class="content-module">
|
<div class="content-module">
|
||||||
<a-card title="副本管理" style="width: 100%; text-align: left">
|
<a-card title="副本管理" style="width: 100%; text-align: left">
|
||||||
<p>
|
<p>
|
||||||
@@ -9,6 +29,13 @@
|
|||||||
<label>说明:</label>
|
<label>说明:</label>
|
||||||
<span>将集群中所有分区leader副本设置为首选副本</span>
|
<span>将集群中所有分区leader副本设置为首选副本</span>
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
<a-button type="primary" @click="openCurrentReassignmentsDialog">
|
||||||
|
副本变更详情
|
||||||
|
</a-button>
|
||||||
|
<label>说明:</label>
|
||||||
|
<span>查看正在进行副本变更/重分配的任务,或者将其取消</span>
|
||||||
|
</p>
|
||||||
</a-card>
|
</a-card>
|
||||||
</div>
|
</div>
|
||||||
<div class="content-module">
|
<div class="content-module">
|
||||||
@@ -66,6 +93,20 @@
|
|||||||
@closeDataSyncSchemeDialog="closeDataSyncSchemeDialog"
|
@closeDataSyncSchemeDialog="closeDataSyncSchemeDialog"
|
||||||
>
|
>
|
||||||
</DataSyncScheme>
|
</DataSyncScheme>
|
||||||
|
<ConfigThrottle
|
||||||
|
:visible="brokerManager.showConfigThrottleDialog"
|
||||||
|
@closeConfigThrottleDialog="closeConfigThrottleDialog"
|
||||||
|
>
|
||||||
|
</ConfigThrottle>
|
||||||
|
<RemoveThrottle
|
||||||
|
:visible="brokerManager.showRemoveThrottleDialog"
|
||||||
|
@closeRemoveThrottleDialog="closeRemoveThrottleDialog"
|
||||||
|
>
|
||||||
|
</RemoveThrottle>
|
||||||
|
<CurrentReassignments
|
||||||
|
:visible="replicationManager.showCurrentReassignmentsDialog"
|
||||||
|
@closeCurrentReassignmentsDialog="closeCurrentReassignmentsDialog"
|
||||||
|
></CurrentReassignments>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -75,6 +116,9 @@ import MinOffsetAlignment from "@/views/op/MinOffsetAlignment";
|
|||||||
import OffsetAlignmentTable from "@/views/op/OffsetAlignmentTable";
|
import OffsetAlignmentTable from "@/views/op/OffsetAlignmentTable";
|
||||||
import ElectPreferredLeader from "@/views/op/ElectPreferredLeader";
|
import ElectPreferredLeader from "@/views/op/ElectPreferredLeader";
|
||||||
import DataSyncScheme from "@/views/op/DataSyncScheme";
|
import DataSyncScheme from "@/views/op/DataSyncScheme";
|
||||||
|
import ConfigThrottle from "@/views/op/ConfigThrottle";
|
||||||
|
import RemoveThrottle from "@/views/op/RemoveThrottle";
|
||||||
|
import CurrentReassignments from "@/views/op/CurrentReassignments";
|
||||||
export default {
|
export default {
|
||||||
name: "Operation",
|
name: "Operation",
|
||||||
components: {
|
components: {
|
||||||
@@ -83,6 +127,9 @@ export default {
|
|||||||
OffsetAlignmentTable,
|
OffsetAlignmentTable,
|
||||||
ElectPreferredLeader,
|
ElectPreferredLeader,
|
||||||
DataSyncScheme,
|
DataSyncScheme,
|
||||||
|
ConfigThrottle,
|
||||||
|
RemoveThrottle,
|
||||||
|
CurrentReassignments,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -94,6 +141,11 @@ export default {
|
|||||||
},
|
},
|
||||||
replicationManager: {
|
replicationManager: {
|
||||||
showElectPreferredLeaderDialog: false,
|
showElectPreferredLeaderDialog: false,
|
||||||
|
showCurrentReassignmentsDialog: false,
|
||||||
|
},
|
||||||
|
brokerManager: {
|
||||||
|
showConfigThrottleDialog: false,
|
||||||
|
showRemoveThrottleDialog: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -128,6 +180,24 @@ export default {
|
|||||||
closeElectPreferredLeaderDialog() {
|
closeElectPreferredLeaderDialog() {
|
||||||
this.replicationManager.showElectPreferredLeaderDialog = false;
|
this.replicationManager.showElectPreferredLeaderDialog = false;
|
||||||
},
|
},
|
||||||
|
openConfigThrottleDialog() {
|
||||||
|
this.brokerManager.showConfigThrottleDialog = true;
|
||||||
|
},
|
||||||
|
closeConfigThrottleDialog() {
|
||||||
|
this.brokerManager.showConfigThrottleDialog = false;
|
||||||
|
},
|
||||||
|
openRemoveThrottleDialog() {
|
||||||
|
this.brokerManager.showRemoveThrottleDialog = true;
|
||||||
|
},
|
||||||
|
closeRemoveThrottleDialog() {
|
||||||
|
this.brokerManager.showRemoveThrottleDialog = false;
|
||||||
|
},
|
||||||
|
openCurrentReassignmentsDialog() {
|
||||||
|
this.replicationManager.showCurrentReassignmentsDialog = true;
|
||||||
|
},
|
||||||
|
closeCurrentReassignmentsDialog() {
|
||||||
|
this.replicationManager.showCurrentReassignmentsDialog = false;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
127
ui/src/views/op/RemoveThrottle.vue
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
<template>
|
||||||
|
<a-modal
|
||||||
|
title="解除限流"
|
||||||
|
:visible="show"
|
||||||
|
:width="1000"
|
||||||
|
:mask="false"
|
||||||
|
:maskClosable="false"
|
||||||
|
okText="确认"
|
||||||
|
cancelText="取消"
|
||||||
|
:destroyOnClose="true"
|
||||||
|
@cancel="handleCancel"
|
||||||
|
@ok="ok"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<a-spin :spinning="loading">
|
||||||
|
<a-form
|
||||||
|
:form="form"
|
||||||
|
:label-col="{ span: 5 }"
|
||||||
|
:wrapper-col="{ span: 12 }"
|
||||||
|
>
|
||||||
|
<a-form-item label="Broker">
|
||||||
|
<a-select
|
||||||
|
mode="multiple"
|
||||||
|
option-filter-prop="children"
|
||||||
|
v-decorator="[
|
||||||
|
'brokerList',
|
||||||
|
{
|
||||||
|
initialValue: brokers,
|
||||||
|
rules: [{ required: true, message: '请选择一个broker!' }],
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
placeholder="请选择一个broker"
|
||||||
|
>
|
||||||
|
<a-select-option v-for="v in brokers" :key="v" :value="v">
|
||||||
|
<span v-if="v == -1">全部</span> <span v-else>{{ v }}</span>
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
<hr />
|
||||||
|
<h4>如何检查是否配置的有限流速率:</h4>
|
||||||
|
kafka的限流速率是通过下面这两项配置的:
|
||||||
|
<ul>
|
||||||
|
<li>leader.replication.throttled.rate</li>
|
||||||
|
<li>follower.replication.throttled.rate</li>
|
||||||
|
</ul>
|
||||||
|
只需通过
|
||||||
|
<strong>集群->属性配置</strong>
|
||||||
|
查看是否存在这两项配置,如果不存在,便是没有配置限流速率。如果未配置限流速率,即使指定某个topic的分区副本进行限流,没有速率也不限流。
|
||||||
|
</a-spin>
|
||||||
|
</div>
|
||||||
|
</a-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import request from "@/utils/request";
|
||||||
|
import { KafkaClusterApi, KafkaOpApi } from "@/utils/api";
|
||||||
|
import notification from "ant-design-vue/lib/notification";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "RemoveThrottle",
|
||||||
|
props: {
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
show: this.visible,
|
||||||
|
loading: false,
|
||||||
|
form: this.$form.createForm(this, { name: "RemoveThrottleForm" }),
|
||||||
|
brokers: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
visible(v) {
|
||||||
|
this.show = v;
|
||||||
|
if (this.show) {
|
||||||
|
this.getClusterInfo();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleCancel() {
|
||||||
|
this.$emit("closeRemoveThrottleDialog", { refresh: false });
|
||||||
|
},
|
||||||
|
getClusterInfo() {
|
||||||
|
this.loading = true;
|
||||||
|
request({
|
||||||
|
url: KafkaClusterApi.getClusterInfo.url,
|
||||||
|
method: KafkaClusterApi.getClusterInfo.method,
|
||||||
|
}).then((res) => {
|
||||||
|
this.loading = false;
|
||||||
|
this.brokers = [];
|
||||||
|
res.data.nodes.forEach((node) => this.brokers.push(node.id));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
ok() {
|
||||||
|
this.form.validateFields((err, values) => {
|
||||||
|
if (!err) {
|
||||||
|
const data = Object.assign({}, values);
|
||||||
|
this.loading = true;
|
||||||
|
request({
|
||||||
|
url: KafkaOpApi.removeThrottle.url,
|
||||||
|
method: KafkaOpApi.removeThrottle.method,
|
||||||
|
data: data,
|
||||||
|
}).then((res) => {
|
||||||
|
this.loading = false;
|
||||||
|
if (res.code == 0) {
|
||||||
|
this.$message.success(res.msg);
|
||||||
|
this.$emit("closeRemoveThrottleDialog", { refresh: false });
|
||||||
|
} else {
|
||||||
|
notification.error({
|
||||||
|
message: "error",
|
||||||
|
description: res.msg,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
163
ui/src/views/topic/ConfigTopicThrottle.vue
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
<template>
|
||||||
|
<a-modal
|
||||||
|
:title="topic + '限流'"
|
||||||
|
:visible="show"
|
||||||
|
:width="1000"
|
||||||
|
:mask="false"
|
||||||
|
:maskClosable="false"
|
||||||
|
okText="确认"
|
||||||
|
cancelText="取消"
|
||||||
|
:destroyOnClose="true"
|
||||||
|
@cancel="handleCancel"
|
||||||
|
@ok="ok"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<a-spin :spinning="loading">
|
||||||
|
<a-form
|
||||||
|
:form="form"
|
||||||
|
:label-col="{ span: 5 }"
|
||||||
|
:wrapper-col="{ span: 12 }"
|
||||||
|
>
|
||||||
|
<a-form-item label="操作">
|
||||||
|
<a-radio-group
|
||||||
|
@change="onChange"
|
||||||
|
v-decorator="[
|
||||||
|
'operation',
|
||||||
|
{
|
||||||
|
initialValue: 'ON',
|
||||||
|
rules: [{ required: true, message: '请选择一个操作!' }],
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<a-radio value="ON"> 配置限流 </a-radio>
|
||||||
|
<a-radio value="OFF"> 移除所有分区限流配置 </a-radio>
|
||||||
|
</a-radio-group>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="选择分区" v-show="showPartition">
|
||||||
|
<a-select
|
||||||
|
mode="multiple"
|
||||||
|
option-filter-prop="children"
|
||||||
|
v-decorator="[
|
||||||
|
'partitions',
|
||||||
|
{
|
||||||
|
initialValue: [-1],
|
||||||
|
rules: [{ required: true, message: '请选择一个分区!' }],
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
placeholder="请选择一个分区"
|
||||||
|
>
|
||||||
|
<a-select-option v-for="v in partitions" :key="v" :value="v">
|
||||||
|
<span v-if="v == -1">全部</span> <span v-else>{{ v }}</span>
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
<hr />
|
||||||
|
<h4>说明:</h4>
|
||||||
|
该限流表示topic的副本的在不同broker之间数据同步占用带宽的限制,该配置是一个topic级别的配置项。如未配置速率,即使配置了这个限流也不会进行实际的限流操作。配置速率在
|
||||||
|
<span style="color: red">运维->配置限流</span> 处进行操作.
|
||||||
|
<h4>如何检查是否对哪些分区启用限流:</h4>
|
||||||
|
topic的限流是通过下面这两项配置的:
|
||||||
|
<ul>
|
||||||
|
<li>leader.replication.throttled.replicas</li>
|
||||||
|
<li>follower.replication.throttled.replicas</li>
|
||||||
|
</ul>
|
||||||
|
只需通过
|
||||||
|
<strong>属性配置</strong>
|
||||||
|
查看这两项配置的值,格式为:"0:0,1:0",左侧为分区,右侧为broker
|
||||||
|
id。示例表示:[分区0的副本:在broker 0上,分区1的副本:在broker 0上]。
|
||||||
|
</a-spin>
|
||||||
|
</div>
|
||||||
|
</a-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import request from "@/utils/request";
|
||||||
|
import { KafkaTopicApi } from "@/utils/api";
|
||||||
|
import notification from "ant-design-vue/lib/notification";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "ConfigTopicThrottle",
|
||||||
|
props: {
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
topic: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
show: this.visible,
|
||||||
|
loading: false,
|
||||||
|
form: this.$form.createForm(this, { name: "RemoveThrottleForm" }),
|
||||||
|
partitions: [],
|
||||||
|
showPartition: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
visible(v) {
|
||||||
|
this.show = v;
|
||||||
|
if (this.show) {
|
||||||
|
this.getPartitionInfo();
|
||||||
|
this.showPartition = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleCancel() {
|
||||||
|
this.$emit("closeThrottleDialog", { refresh: false });
|
||||||
|
},
|
||||||
|
getPartitionInfo() {
|
||||||
|
this.loading = true;
|
||||||
|
request({
|
||||||
|
url: KafkaTopicApi.getPartitionInfo.url + "?topic=" + this.topic,
|
||||||
|
method: KafkaTopicApi.getPartitionInfo.method,
|
||||||
|
}).then((res) => {
|
||||||
|
this.loading = false;
|
||||||
|
if (res.code != 0) {
|
||||||
|
notification.error({
|
||||||
|
message: "error",
|
||||||
|
description: res.msg,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.partitions = res.data.map((e) => e.partition);
|
||||||
|
this.partitions.splice(0, 0, -1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
ok() {
|
||||||
|
this.form.validateFields((err, values) => {
|
||||||
|
if (!err) {
|
||||||
|
const data = Object.assign({}, values, { topic: this.topic });
|
||||||
|
this.loading = true;
|
||||||
|
request({
|
||||||
|
url: KafkaTopicApi.configThrottle.url,
|
||||||
|
method: KafkaTopicApi.configThrottle.method,
|
||||||
|
data: data,
|
||||||
|
}).then((res) => {
|
||||||
|
this.loading = false;
|
||||||
|
if (res.code == 0) {
|
||||||
|
this.$message.success(res.msg);
|
||||||
|
this.$emit("closeThrottleDialog", { refresh: false });
|
||||||
|
} else {
|
||||||
|
notification.error({
|
||||||
|
message: "error",
|
||||||
|
description: res.msg,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onChange(e) {
|
||||||
|
this.showPartition = !(e.target.value == "OFF");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<a-spin :spinning="loading">
|
<a-spin :spinning="loading">
|
||||||
<a-table
|
<a-table
|
||||||
|
bordered
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:data-source="data"
|
:data-source="data"
|
||||||
:rowKey="
|
:rowKey="
|
||||||
@@ -21,19 +22,15 @@
|
|||||||
"
|
"
|
||||||
>
|
>
|
||||||
<ul slot="replicas" slot-scope="text">
|
<ul slot="replicas" slot-scope="text">
|
||||||
<ol v-for="i in text" :key="i">
|
<li v-for="i in text" :key="i">
|
||||||
{{
|
{{ i }}
|
||||||
i
|
</li>
|
||||||
}}
|
|
||||||
</ol>
|
|
||||||
</ul>
|
|
||||||
<ul slot="isr" slot-scope="text">
|
|
||||||
<ol v-for="i in text" :key="i">
|
|
||||||
{{
|
|
||||||
i
|
|
||||||
}}
|
|
||||||
</ol>
|
|
||||||
</ul>
|
</ul>
|
||||||
|
<div slot="isr" slot-scope="text">
|
||||||
|
<span v-for="i in text" :key="i">
|
||||||
|
{{ i }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div slot="operation" slot-scope="record" v-show="!record.internal">
|
<div slot="operation" slot-scope="record" v-show="!record.internal">
|
||||||
<a-popconfirm
|
<a-popconfirm
|
||||||
:title="
|
:title="
|
||||||
@@ -52,7 +49,15 @@
|
|||||||
</a-button>
|
</a-button>
|
||||||
</a-popconfirm>
|
</a-popconfirm>
|
||||||
</div>
|
</div>
|
||||||
|
<p slot="expandedRowRender" slot-scope="record" style="margin: 0">
|
||||||
|
有效消息的时间范围:<span class="red-font">{{
|
||||||
|
formatTime(record.beginTime)
|
||||||
|
}}</span>
|
||||||
|
~
|
||||||
|
<span class="red-font">{{ formatTime(record.endTime) }}</span>
|
||||||
|
</p>
|
||||||
</a-table>
|
</a-table>
|
||||||
|
<p>友情提示:点击+号展开,可以查看当前分区的有效消息的时间范围</p>
|
||||||
</a-spin>
|
</a-spin>
|
||||||
</div>
|
</div>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
@@ -62,6 +67,7 @@
|
|||||||
import request from "@/utils/request";
|
import request from "@/utils/request";
|
||||||
import { KafkaOpApi, KafkaTopicApi } from "@/utils/api";
|
import { KafkaOpApi, KafkaTopicApi } from "@/utils/api";
|
||||||
import notification from "ant-design-vue/es/notification";
|
import notification from "ant-design-vue/es/notification";
|
||||||
|
import moment from "moment";
|
||||||
export default {
|
export default {
|
||||||
name: "PartitionInfo",
|
name: "PartitionInfo",
|
||||||
props: {
|
props: {
|
||||||
@@ -131,6 +137,11 @@ export default {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
formatTime(timestamp) {
|
||||||
|
return timestamp != -1
|
||||||
|
? moment(timestamp).format("YYYY-MM-DD HH:mm:ss:SSS")
|
||||||
|
: timestamp;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -172,6 +183,26 @@ const columns = [
|
|||||||
dataIndex: "diff",
|
dataIndex: "diff",
|
||||||
key: "diff",
|
key: "diff",
|
||||||
},
|
},
|
||||||
|
// {
|
||||||
|
// title: "有效消息起始时间",
|
||||||
|
// dataIndex: "beginTime",
|
||||||
|
// key: "beginTime",
|
||||||
|
// slots: { title: "beginTime" },
|
||||||
|
// scopedSlots: { customRender: "internal" },
|
||||||
|
// customRender: (text) => {
|
||||||
|
// return text != -1 ? moment(text).format("YYYY-MM-DD HH:mm:ss:SSS") : text;
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// title: "有效消息结束时间",
|
||||||
|
// dataIndex: "endTime",
|
||||||
|
// key: "endTime",
|
||||||
|
// slots: { title: "endTime" },
|
||||||
|
// scopedSlots: { customRender: "internal" },
|
||||||
|
// customRender: (text) => {
|
||||||
|
// return text != -1 ? moment(text).format("YYYY-MM-DD HH:mm:ss:SSS") : text;
|
||||||
|
// },
|
||||||
|
// },
|
||||||
{
|
{
|
||||||
title: "操作",
|
title: "操作",
|
||||||
key: "operation",
|
key: "operation",
|
||||||
@@ -180,4 +211,11 @@ const columns = [
|
|||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped>
|
||||||
|
.red-font {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
.green-font {
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
115
ui/src/views/topic/SendStats.vue
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
<template>
|
||||||
|
<a-modal
|
||||||
|
:title="topic + '发送统计'"
|
||||||
|
:visible="show"
|
||||||
|
:width="1000"
|
||||||
|
:mask="false"
|
||||||
|
:destroyOnClose="true"
|
||||||
|
:footer="null"
|
||||||
|
:maskClosable="false"
|
||||||
|
@cancel="handleCancel"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<a-spin :spinning="loading">
|
||||||
|
<h4>今天发送消息数:{{ today.total }}</h4>
|
||||||
|
<a-table
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="today.detail"
|
||||||
|
bordered
|
||||||
|
:rowKey="(record) => record.partition"
|
||||||
|
>
|
||||||
|
</a-table>
|
||||||
|
<hr />
|
||||||
|
<h4>昨天发送消息数:{{ yesterday.total }}</h4>
|
||||||
|
<a-table
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="yesterday.detail"
|
||||||
|
bordered
|
||||||
|
:rowKey="(record) => record.partition"
|
||||||
|
>
|
||||||
|
</a-table>
|
||||||
|
</a-spin>
|
||||||
|
</div>
|
||||||
|
</a-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import request from "@/utils/request";
|
||||||
|
import { KafkaTopicApi } from "@/utils/api";
|
||||||
|
import notification from "ant-design-vue/es/notification";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "SendStats",
|
||||||
|
props: {
|
||||||
|
topic: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
columns: columns,
|
||||||
|
show: this.visible,
|
||||||
|
data: [],
|
||||||
|
loading: false,
|
||||||
|
yesterday: {},
|
||||||
|
today: {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
visible(v) {
|
||||||
|
this.show = v;
|
||||||
|
if (this.show) {
|
||||||
|
this.sendStatus();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
sendStatus() {
|
||||||
|
this.loading = true;
|
||||||
|
const api = KafkaTopicApi.sendStats;
|
||||||
|
request({
|
||||||
|
url: api.url + "?topic=" + this.topic,
|
||||||
|
method: api.method,
|
||||||
|
}).then((res) => {
|
||||||
|
this.loading = false;
|
||||||
|
if (res.code != 0) {
|
||||||
|
notification.error({
|
||||||
|
message: "error",
|
||||||
|
description: res.msg,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.data = res.data;
|
||||||
|
this.yesterday = this.data.yesterday;
|
||||||
|
this.today = this.data.today;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleCancel() {
|
||||||
|
this.data = [];
|
||||||
|
this.yesterday = {};
|
||||||
|
this.today = {};
|
||||||
|
this.$emit("closeMessageStatsDialog", {});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: "分区",
|
||||||
|
dataIndex: "partition",
|
||||||
|
key: "partition",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "数量",
|
||||||
|
dataIndex: "num",
|
||||||
|
key: "num",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -98,6 +98,27 @@
|
|||||||
@click="openTopicConfigDialog(record.name)"
|
@click="openTopicConfigDialog(record.name)"
|
||||||
>属性配置
|
>属性配置
|
||||||
</a-button>
|
</a-button>
|
||||||
|
<a-button
|
||||||
|
size="small"
|
||||||
|
href="javascript:;"
|
||||||
|
class="operation-btn"
|
||||||
|
@click="openUpdateReplicaDialog(record.name)"
|
||||||
|
>变更副本
|
||||||
|
</a-button>
|
||||||
|
<a-button
|
||||||
|
size="small"
|
||||||
|
href="javascript:;"
|
||||||
|
class="operation-btn"
|
||||||
|
@click="openMessageStatsDialog(record.name)"
|
||||||
|
>发送统计
|
||||||
|
</a-button>
|
||||||
|
<a-button
|
||||||
|
size="small"
|
||||||
|
href="javascript:;"
|
||||||
|
class="operation-btn"
|
||||||
|
@click="openThrottleDialog(record.name)"
|
||||||
|
>限流
|
||||||
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
</a-table>
|
</a-table>
|
||||||
<PartitionInfo
|
<PartitionInfo
|
||||||
@@ -126,6 +147,21 @@
|
|||||||
:topic="selectDetail.resourceName"
|
:topic="selectDetail.resourceName"
|
||||||
@closeTopicConfigDialog="closeTopicConfigDialog"
|
@closeTopicConfigDialog="closeTopicConfigDialog"
|
||||||
></TopicConfig>
|
></TopicConfig>
|
||||||
|
<UpdateReplica
|
||||||
|
:visible="showUpdateReplicaDialog"
|
||||||
|
:topic="selectDetail.resourceName"
|
||||||
|
@closeUpdateReplicaDialog="closeUpdateReplicaDialog"
|
||||||
|
></UpdateReplica>
|
||||||
|
<ConfigTopicThrottle
|
||||||
|
:visible="showThrottleDialog"
|
||||||
|
:topic="selectDetail.resourceName"
|
||||||
|
@closeThrottleDialog="closeThrottleDialog"
|
||||||
|
></ConfigTopicThrottle>
|
||||||
|
<SendStats
|
||||||
|
:visible="showSendStatsDialog"
|
||||||
|
:topic="selectDetail.resourceName"
|
||||||
|
@closeMessageStatsDialog="closeMessageStatsDialog"
|
||||||
|
></SendStats>
|
||||||
</div>
|
</div>
|
||||||
</a-spin>
|
</a-spin>
|
||||||
</div>
|
</div>
|
||||||
@@ -140,6 +176,9 @@ import CreateTopic from "@/views/topic/CreateTopic";
|
|||||||
import AddPartition from "@/views/topic/AddPartition";
|
import AddPartition from "@/views/topic/AddPartition";
|
||||||
import ConsumedDetail from "@/views/topic/ConsumedDetail";
|
import ConsumedDetail from "@/views/topic/ConsumedDetail";
|
||||||
import TopicConfig from "@/views/topic/TopicConfig";
|
import TopicConfig from "@/views/topic/TopicConfig";
|
||||||
|
import UpdateReplica from "@/views/topic/UpdateReplica";
|
||||||
|
import ConfigTopicThrottle from "@/views/topic/ConfigTopicThrottle";
|
||||||
|
import SendStats from "@/views/topic/SendStats";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Topic",
|
name: "Topic",
|
||||||
@@ -149,6 +188,9 @@ export default {
|
|||||||
AddPartition,
|
AddPartition,
|
||||||
ConsumedDetail,
|
ConsumedDetail,
|
||||||
TopicConfig,
|
TopicConfig,
|
||||||
|
UpdateReplica,
|
||||||
|
ConfigTopicThrottle,
|
||||||
|
SendStats,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -170,6 +212,9 @@ export default {
|
|||||||
showAddPartition: false,
|
showAddPartition: false,
|
||||||
showConsumedDetailDialog: false,
|
showConsumedDetailDialog: false,
|
||||||
showTopicConfigDialog: false,
|
showTopicConfigDialog: false,
|
||||||
|
showUpdateReplicaDialog: false,
|
||||||
|
showThrottleDialog: false,
|
||||||
|
showSendStatsDialog: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -250,6 +295,27 @@ export default {
|
|||||||
closeTopicConfigDialog() {
|
closeTopicConfigDialog() {
|
||||||
this.showTopicConfigDialog = false;
|
this.showTopicConfigDialog = false;
|
||||||
},
|
},
|
||||||
|
openUpdateReplicaDialog(topic) {
|
||||||
|
this.showUpdateReplicaDialog = true;
|
||||||
|
this.selectDetail.resourceName = topic;
|
||||||
|
},
|
||||||
|
closeUpdateReplicaDialog() {
|
||||||
|
this.showUpdateReplicaDialog = false;
|
||||||
|
},
|
||||||
|
openMessageStatsDialog(topic) {
|
||||||
|
this.showSendStatsDialog = true;
|
||||||
|
this.selectDetail.resourceName = topic;
|
||||||
|
},
|
||||||
|
closeMessageStatsDialog() {
|
||||||
|
this.showSendStatsDialog = false;
|
||||||
|
},
|
||||||
|
openThrottleDialog(topic) {
|
||||||
|
this.showThrottleDialog = true;
|
||||||
|
this.selectDetail.resourceName = topic;
|
||||||
|
},
|
||||||
|
closeThrottleDialog() {
|
||||||
|
this.showThrottleDialog = false;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.getTopicList();
|
this.getTopicList();
|
||||||
@@ -281,7 +347,7 @@ const columns = [
|
|||||||
title: "操作",
|
title: "操作",
|
||||||
key: "operation",
|
key: "operation",
|
||||||
scopedSlots: { customRender: "operation" },
|
scopedSlots: { customRender: "operation" },
|
||||||
width: 500,
|
width: 800,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
211
ui/src/views/topic/UpdateReplica.vue
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
<template>
|
||||||
|
<a-modal
|
||||||
|
title="变更副本"
|
||||||
|
:visible="show"
|
||||||
|
:width="1200"
|
||||||
|
:mask="false"
|
||||||
|
:destroyOnClose="true"
|
||||||
|
:maskClosable="false"
|
||||||
|
@cancel="handleCancel"
|
||||||
|
okText="确认"
|
||||||
|
cancelText="取消"
|
||||||
|
@ok="handleOk"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<a-spin :spinning="loading">
|
||||||
|
<div class="replica-box">
|
||||||
|
<label>设置副本数:</label
|
||||||
|
><a-input-number
|
||||||
|
id="inputNumber"
|
||||||
|
v-model="replicaNums"
|
||||||
|
:min="1"
|
||||||
|
:max="brokerSize"
|
||||||
|
@change="onChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="replica-box">
|
||||||
|
<label>是否要限流:</label
|
||||||
|
><a-input-number
|
||||||
|
id="inputNumber"
|
||||||
|
v-model="data.interBrokerThrottle"
|
||||||
|
:min="-1"
|
||||||
|
:max="102400"
|
||||||
|
/>
|
||||||
|
<strong>
|
||||||
|
|说明:broker之间副本同步带宽限制,默认值为-1表示不限制,不是-1表示限制,该值并不表示流速,至于流速配置,在
|
||||||
|
<span style="color: red">运维->配置限流</span> 处进行操作.</strong
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<a-table
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="data.partitions"
|
||||||
|
bordered
|
||||||
|
:rowKey="
|
||||||
|
(record, index) => {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div slot="replicas" slot-scope="text">
|
||||||
|
<span v-for="i in text" :key="i">
|
||||||
|
{{ i }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</a-table>
|
||||||
|
<p>
|
||||||
|
*正在进行即尚未完成的副本变更的任务,可以在
|
||||||
|
<span style="color: red">运维->副本变更详情</span>
|
||||||
|
处查看,也可以在那里将正在进行的任务取消。
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
*如果是减少副本,不用限流。如果是增加副本数,副本同步的时候如果有大量消息需要同步,可能占用大量带宽,担心会影响集群的稳定,考虑是否开启限流。同步完成可以再把该topic的限流关毕。关闭操作可以点击
|
||||||
|
限流按钮 处理。
|
||||||
|
</p>
|
||||||
|
</a-spin>
|
||||||
|
</div>
|
||||||
|
</a-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import request from "@/utils/request";
|
||||||
|
import { KafkaClusterApi, KafkaTopicApi } from "@/utils/api";
|
||||||
|
import notification from "ant-design-vue/lib/notification";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "UpdateReplica",
|
||||||
|
props: {
|
||||||
|
topic: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
columns: columns,
|
||||||
|
show: this.visible,
|
||||||
|
data: {},
|
||||||
|
loading: false,
|
||||||
|
form: this.$form.createForm(this, { name: "coordinated" }),
|
||||||
|
brokerSize: 0,
|
||||||
|
replicaNums: 0,
|
||||||
|
defaultReplicaNums: 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
visible(v) {
|
||||||
|
this.show = v;
|
||||||
|
if (this.show) {
|
||||||
|
this.getClusterInfo();
|
||||||
|
this.getCurrentReplicaAssignment();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getCurrentReplicaAssignment() {
|
||||||
|
this.loading = true;
|
||||||
|
request({
|
||||||
|
url:
|
||||||
|
KafkaTopicApi.getCurrentReplicaAssignment.url +
|
||||||
|
"?topic=" +
|
||||||
|
this.topic,
|
||||||
|
method: KafkaTopicApi.getCurrentReplicaAssignment.method,
|
||||||
|
}).then((res) => {
|
||||||
|
this.loading = false;
|
||||||
|
if (res.code == 0) {
|
||||||
|
this.data = res.data;
|
||||||
|
if (this.data.partitions.length > 0) {
|
||||||
|
this.replicaNums = this.data.partitions[0].replicas.length;
|
||||||
|
this.defaultReplicaNums = this.replicaNums;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
notification.error({
|
||||||
|
message: "error",
|
||||||
|
description: res.msg,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getClusterInfo() {
|
||||||
|
this.loading = true;
|
||||||
|
request({
|
||||||
|
url: KafkaClusterApi.getClusterInfo.url,
|
||||||
|
method: KafkaClusterApi.getClusterInfo.method,
|
||||||
|
}).then((res) => {
|
||||||
|
this.brokerSize = res.data.nodes.length;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleCancel() {
|
||||||
|
this.data = {};
|
||||||
|
this.$emit("closeUpdateReplicaDialog", { refresh: false });
|
||||||
|
},
|
||||||
|
onChange(value) {
|
||||||
|
if (value < 1 || value > this.brokerSize) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this.data.partitions.length > 0) {
|
||||||
|
this.data.partitions.forEach((p) => {
|
||||||
|
if (value > p.replicas.length) {
|
||||||
|
let num = p.replicas[p.replicas.length - 1];
|
||||||
|
for (let i = p.replicas.length; i < value; i++) {
|
||||||
|
p.replicas.push(++num % this.brokerSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (value < p.replicas.length) {
|
||||||
|
for (let i = p.replicas.length; i > value; i--) {
|
||||||
|
p.replicas.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleOk() {
|
||||||
|
this.loading = true;
|
||||||
|
request({
|
||||||
|
url: KafkaTopicApi.updateReplicaAssignment.url,
|
||||||
|
method: KafkaTopicApi.updateReplicaAssignment.method,
|
||||||
|
data: this.data,
|
||||||
|
}).then((res) => {
|
||||||
|
this.loading = false;
|
||||||
|
if (res.code == 0) {
|
||||||
|
this.$message.success(res.msg);
|
||||||
|
this.$emit("closeUpdateReplicaDialog", { refresh: false });
|
||||||
|
} else {
|
||||||
|
notification.error({
|
||||||
|
message: "error",
|
||||||
|
description: res.msg,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: "Topic",
|
||||||
|
dataIndex: "topic",
|
||||||
|
key: "topic",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "分区",
|
||||||
|
dataIndex: "partition",
|
||||||
|
key: "partition",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "副本",
|
||||||
|
dataIndex: "replicas",
|
||||||
|
key: "replicas",
|
||||||
|
scopedSlots: { customRender: "replicas" },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.replica-box {
|
||||||
|
margin-bottom: 1%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||