Compare commits

...

166 Commits

Author SHA1 Message Date
EricZeng
bf979fa3b3 Merge pull request #252 from didi/dev_2.4.0
Dev 2.4.0
2021-04-26 09:55:30 +08:00
EricZeng
b3b88891e9 Merge pull request #251 from lucasun/master
v2.4.0
2021-04-25 21:01:33 +08:00
lucasun
01c5de60dc Merge branch 'master' into master 2021-04-25 20:54:10 +08:00
孙超
47b8fe5022 V2.4.1 FE 2021-04-25 20:43:20 +08:00
zengqiao
324b37b875 v2.4.0 be code 2021-04-25 18:11:52 +08:00
zengqiao
76e7e192d8 bump version to 2.4.0 2021-04-25 17:40:47 +08:00
EricZeng
f9f3c4d923 Merge pull request #240 from yangvipguang/docker-dev
Docker容器镜像优化
2021-04-25 17:23:36 +08:00
杨光
a476476bd1 Update Dockerfile
添加进程管理器tini 防止僵尸应用
升级基础镜像到Java 16 alpine 
默认使用官方jar 包
默认开启JMX 监控
2021-04-23 14:10:40 +08:00
杨光
82a60a884a Add files via upload 2021-04-23 14:06:55 +08:00
杨光
f17727de18 Merge pull request #1 from didi/master
同步提交
2021-04-23 11:31:35 +08:00
EricZeng
f98446e139 Merge pull request #239 from Liu-XinYuan/i238
fix  create topic failed when not specify peak_byte_in
2021-04-22 19:02:48 +08:00
Liu-XinYuan
57a48dadaa modify from obj ==null to ValidateUtils.isNull 2021-04-22 18:44:34 +08:00
Liu-XinYuan
c65ec68e46 fix create topic failed when not specify peak_byte_in 2021-04-22 18:30:23 +08:00
zengqiao
d6559be3fc 部分后台任务获取Topic列表时不走缓存 2021-04-22 16:06:37 +08:00
zengqiao
59df5b24fe broker元信息中增加Rack信息 2021-04-20 19:28:36 +08:00
zengqiao
3e1544294b 删除无效代码 2021-04-20 17:22:26 +08:00
EricZeng
a12c398816 Merge pull request #232 from didi/dev
应用下线功能权限列表获取优化
2021-04-20 13:54:49 +08:00
EricZeng
0bd3e28348 Merge pull request #228 from PengShuaixin/dev
应用下线审批功能优化
2021-04-20 13:51:32 +08:00
PengShuaixin
ad4e39c088 应用下线功能权限列表获取优化 2021-04-20 11:22:11 +08:00
PengShuaixin
2668d96e6a Merge remote-tracking branch 'origin/dev' into dev
# Conflicts:
#	kafka-manager-extends/kafka-manager-bpm/src/main/java/com/xiaojukeji/kafka/manager/bpm/order/impl/DeleteAppOrder.java
2021-04-20 11:19:49 +08:00
shirenchuang
357c496aad 下线应用的时候 判断先下线topic 2021-04-20 11:18:07 +08:00
shirenchuang
22a513ba22 升级mysql驱动;支持Mysql 8.0+ 2021-04-20 11:18:07 +08:00
zengqiao
e6dd1119be 通过获取类的RequestMapping注解来判断当前请求是否需要登录 2021-04-20 11:18:07 +08:00
EricZeng
2dbe454e04 Merge pull request #231 from didi/master
merge master
2021-04-20 10:46:03 +08:00
zengqiao
e3a59b76eb 修复数据删空之后, 缓存不能被更新的BUG 2021-04-19 20:31:40 +08:00
zengqiao
b67a162d3f bump version to v2.3.1 2021-04-19 14:13:48 +08:00
shirenchuang
8bfde9fbaf Merge branch 'shirc_dev' into dev 2021-04-19 10:19:11 +08:00
shirenchuang
1fdecf8def 下线应用的时候 判断先下线topic 2021-04-19 10:17:29 +08:00
zengqiao
1141d4b833 通过获取类的RequestMapping注解来判断当前请求是否有权限 2021-04-15 18:12:21 +08:00
EricZeng
cdac92ca7b Merge pull request #229 from didi/dev
通过获取类的RequestMapping注解来判断当前请求是否需要登录
2021-04-14 19:47:43 +08:00
zengqiao
2a57c260cc 通过获取类的RequestMapping注解来判断当前请求是否需要登录 2021-04-14 19:40:19 +08:00
PengShuaixin
f41e29ab3a 应用下线功能优化 2021-04-14 12:29:59 +08:00
zengqiao
8f10624073 add jmx prometheus jar 2021-04-12 17:58:24 +08:00
EricZeng
eb1f8be11e Merge pull request #224 from didi/master
merge master
2021-04-12 13:51:14 +08:00
EricZeng
3333501ab9 Merge pull request #222 from zwOvO/master
删除无用import、删除无用代码
2021-04-09 19:28:04 +08:00
zwOvO
0f40820315 删除无用import、删除无用代码 2021-04-09 11:41:06 +08:00
shirenchuang
5f1a839620 升级mysql驱动;支持Mysql 8.0+ 2021-04-06 12:09:52 +08:00
zengqiao
b9bb1c775d change uri filter rule 2021-04-06 10:26:21 +08:00
zengqiao
1059b7376b forbiden request when uri contain .. 2021-04-06 10:01:29 +08:00
EricZeng
f38ab4a9ce Merge pull request #217 from didi/dev
拒绝包含./或/连续过多的接口请求
2021-03-31 20:00:52 +08:00
zengqiao
9e7450c012 拒绝包含./或/连续过多的接口请求 2021-03-31 19:45:18 +08:00
EricZeng
99a3e360fe Merge pull request #216 from didi/dev
接口过滤策略由接口黑名单转成接口白名单
2021-03-30 12:56:19 +08:00
lucasun
d45f8f78d6 Merge pull request #215 from zhangfenhua/master
增加nginx配置:前后端分离&配置多个静态资源
2021-03-30 11:11:58 +08:00
zengqiao
648af61116 接口过滤策略由接口黑名单转成接口白名单 2021-03-29 21:21:23 +08:00
zhangfenhua
eebf1b89b1 nginx配置手册 2021-03-29 11:53:50 +08:00
EricZeng
f8094bb624 Merge pull request #211 from didi/dev
add expert config desc
2021-03-23 15:23:10 +08:00
zengqiao
ed13e0d2c2 add expert config desc 2021-03-23 15:21:48 +08:00
EricZeng
aa830589b4 Merge pull request #210 from didi/dev
fix monitor enable time illegal bug
2021-03-22 17:22:44 +08:00
zengqiao
999a2bd929 fix monitor enable time illegal bug 2021-03-22 17:21:12 +08:00
EricZeng
d69ee98450 Merge pull request #209 from didi/dev
add faq, kafka version supported & apply logical cluster and how to handle it
2021-03-22 13:43:14 +08:00
zengqiao
f6712c24ad merge master 2021-03-22 13:42:09 +08:00
zengqiao
89d2772194 add faq, kafka version supported & apply logical cluster and how to handle it 2021-03-22 13:38:23 +08:00
mike.zhangliang
03352142b6 Update README.md
微信加群方式补充
2021-03-16 14:46:38 +08:00
lucasun
73a51e0c00 Merge pull request #205 from ZQKC/master
add qa
2021-03-10 19:27:01 +08:00
zengqiao
2e26f8caa6 add qa 2021-03-10 19:23:29 +08:00
EricZeng
f9bcce9e43 Merge pull request #3 from didi/master
merge didi Logi-KM
2021-03-10 19:20:39 +08:00
EricZeng
2ecc877ba8 fix add_cluster.md path
fix add_cluster.md path
2021-03-10 15:45:48 +08:00
EricZeng
3f8a3c69e3 Merge pull request #201 from ZQKC/master
optimize ldap
2021-03-10 14:12:35 +08:00
zengqiao
67c37a0984 optimize ldap 2021-03-10 13:52:09 +08:00
EricZeng
a58a55d00d Merge pull request #203 from lucasun/hotfix/v2.3.1
clipbord版本锁定在2.0.6,升级2.0.7会引起ts打包报错
2021-03-09 18:11:02 +08:00
孙超
06d51dd0b8 clipbord版本锁定在2.0.6,升级2.0.7会引起ts打包报错 2021-03-09 18:07:42 +08:00
zengqiao
d5db028f57 optimize ldap 2021-03-09 15:13:55 +08:00
EricZeng
fcb85ff4be Merge pull request #2 from didi/master
merge didi logi-km
2021-03-09 11:07:17 +08:00
EricZeng
3695b4363d Merge pull request #200 from didi/dev
del ResultStatus which in vo
2021-03-09 11:02:46 +08:00
zengqiao
cb11e6437c del ResultStatus in vo 2021-03-09 11:01:21 +08:00
EricZeng
5127bd11ce Merge pull request #198 from didi/master
merge master
2021-03-09 10:42:28 +08:00
EricZeng
91f90aefa1 Merge pull request #195 from fanghanyun/v2.3.0_ldap
support AD LDAP
2021-03-09 10:40:42 +08:00
fanghanyun
0a067bce36 Support AD LDAP 2021-03-09 10:19:08 +08:00
fanghanyun
f0aba433bf Support AD LDAP 2021-03-08 20:31:15 +08:00
EricZeng
f06467a0e3 Merge pull request #197 from didi/dev
delete without used code
2021-03-05 16:12:27 +08:00
zengqiao
68bcd3c710 delete without used code 2021-03-05 16:05:58 +08:00
EricZeng
a645733cc5 Merge pull request #196 from didi/dev
add gateway config docs
2021-03-05 15:31:53 +08:00
zengqiao
49fe5baf94 add gateway config docs 2021-03-05 14:59:40 +08:00
fanghanyun
411ee55653 support AD LDAP 2021-03-05 14:45:54 +08:00
EricZeng
e351ce7411 Merge pull request #194 from didi/dev
reject req when uri contains ..
2021-03-04 17:52:56 +08:00
zengqiao
f33e585a71 reject req when uri contains .. 2021-03-04 17:51:35 +08:00
EricZeng
77f3096e0d Merge pull request #191 from didi/dev
Dev
2021-02-28 22:04:34 +08:00
EricZeng
9a5b18c4e6 Merge pull request #190 from JokerQueue/dev
bug fix:correct way to judge a user does not exist
2021-02-28 14:36:28 +08:00
Joker
0c7112869a bug fix:correct way to judge a user does not exist 2021-02-27 22:35:35 +08:00
EricZeng
f66a4d71ea Merge pull request #188 from JokerQueue/dev
bug fix: unexpected stop of the topic sync task
2021-02-26 22:46:54 +08:00
Joker
9b0ab878df bug fix: unexpected stop of the topic sync task 2021-02-26 19:47:03 +08:00
EricZeng
d30b90dfd0 Merge pull request #186 from ZHAOYINRUI/master
新增releases_notes、更新FAQ
2021-02-26 09:59:18 +08:00
ZHAOYINRUI
efd28f8c27 Update faq.md 2021-02-26 00:03:25 +08:00
ZHAOYINRUI
e05e722387 Add files via upload 2021-02-26 00:01:09 +08:00
EricZeng
748e81956d Update faq.md 2021-02-24 14:10:41 +08:00
EricZeng
c9a41febce Merge pull request #184 from didi/dev
reject illegal zk address
2021-02-23 17:32:20 +08:00
zengqiao
18e244b756 reject illegal zk address 2021-02-23 17:18:49 +08:00
mrazkong
47676139a3 Merge pull request #183 from didi/dev
support dynamic change cluster auth
2021-02-23 16:56:26 +08:00
zengqiao
1ed933b7ad support dynamic change auth 2021-02-23 16:34:21 +08:00
EricZeng
f6a343ccd6 Merge pull request #182 from didi/master
merge master
2021-02-23 15:47:28 +08:00
EricZeng
dd6cdc22e5 Merge pull request #178 from Observe-secretly/v2.2.1_ldap
新功能:增加了对LDAP登录的支持
2021-02-10 12:35:07 +08:00
李民
f70f4348b3 Merge branch 'master' into v2.2.1_ldap 2021-02-10 10:00:32 +08:00
EricZeng
ec7f801929 Merge pull request #180 from didi/dev_2.3.0
Dev 2.3.0
2021-02-09 22:06:51 +08:00
zengqiao
0f8aca382e bump version to 2.3.0 2021-02-09 21:47:56 +08:00
zengqiao
0270f77eaa add upgrade doc 2021-02-09 21:46:55 +08:00
EricZeng
dcba71ada4 Merge pull request #179 from lucasun/dev_2.3.0_fe
迭代V2.5, 修复broker监控问题,增加JMX认证支持等...
2021-02-09 18:48:42 +08:00
孙超
6080f76a9c 迭代V2.5, 修复broker监控问题,增加JMX认证支持等... 2021-02-09 15:26:47 +08:00
李民
e7349161f3 BUG FIX:修改LDAP登录重复注册用户的BUG 2021-02-09 15:22:26 +08:00
李民
2e2907ea09 修改LDAP获取UserDN的时候可能出错的问题 2021-02-09 14:33:53 +08:00
李民
25e84b2a6c 新功能:增加对LDAP的登录的支持 2021-02-09 11:33:54 +08:00
zengqiao
5efd424172 version 2.3.0 2021-02-09 11:20:56 +08:00
EricZeng
2672502c07 Merge pull request #174 from 17hao/issue-153-authority
Tracking delete account
2021-02-07 16:10:56 +08:00
EricZeng
83440cc3d9 Merge pull request #173 from 17hao/issue-153
Tracking changes applied to app
2021-02-07 16:10:01 +08:00
17hao
8e5f93be1c Tracking delete account 2021-02-07 15:54:41 +08:00
17hao
c1afc07955 Tracking changes applied to app 2021-02-07 15:16:26 +08:00
EricZeng
4a83e14878 Merge pull request #172 from 17hao/issue-153
Tracking changes applied to Kafka cluster
2021-02-07 14:38:38 +08:00
17hao
832320abc6 Improve code's cohesion && save jmx properties 2021-02-07 14:20:57 +08:00
17hao
70c237da72 Tracking changes applied to Kafka cluster 2021-02-07 13:23:22 +08:00
EricZeng
edfcc5c023 Merge pull request #169 from 17hao/issue-153
Record topic operation
2021-02-06 22:30:32 +08:00
17hao
0668debec6 Update pom.xml 2021-02-06 18:46:47 +08:00
17hao
02d6463faa Using JsonUtils instead of fastjson 2021-02-06 18:43:36 +08:00
17hao
1fdb85234c Record editting topic 2021-02-05 12:18:50 +08:00
EricZeng
44b7dd1808 Merge pull request #167 from ZHAOYINRUI/master
更新readme、集群接入手册
2021-02-04 19:21:59 +08:00
ZHAOYINRUI
e983ee3101 Update README.md 2021-02-04 19:10:11 +08:00
ZHAOYINRUI
75e7e81c05 Add files via upload 2021-02-04 19:09:02 +08:00
ZHAOYINRUI
31ce3b9c08 Update add_cluster.md 2021-02-04 19:08:28 +08:00
EricZeng
ed93c50fef modify without logical cluster desc 2021-02-04 16:54:42 +08:00
EricZeng
4845660eb5 Merge pull request #163 from 17hao/issue-160
Issue#160: Remove __consumer_offsets from topic list
2021-02-04 16:42:41 +08:00
17hao
c7919210a2 Fix topic list filter condition 2021-02-04 16:32:31 +08:00
17hao
9491418f3b Update if statements 2021-02-04 12:33:32 +08:00
17hao
e8de403286 Hide __transaction_state in topic list && fix logic error 2021-02-04 12:14:44 +08:00
17hao
dfb625377b Using existing topic name constant 2021-02-03 22:30:38 +08:00
EricZeng
2c0f2a8be6 Merge pull request #166 from ZHAOYINRUI/master
更新集群接入文章、资源申请文章
2021-02-03 21:02:38 +08:00
ZHAOYINRUI
787d3cb3e9 Update resource_apply.md 2021-02-03 20:52:44 +08:00
ZHAOYINRUI
96ca17d26c Add files via upload 2021-02-03 19:43:03 +08:00
ZHAOYINRUI
3dd0f7f2c3 Update add_cluster.md 2021-02-03 19:41:33 +08:00
ZHAOYINRUI
10ba0cf976 Update resource_apply.md 2021-02-03 18:18:02 +08:00
ZHAOYINRUI
276c15cc23 Delete docs/user_guide/resource_apply directory 2021-02-03 18:08:15 +08:00
ZHAOYINRUI
2584b848ad Update resource_apply.md 2021-02-03 18:07:34 +08:00
ZHAOYINRUI
6471efed5f Add files via upload 2021-02-03 18:04:40 +08:00
ZHAOYINRUI
5b7d7ad65d Create resource_apply.md 2021-02-03 18:01:42 +08:00
17hao
712851a8a5 Add braces 2021-02-03 16:06:16 +08:00
17hao
63d291cb47 Remove __consumer_offsets from topic list 2021-02-03 15:50:33 +08:00
EricZeng
f825c92111 Merge pull request #159 from didi/dev_2.2.1
storage support s3
2021-02-03 13:49:52 +08:00
EricZeng
419eb2ea41 Merge pull request #158 from didi/dev
change dockerfile and heml location
2021-02-03 10:09:43 +08:00
zengqiao
89b58dd64e storage support s3 2021-02-02 16:42:20 +08:00
zengqiao
6bc5f81440 change dockerfile and heml location 2021-02-02 15:58:46 +08:00
EricZeng
424f4b7b5e Merge pull request #157 from didi/master
merge master
2021-02-02 15:33:51 +08:00
mrazkong
9271a1caac Merge pull request #118 from yangvipguang/helm-dev
增加Dockerfile 和 简单Helm
2021-02-01 10:50:05 +08:00
杨光
0ee4df03f9 Update Dockerfile 2021-01-31 15:34:15 +08:00
杨光
8ac713ce32 Update Dockerfile 2021-01-31 15:30:18 +08:00
杨光
76b2489fe9 Delete docker-depends/agent/config directory 2021-01-31 15:29:50 +08:00
杨光
6786095154 Delete sources.list 2021-01-31 15:29:31 +08:00
杨光
2c5793ef37 Delete settings 2021-01-31 15:29:19 +08:00
杨光
d483f25b96 Add files via upload
add  jmx prometheus
2021-01-31 15:28:59 +08:00
EricZeng
7118368979 Merge pull request #136 from ZHAOYINRUI/patch-10
Create resource_apply.md
2021-01-29 10:54:11 +08:00
EricZeng
59256c2e80 modify jdbc url
modify jdbc url, add useSSL=false
2021-01-29 10:53:35 +08:00
EricZeng
1fb8a0db1e Merge pull request #146 from ZHAOYINRUI/patch-12
Update README.md
2021-01-29 10:03:48 +08:00
ZHAOYINRUI
07d0c8e8fa Update README.md 2021-01-28 22:02:49 +08:00
EricZeng
98452ead17 Merge pull request #145 from didi/dev
faq add how to resolve topic biz data not exist error desc
2021-01-28 16:20:42 +08:00
zengqiao
d8c9f40377 faq add how to resolve topic biz data not exist error desc 2021-01-28 15:50:31 +08:00
EricZeng
8148d5eec6 Merge pull request #144 from didi/dev
optimize result code
2021-01-28 14:11:00 +08:00
zengqiao
4c429ad604 optimize result code 2021-01-28 12:06:06 +08:00
EricZeng
a9c52de8d5 Merge pull request #143 from ZhaoXinlong/patch-1
correcting some typo
2021-01-27 20:28:32 +08:00
ZhaoXinlong
f648aa1f91 correcting some typo
修改文字错误
2021-01-27 16:31:42 +08:00
EricZeng
eaba388bdd Merge pull request #142 from didi/dev
add connect jmx failed desc
2021-01-27 16:06:39 +08:00
zengqiao
73e6afcbc6 add connect jmx failed desc 2021-01-27 16:01:18 +08:00
EricZeng
8c3b72adf2 Merge pull request #139 from didi/dev
optimize message when login failed
2021-01-26 19:57:50 +08:00
zengqiao
ae18ff4262 optimize login failed message 2021-01-26 16:15:08 +08:00
ZHAOYINRUI
1adc8af543 Create resource_apply.md 2021-01-25 19:27:31 +08:00
EricZeng
7413df6f1e Merge pull request #131 from ZHAOYINRUI/patch-9
Update alarm_rules.md
2021-01-25 18:36:06 +08:00
EricZeng
bda8559190 Merge pull request #135 from didi/master
merge master
2021-01-25 18:34:56 +08:00
ZHAOYINRUI
4e0114ab0d Update alarm_rules.md 2021-01-25 13:24:01 +08:00
EricZeng
9aefc55534 Merge pull request #1 from didi/dev
merge didi dev
2021-01-23 11:16:35 +08:00
杨光
a950be2d95 change password 2021-01-20 17:57:14 +08:00
杨光
ba6f5ab984 add helm and dockerfile 2021-01-20 17:49:56 +08:00
211 changed files with 4640 additions and 1277 deletions

View File

@@ -5,66 +5,94 @@
**一站式`Apache Kafka`集群指标监控与运维管控平台** **一站式`Apache Kafka`集群指标监控与运维管控平台**
---
## 主要功能特性
### 快速体验
- 体验地址 http://117.51.146.109:8080 账号密码 admin/admin
### 集群监控维度
- 多版本集群管控,支持从`0.10.2``2.x`版本;
- 集群Topic、Broker等多维度历史与实时关键指标查看
### 集群管控维度 阅读本README文档您可以了解到滴滴Logi-KafkaManager的用户群体、产品定位等信息并通过体验地址快速体验Kafka集群指标监控与运维管控的全流程。<br>若滴滴Logi-KafkaManager已在贵司的生产环境进行使用并想要获得官方更好地支持和指导可以通过[`OCE认证`](http://obsuite.didiyun.com/open/openAuth),加入官方交流平台。
- 集群运维包括逻辑Region方式管理集群
- Broker运维包括优先副本选举
- Topic运维包括创建、查询、扩容、修改属性、数据采样及迁移等
- 消费组运维,包括指定时间或指定偏移两种方式进行重置消费偏移
### 用户使用维度 ## 1 产品简介
滴滴Logi-KafkaManager脱胎于滴滴内部多年的Kafka运营实践经验是面向Kafka用户、Kafka运维人员打造的共享多租户Kafka云平台。专注于Kafka运维管控、监控告警、资源治理等核心场景经历过大规模集群、海量大数据的考验。内部满意度高达90%的同时,还与多家知名企业达成商业化合作。
- Kafka用户、Kafka研发、Kafka运维 视角区分 ### 1.1 快速体验地址
- Kafka用户、Kafka研发、Kafka运维 权限区分 - 体验地址 http://117.51.146.109:8080 账号密码 admin/admin
### 1.2 体验地图
相比较于同类产品的用户视角单一大多为管理员视角滴滴Logi-KafkaManager建立了基于分角色、多场景视角的体验地图。分别是**用户体验地图、运维体验地图、运营体验地图**
#### 1.2.1 用户体验地图
- 平台租户申请&nbsp;&nbsp;申请应用App作为Kafka中的用户名并用 AppID+password作为身份验证
- 集群资源申请&nbsp;&nbsp;:按需申请、按需使用。可使用平台提供的共享集群,也可为应用申请独立的集群
- Topic&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;可根据应用App创建Topic或者申请其他topic的读写权限
- Topic&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Topic数据采样、调整配额、申请分区等操作
-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;基于Topic生产消费各环节耗时统计监控不同分位数性能指标
-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:支持将消费偏移重置至指定时间或指定位置
#### 1.2.2 运维体验地图
- 多版本集群管控&nbsp;&nbsp;:支持从`0.10.2``2.x`版本
-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;集群Topic、Broker等多维度历史与实时关键指标查看建立健康分体系
-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;划分部分Broker作为Region使用Region定义资源划分单位并按照业务、保障能力区分逻辑集群
- Broker&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:包括优先副本选举等操作
- Topic&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:包括创建、查询、扩容、修改属性、迁移、下线等
## kafka-manager架构 #### 1.2.3 运营体验地
-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;沉淀资源治理方法。针对Topic分区热点、分区不足等高频常见问题沉淀资源治理方法实现资源治理专家化
-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;工单体系。Topic创建、调整配额、申请分区等操作由专业运维人员审批规范资源使用保障平台平稳运行
-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;成本控制。Topic资源、集群资源按需申请、按需使用。根据流量核算费用帮助企业建设大数据成本核算体系
### 1.3 核心优势
-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:监控多项核心指标,统计不同分位数据,提供种类丰富的指标监控报表,帮助用户、运维人员快速高效定位问题
- 便&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;按照Region定义集群资源划分单位将逻辑集群根据保障等级划分。在方便资源隔离、提高扩展能力的同时实现对服务端的强管控
-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;基于滴滴内部多年运营实践沉淀资源治理方法建立健康分体系。针对Topic分区热点、分区不足等高频常见问题实现资源治理专家化
-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:与滴滴夜莺监控告警系统打通,集成监控告警、集群部署、集群升级等能力。形成运维生态,凝练专家服务,使运维更高效
### 1.4 滴滴Logi-KafkaManager架构图
![kafka-manager-arch](https://img-ys011.didistatic.com/static/dicloudpub/do1_xgDHNDLj2ChKxctSuf72) ![kafka-manager-arch](https://img-ys011.didistatic.com/static/dicloudpub/do1_xgDHNDLj2ChKxctSuf72)
## 相关文档 ## 2 相关文档
- [kafka-manager 安装手册](docs/install_guide/install_guide_cn.md) ### 2.1 产品文档
- [kafka-manager 接入集群](docs/user_guide/add_cluster/add_cluster.md) - [滴滴Logi-KafkaManager 安装手册](docs/install_guide/install_guide_cn.md)
- [kafka-manager 用户使用手册](docs/user_guide/user_guide_cn.md) - [滴滴Logi-KafkaManager 接入集群](docs/user_guide/add_cluster/add_cluster.md)
- [kafka-manager FAQ](docs/user_guide/faq.md) - [滴滴Logi-KafkaManager 用户使用手册](docs/user_guide/user_guide_cn.md)
- [滴滴Logi-KafkaManager FAQ](docs/user_guide/faq.md)
## 钉钉交流群 ### 2.2 社区文章
- [滴滴云官网产品介绍](https://www.didiyun.com/production/logi-KafkaManager.html)
- [7年沉淀之作--滴滴Logi日志服务套件](https://mp.weixin.qq.com/s/-KQp-Qo3WKEOc9wIR2iFnw)
- [滴滴Logi-KafkaManager 一站式Kafka监控与管控平台](https://mp.weixin.qq.com/s/9qSZIkqCnU6u9nLMvOOjIQ)
- [滴滴Logi-KafkaManager 开源之路](https://xie.infoq.cn/article/0223091a99e697412073c0d64)
- [滴滴Logi-KafkaManager 系列视频教程](https://mp.weixin.qq.com/s/9X7gH0tptHPtfjPPSdGO8g)
- [kafka实践十五滴滴开源Kafka管控平台 Logi-KafkaManager研究--A叶子叶来](https://blog.csdn.net/yezonggang/article/details/113106244)
## 3 滴滴Logi开源用户交流群
![image](https://user-images.githubusercontent.com/5287750/111266722-e531d800-8665-11eb-9242-3484da5a3099.png)
微信加群:关注公众号 Obsuite(官方公众号) 回复 "Logi加群"
![dingding_group](./docs/assets/images/common/dingding_group.jpg) ![dingding_group](./docs/assets/images/common/dingding_group.jpg)
钉钉群ID32821440 钉钉群ID32821440
## OCE认证
OCE是一个认证机制和交流平台为Logi-KafkaManager生产用户量身打造我们会为OCE企业提供更好的技术支持比如专属的技术沙龙、企业一对一的交流机会、专属的答疑群等如果贵司Logi-KafkaManager上了生产[快来加入吧](http://obsuite.didiyun.com/open/openAuth)
## 项目成员 ## 4 OCE认证
OCE是一个认证机制和交流平台为滴滴Logi-KafkaManager生产用户量身打造我们会为OCE企业提供更好的技术支持比如专属的技术沙龙、企业一对一的交流机会、专属的答疑群等如果贵司Logi-KafkaManager上了生产[快来加入吧](http://obsuite.didiyun.com/open/openAuth)
### 内部核心人员
## 5 项目成员
### 5.1 内部核心人员
`iceyuhui``liuyaguang``limengmonty``zhangliangmike``nullhuangyiming``zengqiao``eilenexuzhe``huangjiaweihjw``zhaoyinrui``marzkonglingxu``joysunchao` `iceyuhui``liuyaguang``limengmonty``zhangliangmike``nullhuangyiming``zengqiao``eilenexuzhe``huangjiaweihjw``zhaoyinrui``marzkonglingxu``joysunchao`
### 外部贡献者 ### 5.2 外部贡献者
`fangjunyu``zhoutaiyang` `fangjunyu``zhoutaiyang`
## 协议 ## 6 协议
`kafka-manager`基于`Apache-2.0`协议进行分发和使用,更多信息参见[协议文件](./LICENSE) `kafka-manager`基于`Apache-2.0`协议进行分发和使用,更多信息参见[协议文件](./LICENSE)

97
Releases_Notes.md Normal file
View File

@@ -0,0 +1,97 @@
---
![kafka-manager-logo](./docs/assets/images/common/logo_name.png)
**一站式`Apache Kafka`集群指标监控与运维管控平台**
---
## v2.3.0
版本上线时间2021-02-08
### 能力提升
- 新增支持docker化部署
- 可指定Broker作为候选controller
- 可新增并管理网关配置
- 可获取消费组状态
- 增加集群的JMX认证
### 体验优化
- 优化编辑用户角色、修改密码的流程
- 新增consumerID的搜索功能
- 优化“Topic连接信息”、“消费组重置消费偏移”、“修改Topic保存时间”的文案提示
- 在相应位置增加《资源申请文档》链接
### bug修复
- 修复Broker监控图表时间轴展示错误的问题
- 修复创建夜莺监控告警规则时,使用的告警周期的单位不正确的问题
## v2.2.0
版本上线时间2021-01-25
### 能力提升
- 优化工单批量操作流程
- 增加获取Topic75分位/99分位的实时耗时数据
- 增加定时任务可将无主未落DB的Topic定期写入DB
### 体验优化
- 在相应位置增加《集群接入文档》链接
- 优化物理集群、逻辑集群含义
- 在Topic详情页、Topic扩分区操作弹窗增加展示Topic所属Region的信息
- 优化Topic审批时Topic数据保存时间的配置流程
- 优化Topic/应用申请、审批时的错误提示文案
- 优化Topic数据采样的操作项文案
- 优化运维人员删除Topic时的提示文案
- 优化运维人员删除Region的删除逻辑与提示文案
- 优化运维人员删除逻辑集群的提示文案
- 优化上传集群配置文件时的文件类型限制条件
### bug修复
- 修复填写应用名称时校验特殊字符出错的问题
- 修复普通用户越权访问应用详情的问题
- 修复由于Kafka版本升级导致的数据压缩格式无法获取的问题
- 修复删除逻辑集群或Topic之后界面依旧展示的问题
- 修复进行Leader rebalance操作时执行结果重复提示的问题
## v2.1.0
版本上线时间2020-12-19
### 体验优化
- 优化页面加载时的背景样式
- 优化普通用户申请Topic权限的流程
- 优化Topic申请配额、申请分区的权限限制
- 优化取消Topic权限的文案提示
- 优化申请配额表单的表单项名称
- 优化重置消费偏移的操作流程
- 优化创建Topic迁移任务的表单内容
- 优化Topic扩分区操作的弹窗界面样式
- 优化集群Broker监控可视化图表样式
- 优化创建逻辑集群的表单内容
- 优化集群安全协议的提示文案
### bug修复
- 修复偶发性重置消费偏移失败的问题

View File

@@ -4,7 +4,7 @@ cd $workspace
## constant ## constant
OUTPUT_DIR=./output OUTPUT_DIR=./output
KM_VERSION=2.2.0 KM_VERSION=2.4.0
APP_NAME=kafka-manager APP_NAME=kafka-manager
APP_DIR=${APP_NAME}-${KM_VERSION} APP_DIR=${APP_NAME}-${KM_VERSION}

View File

@@ -0,0 +1,43 @@
FROM openjdk:16-jdk-alpine3.13
LABEL author="yangvipguang"
ENV VERSION 2.3.1
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
RUN apk add --no-cache --virtual .build-deps \
font-adobe-100dpi \
ttf-dejavu \
fontconfig \
curl \
apr \
apr-util \
apr-dev \
tomcat-native \
&& apk del .build-deps
RUN apk add --no-cache tini
ENV AGENT_HOME /opt/agent/
WORKDIR /tmp
COPY $JAR_PATH/kafka-manager.jar app.jar
# COPY application.yml application.yml ##默认使用helm 挂载,防止敏感配置泄露
COPY docker-depends/config.yaml $AGENT_HOME
COPY docker-depends/jmx_prometheus_javaagent-0.15.0.jar $AGENT_HOME
ENV JAVA_AGENT="-javaagent:$AGENT_HOME/jmx_prometheus_javaagent-0.15.0.jar=9999:$AGENT_HOME/config.yaml"
ENV JAVA_HEAP_OPTS="-Xms1024M -Xmx1024M -Xmn100M "
ENV JAVA_OPTS="-verbose:gc \
-XX:MaxMetaspaceSize=256M -XX:+DisableExplicitGC -XX:+UseStringDeduplication \
-XX:+UseG1GC -XX:+HeapDumpOnOutOfMemoryError -XX:-UseContainerSupport"
EXPOSE 8080 9999
ENTRYPOINT ["tini", "--"]
CMD ["sh","-c","java -jar $JAVA_AGENT $JAVA_HEAP_OPTS $JAVA_OPTS app.jar --spring.config.location=application.yml"]

View File

@@ -0,0 +1,5 @@
---
startDelaySeconds: 0
ssl: false
lowercaseOutputName: false
lowercaseOutputLabelNames: false

View File

@@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

24
container/helm/Chart.yaml Normal file
View File

@@ -0,0 +1,24 @@
apiVersion: v2
name: didi-km
description: A Helm chart for Kubernetes
# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.1.0
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "1.16.0"

View File

@@ -0,0 +1,22 @@
1. Get the application URL by running these commands:
{{- if .Values.ingress.enabled }}
{{- range $host := .Values.ingress.hosts }}
{{- range .paths }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
{{- end }}
{{- end }}
{{- else if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "didi-km.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "didi-km.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "didi-km.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "didi-km.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
{{- end }}

View File

@@ -0,0 +1,62 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "didi-km.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "didi-km.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "didi-km.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "didi-km.labels" -}}
helm.sh/chart: {{ include "didi-km.chart" . }}
{{ include "didi-km.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "didi-km.selectorLabels" -}}
app.kubernetes.io/name: {{ include "didi-km.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "didi-km.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "didi-km.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,88 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: km-cm
data:
application.yml: |
server:
port: 8080
tomcat:
accept-count: 1000
max-connections: 10000
max-threads: 800
min-spare-threads: 100
spring:
application:
name: kafkamanager
datasource:
kafka-manager:
jdbc-url: jdbc:mysql://xxxxx:3306/kafka-manager?characterEncoding=UTF-8&serverTimezone=GMT%2B8&useSSL=false
username: admin
password: admin
driver-class-name: com.mysql.jdbc.Driver
main:
allow-bean-definition-overriding: true
profiles:
active: dev
servlet:
multipart:
max-file-size: 100MB
max-request-size: 100MB
logging:
config: classpath:logback-spring.xml
custom:
idc: cn
jmx:
max-conn: 20
store-metrics-task:
community:
broker-metrics-enabled: true
topic-metrics-enabled: true
didi:
app-topic-metrics-enabled: false
topic-request-time-metrics-enabled: false
topic-throttled-metrics: false
save-days: 7
# 任务相关的开关
task:
op:
sync-topic-enabled: false # 未落盘的Topic定期同步到DB中
account:
ldap:
kcm:
enabled: false
storage:
base-url: http://127.0.0.1
n9e:
base-url: http://127.0.0.1:8004
user-token: 12345678
timeout: 300
account: root
script-file: kcm_script.sh
monitor:
enabled: false
n9e:
nid: 2
user-token: 1234567890
mon:
base-url: http://127.0.0.1:8032
sink:
base-url: http://127.0.0.1:8006
rdb:
base-url: http://127.0.0.1:80
notify:
kafka:
cluster-id: 95
topic-name: didi-kafka-notify
order:
detail-url: http://127.0.0.1

View File

@@ -0,0 +1,56 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "didi-km.fullname" . }}
labels:
{{- include "didi-km.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "didi-km.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "didi-km.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "didi-km.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: 8080
protocol: TCP
- name: jmx-metrics
containerPort: 9999
protocol: TCP
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@@ -0,0 +1,28 @@
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "didi-km.fullname" . }}
labels:
{{- include "didi-km.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "didi-km.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
- type: Resource
resource:
name: cpu
targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
- type: Resource
resource:
name: memory
targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,41 @@
{{- if .Values.ingress.enabled -}}
{{- $fullName := include "didi-km.fullname" . -}}
{{- $svcPort := .Values.service.port -}}
{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1beta1
{{- else -}}
apiVersion: extensions/v1beta1
{{- end }}
kind: Ingress
metadata:
name: {{ $fullName }}
labels:
{{- include "didi-km.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
backend:
serviceName: {{ $fullName }}
servicePort: {{ $svcPort }}
{{- end }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "didi-km.fullname" . }}
labels:
{{- include "didi-km.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "didi-km.selectorLabels" . | nindent 4 }}

View File

@@ -0,0 +1,12 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "didi-km.serviceAccountName" . }}
labels:
{{- include "didi-km.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "didi-km.fullname" . }}-test-connection"
labels:
{{- include "didi-km.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": test
spec:
containers:
- name: wget
image: busybox
command: ['wget']
args: ['{{ include "didi-km.fullname" . }}:{{ .Values.service.port }}']
restartPolicy: Never

View File

@@ -0,0 +1,79 @@
# Default values for didi-km.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
replicaCount: 1
image:
repository: docker.io/yangvipguang/km
pullPolicy: IfNotPresent
# Overrides the image tag whose default is the chart appVersion.
tag: "v18"
imagePullSecrets: []
nameOverride: ""
fullnameOverride: "km"
serviceAccount:
# Specifies whether a service account should be created
create: true
# Annotations to add to the service account
annotations: {}
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name: ""
podAnnotations: {}
podSecurityContext: {}
# fsGroup: 2000
securityContext: {}
# capabilities:
# drop:
# - ALL
# readOnlyRootFilesystem: true
# runAsNonRoot: true
# runAsUser: 1000
service:
type: ClusterIP
port: 8080
ingress:
enabled: false
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
hosts:
- host: chart-example.local
paths: []
tls: []
# - secretName: chart-example-tls
# hosts:
# - chart-example.local
resources:
# We usually recommend not to specify default resources and to leave this as a conscious
# choice for the user. This also increases chances charts run on environments with little
# resources, such as Minikube. If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
limits:
cpu: 50m
memory: 2048Mi
requests:
cpu: 10m
memory: 200Mi
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 100
targetCPUUtilizationPercentage: 80
# targetMemoryUtilizationPercentage: 80
nodeSelector: {}
tolerations: []
affinity: {}

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 KiB

View File

@@ -0,0 +1,101 @@
---
![kafka-manager-logo](../assets/images/common/logo_name.png)
**一站式`Apache Kafka`集群指标监控与运维管控平台**
---
## JMX-连接失败问题解决
集群正常接入Logi-KafkaManager之后即可以看到集群的Broker列表此时如果查看不了Topic的实时流量或者是Broker的实时流量信息时那么大概率就是JMX连接的问题了。
下面我们按照步骤来一步一步的检查。
### 1、问题&说明
**类型一JMX配置未开启**
未开启时,直接到`2、解决方法`查看如何开启即可。
![check_jmx_opened](./assets/connect_jmx_failed/check_jmx_opened.jpg)
**类型二:配置错误**
`JMX`端口已经开启的情况下,有的时候开启的配置不正确,此时也会导致出现连接失败的问题。这里大概列举几种原因:
- `JMX`配置错误:见`2、解决方法`
- 存在防火墙或者网络限制:网络通的另外一台机器`telnet`试一下看是否可以连接上。
- 需要进行用户名及密码的认证:见`3、解决方法 —— 认证的JMX`
错误日志例子:
```
# 错误一: 错误提示的是真实的IP这样的话基本就是JMX配置的有问题了。
2021-01-27 10:06:20.730 ERROR 50901 --- [ics-Thread-1-62] c.x.k.m.c.utils.jmx.JmxConnectorWrap : JMX connect exception, host:192.168.0.1 port:9999.
java.rmi.ConnectException: Connection refused to host: 192.168.0.1; nested exception is:
# 错误二错误提示的是127.0.0.1这个IP这个是机器的hostname配置的可能有问题。
2021-01-27 10:06:20.730 ERROR 50901 --- [ics-Thread-1-62] c.x.k.m.c.utils.jmx.JmxConnectorWrap : JMX connect exception, host:127.0.0.1 port:9999.
java.rmi.ConnectException: Connection refused to host: 127.0.0.1;; nested exception is:
```
### 2、解决方法
这里仅介绍一下比较通用的解决方式,如若有更好的方式,欢迎大家指导告知一下。
修改`kafka-server-start.sh`文件:
```
# 在这个下面增加JMX端口的配置
if [ "x$KAFKA_HEAP_OPTS" = "x" ]; then
export KAFKA_HEAP_OPTS="-Xmx1G -Xms1G"
export JMX_PORT=9999 # 增加这个配置, 这里的数值并不一定是要9999
fi
```
&nbsp;
修改`kafka-run-class.sh`文件
```
# JMX settings
if [ -z "$KAFKA_JMX_OPTS" ]; then
KAFKA_JMX_OPTS="-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=${当前机器的IP}"
fi
# JMX port to use
if [ $JMX_PORT ]; then
KAFKA_JMX_OPTS="$KAFKA_JMX_OPTS -Dcom.sun.management.jmxremote.port=$JMX_PORT -Dcom.sun.management.jmxremote.rmi.port=$JMX_PORT"
fi
```
### 3、解决方法 —— 认证的JMX
如果您是直接看的这个部分,建议先看一下上一节:`2、解决方法`以确保`JMX`的配置没有问题了。
在JMX的配置等都没有问题的情况下如果是因为认证的原因导致连接不了的此时可以使用下面介绍的方法进行解决。
**当前这块后端刚刚开发完成,可能还不够完善,有问题随时沟通。**
`Logi-KafkaManager 2.2.0+`之后的版本后端已经支持`JMX`认证方式的连接,但是还没有界面,此时我们可以往`cluster`表的`jmx_properties`字段写入`JMX`的认证信息。
这个数据是`json`格式的字符串,例子如下所示:
```json
{
"maxConn": 10, # KM对单台Broker的最大JMX连接数
"username": "xxxxx", # 用户名
"password": "xxxx", # 密码
"openSSL": true, # 开启SSL, true表示开启ssl, false表示关闭
}
```
&nbsp;
SQL的例子
```sql
UPDATE cluster SET jmx_properties='{ "maxConn": 10, "username": "xxxxx", "password": "xxxx", "openSSL": false }' where id={xxx};
```

View File

@@ -9,6 +9,13 @@
# 动态配置管理 # 动态配置管理
## 0、目录
- 1、Topic定时同步任务
- 2、专家服务——Topic分区热点
- 3、专家服务——Topic分区不足
## 1、Topic定时同步任务 ## 1、Topic定时同步任务
### 1.1、配置的用途 ### 1.1、配置的用途
@@ -63,3 +70,53 @@ task:
] ]
``` ```
---
## 2、专家服务——Topic分区热点
`Region`所圈定的Broker范围内某个Topic的Leader数在这些圈定的Broker上分布不均衡时我们认为该Topic是存在热点的Topic。
备注单纯的查看Leader数的分布确实存在一定的局限性这块欢迎贡献更多的热点定义于代码。
Topic分区热点相关的动态配置(页面在运维管控->平台管理->配置管理)
配置Key
```
REGION_HOT_TOPIC_CONFIG
```
配置Value
```json
{
"maxDisPartitionNum": 2, # Region内Broker间的leader数差距超过2时则认为是存在热点的Topic
"minTopicBytesInUnitB": 1048576, # 流量低于该值的Topic不做统计
"ignoreClusterIdList": [ # 忽略的集群
50
]
}
```
---
## 3、专家服务——Topic分区不足
总流量除以分区数超过指定值时则我们认为存在Topic分区不足。
Topic分区不足相关的动态配置(页面在运维管控->平台管理->配置管理)
配置Key
```
TOPIC_INSUFFICIENT_PARTITION_CONFIG
```
配置Value
```json
{
"maxBytesInPerPartitionUnitB": 3145728, # 单分区流量超过该值, 则认为分区不去
"minTopicBytesInUnitB": 1048576, # 流量低于该值的Topic不做统计
"ignoreClusterIdList": [ # 忽略的集群
50
]
}
```

View File

@@ -0,0 +1,10 @@
---
![kafka-manager-logo](../assets/images/common/logo_name.png)
**一站式`Apache Kafka`集群指标监控与运维管控平台**
---
# Kafka-Gateway 配置说明

View File

@@ -0,0 +1,17 @@
---
![kafka-manager-logo](../../assets/images/common/logo_name.png)
**一站式`Apache Kafka`集群指标监控与运维管控平台**
---
# 升级至`2.3.0`版本
`2.3.0`版本在`gateway_config`表增加了一个描述说明的字段因此需要执行下面的sql进行字段的增加。
```sql
ALTER TABLE `gateway_config`
ADD COLUMN `description` TEXT NULL COMMENT '描述信息' AFTER `version`;
```

View File

@@ -15,7 +15,7 @@
当前因为无法同时兼容`MySQL 8``MySQL 5.7`,因此代码中默认的版本还是`MySQL 5.7` 当前因为无法同时兼容`MySQL 8``MySQL 5.7`,因此代码中默认的版本还是`MySQL 5.7`
当前如需使用`MySQL 8`,则按照下述流程进行简单修改代码。 当前如需使用`MySQL 8`,则按照下述流程进行简单修改代码。
- Step1. 修改application.yml中的MySQL驱动类 - Step1. 修改application.yml中的MySQL驱动类

View File

@@ -203,7 +203,8 @@ CREATE TABLE `gateway_config` (
`type` varchar(128) NOT NULL DEFAULT '' COMMENT '配置类型', `type` varchar(128) NOT NULL DEFAULT '' COMMENT '配置类型',
`name` varchar(128) NOT NULL DEFAULT '' COMMENT '配置名称', `name` varchar(128) NOT NULL DEFAULT '' COMMENT '配置名称',
`value` text COMMENT '配置值', `value` text COMMENT '配置值',
`version` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '版本信息', `version` bigint(20) unsigned NOT NULL DEFAULT '1' COMMENT '版本信息',
`description` text COMMENT '描述信息',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`modify_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间', `modify_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`), PRIMARY KEY (`id`),

View File

@@ -0,0 +1,94 @@
---
![kafka-manager-logo](../assets/images/common/logo_name.png)
**一站式`Apache Kafka`集群指标监控与运维管控平台**
---
## nginx配置-安装手册
# 一、独立部署
请参考参考:[kafka-manager 安装手册](install_guide_cn.md)
# 二、nginx配置
## 1、独立部署配置
```
#nginx 根目录访问配置如下
location / {
proxy_pass http://ip:port;
}
```
## 2、前后端分离&配置多个静态资源
以下配置解决`nginx代理多个静态资源`,实现项目前后端分离,版本更新迭代。
### 1、源码下载
根据所需版本下载对应代码,下载地址:[Github 下载地址](https://github.com/didi/Logi-KafkaManager)
### 2、修改webpack.config.js 配置文件
修改`kafka-manager-console`模块 `webpack.config.js`
以下所有<font color='red'>xxxx</font>为nginx代理路径和打包静态文件加载前缀,<font color='red'>xxxx</font>可根据需求自行更改。
```
cd kafka-manager-console
vi webpack.config.js
# publicPath默认打包方式根目录下修改为nginx代理访问路径。
let publicPath = '/xxxx';
```
### 3、打包
```
npm cache clean --force && npm install
```
ps如果打包过程中报错运行`npm install clipboard@2.0.6`,相反请忽略!
### 4、部署
#### 1、前段静态文件部署
静态资源 `../kafka-manager-web/src/main/resources/templates`
上传到指定目录,目前以`root目录`做demo
#### 2、上传jar包并启动请参考[kafka-manager 安装手册](install_guide_cn.md)
#### 3、修改nginx 配置
```
location /xxxx {
# 静态文件存放位置
alias /root/templates;
try_files $uri $uri/ /xxxx/index.html;
index index.html;
}
location /api {
proxy_pass http://ip:port;
}
#后代端口建议使用/api如果冲突可以使用以下配置
#location /api/v2 {
# proxy_pass http://ip:port;
#}
#location /api/v1 {
# proxy_pass http://ip:port;
#}
```

View File

@@ -5,16 +5,26 @@
**一站式`Apache Kafka`集群指标监控与运维管控平台** **一站式`Apache Kafka`集群指标监控与运维管控平台**
--- ---
# 集群接入 # 集群接入
集群的接入总共需要三个步骤,分别是: ## 主要概念讲解
1. 接入物理集群 面对大规模集群、业务场景复杂的情况引入Region、逻辑集群的概念
2. 创建Region - Region划分部分Broker作为一个 Region用Region定义资源划分的单位提高扩展性和隔离性。如果部分Topic异常也不会影响大面积的Broker
3. 创建逻辑集群 - 逻辑集群逻辑集群由部分Region组成便于对大规模集群按照业务划分、保障能力进行管理
![op_cluster_arch](assets/op_cluster_arch.png)
备注接入集群需要2、3两步是因为普通用户的视角下看到的都是逻辑集群如果没有2、3两步那么普通用户看不到任何信息。 集群的接入总共需要三个步骤,分别是:
1. 接入物理集群:填写机器地址、安全协议等配置信息,接入真实的物理集群
2. 创建Region将部分Broker划分为一个Region
3. 创建逻辑集群逻辑集群由部分Region组成可根据业务划分、保障等级来创建相应的逻辑集群
![op_cluster_flow](assets/op_cluster_flow.png)
**备注接入集群需要2、3两步是因为普通用户的视角下看到的都是逻辑集群如果没有2、3两步那么普通用户看不到任何信息。**
## 1、接入物理集群 ## 1、接入物理集群

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

View File

@@ -1,4 +1,4 @@
![kafka-manager-logo](../assets/images/common/logo_name.png)) ![kafka-manager-logo](../assets/images/common/logo_name.png)
**一站式`Apache Kafka`集群指标监控与运维管控平台** **一站式`Apache Kafka`集群指标监控与运维管控平台**

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

View File

@@ -9,18 +9,35 @@
# FAQ # FAQ
- 1、Topic申请时没有可选择的集群 - 0、支持哪些Kafka版本
- 1、Topic申请、新建监控告警等操作时没有可选择的集群
- 2、逻辑集群 & Region的用途 - 2、逻辑集群 & Region的用途
- 3、登录失败 - 3、登录失败
- 4、页面流量信息等无数据 - 4、页面流量信息等无数据
- 5、如何对接夜莺的监控告警功能 - 5、如何对接夜莺的监控告警功能
- 6、如何使用`MySQL 8` - 6、如何使用`MySQL 8`
- 7、`Jmx`连接失败如何解决?
- 8、`topic biz data not exist`错误及处理方式
- 9、进程启动后如何查看API文档
- 10、如何创建告警组
- 11、连接信息、耗时信息为什么没有数据
- 12、逻辑集群申请审批通过之后为什么看不到逻辑集群
--- ---
### 1、Topic申请时没有可选择的集群 ### 0、支持哪些Kafka版本
- 参看 [kafka-manager 接入集群](docs/user_guide/add_cluster/add_cluster.md) 手册这里的Region和逻辑集群都必须添加 基本上只要所使用的Kafka还依赖于Zookeeper那么该版本的主要功能基本上应该就是支持的
---
### 1、Topic申请、新建监控告警等操作时没有可选择的集群
缺少逻辑集群导致的在Topic管理、监控告警、集群管理这三个Tab下面都是普通用户视角普通用户看到的集群都是逻辑集群因此在这三个Tab下进行操作时都需要有逻辑集群。
逻辑集群的创建参看:
- [kafka-manager 接入集群](add_cluster/add_cluster.md) 手册这里的Region和逻辑集群都必须添加。
--- ---
@@ -29,7 +46,7 @@
主要用途是进行大集群的管理 & 集群细节的屏蔽。 主要用途是进行大集群的管理 & 集群细节的屏蔽。
- 逻辑集群通过逻辑集群概念将集群Broker按业务进行归类方便管理 - 逻辑集群通过逻辑集群概念将集群Broker按业务进行归类方便管理
- Region通过引入Region同时Topic按Region度创建减少Broker间的连接 - Region通过引入Region同时Topic按Region度创建减少Broker间的连接
--- ---
@@ -43,7 +60,7 @@
- 1、检查`Broker JMX`是否正确开启。 - 1、检查`Broker JMX`是否正确开启。
如若还未开启,具体可百度一下看如何开启 如若还未开启,具体可百度一下看如何开启,或者参看:[Jmx连接配置&问题解决说明文档](../dev_guide/connect_jmx_failed.md)
![helpcenter](./assets/faq/jmx_check.jpg) ![helpcenter](./assets/faq/jmx_check.jpg)
@@ -53,7 +70,7 @@
- 3、数据库时区问题。 - 3、数据库时区问题。
检查MySQL的topic_metrics、broker_metrics表,查看是否有数据,如果有数据,那么再检查设置的时区是否正确。 检查MySQL的topic_metrics表查看是否有数据如果有数据那么再检查设置的时区是否正确。
--- ---
@@ -66,3 +83,46 @@
### 6、如何使用`MySQL 8` ### 6、如何使用`MySQL 8`
- 参看 [kafka-manager 使用`MySQL 8`](../dev_guide/use_mysql_8.md) 说明。 - 参看 [kafka-manager 使用`MySQL 8`](../dev_guide/use_mysql_8.md) 说明。
---
### 7、`Jmx`连接失败如何解决?
- 参看 [Jmx连接配置&问题解决](../dev_guide/connect_jmx_failed.md) 说明。
---
### 8、`topic biz data not exist`错误及处理方式
**错误原因**
在进行权限审批的时候可能会出现这个错误出现这个错误的原因是因为Topic相关的业务信息没有在DB中存储或者更具体的说就是该Topic不属于任何应用导致的只需要将这些无主的Topic挂在某个应用下面即可。
**解决方式**
可以在`运维管控->集群列表->Topic信息`下面编辑申请权限的Topic为Topic选择一个应用即可。
以上仅仅只是针对单个Topic的场景如果你有非常多的Topic需要进行初始化的那么此时可以在配置管理中增加一个配置来定时的对无主的Topic进行同步具体见[动态配置管理 - 1、Topic定时同步任务](../dev_guide/dynamic_config_manager.md)
---
### 9、进程启动后如何查看API文档
- 滴滴Logi-KafkaManager采用Swagger-API工具记录API文档。Swagger-API地址 [http://IP:PORT/swagger-ui.html#/](http://IP:PORT/swagger-ui.html#/)
### 10、如何创建告警组
这块需要配合监控系统进行使用,现在默认已经实现了夜莺的对接,当然也可以对接自己内部的监控系统,不过需要实现一些接口。
具体的文档可见:[监控功能对接夜莺](../dev_guide/monitor_system_integrate_with_n9e.md)、[监控功能对接其他系统](../dev_guide/monitor_system_integrate_with_self.md)
### 11、连接信息、耗时信息为什么没有数据
这块需要结合滴滴内部的kafka-gateway一同使用才会有数据滴滴kafka-gateway暂未开源。
### 12、逻辑集群申请审批通过之后为什么看不到逻辑集群
逻辑集群的申请与审批仅仅只是一个工单流程,并不会去实际创建逻辑集群,逻辑集群的创建还需要手动去创建。
具体的操作可见:[kafka-manager 接入集群](add_cluster/add_cluster.md)。

View File

@@ -0,0 +1,32 @@
---
![kafka-manager-logo](../assets/images/common/logo_name.png)
**一站式`Apache Kafka`集群指标监控与运维管控平台**
---
# 资源申请文档
## 主要名词解释
- 应用App作为Kafka中的账户使用AppID+password作为身份标识
- 集群:可使用平台提供的共享集群,也可为某一应用申请单独的集群
- Topic可申请创建Topic或申请其他Topic的生产/消费权限。进行生产/消费时通过Topic+AppID进行身份鉴权
![production_consumption_flow](assets/resource_apply/production_consumption_flow.png)
## 应用申请
应用App作为Kafka中的账户使用AppID+password作为身份标识。对Topic进行生产/消费时通过Topic+AppID进行身份鉴权。
用户申请应用经由运维人员审批审批通过后获得AppID和密钥
## 集群申请
可使用平台提供的共享集群,若对隔离性、稳定性、生产消费速率有更高的需求,可对某一应用申请单独的集群
## Topic申请
- 用户可根据已申请的应用创建Topic。创建后应用负责人默认拥有该Topic的生产/消费权限和管理权限
- 也可申请其他Topic的生产、消费权限。经由Topic所属应用的负责人审批后即可拥有相应权限。

View File

@@ -104,5 +104,10 @@
<groupId>javax.servlet</groupId> <groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId> <artifactId>javax.servlet-api</artifactId>
</dependency> </dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@@ -47,4 +47,13 @@ public enum AccountRoleEnum {
} }
return AccountRoleEnum.UNKNOWN; return AccountRoleEnum.UNKNOWN;
} }
public static AccountRoleEnum getUserRoleEnum(String roleName) {
for (AccountRoleEnum elem: AccountRoleEnum.values()) {
if (elem.message.equalsIgnoreCase(roleName)) {
return elem;
}
}
return AccountRoleEnum.UNKNOWN;
}
} }

View File

@@ -46,7 +46,7 @@ public enum OperateEnum {
public static boolean validate(Integer code) { public static boolean validate(Integer code) {
if (code == null) { if (code == null) {
return false; return true;
} }
for (OperateEnum state : OperateEnum.values()) { for (OperateEnum state : OperateEnum.values()) {
if (state.getCode() == code) { if (state.getCode() == code) {

View File

@@ -1,45 +0,0 @@
package com.xiaojukeji.kafka.manager.common.bizenum;
/**
* 是否上报监控系统
* @author zengqiao
* @date 20/9/25
*/
public enum SinkMonitorSystemEnum {
SINK_MONITOR_SYSTEM(0, "上报监控系统"),
NOT_SINK_MONITOR_SYSTEM(1, "不上报监控系统"),
;
private Integer code;
private String message;
SinkMonitorSystemEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
@Override
public String toString() {
return "SinkMonitorSystemEnum{" +
"code=" + code +
", message='" + message + '\'' +
'}';
}
}

View File

@@ -0,0 +1,32 @@
package com.xiaojukeji.kafka.manager.common.bizenum;
/**
* 过期Topic状态
* @author zengqiao
* @date 21/01/25
*/
public enum TopicExpiredStatusEnum {
ALREADY_NOTIFIED_AND_DELETED(-2, "已通知, 已下线"),
ALREADY_NOTIFIED_AND_CAN_DELETE(-1, "已通知, 可下线"),
ALREADY_EXPIRED_AND_WAIT_NOTIFY(0, "已过期, 待通知"),
ALREADY_NOTIFIED_AND_WAIT_RESPONSE(1, "已通知, 待反馈"),
;
private int status;
private String message;
TopicExpiredStatusEnum(int status, String message) {
this.status = status;
this.message = message;
}
public int getStatus() {
return status;
}
public String getMessage() {
return message;
}
}

View File

@@ -7,18 +7,18 @@ package com.xiaojukeji.kafka.manager.common.constant;
*/ */
public class ApiPrefix { public class ApiPrefix {
public static final String API_PREFIX = "/api/"; public static final String API_PREFIX = "/api/";
public static final String API_V1_PREFIX = API_PREFIX + "v1/"; private static final String API_V1_PREFIX = API_PREFIX + "v1/";
public static final String API_V2_PREFIX = API_PREFIX + "v2/";
// login
public static final String API_V1_SSO_PREFIX = API_V1_PREFIX + "sso/";
// console // console
public static final String API_V1_SSO_PREFIX = API_V1_PREFIX + "sso/";
public static final String API_V1_NORMAL_PREFIX = API_V1_PREFIX + "normal/"; public static final String API_V1_NORMAL_PREFIX = API_V1_PREFIX + "normal/";
public static final String API_V1_RD_PREFIX = API_V1_PREFIX + "rd/"; public static final String API_V1_RD_PREFIX = API_V1_PREFIX + "rd/";
public static final String API_V1_OP_PREFIX = API_V1_PREFIX + "op/"; public static final String API_V1_OP_PREFIX = API_V1_PREFIX + "op/";
// open // open
public static final String API_V1_THIRD_PART_PREFIX = API_V1_PREFIX + "third-part/"; public static final String API_V1_THIRD_PART_PREFIX = API_V1_PREFIX + "third-part/";
public static final String API_V2_THIRD_PART_PREFIX = API_V2_PREFIX + "third-part/";
// gateway // gateway
public static final String GATEWAY_API_V1_PREFIX = "/gateway" + API_V1_PREFIX; public static final String GATEWAY_API_V1_PREFIX = "/gateway" + API_V1_PREFIX;

View File

@@ -97,7 +97,7 @@ public class Result<T> implements Serializable {
return result; return result;
} }
public static <T> Result<T> buildFailure(String message) { public static <T> Result<T> buildGatewayFailure(String message) {
Result<T> result = new Result<T>(); Result<T> result = new Result<T>();
result.setCode(ResultStatus.GATEWAY_INVALID_REQUEST.getCode()); result.setCode(ResultStatus.GATEWAY_INVALID_REQUEST.getCode());
result.setMessage(message); result.setMessage(message);
@@ -105,6 +105,14 @@ public class Result<T> implements Serializable {
return result; return result;
} }
public static <T> Result<T> buildFailure(String message) {
Result<T> result = new Result<T>();
result.setCode(ResultStatus.FAIL.getCode());
result.setMessage(message);
result.setData(null);
return result;
}
public static Result buildFrom(ResultStatus resultStatus) { public static Result buildFrom(ResultStatus resultStatus) {
Result result = new Result(); Result result = new Result();
result.setCode(resultStatus.getCode()); result.setCode(resultStatus.getCode());

View File

@@ -12,125 +12,105 @@ public enum ResultStatus {
SUCCESS(Constant.SUCCESS, "success"), SUCCESS(Constant.SUCCESS, "success"),
LOGIN_FAILED(1, "login failed, please check username and password"), FAIL(1, "操作失败"),
/** /**
* 内部依赖错误, [1000, 1200) * 操作错误[1000, 2000)
* ------------------------------------------------------------------------------------------ * ------------------------------------------------------------------------------------------
*/ */
MYSQL_ERROR(1000, "operate database failed"),
CONNECT_ZOOKEEPER_FAILED(1000, "connect zookeeper failed"),
READ_ZOOKEEPER_FAILED(1000, "read zookeeper failed"),
READ_JMX_FAILED(1000, "read jmx failed"),
// 内部依赖错误 —— Kafka特定错误, [1000, 1100)
BROKER_NUM_NOT_ENOUGH(1000, "broker not enough"),
CONTROLLER_NOT_ALIVE(1000, "controller not alive"),
CLUSTER_METADATA_ERROR(1000, "cluster metadata error"),
TOPIC_CONFIG_ERROR(1000, "topic config error"),
/**
* 外部依赖错误, [1200, 1400)
* ------------------------------------------------------------------------------------------
*/
CALL_CLUSTER_TASK_AGENT_FAILED(1000, " call cluster task agent failed"),
CALL_MONITOR_SYSTEM_ERROR(1000, " call monitor-system failed"),
/**
* 外部用户操作错误, [1400, 1600)
* ------------------------------------------------------------------------------------------
*/
PARAM_ILLEGAL(1400, "param illegal"),
OPERATION_FAILED(1401, "operation failed"), OPERATION_FAILED(1401, "operation failed"),
OPERATION_FORBIDDEN(1402, "operation forbidden"), OPERATION_FORBIDDEN(1402, "operation forbidden"),
API_CALL_EXCEED_LIMIT(1403, "api call exceed limit"), API_CALL_EXCEED_LIMIT(1403, "api call exceed limit"),
USER_WITHOUT_AUTHORITY(1404, "user without authority"),
CHANGE_ZOOKEEPER_FORBIDDEN(1405, "change zookeeper forbidden"),
// 资源不存在
CLUSTER_NOT_EXIST(10000, "cluster not exist"),
BROKER_NOT_EXIST(10000, "broker not exist"),
TOPIC_NOT_EXIST(10000, "topic not exist"),
PARTITION_NOT_EXIST(10000, "partition not exist"),
ACCOUNT_NOT_EXIST(10000, "account not exist"), APP_OFFLINE_FORBIDDEN(1406, "先下线topic才能下线应用"),
APP_NOT_EXIST(1000, "app not exist"),
ORDER_NOT_EXIST(1000, "order not exist"),
CONFIG_NOT_EXIST(1000, "config not exist"),
IDC_NOT_EXIST(1000, "idc not exist"),
TASK_NOT_EXIST(1110, "task not exist"),
AUTHORITY_NOT_EXIST(1000, "authority not exist"),
MONITOR_NOT_EXIST(1110, "monitor not exist"), TOPIC_OPERATION_PARAM_NULL_POINTER(1450, "参数错误"),
TOPIC_OPERATION_PARTITION_NUM_ILLEGAL(1451, "分区数错误"),
TOPIC_OPERATION_BROKER_NUM_NOT_ENOUGH(1452, "Broker数不足错误"),
TOPIC_OPERATION_TOPIC_NAME_ILLEGAL(1453, "Topic名称非法"),
TOPIC_OPERATION_TOPIC_EXISTED(1454, "Topic已存在"),
TOPIC_OPERATION_UNKNOWN_TOPIC_PARTITION(1455, "Topic未知"),
TOPIC_OPERATION_TOPIC_CONFIG_ILLEGAL(1456, "Topic配置错误"),
TOPIC_OPERATION_TOPIC_IN_DELETING(1457, "Topic正在删除"),
TOPIC_OPERATION_UNKNOWN_ERROR(1458, "未知错误"),
QUOTA_NOT_EXIST(1000, "quota not exist, please check clusterId, topicName and appId"), /**
* 参数错误[2000, 3000)
* ------------------------------------------------------------------------------------------
*/
PARAM_ILLEGAL(2000, "param illegal"),
CG_LOCATION_ILLEGAL(2001, "consumer group location illegal"),
ORDER_ALREADY_HANDLED(2002, "order already handled"),
APP_ID_OR_PASSWORD_ILLEGAL(2003, "app or password illegal"),
SYSTEM_CODE_ILLEGAL(2004, "system code illegal"),
CLUSTER_TASK_HOST_LIST_ILLEGAL(2005, "主机列表错误,请检查主机列表"),
JSON_PARSER_ERROR(2006, "json parser error"),
// 资源不存在, 已存在, 已被使用 BROKER_NUM_NOT_ENOUGH(2050, "broker not enough"),
RESOURCE_NOT_EXIST(1200, "资源不存在"), CONTROLLER_NOT_ALIVE(2051, "controller not alive"),
RESOURCE_ALREADY_EXISTED(1200, "资源已经存在"), CLUSTER_METADATA_ERROR(2052, "cluster metadata error"),
RESOURCE_NAME_DUPLICATED(1200, "资源名称重复"), TOPIC_CONFIG_ERROR(2053, "topic config error"),
RESOURCE_ALREADY_USED(1000, "资源早已被使用"),
/**
* 参数错误 - 资源检查错误
* 因为外部系统的问题, 操作时引起的错误, [7000, 8000)
* ------------------------------------------------------------------------------------------
*/
RESOURCE_NOT_EXIST(7100, "资源不存在"),
CLUSTER_NOT_EXIST(7101, "cluster not exist"),
BROKER_NOT_EXIST(7102, "broker not exist"),
TOPIC_NOT_EXIST(7103, "topic not exist"),
PARTITION_NOT_EXIST(7104, "partition not exist"),
ACCOUNT_NOT_EXIST(7105, "account not exist"),
APP_NOT_EXIST(7106, "app not exist"),
ORDER_NOT_EXIST(7107, "order not exist"),
CONFIG_NOT_EXIST(7108, "config not exist"),
IDC_NOT_EXIST(7109, "idc not exist"),
TASK_NOT_EXIST(7110, "task not exist"),
AUTHORITY_NOT_EXIST(7111, "authority not exist"),
MONITOR_NOT_EXIST(7112, "monitor not exist"),
QUOTA_NOT_EXIST(7113, "quota not exist, please check clusterId, topicName and appId"),
CONSUMER_GROUP_NOT_EXIST(7114, "consumerGroup not exist"),
TOPIC_BIZ_DATA_NOT_EXIST(7115, "topic biz data not exist, please sync topic to db"),
// 资源已存在
RESOURCE_ALREADY_EXISTED(7200, "资源已经存在"),
TOPIC_ALREADY_EXIST(7201, "topic already existed"),
// 资源重名
RESOURCE_NAME_DUPLICATED(7300, "资源名称重复"),
// 资源已被使用
RESOURCE_ALREADY_USED(7400, "资源早已被使用"),
/** /**
* 资源参数错误 * 因为外部系统的问题, 操作时引起的错误, [8000, 9000)
* ------------------------------------------------------------------------------------------
*/ */
CG_LOCATION_ILLEGAL(10000, "consumer group location illegal"), MYSQL_ERROR(8010, "operate database failed"),
ORDER_ALREADY_HANDLED(1000, "order already handled"),
APP_ID_OR_PASSWORD_ILLEGAL(1000, "app or password illegal"), ZOOKEEPER_CONNECT_FAILED(8020, "zookeeper connect failed"),
SYSTEM_CODE_ILLEGAL(1000, "system code illegal"), ZOOKEEPER_READ_FAILED(8021, "zookeeper read failed"),
ZOOKEEPER_WRITE_FAILED(8022, "zookeeper write failed"),
ZOOKEEPER_DELETE_FAILED(8023, "zookeeper delete failed"),
CLUSTER_TASK_HOST_LIST_ILLEGAL(1000, "主机列表错误,请检查主机列表"), // 调用集群任务里面的agent失败
CALL_CLUSTER_TASK_AGENT_FAILED(8030, " call cluster task agent failed"),
// 调用监控系统失败
CALL_MONITOR_SYSTEM_ERROR(8040, " call monitor-system failed"),
// 存储相关的调用失败
STORAGE_UPLOAD_FILE_FAILED(8050, "upload file failed"),
STORAGE_FILE_TYPE_NOT_SUPPORT(8051, "File type not support"),
STORAGE_DOWNLOAD_FILE_FAILED(8052, "download file failed"),
LDAP_AUTHENTICATION_FAILED(8053, "ldap authentication failed"),
///////////////////////////////////////////////////////////////
USER_WITHOUT_AUTHORITY(1000, "user without authority"),
JSON_PARSER_ERROR(1000, "json parser error"),
TOPIC_OPERATION_PARAM_NULL_POINTER(2, "参数错误"),
TOPIC_OPERATION_PARTITION_NUM_ILLEGAL(3, "分区数错误"),
TOPIC_OPERATION_BROKER_NUM_NOT_ENOUGH(4, "Broker数不足错误"),
TOPIC_OPERATION_TOPIC_NAME_ILLEGAL(5, "Topic名称非法"),
TOPIC_OPERATION_TOPIC_EXISTED(6, "Topic已存在"),
TOPIC_OPERATION_UNKNOWN_TOPIC_PARTITION(7, "Topic未知"),
TOPIC_OPERATION_TOPIC_CONFIG_ILLEGAL(8, "Topic配置错误"),
TOPIC_OPERATION_TOPIC_IN_DELETING(9, "Topic正在删除"),
TOPIC_OPERATION_UNKNOWN_ERROR(10, "未知错误"),
TOPIC_EXIST_CONNECT_CANNOT_DELETE(10, "topic exist connect cannot delete"),
EXIST_TOPIC_CANNOT_DELETE(10, "exist topic cannot delete"),
/**
* 工单
*/
CHANGE_ZOOKEEPER_FORBIDEN(100, "change zookeeper forbiden"),
// APP_EXIST_TOPIC_AUTHORITY_CANNOT_DELETE(1000, "app exist topic authority cannot delete"),
UPLOAD_FILE_FAIL(1000, "upload file fail"),
FILE_TYPE_NOT_SUPPORT(1000, "File type not support"),
DOWNLOAD_FILE_FAIL(1000, "download file fail"),
TOPIC_ALREADY_EXIST(17400, "topic already existed"),
CONSUMER_GROUP_NOT_EXIST(17411, "consumerGroup not exist"),
; ;
private int code; private int code;

View File

@@ -23,6 +23,8 @@ public class ClusterDetailDTO {
private String securityProperties; private String securityProperties;
private String jmxProperties;
private Integer status; private Integer status;
private Date gmtCreate; private Date gmtCreate;
@@ -103,6 +105,14 @@ public class ClusterDetailDTO {
this.securityProperties = securityProperties; this.securityProperties = securityProperties;
} }
public String getJmxProperties() {
return jmxProperties;
}
public void setJmxProperties(String jmxProperties) {
this.jmxProperties = jmxProperties;
}
public Integer getStatus() { public Integer getStatus() {
return status; return status;
} }
@@ -176,8 +186,9 @@ public class ClusterDetailDTO {
", bootstrapServers='" + bootstrapServers + '\'' + ", bootstrapServers='" + bootstrapServers + '\'' +
", kafkaVersion='" + kafkaVersion + '\'' + ", kafkaVersion='" + kafkaVersion + '\'' +
", idc='" + idc + '\'' + ", idc='" + idc + '\'' +
", mode='" + mode + '\'' + ", mode=" + mode +
", securityProperties='" + securityProperties + '\'' + ", securityProperties='" + securityProperties + '\'' +
", jmxProperties='" + jmxProperties + '\'' +
", status=" + status + ", status=" + status +
", gmtCreate=" + gmtCreate + ", gmtCreate=" + gmtCreate +
", gmtModify=" + gmtModify + ", gmtModify=" + gmtModify +

View File

@@ -1,57 +0,0 @@
package com.xiaojukeji.kafka.manager.common.entity.ao.config;
/**
* @author zengqiao
* @date 20/9/7
*/
public class SinkTopicRequestTimeMetricsConfig {
private Long clusterId;
private String topicName;
private Long startId;
private Long step;
public Long getClusterId() {
return clusterId;
}
public void setClusterId(Long clusterId) {
this.clusterId = clusterId;
}
public String getTopicName() {
return topicName;
}
public void setTopicName(String topicName) {
this.topicName = topicName;
}
public Long getStartId() {
return startId;
}
public void setStartId(Long startId) {
this.startId = startId;
}
public Long getStep() {
return step;
}
public void setStep(Long step) {
this.step = step;
}
@Override
public String toString() {
return "SinkTopicRequestTimeMetricsConfig{" +
"clusterId=" + clusterId +
", topicName='" + topicName + '\'' +
", startId=" + startId +
", step=" + step +
'}';
}
}

View File

@@ -0,0 +1,45 @@
package com.xiaojukeji.kafka.manager.common.entity.dto.op;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import java.util.List;
/**
* @author zengqiao
* @date 21/01/24
*/
@JsonIgnoreProperties(ignoreUnknown = true)
@ApiModel(description="优选为Controller的候选者")
public class ControllerPreferredCandidateDTO {
@ApiModelProperty(value="集群ID")
private Long clusterId;
@ApiModelProperty(value="优选为controller的BrokerId")
private List<Integer> brokerIdList;
public Long getClusterId() {
return clusterId;
}
public void setClusterId(Long clusterId) {
this.clusterId = clusterId;
}
public List<Integer> getBrokerIdList() {
return brokerIdList;
}
public void setBrokerIdList(List<Integer> brokerIdList) {
this.brokerIdList = brokerIdList;
}
@Override
public String toString() {
return "ControllerPreferredCandidateDTO{" +
"clusterId=" + clusterId +
", brokerIdList=" + brokerIdList +
'}';
}
}

View File

@@ -40,6 +40,9 @@ public class TopicCreationDTO extends ClusterTopicDTO {
@ApiModelProperty(value = "Topic属性列表") @ApiModelProperty(value = "Topic属性列表")
private Properties properties; private Properties properties;
@ApiModelProperty(value = "最大写入字节数")
private Long peakBytesIn;
public String getAppId() { public String getAppId() {
return appId; return appId;
} }
@@ -104,6 +107,14 @@ public class TopicCreationDTO extends ClusterTopicDTO {
this.properties = properties; this.properties = properties;
} }
public Long getPeakBytesIn() {
return peakBytesIn;
}
public void setPeakBytesIn(Long peakBytesIn) {
this.peakBytesIn = peakBytesIn;
}
@Override @Override
public String toString() { public String toString() {
return "TopicCreationDTO{" + return "TopicCreationDTO{" +

View File

@@ -102,12 +102,11 @@ public class ClusterDTO {
'}'; '}';
} }
public Boolean legal() { public boolean legal() {
if (ValidateUtils.isNull(clusterName) if (ValidateUtils.isNull(clusterName)
|| ValidateUtils.isNull(zookeeper) || ValidateUtils.isNull(zookeeper)
|| ValidateUtils.isNull(idc) || ValidateUtils.isNull(idc)
|| ValidateUtils.isNull(bootstrapServers) || ValidateUtils.isNull(bootstrapServers)) {
) {
return false; return false;
} }
return true; return true;

View File

@@ -81,11 +81,6 @@ public class OperateRecordDTO {
} }
public boolean legal() { public boolean legal() {
if (!ModuleEnum.validate(moduleId) || return !ValidateUtils.isNull(moduleId) && ModuleEnum.validate(moduleId) && OperateEnum.validate(operateId);
(!ValidateUtils.isNull(operateId) && OperateEnum.validate(operateId))
) {
return false;
}
return true;
} }
} }

View File

@@ -1,6 +1,7 @@
package com.xiaojukeji.kafka.manager.common.entity.pojo; package com.xiaojukeji.kafka.manager.common.entity.pojo;
import java.util.Date; import java.util.Date;
import java.util.Objects;
/** /**
* @author zengqiao * @author zengqiao
@@ -116,4 +117,22 @@ public class ClusterDO implements Comparable<ClusterDO> {
public int compareTo(ClusterDO clusterDO) { public int compareTo(ClusterDO clusterDO) {
return this.id.compareTo(clusterDO.id); return this.id.compareTo(clusterDO.id);
} }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ClusterDO clusterDO = (ClusterDO) o;
return Objects.equals(id, clusterDO.id)
&& Objects.equals(clusterName, clusterDO.clusterName)
&& Objects.equals(zookeeper, clusterDO.zookeeper)
&& Objects.equals(bootstrapServers, clusterDO.bootstrapServers)
&& Objects.equals(securityProperties, clusterDO.securityProperties)
&& Objects.equals(jmxProperties, clusterDO.jmxProperties);
}
@Override
public int hashCode() {
return Objects.hash(id, clusterName, zookeeper, bootstrapServers, securityProperties, jmxProperties);
}
} }

View File

@@ -1,6 +1,7 @@
package com.xiaojukeji.kafka.manager.common.entity.pojo; package com.xiaojukeji.kafka.manager.common.entity.pojo;
import com.xiaojukeji.kafka.manager.common.entity.dto.op.topic.TopicCreationDTO; import com.xiaojukeji.kafka.manager.common.entity.dto.op.topic.TopicCreationDTO;
import com.xiaojukeji.kafka.manager.common.utils.ValidateUtils;
import java.util.Date; import java.util.Date;
@@ -95,6 +96,7 @@ public class TopicDO {
topicDO.setClusterId(dto.getClusterId()); topicDO.setClusterId(dto.getClusterId());
topicDO.setTopicName(dto.getTopicName()); topicDO.setTopicName(dto.getTopicName());
topicDO.setDescription(dto.getDescription()); topicDO.setDescription(dto.getDescription());
topicDO.setPeakBytesIn(ValidateUtils.isNull(dto.getPeakBytesIn()) ? -1L : dto.getPeakBytesIn());
return topicDO; return topicDO;
} }
} }

View File

@@ -17,6 +17,8 @@ public class GatewayConfigDO {
private Long version; private Long version;
private String description;
private Date createTime; private Date createTime;
private Date modifyTime; private Date modifyTime;
@@ -61,6 +63,14 @@ public class GatewayConfigDO {
this.version = version; this.version = version;
} }
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Date getCreateTime() { public Date getCreateTime() {
return createTime; return createTime;
} }
@@ -85,6 +95,7 @@ public class GatewayConfigDO {
", name='" + name + '\'' + ", name='" + name + '\'' +
", value='" + value + '\'' + ", value='" + value + '\'' +
", version=" + version + ", version=" + version +
", description='" + description + '\'' +
", createTime=" + createTime + ", createTime=" + createTime +
", modifyTime=" + modifyTime + ", modifyTime=" + modifyTime +
'}'; '}';

View File

@@ -1,4 +1,4 @@
package com.xiaojukeji.kafka.manager.openapi.common.vo; package com.xiaojukeji.kafka.manager.common.entity.vo.normal.topic;
import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiModelProperty;
@@ -14,7 +14,6 @@ public class TopicStatisticMetricsVO {
public TopicStatisticMetricsVO(Double peakBytesIn) { public TopicStatisticMetricsVO(Double peakBytesIn) {
this.peakBytesIn = peakBytesIn; this.peakBytesIn = peakBytesIn;
} }
public Double getPeakBytesIn() { public Double getPeakBytesIn() {

View File

@@ -28,7 +28,7 @@ public class ExpiredTopicVO {
@ApiModelProperty(value = "负责人") @ApiModelProperty(value = "负责人")
private String principals; private String principals;
@ApiModelProperty(value = "状态, -1:可下线, 0:过期待通知, 1+:已通知待反馈") @ApiModelProperty(value = "状态, -1:已通知可下线, 0:过期待通知, 1+:已通知待反馈")
private Integer status; private Integer status;
public Long getClusterId() { public Long getClusterId() {

View File

@@ -26,6 +26,9 @@ public class GatewayConfigVO {
@ApiModelProperty(value="版本") @ApiModelProperty(value="版本")
private Long version; private Long version;
@ApiModelProperty(value="描述说明")
private String description;
@ApiModelProperty(value="创建时间") @ApiModelProperty(value="创建时间")
private Date createTime; private Date createTime;
@@ -72,6 +75,14 @@ public class GatewayConfigVO {
this.version = version; this.version = version;
} }
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Date getCreateTime() { public Date getCreateTime() {
return createTime; return createTime;
} }
@@ -96,6 +107,7 @@ public class GatewayConfigVO {
", name='" + name + '\'' + ", name='" + name + '\'' +
", value='" + value + '\'' + ", value='" + value + '\'' +
", version=" + version + ", version=" + version +
", description='" + description + '\'' +
", createTime=" + createTime + ", createTime=" + createTime +
", modifyTime=" + modifyTime + ", modifyTime=" + modifyTime +
'}'; '}';

View File

@@ -60,6 +60,13 @@ public class JsonUtils {
return JSON.parseObject(src, clazz); return JSON.parseObject(src, clazz);
} }
public static <T> List<T> stringToArrObj(String src, Class<T> clazz) {
if (ValidateUtils.isBlank(src)) {
return null;
}
return JSON.parseArray(src, clazz);
}
public static List<TopicConnectionDO> parseTopicConnections(Long clusterId, JSONObject jsonObject, long postTime) { public static List<TopicConnectionDO> parseTopicConnections(Long clusterId, JSONObject jsonObject, long postTime) {
List<TopicConnectionDO> connectionDOList = new ArrayList<>(); List<TopicConnectionDO> connectionDOList = new ArrayList<>();
for (String clientType: jsonObject.keySet()) { for (String clientType: jsonObject.keySet()) {

View File

@@ -2,6 +2,7 @@ package com.xiaojukeji.kafka.manager.common.utils;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@@ -11,6 +12,20 @@ import java.util.Set;
* @date 20/4/16 * @date 20/4/16
*/ */
public class ValidateUtils { public class ValidateUtils {
/**
* 任意一个为空, 则返回true
*/
public static boolean anyNull(Object... objects) {
return Arrays.stream(objects).anyMatch(ValidateUtils::isNull);
}
/**
* 是空字符串或者空
*/
public static boolean anyBlank(String... strings) {
return Arrays.stream(strings).anyMatch(StringUtils::isBlank);
}
/** /**
* 为空 * 为空
*/ */

View File

@@ -79,7 +79,7 @@ public class JmxConnectorWrap {
try { try {
Map<String, Object> environment = new HashMap<String, Object>(); Map<String, Object> environment = new HashMap<String, Object>();
if (!ValidateUtils.isBlank(this.jmxConfig.getUsername()) && !ValidateUtils.isBlank(this.jmxConfig.getPassword())) { if (!ValidateUtils.isBlank(this.jmxConfig.getUsername()) && !ValidateUtils.isBlank(this.jmxConfig.getPassword())) {
environment.put(javax.management.remote.JMXConnector.CREDENTIALS, Arrays.asList(this.jmxConfig.getUsername(), this.jmxConfig.getPassword())); environment.put(JMXConnector.CREDENTIALS, Arrays.asList(this.jmxConfig.getUsername(), this.jmxConfig.getPassword()));
} }
if (jmxConfig.isOpenSSL() != null && this.jmxConfig.isOpenSSL()) { if (jmxConfig.isOpenSSL() != null && this.jmxConfig.isOpenSSL()) {
environment.put(Context.SECURITY_PROTOCOL, "ssl"); environment.put(Context.SECURITY_PROTOCOL, "ssl");

View File

@@ -33,7 +33,9 @@ public class ZkPathUtil {
private static final String D_METRICS_CONFIG_ROOT_NODE = CONFIG_ROOT_NODE + ZOOKEEPER_SEPARATOR + "KafkaExMetrics"; private static final String D_METRICS_CONFIG_ROOT_NODE = CONFIG_ROOT_NODE + ZOOKEEPER_SEPARATOR + "KafkaExMetrics";
public static final String D_CONTROLLER_CANDIDATES = CONFIG_ROOT_NODE + ZOOKEEPER_SEPARATOR + "extension/candidates"; public static final String D_CONFIG_EXTENSION_ROOT_NODE = CONFIG_ROOT_NODE + ZOOKEEPER_SEPARATOR + "extension";
public static final String D_CONTROLLER_CANDIDATES = D_CONFIG_EXTENSION_ROOT_NODE + ZOOKEEPER_SEPARATOR + "candidates";
public static String getBrokerIdNodePath(Integer brokerId) { public static String getBrokerIdNodePath(Integer brokerId) {
return BROKER_IDS_ROOT + ZOOKEEPER_SEPARATOR + String.valueOf(brokerId); return BROKER_IDS_ROOT + ZOOKEEPER_SEPARATOR + String.valueOf(brokerId);
@@ -111,6 +113,10 @@ public class ZkPathUtil {
} }
public static String getKafkaExtraMetricsPath(Integer brokerId) { public static String getKafkaExtraMetricsPath(Integer brokerId) {
return D_METRICS_CONFIG_ROOT_NODE + ZOOKEEPER_SEPARATOR + String.valueOf(brokerId); return D_METRICS_CONFIG_ROOT_NODE + ZOOKEEPER_SEPARATOR + brokerId;
}
public static String getControllerCandidatePath(Integer brokerId) {
return D_CONTROLLER_CANDIDATES + ZOOKEEPER_SEPARATOR + brokerId;
} }
} }

View File

@@ -1,8 +1,5 @@
package com.xiaojukeji.kafka.manager.common.zookeeper.znode.brokers; package com.xiaojukeji.kafka.manager.common.zookeeper.znode.brokers;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List; import java.util.List;
/** /**
@@ -18,12 +15,11 @@ import java.util.List;
* "host":null, * "host":null,
* "timestamp":"1546632983233", * "timestamp":"1546632983233",
* "port":-1, * "port":-1,
* "version":4 * "version":4,
* "rack": "CY"
* } * }
*/ */
public class BrokerMetadata implements Cloneable { public class BrokerMetadata implements Cloneable {
private final static Logger LOGGER = LoggerFactory.getLogger(TopicMetadata.class);
private long clusterId; private long clusterId;
private int brokerId; private int brokerId;
@@ -43,6 +39,8 @@ public class BrokerMetadata implements Cloneable {
private long timestamp; private long timestamp;
private String rack;
public long getClusterId() { public long getClusterId() {
return clusterId; return clusterId;
} }
@@ -107,14 +105,12 @@ public class BrokerMetadata implements Cloneable {
this.timestamp = timestamp; this.timestamp = timestamp;
} }
@Override public String getRack() {
public Object clone() { return rack;
try { }
return super.clone();
} catch (CloneNotSupportedException var3) { public void setRack(String rack) {
LOGGER.error("clone BrokerMetadata failed.", var3); this.rack = rack;
}
return null;
} }
@Override @Override
@@ -128,6 +124,7 @@ public class BrokerMetadata implements Cloneable {
", jmxPort=" + jmx_port + ", jmxPort=" + jmx_port +
", version='" + version + '\'' + ", version='" + version + '\'' +
", timestamp=" + timestamp + ", timestamp=" + timestamp +
", rack='" + rack + '\'' +
'}'; '}';
} }
} }

View File

@@ -0,0 +1,18 @@
package com.xiaojukeji.kafka.manager.common.utils;
import org.junit.Assert;
import org.junit.Test;
import java.util.HashMap;
import java.util.Map;
public class JsonUtilsTest {
@Test
public void testMapToJsonString() {
Map<String, Object> map = new HashMap<>();
map.put("key", "value");
map.put("int", 1);
String expectRes = "{\"key\":\"value\",\"int\":1}";
Assert.assertEquals(expectRes, JsonUtils.toJSONString(map));
}
}

View File

@@ -1,6 +1,6 @@
{ {
"name": "mobx-ts-example", "name": "logi-kafka",
"version": "1.0.0", "version": "2.3.1",
"description": "", "description": "",
"scripts": { "scripts": {
"start": "webpack-dev-server", "start": "webpack-dev-server",
@@ -21,7 +21,7 @@
"@types/spark-md5": "^3.0.2", "@types/spark-md5": "^3.0.2",
"antd": "^3.26.15", "antd": "^3.26.15",
"clean-webpack-plugin": "^3.0.0", "clean-webpack-plugin": "^3.0.0",
"clipboard": "^2.0.6", "clipboard": "2.0.6",
"cross-env": "^7.0.2", "cross-env": "^7.0.2",
"css-loader": "^2.1.0", "css-loader": "^2.1.0",
"echarts": "^4.5.0", "echarts": "^4.5.0",

View File

@@ -94,6 +94,9 @@ import 'antd/es/divider/style';
import Upload from 'antd/es/upload'; import Upload from 'antd/es/upload';
import 'antd/es/upload/style'; import 'antd/es/upload/style';
import Transfer from 'antd/es/transfer';
import 'antd/es/transfer/style';
import TimePicker from 'antd/es/time-picker'; import TimePicker from 'antd/es/time-picker';
import 'antd/es/time-picker/style'; import 'antd/es/time-picker/style';
@@ -142,5 +145,6 @@ export {
TimePicker, TimePicker,
RangePickerValue, RangePickerValue,
Badge, Badge,
Popover Popover,
Transfer
}; };

View File

@@ -25,7 +25,7 @@
.editor{ .editor{
height: 100%; height: 100%;
position: absolute; position: absolute;
left: -14%; left: -12%;
width: 120%; width: 120%;
} }
} }

View File

@@ -21,24 +21,12 @@ class Monacoeditor extends React.Component<IEditorProps> {
public state = { public state = {
placeholder: '', placeholder: '',
}; };
// public arr = '{"clusterId":95,"startId":37397856,"step":100,"topicName":"kmo_topic_metrics_tempory_zq"}';
// public Ars(a: string) {
// const obj = JSON.parse(a);
// const newobj: any = {};
// for (const item in obj) {
// if (typeof obj[item] === 'object') {
// this.Ars(obj[item]);
// } else {
// newobj[item] = obj[item];
// }
// }
// return JSON.stringify(newobj);
// }
public async componentDidMount() { public async componentDidMount() {
const { value, onChange } = this.props; const { value, onChange } = this.props;
const format: any = await format2json(value); const format: any = await format2json(value);
this.editor = monaco.editor.create(this.ref, { this.editor = monaco.editor.create(this.ref, {
value: format.result, value: format.result || value,
language: 'json', language: 'json',
lineNumbers: 'off', lineNumbers: 'off',
scrollBeyondLastLine: false, scrollBeyondLastLine: false,
@@ -48,7 +36,7 @@ class Monacoeditor extends React.Component<IEditorProps> {
minimap: { minimap: {
enabled: false, enabled: false,
}, },
// automaticLayout: true, // 自动布局 automaticLayout: true, // 自动布局
glyphMargin: true, // 字形边缘 {},[] glyphMargin: true, // 字形边缘 {},[]
// useTabStops: false, // useTabStops: false,
// formatOnPaste: true, // formatOnPaste: true,

View File

@@ -68,8 +68,8 @@ export class StatusGraghCom<T extends IFlowInfo> extends React.Component {
public render() { public render() {
const statusData = this.getData(); const statusData = this.getData();
const loading = this.getLoading(); const loading = this.getLoading();
if (!statusData) return null;
const data: any[] = []; const data: any[] = [];
if (!statusData) return <Table columns={flowColumns} dataSource={data} />;
Object.keys(statusData).map((key) => { Object.keys(statusData).map((key) => {
if (statusData[key]) { if (statusData[key]) {
const v = key === 'byteIn' || key === 'byteOut' ? statusData[key].map(i => i && (i / 1024).toFixed(2)) : const v = key === 'byteIn' || key === 'byteOut' ? statusData[key].map(i => i && (i / 1024).toFixed(2)) :
@@ -85,7 +85,7 @@ export class StatusGraghCom<T extends IFlowInfo> extends React.Component {
} }
}); });
return ( return (
<Table columns={flowColumns} dataSource={data} pagination={false} loading={loading}/> <Table columns={flowColumns} dataSource={data} pagination={false} loading={loading} />
); );
} }
} }

View File

@@ -1,4 +1,4 @@
.ant-input-number { .ant-input-number, .ant-form-item-children .ant-select {
width: 314px width: 314px
} }

View File

@@ -130,6 +130,8 @@ class XForm extends React.Component<IXFormProps> {
this.renderFormItem(formItem), this.renderFormItem(formItem),
)} )}
{formItem.renderExtraElement ? formItem.renderExtraElement() : null} {formItem.renderExtraElement ? formItem.renderExtraElement() : null}
{/* 添加保存时间提示文案 */}
{formItem.attrs?.prompttype ? <span style={{ color: "#cccccc", fontSize: '12px', lineHeight: '20px', display: 'block' }}>{formItem.attrs.prompttype}</span> : null}
</Form.Item> </Form.Item>
); );
})} })}

View File

@@ -59,6 +59,10 @@ export const adminMenu = [{
href: `/admin/bill`, href: `/admin/bill`,
i: 'k-icon-renwuliebiao', i: 'k-icon-renwuliebiao',
title: '用户账单', title: '用户账单',
},{
href: `/admin/operation-record`,
i: 'k-icon-operationrecord',
title: '操作记录',
}] as ILeftMenu[]; }] as ILeftMenu[];
export const expertMenu = [{ export const expertMenu = [{

View File

@@ -67,7 +67,7 @@ export const timeMonthStr = 'YYYY/MM';
// tslint:disable-next-line:max-line-length // tslint:disable-next-line:max-line-length
export const indexUrl ={ export const indexUrl ={
indexUrl:'https://github.com/didi/kafka-manager', indexUrl:'https://github.com/didi/Logi-KafkaManager/blob/master/docs/user_guide/kafka_metrics_desc.md', // 指标说明
cagUrl:'https://github.com/didi/Logi-KafkaManager/blob/master/docs/user_guide/add_cluster/add_cluster.md', // 集群接入指南 Cluster access Guide cagUrl:'https://github.com/didi/Logi-KafkaManager/blob/master/docs/user_guide/add_cluster/add_cluster.md', // 集群接入指南 Cluster access Guide
} }

View File

@@ -100,7 +100,7 @@ export class ClusterConsumer extends SearchAndFilterContainer {
<div className="k-row"> <div className="k-row">
<ul className="k-tab"> <ul className="k-tab">
<li>{this.props.tab}</li> <li>{this.props.tab}</li>
{this.renderSearch()} {this.renderSearch('', '请输入消费组名称')}
</ul> </ul>
<Table <Table
columns={this.columns} columns={this.columns}

View File

@@ -2,7 +2,8 @@
import * as React from 'react'; import * as React from 'react';
import { SearchAndFilterContainer } from 'container/search-filter'; import { SearchAndFilterContainer } from 'container/search-filter';
import { Table } from 'component/antd'; import { Table, Button, Popconfirm, Modal, Transfer, notification } from 'component/antd';
// import { Transfer } from 'antd'
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { pagination } from 'constants/table'; import { pagination } from 'constants/table';
import Url from 'lib/url-parser'; import Url from 'lib/url-parser';
@@ -16,8 +17,12 @@ import { timeFormat } from 'constants/strategy';
export class ClusterController extends SearchAndFilterContainer { export class ClusterController extends SearchAndFilterContainer {
public clusterId: number; public clusterId: number;
public state = { public state: any = {
searchKey: '', searchKey: '',
searchCandidateKey: '',
isCandidateModel: false,
mockData: [],
targetKeys: [],
}; };
constructor(props: any) { constructor(props: any) {
@@ -37,6 +42,94 @@ export class ClusterController extends SearchAndFilterContainer {
return data; return data;
} }
public getCandidateData<T extends IController>(origin: T[]) {
let data: T[] = origin;
let { searchCandidateKey } = this.state;
searchCandidateKey = (searchCandidateKey + '').trim().toLowerCase();
data = searchCandidateKey ? origin.filter((item: IController) =>
(item.host !== undefined && item.host !== null) && item.host.toLowerCase().includes(searchCandidateKey as string),
) : origin;
return data;
}
// 候选controller
public renderCandidateController() {
const columns = [
{
title: 'BrokerId',
dataIndex: 'brokerId',
key: 'brokerId',
width: '20%',
sorter: (a: IController, b: IController) => b.brokerId - a.brokerId,
render: (r: string, t: IController) => {
return (
<a href={`${this.urlPrefix}/admin/broker-detail?clusterId=${this.clusterId}&brokerId=${t.brokerId}`}>{r}
</a>
);
},
},
{
title: 'BrokerHost',
key: 'host',
dataIndex: 'host',
width: '20%',
// render: (r: string, t: IController) => {
// return (
// <a href={`${this.urlPrefix}/admin/broker-detail?clusterId=${this.clusterId}&brokerId=${t.brokerId}`}>{r}
// </a>
// );
// },
},
{
title: 'Broker状态',
key: 'status',
dataIndex: 'status',
width: '20%',
render: (r: number, t: IController) => {
return (
<span>{r === 1 ? '不在线' : '在线'}</span>
);
},
},
{
title: '创建时间',
dataIndex: 'startTime',
key: 'startTime',
width: '25%',
sorter: (a: IController, b: IController) => b.timestamp - a.timestamp,
render: (t: number) => moment(t).format(timeFormat),
},
{
title: '操作',
dataIndex: 'operation',
key: 'operation',
width: '15%',
render: (r: string, t: IController) => {
return (
<Popconfirm
title="确定删除?"
onConfirm={() => this.deleteCandidateCancel(t)}
cancelText="取消"
okText="确认"
>
<a></a>
</Popconfirm>
);
},
},
];
return (
<Table
columns={columns}
dataSource={this.getCandidateData(admin.controllerCandidate)}
pagination={pagination}
rowKey="key"
/>
);
}
public renderController() { public renderController() {
const columns = [ const columns = [
@@ -58,12 +151,6 @@ export class ClusterController extends SearchAndFilterContainer {
key: 'host', key: 'host',
dataIndex: 'host', dataIndex: 'host',
width: '30%', width: '30%',
// render: (r: string, t: IController) => {
// return (
// <a href={`${this.urlPrefix}/admin/broker-detail?clusterId=${this.clusterId}&brokerId=${t.brokerId}`}>{r}
// </a>
// );
// },
}, },
{ {
title: '变更时间', title: '变更时间',
@@ -87,16 +174,104 @@ export class ClusterController extends SearchAndFilterContainer {
public componentDidMount() { public componentDidMount() {
admin.getControllerHistory(this.clusterId); admin.getControllerHistory(this.clusterId);
admin.getCandidateController(this.clusterId);
admin.getBrokersMetadata(this.clusterId);
}
public addController = () => {
this.setState({ isCandidateModel: true, targetKeys: [] })
}
public addCandidateChange = (targetKeys: any) => {
this.setState({ targetKeys })
}
public handleCandidateCancel = () => {
this.setState({ isCandidateModel: false });
}
public handleCandidateOk = () => {
let brokerIdList = this.state.targetKeys.map((item: any) => {
return admin.brokersMetadata[item].brokerId
})
admin.addCandidateController(this.clusterId, brokerIdList).then(data => {
notification.success({ message: '新增成功' });
admin.getCandidateController(this.clusterId);
}).catch(err => {
notification.error({ message: '新增失败' });
})
this.setState({ isCandidateModel: false, targetKeys: [] });
}
public deleteCandidateCancel = (target: any) => {
admin.deleteCandidateCancel(this.clusterId, [target.brokerId]).then(() => {
notification.success({ message: '删除成功' });
});
this.setState({ isCandidateModel: false });
}
public renderAddCandidateController() {
let filterControllerCandidate = admin.brokersMetadata.filter((item: any) => {
return !admin.filtercontrollerCandidate.includes(item.brokerId)
})
return (
<Modal
title="新增候选Controller"
visible={this.state.isCandidateModel}
// okText="确认"
// cancelText="取消"
maskClosable={false}
// onOk={() => this.handleCandidateOk()}
onCancel={() => this.handleCandidateCancel()}
footer={<>
<Button style={{ width: '60px' }} onClick={() => this.handleCandidateCancel()}></Button>
<Button disabled={this.state.targetKeys.length > 0 ? false : true} style={{ width: '60px' }} type="primary" onClick={() => this.handleCandidateOk()}></Button>
</>
}
>
<Transfer
dataSource={filterControllerCandidate}
targetKeys={this.state.targetKeys}
render={item => item.host}
onChange={(targetKeys) => this.addCandidateChange(targetKeys)}
titles={['未选', '已选']}
locale={{
itemUnit: '项',
itemsUnit: '项',
}}
listStyle={{
width: "45%",
}}
/>
</Modal>
);
} }
public render() { public render() {
return ( return (
<div className="k-row"> <div className="k-row">
<ul className="k-tab"> <ul className="k-tab">
<li>
<span>Controller</span>
<span style={{ display: 'inline-block', color: "#a7a8a9", fontSize: '12px', marginLeft: '15px' }}>Controller将会优先从以下Broker中选举</span>
</li>
<div style={{ display: 'flex' }}>
<div style={{ marginRight: '15px' }}>
<Button onClick={() => this.addController()} type='primary'>Controller</Button>
</div>
{this.renderSearch('', '请查找Host', 'searchCandidateKey')}
</div>
</ul>
{this.renderCandidateController()}
<ul className="k-tab" style={{ marginTop: '10px' }}>
<li>{this.props.tab}</li> <li>{this.props.tab}</li>
{this.renderSearch('', '请输入Host')} {this.renderSearch('', '请输入Host')}
</ul> </ul>
{this.renderController()} {this.renderController()}
{this.renderAddCandidateController()}
</div> </div>
); );
} }

View File

@@ -172,7 +172,7 @@ export class ClusterTopic extends SearchAndFilterContainer {
key: 'appName', key: 'appName',
// width: '10%', // width: '10%',
render: (val: string, record: IClusterTopics) => ( render: (val: string, record: IClusterTopics) => (
<Tooltip placement="bottomLeft" title={record.appId} > <Tooltip placement="bottomLeft" title={val} >
{val} {val}
</Tooltip> </Tooltip>
), ),

View File

@@ -314,8 +314,7 @@ export class ExclusiveCluster extends SearchAndFilterContainer {
> >
<div className="region-prompt"> <div className="region-prompt">
<span> <span>
Region已被逻辑集群 {this.state.logicalClusterName} 使 Region已被逻辑集群 {this.state.logicalClusterName} 使Region与逻辑集群的关系
Region与逻辑集群的关系
</span> </span>
</div> </div>
</Modal> </Modal>

View File

@@ -16,7 +16,7 @@
.traffic-table { .traffic-table {
margin: 10px 0; margin: 10px 0;
min-height: 450px; min-height: 330px;
.traffic-header { .traffic-header {
width: 100%; width: 100%;
height: 44px; height: 44px;
@@ -95,3 +95,9 @@
font-weight: bold; font-weight: bold;
text-align: center; text-align: center;
} }
.asd{
display: flex;
justify-content: space-around;
align-items: center;
}

View File

@@ -32,9 +32,9 @@ export class ClusterDetail extends React.Component {
} }
public render() { public render() {
let content = {} as IMetaData; let content = {} as IMetaData;
content = admin.basicInfo ? admin.basicInfo : content; content = admin.basicInfo ? admin.basicInfo : content;
return ( return (
<> <>
<PageHeader <PageHeader
className="detail topic-detail-header" className="detail topic-detail-header"
@@ -46,7 +46,7 @@ export class ClusterDetail extends React.Component {
<ClusterOverview basicInfo={content} /> <ClusterOverview basicInfo={content} />
</TabPane> </TabPane>
<TabPane tab="Topic信息" key="2"> <TabPane tab="Topic信息" key="2">
<ClusterTopic tab={'Topic信息'}/> <ClusterTopic tab={'Topic信息'} />
</TabPane> </TabPane>
<TabPane tab="Broker信息" key="3"> <TabPane tab="Broker信息" key="3">
<ClusterBroker tab={'Broker信息'} basicInfo={content} /> <ClusterBroker tab={'Broker信息'} basicInfo={content} />
@@ -60,11 +60,11 @@ export class ClusterDetail extends React.Component {
<TabPane tab="逻辑集群信息" key="6"> <TabPane tab="逻辑集群信息" key="6">
<LogicalCluster tab={'逻辑集群信息'} basicInfo={content} /> <LogicalCluster tab={'逻辑集群信息'} basicInfo={content} />
</TabPane> </TabPane>
<TabPane tab="Controller变更历史" key="7"> <TabPane tab="Controller信息" key="7">
<ClusterController tab={'Controller变更历史'} /> <ClusterController tab={'Controller变更历史'} />
</TabPane> </TabPane>
<TabPane tab="限流信息" key="8"> <TabPane tab="限流信息" key="8">
<CurrentLimiting tab={'限流信息'}/> <CurrentLimiting tab={'限流信息'} />
</TabPane> </TabPane>
</Tabs> </Tabs>
</> </>

View File

@@ -4,6 +4,7 @@ import { wrapper } from 'store';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { IXFormWrapper, IMetaData, IRegister } from 'types/base-type'; import { IXFormWrapper, IMetaData, IRegister } from 'types/base-type';
import { admin } from 'store/admin'; import { admin } from 'store/admin';
import { users } from 'store/users';
import { registerCluster, createCluster, pauseMonitoring } from 'lib/api'; import { registerCluster, createCluster, pauseMonitoring } from 'lib/api';
import { SearchAndFilterContainer } from 'container/search-filter'; import { SearchAndFilterContainer } from 'container/search-filter';
import { cluster } from 'store/cluster'; import { cluster } from 'store/cluster';
@@ -12,6 +13,7 @@ import { urlPrefix } from 'constants/left-menu';
import { indexUrl } from 'constants/strategy' import { indexUrl } from 'constants/strategy'
import { region } from 'store'; import { region } from 'store';
import './index.less'; import './index.less';
import Monacoeditor from 'component/editor/monacoEditor';
import { getAdminClusterColumns } from '../config'; import { getAdminClusterColumns } from '../config';
const { confirm } = Modal; const { confirm } = Modal;
@@ -77,34 +79,34 @@ export class ClusterList extends SearchAndFilterContainer {
disabled: item ? true : false, disabled: item ? true : false,
}, },
}, },
{ // {
key: 'idc', // key: 'idc',
label: '数据中心', // label: '数据中心',
defaultValue: region.regionName, // defaultValue: region.regionName,
rules: [{ required: true, message: '请输入数据中心' }], // rules: [{ required: true, message: '请输入数据中心' }],
attrs: { // attrs: {
placeholder: '请输入数据中心', // placeholder: '请输入数据中心',
disabled: true, // disabled: true,
}, // },
}, // },
{ // {
key: 'mode', // key: 'mode',
label: '集群类型', // label: '集群类型',
type: 'select', // type: 'select',
options: cluster.clusterModes.map(ele => { // options: cluster.clusterModes.map(ele => {
return { // return {
label: ele.message, // label: ele.message,
value: ele.code, // value: ele.code,
}; // };
}), // }),
rules: [{ // rules: [{
required: true, // required: true,
message: '请选择集群类型', // message: '请选择集群类型',
}], // }],
attrs: { // attrs: {
placeholder: '请选择集群类型', // placeholder: '请选择集群类型',
}, // },
}, // },
{ {
key: 'kafkaVersion', key: 'kafkaVersion',
label: 'kafka版本', label: 'kafka版本',
@@ -132,6 +134,25 @@ export class ClusterList extends SearchAndFilterContainer {
"security.protocol": "SASL_PLAINTEXT", "security.protocol": "SASL_PLAINTEXT",
"sasl.mechanism": "PLAIN", "sasl.mechanism": "PLAIN",
"sasl.jaas.config": "org.apache.kafka.common.security.plain.PlainLoginModule required username=\\"xxxxxx\\" password=\\"xxxxxx\\";" "sasl.jaas.config": "org.apache.kafka.common.security.plain.PlainLoginModule required username=\\"xxxxxx\\" password=\\"xxxxxx\\";"
}`,
rows: 8,
},
},
{
key: 'jmxProperties',
label: 'JMX认证',
type: 'text_area',
rules: [{
required: false,
message: '请输入JMX认证',
}],
attrs: {
placeholder: `请输入JMX认证例如
{
"maxConn": 10, #KM对单台Broker的最大jmx连接数
"username": "xxxxx", #用户名
"password": "xxxxx", #密码
"openSSL": true, #开启SSLtrue表示开启SSLfalse表示关闭
}`, }`,
rows: 8, rows: 8,
}, },
@@ -256,32 +277,41 @@ export class ClusterList extends SearchAndFilterContainer {
public getColumns = () => { public getColumns = () => {
const cols = getAdminClusterColumns(); const cols = getAdminClusterColumns();
const role = users.currentUser.role;
const col = { const col = {
title: '操作', title: '操作',
render: (value: string, item: IMetaData) => ( render: (value: string, item: IMetaData) => (
<> <>
<a {
onClick={this.createOrRegisterCluster.bind(this, item)} role && role === 2 ? <>
className="action-button"
>
</a>
<Popconfirm
title={`确定${item.status === 1 ? '暂停' : '开始'}${item.clusterName}监控?`}
onConfirm={() => this.pauseMonitor(item)}
cancelText="取消"
okText="确认"
>
<Tooltip title="暂停监控将无法正常监控指标信息,建议开启监控">
<a <a
onClick={this.createOrRegisterCluster.bind(this, item)}
className="action-button" className="action-button"
> >
{item.status === 1 ? '暂停监控' : '开始监控'}
</a> </a>
</Tooltip> <Popconfirm
</Popconfirm> title={`确定${item.status === 1 ? '暂停' : '开始'}${item.clusterName}监控?`}
<a onClick={this.showMonitor.bind(this, item)}> onConfirm={() => this.pauseMonitor(item)}
cancelText="取消"
</a> okText="确认"
>
<Tooltip placement="left" title="暂停监控将无法正常监控指标信息,建议开启监控">
<a
className="action-button"
>
{item.status === 1 ? '暂停监控' : '开始监控'}
</a>
</Tooltip>
</Popconfirm>
<a onClick={this.showMonitor.bind(this, item)}>
</a>
</> : <Tooltip placement="left" title="该功能只对运维人员开放">
<a style={{ color: '#a0a0a0' }} className="action-button"></a>
<a className="action-button" style={{ color: '#a0a0a0' }}>{item.status === 1 ? '暂停监控' : '开始监控'}</a>
<a style={{ color: '#a0a0a0' }}></a>
</Tooltip>
}
</> </>
), ),
}; };
@@ -290,6 +320,7 @@ export class ClusterList extends SearchAndFilterContainer {
} }
public renderClusterList() { public renderClusterList() {
const role = users.currentUser.role;
return ( return (
<> <>
<div className="container"> <div className="container">
@@ -298,7 +329,14 @@ export class ClusterList extends SearchAndFilterContainer {
{this.renderSearch('', '请输入集群名称')} {this.renderSearch('', '请输入集群名称')}
<li className="right-btn-1"> <li className="right-btn-1">
<a style={{ display: 'inline-block', marginRight: '20px' }} href={indexUrl.cagUrl} target="_blank"></a> <a style={{ display: 'inline-block', marginRight: '20px' }} href={indexUrl.cagUrl} target="_blank"></a>
<Button type="primary" onClick={this.createOrRegisterCluster.bind(this, null)}></Button> {
role && role === 2 ?
<Button type="primary" onClick={this.createOrRegisterCluster.bind(this, null)}></Button>
:
<Tooltip placement="left" title="该功能只对运维人员开放" trigger='hover'>
<Button disabled type="primary"></Button>
</Tooltip>
}
</li> </li>
</ul> </ul>
</div> </div>

View File

@@ -1,8 +1,8 @@
import * as React from 'react'; import * as React from 'react';
import { IUser, IUploadFile, IConfigure, IMetaData, IBrokersPartitions } from 'types/base-type'; import { IUser, IUploadFile, IConfigure, IConfigGateway, IMetaData } from 'types/base-type';
import { users } from 'store/users'; import { users } from 'store/users';
import { version } from 'store/version'; import { version } from 'store/version';
import { showApplyModal, showModifyModal, showConfigureModal } from 'container/modal/admin'; import { showApplyModal, showApplyModalModifyPassword, showModifyModal, showConfigureModal, showConfigGatewayModal } from 'container/modal/admin';
import { Popconfirm, Tooltip } from 'component/antd'; import { Popconfirm, Tooltip } from 'component/antd';
import { admin } from 'store/admin'; import { admin } from 'store/admin';
import { cellStyle } from 'constants/table'; import { cellStyle } from 'constants/table';
@@ -27,6 +27,7 @@ export const getUserColumns = () => {
return ( return (
<span className="table-operation"> <span className="table-operation">
<a onClick={() => showApplyModal(record)}></a> <a onClick={() => showApplyModal(record)}></a>
<a onClick={() => showApplyModalModifyPassword(record)}></a>
<Popconfirm <Popconfirm
title="确定删除?" title="确定删除?"
onConfirm={() => users.deleteUser(record.username)} onConfirm={() => users.deleteUser(record.username)}
@@ -184,6 +185,87 @@ export const getConfigureColumns = () => {
return columns; return columns;
}; };
// 网关配置
export const getConfigColumns = () => {
const columns = [
{
title: '配置类型',
dataIndex: 'type',
key: 'type',
width: '25%',
ellipsis: true,
sorter: (a: IConfigGateway, b: IConfigGateway) => a.type.charCodeAt(0) - b.type.charCodeAt(0),
},
{
title: '配置键',
dataIndex: 'name',
key: 'name',
width: '15%',
ellipsis: true,
sorter: (a: IConfigGateway, b: IConfigGateway) => a.name.charCodeAt(0) - b.name.charCodeAt(0),
},
{
title: '配置值',
dataIndex: 'value',
key: 'value',
width: '20%',
ellipsis: true,
sorter: (a: IConfigGateway, b: IConfigGateway) => a.value.charCodeAt(0) - b.value.charCodeAt(0),
render: (t: string) => {
return t.substr(0, 1) === '{' && t.substr(0, -1) === '}' ? JSON.stringify(JSON.parse(t), null, 4) : t;
},
},
{
title: '修改时间',
dataIndex: 'modifyTime',
key: 'modifyTime',
width: '15%',
sorter: (a: IConfigGateway, b: IConfigGateway) => b.modifyTime - a.modifyTime,
render: (t: number) => moment(t).format(timeFormat),
},
{
title: '版本号',
dataIndex: 'version',
key: 'version',
width: '10%',
ellipsis: true,
sorter: (a: IConfigGateway, b: IConfigGateway) => b.version.charCodeAt(0) - a.version.charCodeAt(0),
},
{
title: '描述信息',
dataIndex: 'description',
key: 'description',
width: '20%',
ellipsis: true,
onCell: () => ({
style: {
maxWidth: 180,
...cellStyle,
},
}),
},
{
title: '操作',
width: '10%',
render: (text: string, record: IConfigGateway) => {
return (
<span className="table-operation">
<a onClick={() => showConfigGatewayModal(record)}></a>
<Popconfirm
title="确定删除?"
onConfirm={() => admin.deleteConfigGateway({ id: record.id })}
cancelText="取消"
okText="确认"
>
<a></a>
</Popconfirm>
</span>);
},
},
];
return columns;
};
const renderClusterHref = (value: number | string, item: IMetaData, key: number) => { const renderClusterHref = (value: number | string, item: IMetaData, key: number) => {
return ( // 0 暂停监控--不可点击 1 监控中---可正常点击 return ( // 0 暂停监控--不可点击 1 监控中---可正常点击
<> <>

View File

@@ -3,11 +3,11 @@ import { SearchAndFilterContainer } from 'container/search-filter';
import { Table, Button, Spin } from 'component/antd'; import { Table, Button, Spin } from 'component/antd';
import { admin } from 'store/admin'; import { admin } from 'store/admin';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { IConfigure } from 'types/base-type'; import { IConfigure, IConfigGateway } from 'types/base-type';
import { users } from 'store/users'; import { users } from 'store/users';
import { pagination } from 'constants/table'; import { pagination } from 'constants/table';
import { getConfigureColumns } from './config'; import { getConfigureColumns, getConfigColumns } from './config';
import { showConfigureModal } from 'container/modal/admin'; import { showConfigureModal, showConfigGatewayModal } from 'container/modal/admin';
@observer @observer
export class ConfigureManagement extends SearchAndFilterContainer { export class ConfigureManagement extends SearchAndFilterContainer {
@@ -17,7 +17,12 @@ export class ConfigureManagement extends SearchAndFilterContainer {
}; };
public componentDidMount() { public componentDidMount() {
admin.getConfigure(); if (this.props.isShow) {
admin.getGatewayList();
admin.getGatewayType();
} else {
admin.getConfigure();
}
} }
public getData<T extends IConfigure>(origin: T[]) { public getData<T extends IConfigure>(origin: T[]) {
@@ -34,15 +39,34 @@ export class ConfigureManagement extends SearchAndFilterContainer {
return data; return data;
} }
public getGatewayData<T extends IConfigGateway>(origin: T[]) {
let data: T[] = origin;
let { searchKey } = this.state;
searchKey = (searchKey + '').trim().toLowerCase();
data = searchKey ? origin.filter((item: IConfigGateway) =>
((item.name !== undefined && item.name !== null) && item.name.toLowerCase().includes(searchKey as string))
|| ((item.value !== undefined && item.value !== null) && item.value.toLowerCase().includes(searchKey as string))
|| ((item.description !== undefined && item.description !== null) &&
item.description.toLowerCase().includes(searchKey as string)),
) : origin;
return data;
}
public renderTable() { public renderTable() {
return ( return (
<Spin spinning={users.loading}> <Spin spinning={users.loading}>
<Table {this.props.isShow ? <Table
rowKey="key"
columns={getConfigColumns()}
dataSource={this.getGatewayData(admin.configGatewayList)}
pagination={pagination}
/> : <Table
rowKey="key" rowKey="key"
columns={getConfigureColumns()} columns={getConfigureColumns()}
dataSource={this.getData(admin.configureList)} dataSource={this.getData(admin.configureList)}
pagination={pagination} pagination={pagination}
/> />}
</Spin> </Spin>
); );
@@ -53,7 +77,7 @@ export class ConfigureManagement extends SearchAndFilterContainer {
<ul> <ul>
{this.renderSearch('', '请输入配置键、值或描述')} {this.renderSearch('', '请输入配置键、值或描述')}
<li className="right-btn-1"> <li className="right-btn-1">
<Button type="primary" onClick={() => showConfigureModal()}></Button> <Button type="primary" onClick={() => this.props.isShow ? showConfigGatewayModal() : showConfigureModal()}></Button>
</li> </li>
</ul> </ul>
); );

View File

@@ -6,6 +6,7 @@ import { curveKeys, CURVE_KEY_MAP, PERIOD_RADIO_MAP, PERIOD_RADIO } from './conf
import moment = require('moment'); import moment = require('moment');
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { timeStampStr } from 'constants/strategy'; import { timeStampStr } from 'constants/strategy';
import { adminMonitor } from 'store/admin-monitor';
@observer @observer
export class DataCurveFilter extends React.Component { export class DataCurveFilter extends React.Component {
@@ -21,6 +22,7 @@ export class DataCurveFilter extends React.Component {
} }
public refreshAll = () => { public refreshAll = () => {
adminMonitor.setRequestId(null);
Object.keys(curveKeys).forEach((c: curveKeys) => { Object.keys(curveKeys).forEach((c: curveKeys) => {
const { typeInfo, curveInfo: option } = CURVE_KEY_MAP.get(c); const { typeInfo, curveInfo: option } = CURVE_KEY_MAP.get(c);
const { parser } = typeInfo; const { parser } = typeInfo;
@@ -32,7 +34,7 @@ export class DataCurveFilter extends React.Component {
return ( return (
<> <>
<Radio.Group onChange={this.radioChange} defaultValue={curveInfo.periodKey}> <Radio.Group onChange={this.radioChange} defaultValue={curveInfo.periodKey}>
{PERIOD_RADIO.map(p => <Radio.Button key={p.key} value={p.key}>{p.label}</Radio.Button>)} {PERIOD_RADIO.map(p => <Radio.Button key={p.key} value={p.key}>{p.label}</Radio.Button>)}
</Radio.Group> </Radio.Group>
<DatePicker.RangePicker <DatePicker.RangePicker
format={timeStampStr} format={timeStampStr}

View File

@@ -11,3 +11,5 @@ export * from './operation-management/migration-detail';
export * from './configure-management'; export * from './configure-management';
export * from './individual-bill'; export * from './individual-bill';
export * from './bill-detail'; export * from './bill-detail';
export * from './operation-record';

View File

@@ -0,0 +1,134 @@
import * as React from 'react';
import { cellStyle } from 'constants/table';
import { Tooltip } from 'antd';
import { admin } from 'store/admin';
import moment = require('moment');
const moduleList = [
{ moduleId: 0, moduleName: 'Topic' },
{ moduleId: 1, moduleName: '应用' },
{ moduleId: 2, moduleName: '配额' },
{ moduleId: 3, moduleName: '权限' },
{ moduleId: 4, moduleName: '集群' },
{ moduleId: 5, moduleName: '分区' },
{ moduleId: 6, moduleName: 'Gateway配置' },
]
export const operateList = {
0: '新增',
1: '删除',
2: '修改'
}
// [
// { operate: '新增', operateId: 0 },
// { operate: '删除', operateId: 1 },
// { operate: '修改', operateId: 2 },
// ]
export const getJarFuncForm: any = (props: any) => {
const formMap = [
{
key: 'moduleId',
label: '模块',
type: 'select',
attrs: {
style: {
width: '130px'
},
placeholder: '请选择模块',
},
options: moduleList.map(item => {
return {
label: item.moduleName,
value: item.moduleId
}
}),
formAttrs: {
initialvalue: 0,
},
},
{
key: 'operator',
label: '操作人',
type: 'input',
attrs: {
style: {
width: '170px'
},
placeholder: '请输入操作人'
},
getvaluefromevent: (event: any) => {
return event.target.value.replace(/\s+/g, '')
},
},
// {
// key: 'resource',
// label: '资源名称',
// type: 'input',
// attrs: {
// style: {
// width: '170px'
// },
// placeholder: '请输入资源名称'
// },
// },
// {
// key: 'content',
// label: '操作内容',
// type: 'input',
// attrs: {
// style: {
// width: '170px'
// },
// placeholder: '请输入操作内容'
// },
// },
]
return formMap;
}
export const getOperateColumns = () => {
const columns: any = [
{
title: '模块',
dataIndex: 'module',
key: 'module',
align: 'center',
width: '12%'
},
{
title: '资源名称',
dataIndex: 'resource',
key: 'resource',
align: 'center',
width: '12%'
},
{
title: '操作内容',
dataIndex: 'content',
key: 'content',
align: 'center',
width: '25%',
onCell: () => ({
style: {
maxWidth: 350,
...cellStyle,
},
}),
render: (text: string, record: any) => {
return (
<Tooltip placement="topLeft" title={text} >{text}</Tooltip>);
},
},
{
title: '操作人',
dataIndex: 'operator',
align: 'center',
width: '12%'
},
];
return columns
}

View File

@@ -0,0 +1,130 @@
import * as React from 'react';
import { observer } from 'mobx-react';
import { SearchAndFilterContainer } from 'container/search-filter';
import { IXFormWrapper, IMetaData, IRegister } from 'types/base-type';
import { admin } from 'store/admin';
import { customPagination, cellStyle } from 'constants/table';
import { Table, Tooltip } from 'component/antd';
import { timeFormat } from 'constants/strategy';
import { SearchFormComponent } from '../searchForm';
import { getJarFuncForm, operateList, getOperateColumns } from './config'
import moment = require('moment');
import { tableFilter } from 'lib/utils';
@observer
export class OperationRecord extends SearchAndFilterContainer {
public state: any = {
searchKey: '',
filteredInfo: null,
sortedInfo: null,
};
public getData<T extends IMetaData>(origin: T[]) {
let data: T[] = origin;
let { searchKey } = this.state;
searchKey = (searchKey + '').trim().toLowerCase();
data = searchKey ? origin.filter((item: IMetaData) =>
(item.clusterName !== undefined && item.clusterName !== null) && item.clusterName.toLowerCase().includes(searchKey as string),
) : origin;
return data;
};
public searchForm = (params: any) => {
// this.props.setFuncSubValue(params)
// getSystemFuncList(params).then(res => {
// this.props.setSysFuncList(res.data)
// this.props.setPagination(res.pagination)
// })
const { operator, moduleId } = params || {}
operator ? admin.getOperationRecordData(params) : admin.getOperationRecordData({ moduleId })
// getJarList(params).then(res => {
// this.props.setJarList(res.data)
// this.props.setPagination(res.pagination)
// })
}
public clearAll = () => {
this.setState({
filteredInfo: null,
sortedInfo: null,
});
};
public setHandleChange = (pagination: any, filters: any, sorter: any) => {
this.setState({
filteredInfo: filters,
sortedInfo: sorter,
});
}
public renderOperationRecordList() {
let { sortedInfo, filteredInfo } = this.state;
sortedInfo = sortedInfo || {};
filteredInfo = filteredInfo || {};
const operatingTime = Object.assign({
title: '操作时间',
dataIndex: 'modifyTime',
key: 'modifyTime',
align: 'center',
sorter: (a: any, b: any) => a.modifyTime - b.modifyTime,
render: (t: number) => moment(t).format(timeFormat),
width: '15%',
sortOrder: sortedInfo.columnKey === 'modifyTime' && sortedInfo.order,
});
const operatingPractice = Object.assign({
title: '行为',
dataIndex: 'operate',
key: 'operate',
align: 'center',
width: '12%',
filters: tableFilter<any>(this.getData(admin.oRList), 'operateId', operateList),
// filteredValue: filteredInfo.operate || null,
onFilter: (value: any, record: any) => {
return record.operateId === value
}
}, this.renderColumnsFilter('modifyTime'))
const columns = getOperateColumns()
columns.splice(0, 0, operatingTime);
columns.splice(3, 0, operatingPractice);
return (
<>
<div className="container">
<div className="table-operation-panel">
<SearchFormComponent
formMap={getJarFuncForm()}
onSubmit={(params: any) => this.searchForm(params)}
clearAll={() => this.clearAll()}
isReset={true}
/>
</div>
<div className="table-wrapper">
<Table
rowKey="key"
loading={admin.loading}
dataSource={this.getData(admin.oRList)}
columns={columns}
pagination={customPagination}
bordered
onChange={this.setHandleChange}
/>
</div>
</div>
</>
)
};
componentDidMount() {
admin.getOperationRecordData({ moduleId: 0 });
}
render() {
return <div>
{
this.renderOperationRecordList()
}
</div>
}
}

View File

@@ -13,17 +13,20 @@ export class PlatformManagement extends React.Component {
public render() { public render() {
return ( return (
<> <>
<Tabs activeKey={location.hash.substr(1) || '1'} type="card" onChange={handleTabKey}> <Tabs activeKey={location.hash.substr(1) || '1'} type="card" onChange={handleTabKey}>
<TabPane tab="应用管理" key="1"> <TabPane tab="应用管理" key="1">
<AdminAppList /> <AdminAppList />
</TabPane> </TabPane>
<TabPane tab="用户管理" key="2"> <TabPane tab="用户管理" key="2">
<UserManagement /> <UserManagement />
</TabPane> </TabPane>
<TabPane tab="配置管理" key="3"> <TabPane tab="平台配置" key="3">
<ConfigureManagement /> <ConfigureManagement isShow={false} />
</TabPane> </TabPane>
</Tabs> <TabPane tab="网关配置" key="4">
<ConfigureManagement isShow={true} />
</TabPane>
</Tabs>
</> </>
); );
} }

View File

@@ -0,0 +1,120 @@
import * as React from 'react';
import { Select, Input, InputNumber, Form, Switch, Checkbox, DatePicker, Radio, Upload, Button, Icon, Tooltip } from 'component/antd';
// import './index.less';
const Search = Input.Search;
export interface IFormItem {
key: string;
label: string;
type: string;
value?: string;
// 内部组件属性注入
attrs?: any;
// form属性注入
formAttrs?: any;
defaultValue?: string | number | any[];
rules?: any[];
invisible?: boolean;
getvaluefromevent: Function;
}
interface SerachFormProps {
formMap: IFormItem[];
// formData: any;
form: any;
onSubmit: Function;
isReset?: boolean;
clearAll: Function;
layout?: 'inline' | 'horizontal' | 'vertical';
}
export interface IFormSelect extends IFormItem {
options: Array<{ key?: string | number, value: string | number, label: string }>;
}
class SearchForm extends React.Component<SerachFormProps>{
public onSubmit = () => {
// this.props.onSubmit()
//
}
public renderFormItem(item: IFormItem) {
switch (item.type) {
default:
case 'input':
return <Input key={item.key} {...item.attrs} />;
case 'select':
return (
<Select
// size="small"
key={item.key}
{...item.attrs}
invisibleValue={item.formAttrs.invisibleValue}
>
{(item as IFormSelect).options && (item as IFormSelect).options.map((v, index) => (
<Select.Option
key={v.value || v.key || index}
value={v.value}
>
{v.label}
{/* <Tooltip placement='left' title={v.value}>
{v.label}
</Tooltip> */}
</Select.Option>
))}
</Select>
);
}
}
public theQueryClick = (value: any) => {
this.props.onSubmit(value)
this.props.clearAll()
// this.props.form.resetFields()
}
public resetClick = () => {
this.props.form.resetFields()
this.props.clearAll()
this.theQueryClick(this.props.form.getFieldsValue())
}
public render() {
const { form, formMap, isReset } = this.props;
const { getFieldDecorator, getFieldsValue } = form;
return (
<Form layout='inline' onSubmit={this.onSubmit}>
{
formMap.map(formItem => {
// const { initialValue, valuePropName } = this.handleFormItem(formItem, formData);
// const getFieldValue = {
// initialValue,
// rules: formItem.rules || [{ required: false, message: '' }],
// valuePropName,
// };
return (
<Form.Item
key={formItem.key}
label={formItem.label}
{...formItem.formAttrs}
>
{getFieldDecorator(formItem.key, {
initialValue: formItem.formAttrs?.initialvalue,
getValueFromEvent: formItem?.getvaluefromevent,
})(
this.renderFormItem(formItem),
)}
</Form.Item>
);
})
}
<Form.Item>
{
isReset && <Button style={{ width: '80px', marginRight: '20px' }} type="primary" onClick={() => this.resetClick()}></Button>
}
<Button style={{ width: '80px' }} type="primary" onClick={() => this.theQueryClick(getFieldsValue())}></Button>
</Form.Item>
</Form>
);
}
}
export const SearchFormComponent = Form.create<SerachFormProps>({ name: 'search-form' })(SearchForm);

View File

@@ -29,7 +29,7 @@ export class UserManagement extends SearchAndFilterContainer {
searchKey = (searchKey + '').trim().toLowerCase(); searchKey = (searchKey + '').trim().toLowerCase();
data = searchKey ? origin.filter((item: IUser) => data = searchKey ? origin.filter((item: IUser) =>
(item.username !== undefined && item.username !== null) && item.username.toLowerCase().includes(searchKey as string)) : origin ; (item.username !== undefined && item.username !== null) && item.username.toLowerCase().includes(searchKey as string)) : origin;
return data; return data;
} }

View File

@@ -1,7 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import { alarm } from 'store/alarm'; import { alarm } from 'store/alarm';
import { IMonitorGroups } from 'types/base-type'; import { IMonitorGroups } from 'types/base-type';
import { getValueFromLocalStorage, setValueToLocalStorage } from 'lib/local-storage'; import { getValueFromLocalStorage, setValueToLocalStorage, deleteValueFromLocalStorage } from 'lib/local-storage';
import { VirtualScrollSelect } from '../../../component/virtual-scroll-select'; import { VirtualScrollSelect } from '../../../component/virtual-scroll-select';
interface IAlarmSelectProps { interface IAlarmSelectProps {
@@ -36,6 +36,10 @@ export class AlarmSelect extends React.Component<IAlarmSelectProps> {
onChange && onChange(params); onChange && onChange(params);
} }
public componentWillUnmount() {
deleteValueFromLocalStorage('monitorGroups');
}
public render() { public render() {
const { value, isDisabled } = this.props; const { value, isDisabled } = this.props;
return ( return (

View File

@@ -149,9 +149,9 @@ export class DynamicSetFilter extends React.Component<IDynamicProps> {
public handleSelectChange = (e: string, type: 'topic' | 'consumerGroup' | 'location') => { public handleSelectChange = (e: string, type: 'topic' | 'consumerGroup' | 'location') => {
switch (type) { switch (type) {
case 'topic': case 'topic':
if (!this.clusterId) { // if (!this.clusterId) {
return message.info('请选择集群'); // return message.info('请选择集群');
} // }
this.topicName = e; this.topicName = e;
const type = this.dealMonitorType(); const type = this.dealMonitorType();
if (['kafka-consumer-maxLag', 'kafka-consumer-maxDelayTime', 'kafka-consumer-lag'].indexOf(type) > -1) { if (['kafka-consumer-maxLag', 'kafka-consumer-maxDelayTime', 'kafka-consumer-lag'].indexOf(type) > -1) {

View File

@@ -9,6 +9,7 @@ import { pagination } from 'constants/table';
import { urlPrefix } from 'constants/left-menu'; import { urlPrefix } from 'constants/left-menu';
import { alarm } from 'store/alarm'; import { alarm } from 'store/alarm';
import 'styles/table-filter.less'; import 'styles/table-filter.less';
import { Link } from 'react-router-dom';
@observer @observer
export class AlarmList extends SearchAndFilterContainer { export class AlarmList extends SearchAndFilterContainer {
@@ -24,7 +25,7 @@ export class AlarmList extends SearchAndFilterContainer {
if (app.active !== '-1' || searchKey !== '') { if (app.active !== '-1' || searchKey !== '') {
data = origin.filter(d => data = origin.filter(d =>
((d.name !== undefined && d.name !== null) && d.name.toLowerCase().includes(searchKey as string) ((d.name !== undefined && d.name !== null) && d.name.toLowerCase().includes(searchKey as string)
|| ((d.operator !== undefined && d.operator !== null) && d.operator.toLowerCase().includes(searchKey as string))) || ((d.operator !== undefined && d.operator !== null) && d.operator.toLowerCase().includes(searchKey as string)))
&& (app.active === '-1' || d.appId === (app.active + '')), && (app.active === '-1' || d.appId === (app.active + '')),
); );
} else { } else {
@@ -55,9 +56,7 @@ export class AlarmList extends SearchAndFilterContainer {
{this.renderSearch('名称:', '请输入告警规则或者操作人')} {this.renderSearch('名称:', '请输入告警规则或者操作人')}
<li className="right-btn-1"> <li className="right-btn-1">
<Button type="primary"> <Button type="primary">
<a href={`${urlPrefix}/alarm/add`}> <Link to={`/alarm/add`}></Link>
</a>
</Button> </Button>
</li> </li>
</> </>
@@ -68,6 +67,9 @@ export class AlarmList extends SearchAndFilterContainer {
if (!alarm.monitorStrategies.length) { if (!alarm.monitorStrategies.length) {
alarm.getMonitorStrategies(); alarm.getMonitorStrategies();
} }
if (!app.data.length) {
app.getAppList();
}
} }
public render() { public render() {

View File

@@ -52,8 +52,7 @@ export class CommonAppList extends SearchAndFilterContainer {
}, },
}), }),
render: (text: string, record: IAppItem) => { render: (text: string, record: IAppItem) => {
return ( return (<Tooltip placement="bottomLeft" title={record.name}>{text}</Tooltip>);
<Tooltip placement="bottomLeft" title={record.name}>{text}</Tooltip>);
}, },
}, },
{ {
@@ -103,7 +102,7 @@ export class CommonAppList extends SearchAndFilterContainer {
} }
public getOnlineConnect(record: IAppItem) { public getOnlineConnect(record: IAppItem) {
modal.showOfflineAppModal(record.appId); modal.showOfflineAppNewModal(record.appId);
} }
public getData<T extends IAppItem>(origin: T[]) { public getData<T extends IAppItem>(origin: T[]) {
@@ -114,7 +113,7 @@ export class CommonAppList extends SearchAndFilterContainer {
data = searchKey ? origin.filter((item: IAppItem) => data = searchKey ? origin.filter((item: IAppItem) =>
((item.name !== undefined && item.name !== null) && item.name.toLowerCase().includes(searchKey as string)) || ((item.name !== undefined && item.name !== null) && item.name.toLowerCase().includes(searchKey as string)) ||
((item.principals !== undefined && item.principals !== null) && item.principals.toLowerCase().includes(searchKey as string)) || ((item.principals !== undefined && item.principals !== null) && item.principals.toLowerCase().includes(searchKey as string)) ||
((item.appId !== undefined && item.appId !== null) && item.appId.toLowerCase().includes(searchKey as string)) ) : origin; ((item.appId !== undefined && item.appId !== null) && item.appId.toLowerCase().includes(searchKey as string))) : origin;
return data; return data;
} }

View File

@@ -29,16 +29,16 @@ export class MyCluster extends SearchAndFilterContainer {
public applyCluster() { public applyCluster() {
const xFormModal = { const xFormModal = {
formMap: [ formMap: [
{ // {
key: 'idc', // key: 'idc',
label: '数据中心', // label: '数据中心',
defaultValue: region.regionName, // defaultValue: region.regionName,
rules: [{ required: true, message: '请输入数据中心' }], // rules: [{ required: true, message: '请输入数据中心' }],
attrs: { // attrs: {
placeholder: '请输入数据中心', // placeholder: '请输入数据中心',
disabled: true, // disabled: true,
}, // },
}, // },
{ {
key: 'appId', key: 'appId',
label: '所属应用', label: '所属应用',
@@ -91,7 +91,7 @@ export class MyCluster extends SearchAndFilterContainer {
], ],
formData: {}, formData: {},
visible: true, visible: true,
title: '申请集群', title: <div><span></span><a className='applicationDocument' href="https://github.com/didi/Logi-KafkaManager/blob/master/docs/user_guide/resource_apply.md" target='_blank'></a></div>,
okText: '确认', okText: '确认',
onSubmit: (value: any) => { onSubmit: (value: any) => {
value.idc = region.currentRegion; value.idc = region.currentRegion;

View File

@@ -117,12 +117,12 @@ class DataMigrationFormTable extends React.Component<IFormTableProps> {
key: 'maxThrottle', key: 'maxThrottle',
editable: true, editable: true,
}, { }, {
title: '迁移保存时间(h)', title: '迁移后Topic保存时间(h)',
dataIndex: 'reassignRetentionTime', dataIndex: 'reassignRetentionTime',
key: 'reassignRetentionTime', key: 'reassignRetentionTime',
editable: true, editable: true,
}, { }, {
title: '原保存时间(h)', title: '原Topic保存时间(h)',
dataIndex: 'retentionTime', dataIndex: 'retentionTime',
key: 'retentionTime', // originalRetentionTime key: 'retentionTime', // originalRetentionTime
width: '132px', width: '132px',

View File

@@ -133,15 +133,15 @@ export class GovernanceTopic extends SearchAndFilterContainer {
width: '30%', width: '30%',
sorter: (a: IResource, b: IResource) => a.topicName.charCodeAt(0) - b.topicName.charCodeAt(0), sorter: (a: IResource, b: IResource) => a.topicName.charCodeAt(0) - b.topicName.charCodeAt(0),
render: (text: string, item: IResource) => render: (text: string, item: IResource) =>
( (
<Tooltip placement="bottomLeft" title={text}> <Tooltip placement="bottomLeft" title={text}>
<a <a
// tslint:disable-next-line:max-line-length // tslint:disable-next-line:max-line-length
href={`${this.urlPrefix}/topic/topic-detail?clusterId=${item.clusterId}&topic=${item.topicName}&isPhysicalClusterId=true&region=${region.currentRegion}`} href={`${this.urlPrefix}/topic/topic-detail?clusterId=${item.clusterId}&topic=${item.topicName}&isPhysicalClusterId=true&region=${region.currentRegion}`}
> >
{text} {text}
</a> </a>
</Tooltip>), </Tooltip>),
}, },
{ {
title: '所在集群', title: '所在集群',
@@ -215,7 +215,7 @@ export class GovernanceTopic extends SearchAndFilterContainer {
return ( return (
<> <>
{this.pendingTopic(this.getData(expert.resourceData))} {this.pendingTopic(this.getData(expert.resourceData))}
</> </>
); );
} }

View File

@@ -16,6 +16,14 @@
line-height: 64px; line-height: 64px;
vertical-align: middle; vertical-align: middle;
} }
.kafka-header-version{
display: inline-block;
vertical-align: middle;
padding-top:5px;
font-size: 12px;
margin-left:10px;
color:#a0a0a0;
}
} }
.mid-content { .mid-content {

View File

@@ -145,6 +145,8 @@ export const Header = observer((props: IHeader) => {
<div className="left-content"> <div className="left-content">
<img className="kafka-header-icon" src={logoUrl} alt="" /> <img className="kafka-header-icon" src={logoUrl} alt="" />
<span className="kafka-header-text">Kafka Manager</span> <span className="kafka-header-text">Kafka Manager</span>
<a className='kafka-header-version' href="https://github.com/didi/Logi-KafkaManager/releases" target='_blank'>v2.4.0</a>
{/* 添加版本超链接 */}
</div> </div>
<div className="mid-content"> <div className="mid-content">
{headerMenu.map((item: IMenuItem, index: number) => {headerMenu.map((item: IMenuItem, index: number) =>

View File

@@ -22,11 +22,11 @@ export const showEditClusterTopic = (item: IClusterTopics) => {
}, },
{ {
key: 'appId', key: 'appId',
label: '应用ID', label: '应用名称',
type: 'select', type: 'select',
options: app.adminAppData.map(item => { options: app.adminAppData.map(item => {
return { return {
label: item.appId, label: item.name,
value: item.appId, value: item.appId,
}; };
}), }),
@@ -61,6 +61,7 @@ export const showEditClusterTopic = (item: IClusterTopics) => {
attrs: { attrs: {
placeholder: '请输入保存时间', placeholder: '请输入保存时间',
suffix: '小时', suffix: '小时',
prompttype: '修改保存时间,预计一分钟左右生效!'
}, },
}, },
{ {

View File

@@ -35,7 +35,6 @@ class CustomForm extends React.Component<IXFormProps> {
this.props.form.validateFields((err: any, values: any) => { this.props.form.validateFields((err: any, values: any) => {
const deleteData = this.props.formData; const deleteData = this.props.formData;
if (!err) { if (!err) {
// console.log('values', values);
if (values.topicName !== this.props.formData.topicName) { if (values.topicName !== this.props.formData.topicName) {
notification.error({ message: 'topic名称不正确请重新输入' }); notification.error({ message: 'topic名称不正确请重新输入' });
} else { } else {
@@ -77,7 +76,6 @@ class CustomForm extends React.Component<IXFormProps> {
} }
public render() { public render() {
// console.log('props', this.props);
const { formData = {} as any, visible } = this.props; const { formData = {} as any, visible } = this.props;
const { getFieldDecorator } = this.props.form; const { getFieldDecorator } = this.props.form;
let metadata = [] as IBrokersMetadata[]; let metadata = [] as IBrokersMetadata[];

View File

@@ -111,11 +111,11 @@ class CustomForm extends React.Component<IXFormProps> {
})(<Input placeholder="请输入分区数" />)} })(<Input placeholder="请输入分区数" />)}
</Form.Item> </Form.Item>
<Form.Item label="类型"> <Form.Item label="类型">
{/* <Form.Item label={this.state.checked ? 'Region类型' : 'Borker类型'} > */} {/* <Form.Item label={this.state.checked ? 'Region类型' : 'Broker类型'} > */}
{/* <Switch onChange={(checked) => this.onSwitchChange(checked)} /> */} {/* <Switch onChange={(checked) => this.onSwitchChange(checked)} /> */}
<Radio.Group value={this.state.checked ? 'region' : 'broker'} onChange={(e) => { this.onSwitchChange(e.target.value === 'region' ? true : false); }}> <Radio.Group value={this.state.checked ? 'region' : 'broker'} onChange={(e) => { this.onSwitchChange(e.target.value === 'region' ? true : false); }}>
<Radio.Button value="region">Region类型</Radio.Button> <Radio.Button value="region">Region类型</Radio.Button>
<Radio.Button value="broker">Borker类型</Radio.Button> <Radio.Button value="broker">Broker类型</Radio.Button>
</Radio.Group> </Radio.Group>
</Form.Item> </Form.Item>
<Form.Item label="brokerIdList" style={{ display: this.state.checked ? 'none' : '' }}> <Form.Item label="brokerIdList" style={{ display: this.state.checked ? 'none' : '' }}>

View File

@@ -28,8 +28,8 @@ const updateInputModal = (status?: string) => {
formMap[4].invisible = status === 'region'; formMap[4].invisible = status === 'region';
formMap[5].invisible = status !== 'region'; formMap[5].invisible = status !== 'region';
formMap[4].rules = [{required: status !== 'region'}]; formMap[4].rules = [{ required: status !== 'region' }];
formMap[5].rules = [{required: status === 'region'}]; formMap[5].rules = [{ required: status === 'region' }];
// tslint:disable-next-line:no-unused-expression // tslint:disable-next-line:no-unused-expression
wrapper.ref && wrapper.ref.updateFormMap$(formMap, wrapper.xFormWrapper.formData); wrapper.ref && wrapper.ref.updateFormMap$(formMap, wrapper.xFormWrapper.formData);
}; };
@@ -103,7 +103,7 @@ export const createMigrationTasks = () => {
label: 'Region', label: 'Region',
value: 'region', value: 'region',
}, { }, {
label: 'Borker', label: 'Broker',
value: 'broker', value: 'broker',
}], }],
rules: [{ rules: [{
@@ -158,26 +158,26 @@ export const createMigrationTasks = () => {
}, },
{ {
key: 'originalRetentionTime', key: 'originalRetentionTime',
label: '原保存时间', label: '原Topic保存时间',
rules: [{ rules: [{
required: true, required: true,
message: '请输入原保存时间', message: '请输入原Topic保存时间',
}], }],
attrs: { attrs: {
disabled: true, disabled: true,
placeholder: '请输入原保存时间', placeholder: '请输入原Topic保存时间',
suffix: '小时', suffix: '小时',
}, },
}, },
{ {
key: 'reassignRetentionTime', key: 'reassignRetentionTime',
label: '迁移保存时间', label: '迁移后Topic保存时间',
rules: [{ rules: [{
required: true, required: true,
message: '请输入迁移保存时间', message: '请输入迁移后Topic保存时间',
}], }],
attrs: { attrs: {
placeholder: '请输入迁移保存时间', placeholder: '请输入迁移后Topic保存时间',
suffix: '小时', suffix: '小时',
}, },
}, },

View File

@@ -24,26 +24,111 @@ export const showApplyModal = (record?: IUser) => {
value: +item, value: +item,
})), })),
rules: [{ required: true, message: '请选择角色' }], rules: [{ required: true, message: '请选择角色' }],
}, {
key: 'password',
label: '密码',
type: FormItemType.inputPassword,
rules: [{ required: !record, message: '请输入密码' }],
}, },
// {
// key: 'password',
// label: '密码',
// type: FormItemType.inputPassword,
// rules: [{ required: !record, message: '请输入密码' }],
// },
], ],
formData: record || {}, formData: record || {},
visible: true, visible: true,
title: record ? '修改用户' : '新增用户', title: record ? '修改用户' : '新增用户',
onSubmit: (value: IUser) => { onSubmit: (value: IUser) => {
if (record) { if (record) {
return users.modfiyUser(value).then(() => { return users.modfiyUser(value)
message.success('操作成功');
});
} }
return users.addUser(value).then(() => { return users.addUser(value).then(() => {
message.success('操作成功'); message.success('操作成功');
}); });
}, },
}; };
if(!record){
let formMap: any = xFormModal.formMap
formMap.splice(2, 0,{
key: 'password',
label: '密码',
type: FormItemType.inputPassword,
rules: [{ required: !record, message: '请输入密码' }],
},)
}
wrapper.open(xFormModal); wrapper.open(xFormModal);
}; };
// const handleCfPassword = (rule:any, value:any, callback:any)=>{
// if()
// }
export const showApplyModalModifyPassword = (record: IUser) => {
const xFormModal:any = {
formMap: [
// {
// key: 'oldPassword',
// label: '旧密码',
// type: FormItemType.inputPassword,
// rules: [{
// required: true,
// message: '请输入旧密码',
// }]
// },
{
key: 'newPassword',
label: '新密码',
type: FormItemType.inputPassword,
rules: [
{
required: true,
message: '请输入新密码',
}
],
attrs:{
onChange:(e:any)=>{
users.setNewPassWord(e.target.value)
}
}
},
{
key: 'confirmPassword',
label: '确认密码',
type: FormItemType.inputPassword,
rules: [
{
required: true,
message: '请确认密码',
validator:(rule:any, value:any, callback:any) => {
// 验证新密码的一致性
if(users.newPassWord){
if(value!==users.newPassWord){
rule.message = "两次密码输入不一致";
callback('两次密码输入不一致')
}else{
callback()
}
}else if(!value){
rule.message = "请确认密码";
callback('请确认密码');
}else{
callback()
}
},
}
],
},
],
formData: record || {},
visible: true,
title: '修改密码',
onSubmit: (value: IUser) => {
let params:any = {
username:record?.username,
password:value.confirmPassword,
role:record?.role,
}
return users.modfiyUser(params).then(() => {
message.success('操作成功');
});
},
}
wrapper.open(xFormModal);
};

View File

@@ -1,6 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { notification } from 'component/antd'; import { notification, Select } from 'component/antd';
import { IUploadFile, IConfigure } from 'types/base-type'; import { IUploadFile, IConfigure, IConfigGateway } from 'types/base-type';
import { version } from 'store/version'; import { version } from 'store/version';
import { admin } from 'store/admin'; import { admin } from 'store/admin';
import { wrapper } from 'store'; import { wrapper } from 'store';
@@ -97,8 +97,8 @@ const updateFormModal = (type: number) => {
formMap[2].attrs = { formMap[2].attrs = {
accept: version.fileSuffix, accept: version.fileSuffix,
}, },
// tslint:disable-next-line:no-unused-expression // tslint:disable-next-line:no-unused-expression
wrapper.ref && wrapper.ref.updateFormMap$(formMap, wrapper.xFormWrapper.formData, true); wrapper.ref && wrapper.ref.updateFormMap$(formMap, wrapper.xFormWrapper.formData, true);
} }
}; };
@@ -157,8 +157,8 @@ export const showModifyModal = (record: IUploadFile) => {
export const showConfigureModal = async (record?: IConfigure) => { export const showConfigureModal = async (record?: IConfigure) => {
if (record) { if (record) {
const result:any = await format2json(record.configValue); const result: any = await format2json(record.configValue);
record.configValue = result.result; record.configValue = result.result || record.configValue;
} }
const xFormModal = { const xFormModal = {
formMap: [ formMap: [
@@ -193,10 +193,69 @@ export const showConfigureModal = async (record?: IConfigure) => {
return admin.editConfigure(value).then(data => { return admin.editConfigure(value).then(data => {
notification.success({ message: '编辑配置成功' }); notification.success({ message: '编辑配置成功' });
}); });
} else {
return admin.addNewConfigure(value).then(data => {
notification.success({ message: '新建配置成功' });
});
}
},
};
wrapper.open(xFormModal);
};
export const showConfigGatewayModal = async (record?: IConfigGateway) => {
const xFormModal = {
formMap: [
{
key: 'type',
label: '配置类型',
rules: [{ required: true, message: '请选择配置类型' }],
type: "select",
options: admin.gatewayType.map((item: any, index: number) => ({
key: index,
label: item.configName,
value: item.configType,
})),
attrs: {
disabled: record ? true : false,
}
}, {
key: 'name',
label: '配置键',
rules: [{ required: true, message: '请输入配置键' }],
attrs: {
disabled: record ? true : false,
},
}, {
key: 'value',
label: '配置值',
type: 'text_area',
rules: [{
required: true,
message: '请输入配置值',
}],
}, {
key: 'description',
label: '描述',
type: 'text_area',
rules: [{ required: true, message: '请输入备注' }],
},
],
formData: record || {},
visible: true,
isWaitting: true,
title: `${record ? '编辑配置' : '新建配置'}`,
onSubmit: async (parmas: IConfigGateway) => {
if (record) {
parmas.id = record.id;
return admin.editConfigGateway(parmas).then(data => {
notification.success({ message: '编辑配置成功' });
});
} else {
return admin.addNewConfigGateway(parmas).then(data => {
notification.success({ message: '新建配置成功' });
});
} }
return admin.addNewConfigure(value).then(data => {
notification.success({ message: '新建配置成功' });
});
}, },
}; };
wrapper.open(xFormModal); wrapper.open(xFormModal);

Some files were not shown because too many files have changed in this diff Show More