feat: 添加自定监控

This commit is contained in:
Captain.B 2021-09-16 17:15:10 +08:00 committed by 刘瑞斌
parent 85facc5364
commit 5c69806d00
11 changed files with 327 additions and 100 deletions

View File

@ -18,6 +18,7 @@ import io.metersphere.dto.LogDetailDTO;
import io.metersphere.performance.base.*;
import io.metersphere.performance.dto.LoadTestExportJmx;
import io.metersphere.performance.dto.MetricData;
import io.metersphere.performance.dto.Monitor;
import io.metersphere.performance.service.MetricQueryService;
import io.metersphere.performance.service.PerformanceReportService;
import io.metersphere.performance.service.PerformanceTestService;
@ -239,7 +240,7 @@ public class ShareController {
}
@GetMapping("/metric/query/resource/{shareId}/{id}")
public List<String> queryReportResource(@PathVariable String shareId, @PathVariable("id") String reportId) {
public List<Monitor> queryReportResource(@PathVariable String shareId, @PathVariable("id") String reportId) {
return metricService.queryReportResource(reportId);
}

View File

@ -1,6 +1,7 @@
package io.metersphere.performance.controller;
import io.metersphere.performance.dto.MetricData;
import io.metersphere.performance.dto.Monitor;
import io.metersphere.performance.service.MetricQueryService;
import org.springframework.web.bind.annotation.*;
@ -20,7 +21,7 @@ public class MetricQueryController {
}
@GetMapping("/query/resource/{id}")
public List<String> queryReportResource(@PathVariable("id") String reportId) {
public List<Monitor> queryReportResource(@PathVariable("id") String reportId) {
return metricService.queryReportResource(reportId);
}
}

View File

@ -3,14 +3,14 @@ package io.metersphere.performance.dto;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
@Getter
@Setter
public class Monitor {
private String name;
private String environmentId;
private String environmentName;
private String ip;
private Integer port;
private String description;
private String monitorStatus;
private List<MonitorItem> monitorConfig;
}

View File

@ -0,0 +1,11 @@
package io.metersphere.performance.dto;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class MonitorItem {
private String value; // 表达式
private String name; // 监控项
}

View File

@ -4,7 +4,6 @@ package io.metersphere.performance.service;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.nacos.client.utils.StringUtils;
import io.metersphere.base.domain.LoadTestReportWithBLOBs;
import io.metersphere.base.domain.TestResource;
import io.metersphere.base.mapper.LoadTestReportMapper;
@ -21,9 +20,11 @@ import io.metersphere.performance.controller.request.MetricQuery;
import io.metersphere.performance.controller.request.MetricRequest;
import io.metersphere.performance.dto.MetricData;
import io.metersphere.performance.dto.Monitor;
import io.metersphere.performance.dto.MonitorItem;
import io.metersphere.service.SystemParameterService;
import io.metersphere.service.TestResourceService;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.RestTemplate;
@ -156,7 +157,7 @@ public class MetricQueryService {
}
public List<MetricData> queryMetric(String reportId) {
List<String> instances = new ArrayList<>();
List<Monitor> instances = new ArrayList<>();
LoadTestReportWithBLOBs report = loadTestReportMapper.selectByPrimaryKey(reportId);
String poolId = report.getTestResourcePoolId();
List<TestResource> resourceList = testResourceService.getTestResourceList(poolId);
@ -166,7 +167,10 @@ public class MetricQueryService {
NodeDTO dto = JSON.parseObject(resource.getConfiguration(), NodeDTO.class);
if (StringUtils.isNotBlank(dto.getIp())) {
int port = dto.getMonitorPort() == null ? 9100 : dto.getMonitorPort();
instances.add(dto.getIp() + ":" + port);
Monitor e = new Monitor();
e.setIp(dto.getIp());
e.setPort(port);
instances.add(e);
}
});
}
@ -179,11 +183,14 @@ public class MetricQueryService {
List<MetricDataRequest> list = new ArrayList<>();
// 加入高级设置中的监控配置
for (int i = 0; i < monitorParams.size(); i++) {
Monitor monitor = monitorParams.getObject(i, Monitor.class);
String instance = monitor.getIp() + ":" + monitor.getPort();
if (!instances.contains(instance)) {
instances.add(instance);
}
JSONObject o = monitorParams.getJSONObject(i);
Monitor monitor = new Monitor();
monitor.setIp(o.getString("ip"));
monitor.setName(o.getString("name"));
monitor.setPort(o.getInteger("port"));
monitor.setDescription(o.getString("description"));
monitor.setMonitorConfig(JSONObject.parseArray(o.getString("monitorConfig"), MonitorItem.class));
instances.add(monitor);
}
instances.forEach(instance -> {
@ -203,38 +210,55 @@ public class MetricQueryService {
return queryMetricData(metricRequest);
}
private void getRequest(String instance, List<MetricDataRequest> list) {
Map<String, String> map = MetricQuery.getMetricQueryMap();
Set<String> set = map.keySet();
set.forEach(s -> {
MetricDataRequest request = new MetricDataRequest();
String promQL = map.get(s);
request.setPromQL(promQL);
request.setSeriesName(s);
request.setInstance(instance);
list.add(request);
});
private void getRequest(Monitor monitor, List<MetricDataRequest> list) {
if (CollectionUtils.isNotEmpty(monitor.getMonitorConfig())) {
monitor.getMonitorConfig().forEach(c -> {
if (StringUtils.isBlank(c.getValue())) {
return;
}
MetricDataRequest request = new MetricDataRequest();
String promQL = c.getValue();
request.setPromQL(promQL);
request.setSeriesName(c.getName());
request.setInstance(monitor.getIp() + ":" + monitor.getPort());
list.add(request);
});
} else {
Map<String, String> map = MetricQuery.getMetricQueryMap();
Set<String> set = map.keySet();
set.forEach(s -> {
MetricDataRequest request = new MetricDataRequest();
String promQL = map.get(s);
request.setPromQL(promQL);
request.setSeriesName(s);
request.setInstance(monitor.getIp() + ":" + monitor.getPort());
list.add(request);
});
}
}
public List<String> queryReportResource(String reportId) {
List<String> result = new ArrayList<>();
public List<Monitor> queryReportResource(String reportId) {
List<Monitor> result = new ArrayList<>();
List<String> resourceIdAndIndexes = extLoadTestReportMapper.selectResourceId(reportId);
resourceIdAndIndexes.forEach(resourceIdAndIndex -> {
String[] split = org.apache.commons.lang3.StringUtils.split(resourceIdAndIndex, "_");
String[] split = StringUtils.split(resourceIdAndIndex, "_");
String resourceId = split[0];
TestResource testResource = testResourceService.getTestResource(resourceId);
if (testResource == null) {
return;
}
String configuration = testResource.getConfiguration();
if (org.apache.commons.lang3.StringUtils.isBlank(configuration)) {
if (StringUtils.isBlank(configuration)) {
return;
}
NodeDTO dto = JSON.parseObject(configuration, NodeDTO.class);
if (StringUtils.isNotBlank(dto.getIp())) {
Integer monitorPort = dto.getMonitorPort();
int port = monitorPort == null ? 9100 : monitorPort;
result.add(dto.getIp() + ":" + port);
Monitor monitor = new Monitor();
monitor.setIp(dto.getIp());
monitor.setPort(port);
result.add(monitor);
}
});
@ -247,11 +271,14 @@ public class MetricQueryService {
}
for (int i = 0; i < monitorParams.size(); i++) {
Monitor monitor = monitorParams.getObject(i, Monitor.class);
String instance = monitor.getIp() + ":" + monitor.getPort();
if (!result.contains(instance)) {
result.add(instance);
}
JSONObject o = monitorParams.getJSONObject(i);
Monitor monitor = new Monitor();
monitor.setIp(o.getString("ip"));
monitor.setName(o.getString("name"));
monitor.setPort(o.getInteger("port"));
monitor.setDescription(o.getString("description"));
monitor.setMonitorConfig(JSONObject.parseArray(o.getString("monitorConfig"), MonitorItem.class));
result.add(monitor);
}
return result;

View File

@ -6,6 +6,7 @@ import io.metersphere.dto.LogDetailDTO;
import io.metersphere.performance.base.*;
import io.metersphere.performance.dto.LoadTestExportJmx;
import io.metersphere.performance.dto.MetricData;
import io.metersphere.performance.dto.Monitor;
import lombok.Getter;
import lombok.Setter;
@ -42,7 +43,7 @@ public class TestPlanLoadCaseDTO extends TestPlanLoadCase {
private List<Errors> reportErrors;
private List<ErrorsTop5> reportErrorsTop5;
private List<LogDetailDTO> reportLogResource;
private List<String> reportResource;
private List<Monitor> reportResource;
private List<MetricData> metricData;
}
}

View File

@ -41,6 +41,7 @@ import io.metersphere.notice.service.NoticeSendService;
import io.metersphere.performance.base.*;
import io.metersphere.performance.dto.LoadTestExportJmx;
import io.metersphere.performance.dto.MetricData;
import io.metersphere.performance.dto.Monitor;
import io.metersphere.performance.request.RunTestPlanRequest;
import io.metersphere.performance.service.MetricQueryService;
import io.metersphere.performance.service.PerformanceReportService;
@ -1573,7 +1574,7 @@ public class TestPlanService {
response.setReportLogResource(reportLogResource);
// performanceReportService.getReportLogs(reportId, resourceId);
List<String> reportResource = metricQueryService.queryReportResource(reportId);
List<Monitor> reportResource = metricQueryService.queryReportResource(reportId);
response.setReportResource(reportResource);
List<MetricData> metricData = metricQueryService.queryMetric(reportId);
response.setMetricData(metricData);

@ -1 +1 @@
Subproject commit a6a66f87a57346cdd7da22086bac5e82a7e47bdb
Subproject commit 6e2b1d3d7d79f985e6edf81eb593670b8da87465

View File

@ -6,10 +6,10 @@
<el-select v-model="currentInstance" placeholder="" size="small" style="width: 100%"
@change="handleChecked(currentInstance)">
<el-option
v-for="item in instances"
:key="item"
:label="item"
:value="item">
v-for="item in instances"
:key="item.ip+item.port"
:value="item.ip+':'+item.port">
{{ item.ip }} {{ item.name }}
</el-option>
</el-select>
</div>
@ -35,47 +35,47 @@
<el-row>
<el-col :offset="2" :span="20">
<el-table
:data="tableData"
stripe
border
style="width: 100%">
:data="tableData"
stripe
border
style="width: 100%">
<el-table-column label="Label" align="center">
<el-table-column
prop="label"
label="Label"
sortable>
prop="label"
label="Label"
sortable>
</el-table-column>
</el-table-column>
<el-table-column label="Aggregate" align="center">
<el-table-column
prop="avg"
label="Avg."
width="100"
sortable
prop="avg"
label="Avg."
width="100"
sortable
/>
<el-table-column
prop="min"
label="Min."
width="100"
sortable
prop="min"
label="Min."
width="100"
sortable
/>
<el-table-column
prop="max"
label="Max."
width="100"
sortable
prop="max"
label="Max."
width="100"
sortable
/>
</el-table-column>
<el-table-column label="Range" align="center">
<el-table-column
prop="startTime"
label="Start"
width="160"
prop="startTime"
label="Start"
width="160"
/>
<el-table-column
prop="endTime"
label="End"
width="160"
prop="endTime"
label="End"
width="160"
/>
</el-table-column>
</el-table>
@ -97,6 +97,14 @@ import {
} from "@/network/load-test";
const color = ['#60acfc', '#32d3eb', '#5bc49f', '#feb64d', '#ff7c7c', '#9287e7', '#ca8622', '#bda29a', '#6e7074', '#546570', '#c4ccd3'];
const checkList = ['CPU', 'Memory', 'Disk', 'Network In', 'Network Out'];
const checkOptions = [
{key: 'cpu', label: 'CPU'},
{key: 'memory', label: 'Memory'},
{key: 'disk', label: 'Disk'},
{key: 'netIn', label: 'Network In'},
{key: 'netOut', label: 'Network Out'}
];
export default {
name: "MonitorCard",
@ -113,14 +121,8 @@ export default {
instances: [],
data: [],
tableData: [],
checkList: ['CPU', 'Memory', 'Disk', 'Network In', 'Network Out'],
checkOptions: [
{key: 'cpu', label: 'CPU'},
{key: 'memory', label: 'Memory'},
{key: 'disk', label: 'Disk'},
{key: 'netIn', label: 'Network In'},
{key: 'netOut', label: 'Network Out'}
],
checkList: checkList,
checkOptions: checkOptions,
baseOption: {
color: color,
grid: {
@ -175,14 +177,14 @@ export default {
// this.init = true;
if (this.planReportTemplate) {
this.instances = this.planReportTemplate.reportResource;
this.currentInstance = this.instances[0];
this.currentInstance = this.instances[0].ip + ":" + this.instances[0].port;
this.data = this.planReportTemplate.metricData;
this.totalOption = this.getOption(this.currentInstance);
} else if (this.isShare) {
getSharePerformanceMetricQueryResource(this.shareId, this.id).then(response => {
this.instances = response.data.data;
if (!this.currentInstance) {
this.currentInstance = this.instances[0];
this.currentInstance = this.instances[0].ip + ":" + this.instances[0].port;
}
getSharePerformanceMetricQuery(this.shareId, this.id).then(result => {
if (result) {
@ -196,7 +198,7 @@ export default {
getPerformanceMetricQueryResource(this.id).then(response => {
this.instances = response.data.data;
if (!this.currentInstance) {
this.currentInstance = this.instances[0];
this.currentInstance = this.instances[0].ip + ":" + this.instances[0].port;
}
getPerformanceMetricQuery(this.id).then(result => {
if (result) {
@ -211,6 +213,18 @@ export default {
}
},
handleChecked(id) {
let curr = this.instances.filter(instance => id === instance.ip + ":" + instance.port)[0];
if (curr.monitorConfig) {
this.checkList = [];
this.checkOptions = curr.monitorConfig.filter(mc => mc.value && mc.name)
.map(mc => {
this.checkList.push(mc.name);
return {key: mc.name, label: mc.name,};
});
} else {
this.checkOptions = checkOptions;
this.checkList = checkList;
}
this.totalOption = {};
this.$nextTick(() => {
this.totalOption = this.getOption(id);

View File

@ -8,27 +8,36 @@
:destroy-on-close="true"
v-loading="result.loading"
>
<el-form :model="form" label-position="right" label-width="80px" size="small" :rules="rule" ref="monitorForm">
<el-form-item :label="$t('commons.name')" prop="name">
<el-input v-model="form.name" autocomplete="off"/>
</el-form-item>
<h4 style="margin-left: 80px;">监控配置</h4>
<el-row>
<el-col :span="12">
<el-form-item label="IP" prop="ip">
<el-input v-model="form.ip" autocomplete="off"/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="Port" prop="port">
<el-input-number v-model="form.port" :min="1" :max="65535"/>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="描述" prop="description">
<el-input v-model="form.description" autocomplete="off"/>
</el-form-item>
</el-form>
<div style="height: 50vh;overflow-y: auto;">
<el-form :model="form" label-position="right" label-width="80px" size="small" :rules="rule" ref="monitorForm">
<el-form-item :label="$t('commons.name')" prop="name">
<el-input v-model="form.name" autocomplete="off"/>
</el-form-item>
<h4 style="margin-left: 80px;">监控配置</h4>
<el-row>
<el-col :span="12">
<el-form-item label="IP" prop="ip">
<el-input v-model="form.ip" autocomplete="off"/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="Port" prop="port">
<el-input-number v-model="form.port" :min="1" :max="65535"/>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="描述" prop="description">
<el-input v-model="form.description" autocomplete="off"/>
</el-form-item>
<h4 style="margin-left: 80px;">监控项</h4>
<el-row>
<el-col :span="20" :offset="2">
<monitor-key-value key-placeholder="Label" value-placeholder="promQL" :items="monitorList"
:suggestions="metricSuggestions"/>
</el-col>
</el-row>
</el-form>
</div>
<template v-slot:footer>
<ms-dialog-footer
@ -47,11 +56,11 @@
<script>
import MsDialogFooter from "@/business/components/common/components/MsDialogFooter";
import {CONFIG_TYPE} from "@/common/js/constants";
import MonitorKeyValue from "@/business/components/performance/test/components/MonitorKeyValue";
export default {
name: "EditMonitor",
components: {MsDialogFooter},
components: {MonitorKeyValue, MsDialogFooter},
props: {
testId: String,
list: Array,
@ -67,6 +76,8 @@ export default {
port: {required: true, message: "port必填", trigger: 'blur'},
},
index: '',
monitorList: [],
metricSuggestions: [{value: 'cpu'}, {value: 'memory'}, {value: 'disk'}, {value: 'netIn'}, {value: 'netOut'}]
};
},
methods: {
@ -75,6 +86,9 @@ export default {
this.dialogVisible = true;
if (data) {
const copy = JSON.parse(JSON.stringify(data));
if (copy.monitorConfig) {
this.monitorList = JSON.parse(copy.monitorConfig);
}
this.form = copy;
}
if (index !== '' && index !== undefined) {
@ -88,6 +102,7 @@ export default {
update() {
this.$refs.monitorForm.validate(valid => {
if (valid) {
this.form.monitorConfig = JSON.stringify(this.monitorList);
this.list.splice(this.index, 1, this.form);
this.$emit("update:list", this.list);
this.dialogVisible = false;
@ -99,6 +114,7 @@ export default {
create() {
this.$refs.monitorForm.validate(valid => {
if (valid) {
this.form.monitorConfig = JSON.stringify(this.monitorList);
// this.form.monitorStatus = CONFIG_TYPE.NOT;
this.list.push(this.form);
this.$emit("update:list", this.list);

View File

@ -0,0 +1,155 @@
<template>
<div>
<span class="kv-description" v-if="description">
{{ description }}
</span>
<div class="kv-row" v-for="(item, index) in items" :key="index">
<el-row type="flex" :gutter="20" justify="space-between" align="middle">
<el-col>
<el-input v-if="!suggestions" :disabled="isReadOnly" v-model="item.name" size="small" maxlength="200"
@change="changeKey(item)"
:placeholder="keyText" show-word-limit/>
<el-autocomplete :disabled="isReadOnly" :maxlength="200" v-if="suggestions" v-model="item.name" size="small"
:fetch-suggestions="querySearch" :placeholder="keyText"
@select="changeKey(item)"
show-word-limit/>
</el-col>
<el-col>
<el-input :disabled="isReadOnly" v-model="item.value" size="small" @change="change"
:placeholder="valueText" show-word-limit/>
</el-col>
<el-col class="kv-delete">
<el-button size="mini" class="el-icon-delete-solid" circle @click="remove(index)"
:disabled="isDisable(index) || isReadOnly"/>
</el-col>
</el-row>
</div>
</div>
</template>
<script>
const CPU_QL = "100 - (avg by (instance) (irate(node_cpu_seconds_total{instance='%1$s', mode='idle'}[1m])) * 100)";
const MEMORY_QL = "(node_memory_MemTotal_bytes{instance='%1$s'} - node_memory_MemAvailable_bytes{instance='%1$s'}) / node_memory_MemTotal_bytes{instance='%1$s'} * 100";
const DISK_QL = "100 - node_filesystem_free_bytes{instance='%1$s'} / node_filesystem_size_bytes{instance='%1$s'} * 100";
const NET_IN_QL = "sum by (instance) (irate(node_network_receive_bytes_total{instance='%1$s',device!~'bond.*?|lo'}[1m])/1024)";
const NET_OUT_QL = "sum by (instance) (irate(node_network_transmit_bytes_total{instance='%1$s',device!~'bond.*?|lo'}[1m])/1024)";
import {KeyValue} from "@/business/components/api/test/model/ScenarioModel";
export default {
name: "MonitorKeyValue",
props: {
keyPlaceholder: String,
valuePlaceholder: String,
description: String,
items: Array,
isReadOnly: {
type: Boolean,
default: false
},
suggestions: Array
},
data() {
return {};
},
computed: {
keyText() {
return this.keyPlaceholder || this.$t("api_test.key");
},
valueText() {
return this.valuePlaceholder || this.$t("api_test.value");
}
},
methods: {
remove: function (index) {
//
this.items.splice(index, 1);
this.$emit('change', this.items);
},
change: function () {
let isNeedCreate = true;
let removeIndex = -1;
this.items.forEach((item, index) => {
if (!item.name && !item.value) {
//
if (index !== this.items.length - 1) {
removeIndex = index;
}
//
isNeedCreate = false;
}
});
if (isNeedCreate) {
this.items.push(new KeyValue({enable: true}));
}
this.$emit('change', this.items);
// TODO key
},
changeKey: function (curr) {
//
if (curr.name === 'cpu') {
curr.value = CPU_QL;
}
if (curr.name === 'memory') {
curr.value = MEMORY_QL;
}
if (curr.name === 'disk') {
curr.value = DISK_QL;
}
if (curr.name === 'netIn') {
curr.value = NET_IN_QL;
}
if (curr.name === 'netOut') {
curr.value = NET_OUT_QL;
}
this.change();
},
isDisable: function (index) {
return this.items.length - 1 === index;
},
querySearch(queryString, cb) {
let suggestions = this.suggestions;
let results = queryString ? suggestions.filter(this.createFilter(queryString)) : suggestions;
cb(results);
},
createFilter(queryString) {
return (restaurant) => {
return (restaurant.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0);
};
},
},
created() {
if (this.items.length === 0 || this.items[this.items.length - 1].name) {
this.items.push(new KeyValue({enable: true}));
}
}
};
</script>
<style scoped>
.kv-description {
font-size: 13px;
}
.kv-row {
margin-top: 10px;
}
.kv-checkbox {
width: 20px;
margin-right: 10px;
}
.kv-delete {
width: 60px;
}
.el-autocomplete {
width: 100%;
}
</style>