19 Commits

Author SHA1 Message Date
许晓东
745aa6e9bc 升级v1.0.13版本 2025-07-01 20:34:22 +08:00
许晓东
1e86aa4569 增加消息转发功能. 2025-06-16 20:28:26 +08:00
许晓东
780d05bc06 增加消息转发接口. 2025-06-11 20:12:09 +08:00
许晓东
a755ffb010 消息详情对话框点击模态框的遮罩层关闭模态框. 2025-05-27 20:52:37 +08:00
许晓东
39544327ee 补充TopicConsole一个注释. 2025-05-20 21:07:08 +08:00
许晓东
9b7263241c broker配置mask close默认设置为true 2025-04-24 20:28:33 +08:00
许晓东
680630d372 Topic菜单消费详情支持点击刷新 2025-03-16 19:47:42 +08:00
许晓东
d554053e44 发布v1.0.12版本 2025-02-24 22:53:02 +08:00
许晓东
ba0a12fd5f 升级v1.0.12版本 2025-02-24 22:39:27 +08:00
许晓东
a927dd412e 消费组支持模糊过滤查询. 2025-02-22 20:56:00 +08:00
许晓东
a48812ed0e message提示框显示1秒,最多显示1条. 2025-01-12 22:02:49 +08:00
许晓东
dd2863e51d topic-发送统计: 增加刷新按钮 2024-12-16 20:50:54 +08:00
许晓东
89846018cc 发布1.0.11版本 2024-10-06 12:55:43 +08:00
许晓东
19cc635151 升级1.0.11版本 2024-10-06 11:30:08 +08:00
许晓东
a1206fb094 主页显示broker版本 2024-09-01 21:47:35 +08:00
许晓东
83c018f6a7 计算broker版本 2024-08-30 22:57:06 +08:00
许晓东
2729627a80 更新jetbrains 最新logo 2024-08-14 21:42:20 +08:00
许晓东
096792beb0 集群信息/topic:副本,配置模态框点击背景关闭#26 2024-07-07 21:10:00 +08:00
许晓东
fd495351bc 发布v1.0.10版本. 2024-06-16 21:06:54 +08:00
28 changed files with 604 additions and 25 deletions

View File

@@ -25,11 +25,11 @@ v1.0.6版本之前如果kafka集群启用了ACL但是控制台没看到Acl
![功能特性](./document/img/功能特性.png)
## 安装包下载
点击下载(v1.0.9版本)[kafka-console-ui.zip](https://github.com/xxd763795151/kafka-console-ui/releases/download/v1.0.9/kafka-console-ui-1.0.9.zip)
点击下载(v1.0.13版本)[kafka-console-ui.zip](https://github.com/xxd763795151/kafka-console-ui/releases/download/v1.0.13/kafka-console-ui-1.0.13.zip)
如果安装包下载的比较慢,可以查看下面的源码打包说明,把代码下载下来,本地快速打包.
github下载慢也可以试试从gitee下载点击下载[gitee来源kafka-console-ui.zip](https://gitee.com/xiaodong_xu/kafka-console-ui/releases/download/v1.0.9/kafka-console-ui-1.0.9.zip)
github下载慢也可以试试从gitee下载点击下载[gitee来源kafka-console-ui.zip](https://gitee.com/xiaodong_xu/kafka-console-ui/releases/download/v1.0.13/kafka-console-ui-1.0.13.zip)
## 快速使用
### Windows
@@ -68,7 +68,7 @@ sh bin/shutdown.sh
在新增集群的时候除了集群地址还可以输入集群的其它属性配置比如请求超时ACL配置等。如果开启了ACL切换到该集群的时候导航栏上便会出现ACL菜单支持进行相关操作目前是基于SASL_SCRAM认证授权管理支持的最完善其它的我也没验证过虽然是我开发的但是我也没具体全部验证这一块功能授权部分应该是通用的
## kafka版本
* 当前使用的kafka 3.2.0
* 当前使用的kafka 3.5.0
## 监控
仅提供运维管理功能监控、告警需要配合其它组件如有需要建议请查看https://blog.csdn.net/x763795151/article/details/119705372
@@ -102,6 +102,8 @@ auth:
感谢jetbrains的开源支持如果有朋友愿意一起维护很欢迎提pr.
[![jetbrains](./document/img/jb_beam.svg "jetbrains")](https://jb.gg/OpenSourceSupport)
jetbrains官方地址: https://www.jetbrains.com/
## 联系方式
+ 微信群
@@ -109,6 +111,14 @@ auth:
[//]: # (<img src="https://github.com/xxd763795151/kafka-console-ui/blob/main/document/contact/weixin_contact.jpg" width="40%"/>)
+ 若联系方式失效, 请联系加一下微信, 说明意图
- xxd763795151
- wxid_7jy2ezljvebt12
抱歉,后面就不再提供新的联系方式加群了。
在很早之前,有个兄弟提了个建议,可以拉个群,大家可以一起交流,所以成立了一个群,当时我以为没有多少朋友愿意加入。
可是后来确实有不少朋友进群了这让我很惶恐其实这个平台我自己并不觉得有太多技术深度却有一些朋友愿意来捧场这个让我觉得很惭愧所以现在考虑了下如果真有使用问题可以留个issue。
另外,我自己也确实挺忙,对于这个项目的需求处理不够及时,实在是时间和精力上有限,所以有朋友希望新增的一些能力,拖了这么久我也没有下文,实在是抱歉。
对于一些不太耗时的功能,我还是可以积极处理的。
另外有些功能,是想要放到后面再加的,所以迟迟没有动手。

View File

@@ -1 +1,13 @@
<svg height="180" viewBox="0 0 180 180" width="180" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="32.64" x2="82.77" y1="61.16" y2="85.54"><stop offset=".21" stop-color="#fe2857"/><stop offset="1" stop-color="#293896"/></linearGradient><linearGradient id="b" gradientUnits="userSpaceOnUse" x1="17.38" x2="82.95" y1="69.86" y2="21.23"><stop offset="0" stop-color="#fe2857"/><stop offset=".01" stop-color="#fe2857"/><stop offset=".86" stop-color="#ff318c"/></linearGradient><linearGradient id="c" gradientUnits="userSpaceOnUse" x1="74.17" x2="160.27" y1="21.58" y2="99.76"><stop offset=".02" stop-color="#ff318c"/><stop offset=".21" stop-color="#fe2857"/><stop offset=".86" stop-color="#fdb60d"/></linearGradient><linearGradient id="d" gradientUnits="userSpaceOnUse" x1="155.46" x2="55.07" y1="89.8" y2="158.9"><stop offset=".01" stop-color="#fdb60d"/><stop offset=".86" stop-color="#fcf84a"/></linearGradient><path d="m81.56 83.71-41.35-35a15 15 0 1 0 -14.47 25.7h.15l.39.12 52.16 15.89a3.53 3.53 0 0 0 1.18.21 3.73 3.73 0 0 0 1.93-6.91z" fill="url(#a)"/><path d="m89.85 25.93a10.89 10.89 0 0 0 -16.85-9.18l-50.5 30.66a15 15 0 1 0 17.9 24l45.27-36.89.36-.3a10.93 10.93 0 0 0 3.82-8.29z" fill="url(#b)"/><path d="m163.29 92-76.62-73.79a10.91 10.91 0 1 0 -14.81 16l.14.12 81.4 68.58a7.36 7.36 0 0 0 12.09-5.65 7.39 7.39 0 0 0 -2.2-5.26z" fill="url(#c)"/><path d="m165.5 97.29a7.35 7.35 0 0 0 -11.67-6l-92.71 45.3a15 15 0 1 0 15.48 25.59l85.73-58.84a7.35 7.35 0 0 0 3.17-6.05z" fill="url(#d)"/><path d="m60 60h60v60h-60z"/><g fill="#fff"><path d="m66.53 108.75h22.5v3.75h-22.5z"/><path d="m65.59 75.47 1.67-1.58a1.88 1.88 0 0 0 1.47.87c.64 0 1.06-.45 1.06-1.32v-5.92h2.58v5.94a3.44 3.44 0 0 1 -.92 2.63 3.52 3.52 0 0 1 -2.57 1 3.84 3.84 0 0 1 -3.29-1.62z"/><path d="m73.53 67.52h7.53v2.19h-5v1.43h4.49v2h-4.45v1.49h5v2.2h-7.6z"/><path d="m84.73 69.79h-2.8v-2.27h8.21v2.27h-2.81v7.09h-2.6z"/><path d="m66.63 80.58h4.42a3.47 3.47 0 0 1 2.55.83 2.09 2.09 0 0 1 .61 1.52 2.18 2.18 0 0 1 -1.45 2.09 2.27 2.27 0 0 1 1.86 2.29c0 1.69-1.31 2.69-3.55 2.69h-4.44zm5 2.89c0-.52-.42-.8-1.18-.8h-1.29v1.64h1.25c.78 0 1.24-.27 1.24-.81zm-.9 2.66h-1.57v1.73h1.62c.8 0 1.24-.31 1.24-.86-.02-.53-.4-.87-1.27-.87z"/><path d="m75.45 80.58h4.15a4.14 4.14 0 0 1 3.05 1 2.92 2.92 0 0 1 .83 2.18 3 3 0 0 1 -1.93 2.89l2.24 3.35h-3l-1.89-2.84h-.87v2.84h-2.6zm4 4.5c.87 0 1.4-.43 1.4-1.12 0-.75-.55-1.13-1.41-1.13h-1.39v2.27z"/><path d="m87.09 80.51h2.5l4 9.44h-2.79l-.67-1.69h-3.63l-.67 1.74h-2.71zm2.28 5.73-1.05-2.65-1.06 2.65z"/><path d="m94 80.55h2.6v9.37h-2.6z"/><path d="m97.56 80.55h2.44l3.37 5v-5h2.57v9.37h-2.27l-3.53-5.14v5.14h-2.58z"/><path d="m106.37 88.53 1.44-1.73a4.86 4.86 0 0 0 3 1.13c.71 0 1.08-.25 1.08-.65 0-.41-.3-.61-1.59-.91-2-.46-3.53-1-3.53-2.93 0-1.74 1.38-3 3.63-3a5.88 5.88 0 0 1 3.85 1.25l-1.25 1.78a4.56 4.56 0 0 0 -2.62-.92c-.63 0-.94.25-.94.6 0 .43.32.62 1.63.91 2.15.47 3.48 1.17 3.48 2.92 0 1.91-1.51 3-3.78 3a6.56 6.56 0 0 1 -4.4-1.45z"/></g><path d="m0 0h180v180h-180z" fill="none"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="298" height="64" fill="none" viewBox="0 0 298 64">
<defs>
<linearGradient id="a" x1=".850001" x2="62.62" y1="62.72" y2="1.81" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF9419"/>
<stop offset=".43" stop-color="#FF021D"/>
<stop offset=".99" stop-color="#E600FF"/>
</linearGradient>
</defs>
<path fill="#000" d="M86.4844 40.5858c0 .8464-.1792 1.5933-.5377 2.2505-.3585.6573-.8564 1.1651-1.5137 1.5236-.6572.3585-1.3941.5378-2.2406.5378H78v6.1044h5.0787c1.912 0 3.6248-.4282 5.1484-1.2846 1.5236-.8564 2.7186-2.0415 3.585-3.5452.8663-1.5037 1.3045-3.1966 1.3045-5.0886V21.0178h-6.6322v19.568Zm17.8556-1.8224h13.891v-5.6065H104.34v-6.3633h15.355v-5.7758H97.8766v29.9743h22.2464v-5.7757H104.34v-6.453Zm17.865-11.8005h8.882v24.0193h6.633V26.9629h8.842v-5.9451h-24.367v5.9551l.01-.01Zm47.022 9.0022c-.517-.2788-1.085-.4879-1.673-.6472.449-.1295.877-.2888 1.275-.488 1.096-.5676 1.962-1.3643 2.579-2.39.618-1.0257.936-2.2007.936-3.5351 0-1.5237-.418-2.8879-1.244-4.0929-.827-1.195-1.992-2.131-3.486-2.8082-1.494-.6672-3.206-1.0058-5.118-1.0058h-13.315v29.9743h13.574c2.011 0 3.804-.3485 5.387-1.0556 1.573-.707 2.798-1.6829 3.675-2.9476.866-1.2547 1.304-2.6887 1.304-4.302 0-1.4837-.338-2.8082-1.026-3.9833-.687-1.175-1.633-2.0812-2.858-2.7285l-.01.0099Zm-13.603-9.9184h5.886c.816 0 1.533.1494 2.161.4382.627.2888 1.115.707 1.464 1.2547.348.5378.527 1.1751.527 1.9021 0 .7269-.179 1.414-.527 1.9817-.349.5676-.837.9958-1.464 1.3045-.628.3087-1.345.4581-2.161.4581h-5.886v-7.3492.0099Zm10.138 18.134c-.378.5676-.916 1.0058-1.603 1.3145-.697.3087-1.484.4581-2.39.4581h-6.145v-7.6878h6.145c.886 0 1.673.1693 2.37.4979.687.3286 1.235.7867 1.613 1.3842.378.5975.578 1.2747.578 2.0414 0 .7668-.19 1.4241-.568 1.9917Zm29.596-5.3077c1.663-.7967 2.947-1.922 3.864-3.3659.916-1.444 1.374-3.117 1.374-5.0289 0-1.912-.448-3.5253-1.344-4.9592-.897-1.434-2.171-2.5394-3.814-3.3261-1.644-.7867-3.546-1.1751-5.717-1.1751h-13.124v29.9743h6.642V40.0779h4.322l6.084 10.9142h7.578l-6.851-11.7208c.339-.1195.677-.249.996-.3983h-.01Zm-2.151-6.1244c-.369.6274-.896 1.1154-1.583 1.444-.688.3386-1.494.5079-2.42.5079h-5.975v-8.2953h5.975c.926 0 1.732.1693 2.42.4979.687.3287 1.214.8166 1.583 1.434.368.6174.558 1.3544.558 2.1908 0 .8365-.19 1.5734-.558 2.2008v.0199Zm20.594-11.7308-10.706 29.9743h6.742l2.121-6.6122h11.114l2.27 6.6122h6.612L220.99 21.0178h-7.189Zm-.339 18.3431 3.445-10.5756.409-1.922.408 1.922 3.685 10.5756h-7.947Zm20.693 11.6312h6.851V21.0178h-6.851v29.9743Zm31.02-9.6993-12.896-20.275h-6.463v29.9743h6.055V30.7172l12.826 20.2749h6.533V21.0178h-6.055v20.275Zm31.528-3.3559c-.647-1.2448-1.564-2.2904-2.729-3.1369-1.165-.8464-2.509-1.4041-4.023-1.6929l-5.098-1.0456c-.797-.1892-1.434-.5178-1.902-.9958-.469-.478-.708-1.0755-.708-1.7825 0-.6473.17-1.205.518-1.683.339-.478.827-.8464 1.444-1.1153.618-.2689 1.335-.3983 2.151-.3983.817 0 1.554.1394 2.181.4182.627.2788 1.115.6672 1.464 1.1751s.528 1.0755.528 1.7228h6.642c-.04-1.7427-.528-3.2863-1.444-4.6207-.916-1.3443-2.201-2.3899-3.834-3.1468-1.633-.7568-3.505-1.1352-5.597-1.1352-2.091 0-3.943.3884-5.566 1.1751-1.623.7867-2.898 1.8721-3.804 3.2663-.906 1.3941-1.364 2.9775-1.364 4.76 0 1.444.288 2.7485.876 3.9036.587 1.1652 1.414 2.1311 2.479 2.8979 1.076.7668 2.311 1.3045 3.725 1.6033l5.397 1.1153c.886.2091 1.584.5975 2.101 1.1551.518.5577.767 1.2448.767 2.0813 0 .6672-.189 1.2747-.567 1.8025-.379.5277-.907.936-1.584 1.2248-.677.2888-1.474.4282-2.39.4282-.916 0-1.782-.1593-2.529-.478-.747-.3186-1.325-.7767-1.733-1.3742-.418-.5875-.617-1.2747-.617-2.0414h-6.642c.029 1.8721.527 3.5152 1.513 4.9492.976 1.424 2.32 2.5394 4.033 3.336 1.713.7967 3.675 1.195 5.886 1.195 2.21 0 4.202-.4083 5.915-1.2249 1.723-.8165 3.057-1.9418 4.023-3.3758.966-1.434 1.444-3.0572 1.444-4.8696 0-1.4838-.329-2.848-.976-4.1028l.02.01Z"/>
<path fill="url(#a)" d="M20.34 3.66 3.66 20.34C1.32 22.68 0 25.86 0 29.18V59c0 2.76 2.24 5 5 5h29.82c3.32 0 6.49-1.32 8.84-3.66l16.68-16.68c2.34-2.34 3.66-5.52 3.66-8.84V5c0-2.76-2.24-5-5-5H29.18c-3.32 0-6.49 1.32-8.84 3.66Z"/>
<path fill="#000" d="M48 16H8v40h40V16Z"/>
<path fill="#fff" d="M30 47H13v4h17v-4Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -10,7 +10,7 @@
</parent>
<groupId>com.xuxd</groupId>
<artifactId>kafka-console-ui</artifactId>
<version>1.1.0</version>
<version>1.0.13</version>
<name>kafka-console-ui</name>
<description>Kafka console manage ui</description>
<properties>

View File

@@ -0,0 +1,30 @@
package com.xuxd.kafka.console.beans;
import lombok.Data;
/**
* 消息转发请求参数.
*
* @author: xuxd
* @since: 2025/6/5 16:52
**/
@Data
public class ForwardMessage {
private SendMessage message;
/**
* 是否还发到同一个分区.
*/
private boolean samePartition;
/**
* 目标集群id.
*/
private long targetClusterId;
/**
* 目标topic.
*/
private String targetTopic;
}

View File

@@ -21,4 +21,6 @@ public class BrokerApiVersionVO {
private int unSupportNums;
private List<String> versionInfo;
private String brokerVersion;
}

View File

@@ -2,6 +2,7 @@ package com.xuxd.kafka.console.controller;
import com.xuxd.kafka.console.aspect.annotation.ControllerLog;
import com.xuxd.kafka.console.aspect.annotation.Permission;
import com.xuxd.kafka.console.beans.ForwardMessage;
import com.xuxd.kafka.console.beans.QueryMessage;
import com.xuxd.kafka.console.beans.ResponseData;
import com.xuxd.kafka.console.beans.SendMessage;
@@ -83,4 +84,11 @@ public class MessageController {
}
return messageService.sendStatisticsByTime(dto);
}
@Permission("message:forward")
@ControllerLog("消息转发")
@PostMapping("/forward")
public Object forward(@RequestBody ForwardMessage message) {
return messageService.forward(message);
}
}

View File

@@ -49,6 +49,10 @@ public class ContextSetFilter implements Filter {
String uri = request.getRequestURI();
if (!excludes.contains(uri)) {
String headerId = request.getHeader(Header.ID);
String specificId = request.getHeader(Header.SPECIFIC_ID);
if (StringUtils.isNotBlank(specificId)) {
headerId = specificId;
}
if (StringUtils.isBlank(headerId)) {
// ResponseData failed = ResponseData.create().failed("Cluster info is null.");
ResponseData failed = ResponseData.create().failed("没有集群信息,请先切换集群");
@@ -84,5 +88,6 @@ public class ContextSetFilter implements Filter {
interface Header {
String ID = "X-Cluster-Info-Id";
String NAME = "X-Cluster-Info-Name";
String SPECIFIC_ID = "X-Specific-Cluster-Info-Id";
}
}

View File

@@ -1,5 +1,6 @@
package com.xuxd.kafka.console.service;
import com.xuxd.kafka.console.beans.ForwardMessage;
import com.xuxd.kafka.console.beans.QueryMessage;
import com.xuxd.kafka.console.beans.dto.QuerySendStatisticsDTO;
import com.xuxd.kafka.console.beans.ResponseData;
@@ -32,4 +33,6 @@ public interface MessageService {
ResponseData delete(List<QueryMessage> messages);
ResponseData sendStatisticsByTime(QuerySendStatisticsDTO request);
ResponseData forward(ForwardMessage message);
}

View File

@@ -14,6 +14,7 @@ import com.xuxd.kafka.console.dao.ClusterInfoMapper;
import com.xuxd.kafka.console.dao.ClusterRoleRelationMapper;
import com.xuxd.kafka.console.filter.CredentialsContext;
import com.xuxd.kafka.console.service.ClusterService;
import kafka.console.BrokerVersion;
import kafka.console.ClusterConsole;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
@@ -188,6 +189,9 @@ public class ClusterServiceImpl implements ClusterService {
versionInfo = versionInfo.substring(1, versionInfo.length() - 2);
vo.setVersionInfo(Arrays.asList(StringUtils.split(versionInfo, ",")));
list.add(vo);
// 推测broker版本
String vs = BrokerVersion.guessBrokerVersion(versions);
vo.setBrokerVersion(vs);
}));
Collections.sort(list, Comparator.comparingInt(BrokerApiVersionVO::getBrokerId));
return ResponseData.create().data(list).success();

View File

@@ -1,14 +1,15 @@
package com.xuxd.kafka.console.service.impl;
import com.xuxd.kafka.console.beans.MessageFilter;
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.*;
import com.xuxd.kafka.console.beans.dos.ClusterInfoDO;
import com.xuxd.kafka.console.beans.dto.QuerySendStatisticsDTO;
import com.xuxd.kafka.console.beans.enums.FilterType;
import com.xuxd.kafka.console.beans.vo.ConsumerRecordVO;
import com.xuxd.kafka.console.beans.vo.MessageDetailVO;
import com.xuxd.kafka.console.beans.vo.QuerySendStatisticsVO;
import com.xuxd.kafka.console.config.ContextConfig;
import com.xuxd.kafka.console.config.ContextConfigHolder;
import com.xuxd.kafka.console.dao.ClusterInfoMapper;
import com.xuxd.kafka.console.service.ConsumerService;
import com.xuxd.kafka.console.service.MessageService;
import kafka.console.ConsumerConsole;
@@ -52,6 +53,9 @@ public class MessageServiceImpl implements MessageService, ApplicationContextAwa
@Autowired
private ConsumerConsole consumerConsole;
@Autowired
private ClusterInfoMapper clusterInfoMapper;
private ApplicationContext applicationContext;
private Map<String, Deserializer> deserializerDict = new HashMap<>();
@@ -304,6 +308,47 @@ public class MessageServiceImpl implements MessageService, ApplicationContextAwa
return ResponseData.create().data(vo).success();
}
@Override
public ResponseData forward(ForwardMessage message) {
ClusterInfoDO clusterInfoDO = clusterInfoMapper.selectById(message.getTargetClusterId());
if (clusterInfoDO == null) {
return ResponseData.create().failed("Target cluster not found.");
}
SendMessage sendMessage = message.getMessage();
// first, search message detail
Map<TopicPartition, Object> offsetTable = new HashMap<>();
TopicPartition topicPartition = new TopicPartition(sendMessage.getTopic(), sendMessage.getPartition());
offsetTable.put(topicPartition, sendMessage.getOffset());
Map<TopicPartition, ConsumerRecord<byte[], byte[]>> recordMap = messageConsole.searchBy(offsetTable);
ConsumerRecord<byte[], byte[]> consumerRecord = recordMap.get(topicPartition);
if (consumerRecord == null) {
return ResponseData.create().failed("Source message not found.");
}
String topic = message.getTargetTopic();
if (StringUtils.isEmpty(topic)) {
topic = sendMessage.getTopic();
}
// copy from consumer record.
ProducerRecord<byte[], byte[]> record = new ProducerRecord<>(topic,
message.isSamePartition() ? consumerRecord.partition() : null,
consumerRecord.key(),
consumerRecord.value(),
consumerRecord.headers());
ContextConfig config = ContextConfigHolder.CONTEXT_CONFIG.get();
config.setClusterInfoId(clusterInfoDO.getId());
config.setClusterName(clusterInfoDO.getClusterName());
config.setBootstrapServer(clusterInfoDO.getAddress());
// send.
Tuple2<Object, String> tuple2 = messageConsole.sendSync(record);
boolean success = (boolean) tuple2._1;
if (!success) {
return ResponseData.create().failed(tuple2._2);
}
return ResponseData.create().success();
}
private Map<TopicPartition, ConsumerRecord<byte[], byte[]>> searchRecordByOffset(QueryMessage queryMessage) {
Set<TopicPartition> partitions = getPartitions(queryMessage);

View File

@@ -15,7 +15,7 @@ kafka:
# 缓存连接,不缓存的情况下,每次请求建立连接. 即使每次请求建立连接其实也很快某些情况下开启ACL查询可能很慢可以设置连接缓存为true
# 或者想提高查询速度也可以设置下面连接缓存为true
# 缓存 admin client的连接
cache-admin-connection: false
cache-admin-connection: true
# 缓存 producer的连接
cache-producer-connection: false
# 缓存 consumer的连接

View File

@@ -43,6 +43,7 @@ insert into t_sys_permission(id, name,type,parent_id,permission) values(65,'在
insert into t_sys_permission(id, name,type,parent_id,permission) values(66,'消息详情',1,61,'message:detail');
insert into t_sys_permission(id, name,type,parent_id,permission) values(67,'重新发送',1,61,'message:resend');
insert into t_sys_permission(id, name,type,parent_id,permission) values(68,'发送统计',1,61,'message:send-statistics');
insert into t_sys_permission(id, name,type,parent_id,permission) values(69,'消息转发',1,61,'message:forward');
insert into t_sys_permission(id, name,type,parent_id,permission) values(80,'限流',0,null,'quota');
insert into t_sys_permission(id, name,type,parent_id,permission) values(81,'用户',1,80,'quota:user');
@@ -102,8 +103,8 @@ insert into t_sys_permission(id, name,type,parent_id,permission) values(171,'取
-- t_sys_permission end--
-- t_sys_role start--
insert into t_sys_role(id, role_name, description, permission_ids) VALUES (1,'超级管理员','超级管理员','12,13,14,22,23,24,25,26,27,28,29,30,34,35,31,32,33,42,43,44,45,46,47,48,49,50,62,63,64,65,66,67,68,81,82,83,84,85,86,87,88,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,141,142,143,144,145,146,147,148,149,150,151,152,153,161,162,163,164,165,166,167,168,169,171,170');
insert into t_sys_role(id, role_name, description, permission_ids) VALUES (2,'普通管理员','普通管理员,不能更改用户信息','12,13,14,22,23,24,25,26,27,28,29,30,34,35,31,32,33,42,43,44,45,46,47,48,49,50,62,63,64,65,66,67,68,81,82,83,84,85,86,87,88,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,141,146,149,150,161,162,163,164,165,166,167,168,169,171,170');
insert into t_sys_role(id, role_name, description, permission_ids) VALUES (1,'超级管理员','超级管理员','12,13,14,22,23,24,25,26,27,28,29,30,34,35,31,32,33,42,43,44,45,46,47,48,49,50,62,63,64,65,66,67,68,69,81,82,83,84,85,86,87,88,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,141,142,143,144,145,146,147,148,149,150,151,152,153,161,162,163,164,165,166,167,168,169,171,170');
insert into t_sys_role(id, role_name, description, permission_ids) VALUES (2,'普通管理员','普通管理员,不能更改用户信息','12,13,14,22,23,24,25,26,27,28,29,30,34,35,31,32,33,42,43,44,45,46,47,48,49,50,62,63,64,65,66,67,68,69,81,82,83,84,85,86,87,88,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,141,146,149,150,161,162,163,164,165,166,167,168,169,171,170');
-- insert into t_sys_role(id, role_name, description, permission_ids) VALUES (2,'访客','访客','12,13,22,26,29,32,44,45,50,62,63,81,83,85,141,146,149,150,161,163');
-- t_sys_role end--

View File

@@ -0,0 +1,101 @@
package kafka.console
import org.apache.kafka.clients.NodeApiVersions
import org.apache.kafka.common.message.ApiVersionsResponseData.ApiVersion
import java.util.Collections
import scala.jdk.CollectionConverters.SeqHasAsJava
import scala.reflect.runtime.universe._
import scala.reflect.runtime.{universe => ru}
/**
* broker version with api version.
*
* @author: xuxd
* @since: 2024/8/29 16:12
* */
class BrokerVersion(val apiVersion: Int, val brokerVersion: String)
object BrokerVersion {
val define: List[BrokerVersion] = List(
new BrokerVersion(33, "0.11"),
new BrokerVersion(37, "1.0"),
new BrokerVersion(42, "1.1"),
new BrokerVersion(43, "2.2"),
new BrokerVersion(44, "2.3"),
new BrokerVersion(47, "2.4~2.5"),
new BrokerVersion(49, "2.6"),
new BrokerVersion(57, "2.7"),
new BrokerVersion(64, "2.8"),
new BrokerVersion(67, "3.0~3.4"),
new BrokerVersion(68, "3.5~3.6"),
new BrokerVersion(74, "3.7"),
new BrokerVersion(75, "3.8")
)
def getDefineWithJavaList(): java.util.List[BrokerVersion] = {
define.toBuffer.asJava
}
def guessBrokerVersion(nodeVersion: NodeApiVersions): String = {
// if (nodeVersion.)
val unknown = "unknown";
var guessVersion = unknown
var maxApiKey: Short = -1
if (nodeVersion.toString().contains("UNKNOWN")) {
val unknownApis = getFieldValueByName(nodeVersion, "unknownApis")
unknownApis match {
case Some(unknownApis: java.util.List[ApiVersion]) => {
if (unknownApis.size > 0) {
maxApiKey = unknownApis.get(unknownApis.size() - 1).apiKey()
}
}
case _ => -1
}
}
if (maxApiKey < 0) {
val versions = new java.util.ArrayList[ApiVersion](nodeVersion.allSupportedApiVersions().values())
Collections.sort(versions, (o1: ApiVersion, o2: ApiVersion) => o2.apiKey() - o1.apiKey)
maxApiKey = versions.get(0).apiKey()
}
if (maxApiKey > 0) {
if (maxApiKey > define.last.apiVersion) {
guessVersion = "> " + define.last.brokerVersion
} else if (maxApiKey < define.head.apiVersion) {
guessVersion = "< " + define.head.brokerVersion
} else {
for (i <- define.indices) {
if (maxApiKey <= define(i).apiVersion && guessVersion == unknown) {
guessVersion = define(i).brokerVersion
}
}
}
}
guessVersion
}
def getFieldValueByName(obj: Object, fieldName: String): Option[Any] = {
val runtimeMirror = ru.runtimeMirror(obj.getClass.getClassLoader)
val instanceMirror = runtimeMirror.reflect(obj)
val typeOfObj = instanceMirror.symbol.toType
// 查找名为 fieldName 的字段
val fieldSymbol = typeOfObj.member(newTermName(fieldName)).asTerm
// 检查字段是否存在并且不是私有字段
if (fieldSymbol.isPrivate || fieldSymbol.isPrivateThis) {
// None // 如果字段是私有的,返回 None
val fieldMirror = runtimeMirror.reflect(obj).reflectField(fieldSymbol)
Some(fieldMirror.get)
} else {
// 反射获取字段值
val fieldMirror = instanceMirror.reflectField(fieldSymbol)
Some(fieldMirror.get)
}
}
}

View File

@@ -15,7 +15,7 @@ import scala.collection.{Map, Seq}
import scala.jdk.CollectionConverters.{CollectionHasAsScala, MapHasAsJava, MapHasAsScala, SeqHasAsJava, SetHasAsJava}
/**
* kafka-console-ui.
* kafka topic console.
*
* @author xuxd
* @date 2021-09-08 19:52:27
@@ -52,6 +52,12 @@ class TopicConsole(config: KafkaConfig) extends KafkaConsole(config: KafkaConfig
}).asInstanceOf[Set[String]]
}
/**
* get topic list by topic name list.
*
* @param topics topic name list.
* @return topic list.
*/
def getTopicList(topics: Set[String]): List[TopicDescription] = {
if (topics == null || topics.isEmpty) {
Collections.emptyList()

View File

@@ -17,3 +17,8 @@ new Vue({
store,
render: (h) => h(App),
}).$mount("#app");
Vue.prototype.$message.config({
duration: 1,
maxCount: 1,
});

View File

@@ -300,6 +300,10 @@ export const KafkaMessageApi = {
url: "/message/send/statistics",
method: "post",
},
forward: {
url: "/message/forward",
method: "post",
},
};
export const KafkaClientQuotaApi = {

View File

@@ -6,6 +6,10 @@
<p></p>
<hr />
<h3>kafka API 版本兼容性</h3>
<div class="green">
broker版本说明:
该值并不保证broker实际版本一定是该值(大概是这个版本范围)broker使用不同的模式(如kraft)可能显示不同的值
</div>
<a-spin :spinning="apiVersionInfoLoading">
<a-table
:columns="columns"
@@ -104,6 +108,11 @@ const columns = [
dataIndex: "host",
key: "host",
},
{
title: "broker版本",
dataIndex: "brokerVersion",
key: "brokerVersion",
},
{
title: "支持的api数量",
dataIndex: "supportNums",
@@ -125,4 +134,7 @@ const columns = [
.card-style {
width: 100%;
}
.green {
color: green;
}
</style>

View File

@@ -6,7 +6,7 @@
:mask="false"
:destroyOnClose="true"
:footer="null"
:maskClosable="false"
:maskClosable="true"
@cancel="handleCancel"
>
<div>

View File

@@ -6,7 +6,7 @@
:mask="false"
:destroyOnClose="true"
:footer="null"
:maskClosable="false"
:maskClosable="true"
@cancel="handleCancel"
>
<div>

View File

@@ -58,6 +58,22 @@
</a-form-item>
</a-col>
</a-row>
<hr class="hr" />
<a-row :gutter="24">
<a-col :span="24">
<a-form-item label="过滤消费组">
<a-input
placeholder="groupId 模糊过滤"
class="input-w"
v-decorator="['filterGroupId']"
@change="onFilterGroupIdUpdate"
/>
<span>
仅过滤当前已查出来的消费组如果要查询服务端最新消费组请点击查询按钮</span
>
</a-form-item>
</a-col>
</a-row>
</a-form>
</div>
<div class="operation-row-button">
@@ -70,7 +86,7 @@
</div>
<a-table
:columns="columns"
:data-source="data"
:data-source="filteredData"
bordered
row-key="groupId"
>
@@ -169,6 +185,7 @@ export default {
return {
queryParam: {},
data: [],
filteredData: [],
columns,
selectRow: {},
form: this.$form.createForm(this, {
@@ -186,6 +203,7 @@ export default {
showConsumerDetailDialog: false,
showAddSubscriptionDialog: false,
showOffsetPartitionDialog: false,
filterGroupId: "",
};
},
methods: {
@@ -209,6 +227,7 @@ export default {
this.loading = false;
if (res.code == 0) {
this.data = res.data.list;
this.filter();
} else {
notification.error({
message: "error",
@@ -268,6 +287,19 @@ export default {
closeOffsetPartitionDialog() {
this.showOffsetPartitionDialog = false;
},
onFilterGroupIdUpdate(input) {
this.filterGroupId = input.target.value;
this.filter();
},
filter() {
if (this.filterGroupId) {
this.filteredData = this.data.filter(
(e) => e.groupId.indexOf(this.filterGroupId) != -1
);
} else {
this.filteredData = this.data;
}
},
},
created() {
this.getConsumerGroupList();
@@ -372,4 +404,9 @@ const columns = [
.type-select {
width: 200px !important;
}
.hr {
height: 1px;
border: none;
border-top: 1px dashed #0066cc;
}
</style>

View File

@@ -0,0 +1,248 @@
<template>
<a-modal
title="转发消息"
:visible="show"
:width="600"
:mask="false"
:destroyOnClose="true"
:footer="null"
:maskClosable="true"
@cancel="handleCancel"
>
<div>
<a-spin :spinning="loading">
<div>
<h4>选择集群</h4>
<hr />
<div class="message-detail" id="message-detail">
<a-form
:form="form"
:label-col="{ span: 5 }"
:wrapper-col="{ span: 18 }"
@submit="handleSubmit"
>
<a-form-item label="集群">
<a-select
class="select-width"
@change="clusterChange"
v-decorator="[
'targetClusterId',
{
rules: [{ required: true, message: '请选择一个集群!' }],
},
]"
placeholder="请选择一个集群"
>
<a-select-option
v-for="v in clusterList"
:key="v.id"
:value="v.id"
>
{{ v.clusterName }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Topic">
<a-select
class="select-width"
show-search
option-filter-prop="children"
v-decorator="[
'targetTopic',
{
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-radio-group
v-decorator="[
'samePartition',
{
initialValue: 'false',
rules: [{ required: true, message: '请选择!' }],
},
]"
>
<a-radio value="false"> 否</a-radio>
<a-radio value="true"> 是</a-radio>
</a-radio-group>
<span class="mar-left">和原消息保持同一个分区</span>
</a-form-item>
<a-form-item>
<div class="form-footer">
<a-button type="primary" html-type="submit"> 提交</a-button>
</div>
</a-form-item>
</a-form>
</div>
</div>
</a-spin>
</div>
</a-modal>
</template>
<script>
import request from "@/utils/request";
import { KafkaClusterApi, KafkaMessageApi, KafkaTopicApi } from "@/utils/api";
import notification from "ant-design-vue/lib/notification";
import moment from "moment";
export default {
name: "ForwardMessage",
props: {
record: {},
visible: {
type: Boolean,
default: false,
},
},
data() {
return {
show: this.visible,
data: {},
loading: false,
showForwardDialog: false,
targetClusterId: -1,
clusterList: [],
partition: -1,
topicList: [],
form: this.$form.createForm(this, { name: "ForwardMessageForm" }),
};
},
watch: {
visible(v) {
this.show = v;
if (this.show) {
this.getClusterList();
}
},
},
methods: {
getClusterList() {
this.loading = true;
request({
url: KafkaClusterApi.getClusterInfoList.url,
method: KafkaClusterApi.getClusterInfoList.method,
}).then((res) => {
this.loading = false;
if (res.code != 0) {
notification.error({
message: "error",
description: res.msg,
});
} else {
this.clusterList = res.data;
this.targetClusterId = this.clusterList[0].id;
}
});
},
handleSubmit(e) {
e.preventDefault();
this.form.validateFields((err, values) => {
if (!err) {
const params = {
message: Object.assign({}, this.record),
};
this.forward({ ...params, ...values });
}
});
},
handleCancel() {
this.$emit("closeForwardDialog", { refresh: false });
},
formatTime(time) {
return time == -1 ? -1 : moment(time).format("YYYY-MM-DD HH:mm:ss:SSS");
},
clusterChange(e) {
this.getTopicNameList(e);
},
forward(params) {
this.loading = true;
request({
url: KafkaMessageApi.forward.url,
method: KafkaMessageApi.forward.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);
}
});
},
openForwardDialog() {
this.showForwardDialog = true;
},
closeForwardDialog() {
this.showForwardDialog = false;
},
getTopicNameList(clusterInfoId) {
this.loading = true;
request({
url: KafkaTopicApi.getTopicNameList.url,
method: KafkaTopicApi.getTopicNameList.method,
headers: {
"X-Specific-Cluster-Info-Id": clusterInfoId,
},
}).then((res) => {
this.loading = false;
if (res.code == 0) {
this.topicList = res.data;
} else {
notification.error({
message: "error",
description: res.msg,
});
}
});
},
},
};
</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;
}
.center {
text-align: center;
}
.mar-left {
margin-left: 1%;
}
.select-width {
width: 80%;
}
.form-footer {
text-align: center;
margin-top: 3%;
}
</style>

View File

@@ -6,7 +6,7 @@
:mask="false"
:destroyOnClose="true"
:footer="null"
:maskClosable="false"
:maskClosable="true"
@cancel="handleCancel"
>
<div>
@@ -120,8 +120,22 @@
重新发送
</a-button>
</a-popconfirm>
<a-button
type="dashed"
class="mar-left"
icon="plus"
v-action:message:forward
@click="openForwardDialog()"
>
转发消息
</a-button>
</div>
</a-spin>
<ForwardMessage
:visible="showForwardDialog"
:record="data"
@closeForwardDialog="closeForwardDialog"
></ForwardMessage>
</div>
</a-modal>
</template>
@@ -131,9 +145,11 @@ import request from "@/utils/request";
import { KafkaMessageApi } from "@/utils/api";
import notification from "ant-design-vue/lib/notification";
import moment from "moment";
import ForwardMessage from "@/views/message/ForwardMessage.vue";
export default {
name: "MessageDetail",
components: { ForwardMessage },
props: {
record: {},
visible: {
@@ -151,6 +167,7 @@ export default {
valueDeserializer: "String",
consumerDetail: [],
columns,
showForwardDialog: false,
};
},
watch: {
@@ -232,6 +249,12 @@ export default {
}
});
},
openForwardDialog() {
this.showForwardDialog = true;
},
closeForwardDialog() {
this.showForwardDialog = false;
},
},
};
const columns = [
@@ -264,4 +287,7 @@ const columns = [
max-width: 80% !important;
vertical-align: top !important;
}
.mar-left {
margin-left: 1%;
}
</style>

View File

@@ -6,7 +6,7 @@
:mask="false"
:destroyOnClose="true"
:footer="null"
:maskClosable="false"
:maskClosable="true"
@cancel="handleCancel"
>
<div>

View File

@@ -14,6 +14,15 @@
<div v-for="(v, k) in data" :key="k">
<strong>消费组: </strong><span class="color-font">{{ k }}</span
><strong> | 积压: </strong><span class="color-font">{{ v.lag }}</span>
<a-button
type="primary"
icon="reload"
size="small"
style="float: right"
@click="getConsumerDetail"
>
刷新
</a-button>
<hr />
<a-table
:columns="columns"

View File

@@ -6,7 +6,7 @@
:mask="false"
:destroyOnClose="true"
:footer="null"
:maskClosable="false"
:maskClosable="true"
@cancel="handleCancel"
>
<div>

View File

@@ -11,7 +11,18 @@
>
<div>
<a-spin :spinning="loading">
<h4>今天发送消息数{{ today.total }}</h4>
<h4>
今天发送消息数{{ today.total
}}<a-button
type="primary"
icon="reload"
size="small"
style="float: right"
@click="sendStatus"
>
刷新
</a-button>
</h4>
<a-table
:columns="columns"
:data-source="today.detail"

View File

@@ -6,7 +6,7 @@
:mask="false"
:destroyOnClose="true"
:footer="null"
:maskClosable="false"
:maskClosable="true"
@cancel="handleCancel"
>
<div>

View File

@@ -5,7 +5,7 @@
:width="1200"
:mask="false"
:destroyOnClose="true"
:maskClosable="false"
:maskClosable="true"
@cancel="handleCancel"
okText="确认"
cancelText="取消"