限流,支持客户端ID修改、删除.

This commit is contained in:
许晓东
2023-02-05 22:01:37 +08:00
parent ee6defe5d2
commit 608f7cdc47
10 changed files with 541 additions and 54 deletions

View File

@@ -11,6 +11,8 @@ import java.util.List;
@Data
public class AlterClientQuotaDTO {
private String type;
private List<String> types;
private List<String> names;
@@ -20,4 +22,6 @@ public class AlterClientQuotaDTO {
private String producerRate;
private String requestPercentage;
private List<String> deleteConfigs;
}

View File

@@ -68,7 +68,7 @@ public class ClientQuotaEntityVO {
double _1kb = 1024;
double _1mb = 1024 * _1kb;
if (value < _1kb) {
return value + "Byte";
return value + " Byte";
}
if (value < _1mb) {
return String.format("%.1f KB", (value / _1kb));

View File

@@ -5,10 +5,7 @@ import com.xuxd.kafka.console.beans.dto.AlterClientQuotaDTO;
import com.xuxd.kafka.console.beans.dto.QueryClientQuotaDTO;
import com.xuxd.kafka.console.service.ClientQuotaService;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
/**
* @author: xuxd
@@ -38,4 +35,14 @@ public class ClientQuotaController {
}
return clientQuotaService.alterClientQuotaConfigs(request);
}
@DeleteMapping
public Object deleteClientQuotaConfigs(@RequestBody AlterClientQuotaDTO request) {
if (CollectionUtils.isEmpty(request.getTypes())
|| CollectionUtils.isEmpty(request.getNames())
|| request.getTypes().size() != request.getNames().size()) {
return ResponseData.create().failed("types length and names length is invalid.");
}
return clientQuotaService.deleteClientQuotaConfigs(request);
}
}

View File

@@ -13,4 +13,6 @@ public interface ClientQuotaService {
ResponseData getClientQuotaConfigs(List<String> types, List<String> names);
ResponseData alterClientQuotaConfigs(AlterClientQuotaDTO request);
ResponseData deleteClientQuotaConfigs(AlterClientQuotaDTO request);
}

View File

@@ -6,7 +6,9 @@ import com.xuxd.kafka.console.beans.vo.ClientQuotaEntityVO;
import com.xuxd.kafka.console.service.ClientQuotaService;
import kafka.console.ClientQuotaConsole;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.kafka.common.config.internals.QuotaConfigs;
import org.apache.kafka.common.quota.ClientQuotaEntity;
import org.springframework.stereotype.Service;
@@ -24,10 +26,22 @@ public class ClientQuotaServiceImpl implements ClientQuotaService {
private final Map<String, String> typeDict = new HashMap<>();
private final Map<String, String> configDict = new HashMap<>();
private final String USER = "user";
private final String CLIENT_ID = "client-id";
private final String IP = "ip";
private final String USER_CLIENT = "user&client-id";
{
typeDict.put("user", ClientQuotaEntity.USER);
typeDict.put("client-id", ClientQuotaEntity.CLIENT_ID);
typeDict.put("ip", ClientQuotaEntity.IP);
typeDict.put(USER, ClientQuotaEntity.USER);
typeDict.put(CLIENT_ID, ClientQuotaEntity.CLIENT_ID);
typeDict.put(IP, ClientQuotaEntity.IP);
typeDict.put(USER_CLIENT, USER_CLIENT);
configDict.put("producerRate", QuotaConfigs.PRODUCER_BYTE_RATE_OVERRIDE_CONFIG);
configDict.put("consumerRate", QuotaConfigs.CONSUMER_BYTE_RATE_OVERRIDE_CONFIG);
configDict.put("requestPercentage", QuotaConfigs.REQUEST_PERCENTAGE_OVERRIDE_CONFIG);
}
public ClientQuotaServiceImpl(ClientQuotaConsole clientQuotaConsole) {
@@ -65,9 +79,84 @@ public class ClientQuotaServiceImpl implements ClientQuotaService {
@Override
public ResponseData alterClientQuotaConfigs(AlterClientQuotaDTO request) {
return ResponseData.create().failed();
if (StringUtils.isEmpty(request.getType()) || !typeDict.containsKey(request.getType())) {
return ResponseData.create().failed("Unknown type.");
}
List<String> types = new ArrayList<>();
List<String> names = new ArrayList<>();
parseTypesAndNames(request, types, names, request.getType());
Map<String, String> configsToBeAddedMap = new HashMap<>();
if (StringUtils.isNotEmpty(request.getProducerRate())) {
configsToBeAddedMap.put(QuotaConfigs.PRODUCER_BYTE_RATE_OVERRIDE_CONFIG, String.valueOf(Math.floor(Double.valueOf(request.getProducerRate()))));
}
if (StringUtils.isNotEmpty(request.getConsumerRate())) {
configsToBeAddedMap.put(QuotaConfigs.CONSUMER_BYTE_RATE_OVERRIDE_CONFIG, String.valueOf(Math.floor(Double.valueOf(request.getConsumerRate()))));
}
if (StringUtils.isNotEmpty(request.getRequestPercentage())) {
configsToBeAddedMap.put(QuotaConfigs.REQUEST_PERCENTAGE_OVERRIDE_CONFIG, String.valueOf(Math.floor(Double.valueOf(request.getRequestPercentage()))));
}
clientQuotaConsole.addQuotaConfigs(types, names, configsToBeAddedMap);
if (CollectionUtils.isNotEmpty(request.getDeleteConfigs())) {
List<String> delete = request.getDeleteConfigs().stream().map(key -> configDict.get(key)).collect(Collectors.toList());
clientQuotaConsole.deleteQuotaConfigs(types, names, delete);
}
return ResponseData.create().success();
}
@Override
public ResponseData deleteClientQuotaConfigs(AlterClientQuotaDTO request) {
if (StringUtils.isEmpty(request.getType()) || !typeDict.containsKey(request.getType())) {
return ResponseData.create().failed("Unknown type.");
}
List<String> types = new ArrayList<>();
List<String> names = new ArrayList<>();
parseTypesAndNames(request, types, names, request.getType());
List<String> configs = new ArrayList<>();
configs.add(QuotaConfigs.PRODUCER_BYTE_RATE_OVERRIDE_CONFIG);
configs.add(QuotaConfigs.CONSUMER_BYTE_RATE_OVERRIDE_CONFIG);
configs.add(QuotaConfigs.REQUEST_PERCENTAGE_OVERRIDE_CONFIG);
clientQuotaConsole.deleteQuotaConfigs(types, names, configs);
return ResponseData.create().success();
}
private void parseTypesAndNames(AlterClientQuotaDTO request, List<String> types, List<String> names, String type) {
switch (request.getType()) {
case USER:
getTypesAndNames(request, types, names, USER);
break;
case CLIENT_ID:
getTypesAndNames(request, types, names, CLIENT_ID);
break;
case IP:
getTypesAndNames(request, types, names, IP);
break;
case USER_CLIENT:
getTypesAndNames(request, types, names, USER);
getTypesAndNames(request, types, names, CLIENT_ID);
break;
}
}
private void getTypesAndNames(AlterClientQuotaDTO request, List<String> types, List<String> names, String type) {
int index = -1;
for (int i = 0; i < request.getTypes().size(); i++) {
if (type.equals(request.getTypes().get(i))) {
index = i;
break;
}
}
if (index < 0) {
throw new IllegalArgumentException("Does not contain the type" + type);
}
types.add(request.getTypes().get(index));
if (CollectionUtils.isNotEmpty(request.getNames()) && request.getNames().size() > index) {
names.add(request.getNames().get(index));
} else {
names.add("");
}
}
}

View File

@@ -303,4 +303,8 @@ export const KafkaClientQuotaApi = {
url: "/client/quota",
method: "post",
},
deleteClientQuotaConfigs: {
url: "/client/quota",
method: "delete",
},
};

View File

@@ -1,3 +1,4 @@
<script src="../../store/index.js"></script>
<template>
<a-modal
title="新增配置"
@@ -17,28 +18,28 @@
:wrapper-col="{ span: 12 }"
@submit="handleSubmit"
>
<a-form-item label="用户">
<a-form-item label="用户" v-show="showUser">
<a-input
v-decorator="[
'user',
]"
placeholder="输入用户主体标识,比如:用户名!"
placeholder="输入用户主体标识,比如:用户名,未指定表示用户默认设置"
/>
</a-form-item>
<a-form-item label="客户端ID">
<a-form-item label="客户端ID" v-show="showClientId">
<a-input
v-decorator="[
'client-id',
'client',
]"
placeholder="输入用户客户端ID!"
placeholder="输入用户客户端ID,未指定表示默认客户端设置"
/>
</a-form-item>
<a-form-item label="IP">
<a-form-item label="IP" v-show="showIP">
<a-input
v-decorator="[
'ip',
]"
placeholder="输入客户端IP!"
placeholder="输入客户端IP"
/>
</a-form-item>
<a-form-item label="生产速率">
@@ -89,17 +90,29 @@
<script>
import request from "@/utils/request";
import {KafkaTopicApi} from "@/utils/api";
import {KafkaClientQuotaApi} from "@/utils/api";
import notification from "ant-design-vue/es/notification";
export default {
name: "AddQuotaConfig",
props: {
topic: {
visible: {
type: Boolean,
default: false,
},
type: {
type: String,
default: "",
},
visible: {
showClientId: {
type: Boolean,
default: false,
},
showUser: {
type: Boolean,
default: false,
},
showIP: {
type: Boolean,
default: false,
},
@@ -120,29 +133,47 @@ export default {
},
},
methods: {
handleSubmit(e) {
e.preventDefault();
handleSubmit() {
this.form.validateFields((err, values) => {
if (!err) {
if (values.configs) {
const config = {};
values.configs.split("\n").forEach((e) => {
const c = e.split("=");
if (c.length > 1) {
let k = c[0].trim(),
v = c[1].trim();
if (k && v) {
config[k] = v;
}
}
});
values.configs = config;
const params = Object.assign({type: this.type}, values);
const unitMap = {MB: 1024 * 1024, KB: 1024, Byte: 1};
if (values.consumerRate) {
params.consumerRate = params.consumerRate * unitMap[this.consumerRateUnit];
}
if (values.producerRate) {
params.producerRate = params.producerRate * unitMap[this.producerRateUnit];
}
params.types = [];
if (this.showUser) {
params.types.push("user");
if (params.user) {
params.names = [params.user.trim()];
} else {
params.names = [""];
}
}
if (this.showClientId) {
params.types.push("client-id");
if (params.client) {
params.names = [params.client.trim()];
} else {
params.names = [""];
}
}
if (this.showIP) {
params.types.push("ip");
if (params.ip) {
params.names = [params.ip.trim()];
} else {
params.names = [""];
}
}
this.loading = true;
request({
url: KafkaTopicApi.creatTopic.url,
method: KafkaTopicApi.creatTopic.method,
data: values,
url: KafkaClientQuotaApi.alterClientQuotaConfigs.url,
method: KafkaClientQuotaApi.alterClientQuotaConfigs.method,
data: params,
}).then((res) => {
this.loading = false;
if (res.code == 0) {
@@ -161,6 +192,12 @@ export default {
handleCancel() {
this.data = [];
this.$emit("closeAddQuotaDialog", {refresh: false});
this.producerRateUnit = "MB";
this.consumerRateUnit = "MB";
},
create() {
this.producerRateUnit = "MB";
this.consumerRateUnit = "MB";
},
},
};

View File

@@ -31,8 +31,8 @@
>新增配置
</a-button>
</div>
<QuotaList :columns="columns" :data="data"></QuotaList>
<AddQuotaConfig :visible="showAddQuotaDialog" @closeAddQuotaDialog="closeAddQuotaDialog"></AddQuotaConfig>
<QuotaList type="client-id" :columns="columns" :data="data" @refreshQuotaList="refresh"></QuotaList>
<AddQuotaConfig type="client-id" :visible="showAddQuotaDialog" :showClientId="true" @closeAddQuotaDialog="closeAddQuotaDialog"></AddQuotaConfig>
</a-spin>
</div>
</template>
@@ -116,9 +116,15 @@ export default {
openAddQuotaDialog() {
this.showAddQuotaDialog = true;
},
closeAddQuotaDialog() {
closeAddQuotaDialog(p) {
if (p.refresh) {
this.handleSearch();
}
this.showAddQuotaDialog = false;
},
refresh() {
this.handleSearch();
},
},
created() {
this.handleSearch();

View File

@@ -1,28 +1,56 @@
<template>
<div>
<a-table
:columns="columns"
:data-source="data"
bordered
:row-key="
<a-spin :spinning="loading">
<a-table
:columns="columns"
:data-source="data"
bordered
:row-key="
(record, index) => {
return index;
}
"
@change="handleChange"
>
<div slot="client" slot-scope="text">
<span v-if="text" >{{text}}</span><span v-else style="color: red">默认配置</span>
</div>
</a-table>
@change="handleChange"
>
<div slot="client" slot-scope="text">
<span v-if="text">{{ text }}</span><span v-else style="color: red">默认配置</span>
</div>
<div slot="operation" slot-scope="record">
<a-popconfirm
:title="'删除当前配置?'"
ok-text="确认"
cancel-text="取消"
@confirm="deleteConfig(record)"
>
<a-button size="small" href="javascript:;" class="operation-btn"
>删除
</a-button>
</a-popconfirm>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="openUpdateDialog(record)"
>修改
</a-button>
</div>
</a-table>
<UpdateQuotaConfig :type="type" :record="selectRow" :visible="showUpdateDialog"
@closeUpdateQuotaDialog="closeUpdateQuotaDialog"></UpdateQuotaConfig>
</a-spin>
</div>
</template>
<script>
import {KafkaClientQuotaApi} from "@/utils/api";
import request from "@/utils/request";
import notification from "ant-design-vue/lib/notification";
import UpdateQuotaConfig from "@/views/quota/UpdateQuotaConfig.vue";
export default {
name: "QuotaList",
components: {},
components: {UpdateQuotaConfig},
props: {
columns: {
type: Array,
@@ -30,12 +58,18 @@ export default {
data: {
type: Array,
},
type: {
type: String,
default: "",
},
},
data() {
return {
showDetailDialog: false,
record: {},
sortedInfo: null,
loading: false,
selectRow: {},
showUpdateDialog: false,
};
},
methods: {
@@ -49,8 +83,74 @@ export default {
handleChange() {
this.sortedInfo = arguments[2];
},
deleteConfig(record) {
this.loading = true;
const params = {type: this.type};
params.types = [];
if (this.type == "user") {
params.types.push("user");
if (record.user) {
params.names = [record.user.trim()];
} else {
params.names = [""];
}
} else if (this.type == "client-id") {
params.types.push("client-id");
if (record.client) {
params.names = [record.client.trim()];
} else {
params.names = [""];
}
}
if (this.type == "ip") {
params.types.push("ip");
if (record.ip) {
params.names = [record.ip.trim()];
} else {
params.names = [""];
}
}
request({
url: KafkaClientQuotaApi.deleteClientQuotaConfigs.url,
method: KafkaClientQuotaApi.deleteClientQuotaConfigs.method,
data: params,
}).then((res) => {
this.loading = false;
if (res.code == 0) {
this.$message.success(res.msg);
this.$emit("refreshQuotaList");
} else {
notification.error({
message: "error",
description: res.msg,
});
}
});
},
openUpdateDialog(record) {
this.selectRow = record;
this.showUpdateDialog = true;
},
closeUpdateQuotaDialog(event) {
this.selectRow = {};
this.showUpdateDialog = false;
if (event.refresh) {
this.$emit("refreshQuotaList");
}
},
},
created() {
this.columns.push({
title: "操作",
key: "operation",
scopedSlots: {customRender: "operation"},
});
},
};
</script>
<style scoped></style>
<style scoped>
.operation-btn {
margin-right: 3%;
}
</style>

View File

@@ -0,0 +1,238 @@
<script src="../../store/index.js"></script>
<template>
<a-modal
title="修改配置"
:visible="show"
:width="800"
:mask="false"
:destroyOnClose="true"
:footer="null"
:maskClosable="false"
@cancel="handleCancel"
>
<div>
<a-spin :spinning="loading">
<a-form
:form="form"
:label-col="{ span: 5 }"
:wrapper-col="{ span: 12 }"
@submit="handleSubmit"
>
<a-form-item label="用户" v-show="showUser">
<a-input
:disabled="true"
v-decorator="[
'user', { initialValue: record.user }
]"
placeholder="输入用户主体标识,比如:用户名,未指定表示用户默认设置"
/>
</a-form-item>
<a-form-item label="客户端ID" v-show="showClientId">
<a-input
:disabled="true"
v-decorator="[
'client', { initialValue: record.client }
]"
placeholder="输入用户客户端ID未指定表示默认客户端设置"
/>
</a-form-item>
<a-form-item label="IP" v-show="showIP">
<a-input
:disabled="true"
v-decorator="[
'ip', { initialValue: record.ip }
]"
placeholder="输入客户端IP"
/>
</a-form-item>
<a-form-item label="生产速率">
<a-input-number
:min="1"
:max="102400000"
v-decorator="[
'producerRate', { initialValue: record.producerRate }
]"
/>
<a-select default-value="MB" v-model="producerRateUnit" style="width: 100px">
<a-select-option value="MB"> MB/s</a-select-option>
<a-select-option value="KB"> KB/s</a-select-option>
<a-select-option value="Byte"> Byte/s</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="消费速率">
<a-input-number
:min="1"
:max="102400000"
v-decorator="[
'consumerRate', { initialValue: record.consumerRate }
]"
/>
<a-select default-value="MB" v-model="consumerRateUnit" style="width: 100px">
<a-select-option value="MB"> MB/s</a-select-option>
<a-select-option value="KB"> KB/s</a-select-option>
<a-select-option value="Byte"> Byte/s</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="吞吐量">
<a-input-number
:min="1"
:max="102400000"
v-decorator="[
'requestPercentage', { initialValue: record.requestPercentage }
]"
/>
</a-form-item>
<a-form-item :wrapper-col="{ span: 12, offset: 5 }">
<a-button type="primary" html-type="submit"> 提交</a-button>
</a-form-item>
</a-form>
</a-spin>
</div>
</a-modal>
</template>
<script>
import request from "@/utils/request";
import {KafkaClientQuotaApi} from "@/utils/api";
import notification from "ant-design-vue/es/notification";
export default {
name: "UpdateQuotaConfig",
props: {
visible: {
type: Boolean,
default: false,
},
type: {
type: String,
default: "",
},
record: {
type: Object,
default: function () {
return {}
},
},
},
data() {
return {
show: this.visible,
data: [],
loading: false,
form: this.$form.createForm(this, {name: "coordinated"}),
producerRateUnit: "MB",
consumerRateUnit: "MB",
showUser: false,
showIP: false,
showClientId: false,
};
},
watch: {
visible(v) {
this.show = v;
if (this.show) {
this.init();
}
},
},
methods: {
handleSubmit() {
this.form.validateFields((err, values) => {
if (!err) {
const params = {type: this.type, deleteConfigs: []};
const unitMap = {MB: 1024 * 1024, KB: 1024, Byte: 1};
if (values.consumerRate) {
const num = typeof (values.consumerRate) == "string" && values.consumerRate.indexOf(" ") > 0 ? values.consumerRate.split(" ")[0] : values.consumerRate;
params.consumerRate = num * unitMap[this.consumerRateUnit];
} else {
params.deleteConfigs.push("consumerRate");
}
if (values.producerRate) {
const num = typeof (values.producerRate) && values.producerRate.indexOf(" ") > 0 ? values.producerRate.split(" ")[0] : values.producerRate;
params.producerRate = num * unitMap[this.producerRateUnit];
} else {
params.deleteConfigs.push("producerRate");
}
if (values.requestPercentage) {
params.requestPercentage = values.requestPercentage;
} else {
params.deleteConfigs.push("requestPercentage");
}
params.types = [];
if (this.showUser) {
params.types.push("user");
if (values.user) {
params.names = [values.user.trim()];
} else {
params.names = [""];
}
}
if (this.showClientId) {
params.types.push("client-id");
if (values.client) {
params.names = [values.client.trim()];
} else {
params.names = [""];
}
}
if (this.showIP) {
params.types.push("ip");
if (values.ip) {
params.names = [values.ip.trim()];
} else {
params.names = [""];
}
}
console.log(params)
this.loading = true;
request({
url: KafkaClientQuotaApi.alterClientQuotaConfigs.url,
method: KafkaClientQuotaApi.alterClientQuotaConfigs.method,
data: params,
}).then((res) => {
this.loading = false;
if (res.code == 0) {
this.$message.success(res.msg);
this.$emit("closeUpdateQuotaDialog", {refresh: true});
} else {
notification.error({
message: "error",
description: res.msg,
});
}
});
}
});
},
handleCancel() {
this.data = [];
this.$emit("closeUpdateQuotaDialog", {refresh: false});
},
init() {
this.producerRateUnit = "MB";
if (this.record.producerRate) {
this.producerRateUnit = this.record.producerRate.split(" ")[1];
}
this.consumerRateUnit = "MB";
if (this.record.consumerRate) {
this.consumerRateUnit = this.record.consumerRate.split(" ")[1];
}
if (this.type == "user") {
this.showUser = true;
} else if (this.type == "client-id") {
this.showClientId = true;
} else if (this.type == "ip") {
this.showIP = true;
} else if (this.type == "user&client-id") {
this.showUser = true;
this.showClientId = true;
}
},
},
created() {
},
};
</script>
<style scoped></style>