feat(性能测试): 测试报告页面添加监控Tab

This commit is contained in:
shiziyuan9527 2021-04-14 12:02:21 +08:00 committed by 刘瑞斌
parent 814b5129eb
commit 1807ff3538
12 changed files with 588 additions and 86 deletions

View File

@ -0,0 +1,5 @@
package io.metersphere.performance.base;
public enum MonitorStatus {
NOT, NORMAL, ABNORMAL
}

View File

@ -0,0 +1,21 @@
package io.metersphere.performance.controller;
import io.metersphere.performance.dto.MetricData;
import io.metersphere.performance.service.MetricQueryService;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
@RestController
@RequestMapping("/metric")
public class MetricQueryController {
@Resource
private MetricQueryService metricService;
@GetMapping("/query/{id}")
public List<MetricData> queryMetric(@PathVariable("id") String reportId) {
return metricService.queryMetric(reportId);
}
}

View File

@ -0,0 +1,12 @@
package io.metersphere.performance.controller.request;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class MetricDataRequest {
private String seriesName;
private String promQL;
private String instance;
}

View File

@ -0,0 +1,19 @@
package io.metersphere.performance.controller.request;
import java.util.HashMap;
import java.util.Map;
public class MetricQuery {
public static Map<String, String> getMetricQueryMap() {
return new HashMap<String, String>(16) {{
// 指标名:promQL
put("cpu", "100 - (avg by (instance) (irate(node_cpu_seconds_total{instance='%1$s', mode='idle'}[1m])) * 100)");
put("disk", "100 - node_filesystem_free_bytes{instance='%1$s',fstype!~'rootfs|selinuxfs|autofs|rpc_pipefs|tmpfs|udev|none|devpts|sysfs|debugfs|fuse.*'} / node_filesystem_size_bytes{instance='%1$s',fstype!~'rootfs|selinuxfs|autofs|rpc_pipefs|tmpfs|udev|none|devpts|sysfs|debugfs|fuse.*'} * 100");
put("memory", "(node_memory_MemTotal_bytes{instance='%1$s'} - node_memory_MemFree_bytes{instance='%1$s'}) / node_memory_MemTotal_bytes{instance='%1$s'} * 100");
put("netIn", "sum by (instance) (irate(node_network_receive_bytes_total{instance='%1$s',device!~'bond.*?|lo'}[1m])/128)");
put("netOut", "sum by (instance) (irate(node_network_transmit_bytes_total{instance='%1$s',device!~'bond.*?|lo'}[1m])/128)");
}};
}
}

View File

@ -0,0 +1,16 @@
package io.metersphere.performance.controller.request;
import lombok.Getter;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
@Getter
@Setter
public class MetricRequest {
private List<MetricDataRequest> metricDataQueries = new ArrayList<>();
private long startTime;
private long endTime;
private int step = 15;
}

View File

@ -0,0 +1,16 @@
package io.metersphere.performance.dto;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
@Getter
@Setter
public class MetricData {
private String uniqueLabel;
private String seriesName;
private List<Double> values;
private List<String> timestamps;
private String instance;
}

View File

@ -0,0 +1,16 @@
package io.metersphere.performance.dto;
import lombok.Getter;
import lombok.Setter;
@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;
}

View File

@ -0,0 +1,187 @@
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.LoadTestWithBLOBs;
import io.metersphere.base.mapper.LoadTestMapper;
import io.metersphere.base.mapper.LoadTestReportMapper;
import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.DateUtils;
import io.metersphere.commons.utils.LogUtil;
import io.metersphere.performance.base.ReportTimeInfo;
import io.metersphere.performance.controller.request.MetricDataRequest;
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 org.apache.commons.collections.CollectionUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.*;
@Service
@Transactional(rollbackFor = Exception.class)
public class MetricQueryService {
private String prometheusHost = "http://192.168.1.8:9090";
@Resource
private RestTemplate restTemplate;
@Resource
private LoadTestReportMapper loadTestReportMapper;
@Resource
private LoadTestMapper loadTestMapper;
@Resource
private ReportService reportService;
public List<MetricData> queryMetricData(MetricRequest metricRequest) {
List<MetricData> metricDataList = new ArrayList<>();
long endTime = metricRequest.getEndTime();
long startTime = metricRequest.getStartTime();
int step = metricRequest.getStep();
long reliableEndTime;
if (endTime > System.currentTimeMillis()) {
reliableEndTime = System.currentTimeMillis();
} else {
reliableEndTime = endTime;
}
Optional.ofNullable(metricRequest.getMetricDataQueries()).ifPresent(metricDataQueries -> metricDataQueries.forEach(query -> {
String promQL = query.getPromQL();
promQL = String.format(promQL, query.getInstance());
if (StringUtils.isEmpty(promQL)) {
MSException.throwException("promQL is null");
} else {
Optional.of(queryPrometheusMetric(promQL, query.getSeriesName(), startTime, reliableEndTime, step, query.getInstance())).ifPresent(metricDataList::addAll);
}
}));
return metricDataList;
}
private List<MetricData> queryPrometheusMetric(String promQL, String seriesName, long startTime, long endTime, int step, String instance) {
DecimalFormat df = new DecimalFormat("#.###");
String start = df.format(startTime / 1000.0);
String end = df.format(endTime / 1000.0);
JSONObject response = restTemplate.getForObject(prometheusHost + "/api/v1/query_range?query={promQL}&start={start}&end={end}&step={step}", JSONObject.class, promQL, start, end, step);
return handleResult(seriesName, response, instance);
}
private List<MetricData> handleResult(String seriesName, JSONObject response, String instance) {
List<MetricData> list = new ArrayList<>();
Map<String, Set<String>> labelMap = new HashMap<>();
if (response != null && StringUtils.equals(response.getString("status"), "success")) {
JSONObject data = response.getJSONObject("data");
JSONArray result = data.getJSONArray("result");
if (result.size() > 1) {
result.forEach(rObject -> {
JSONObject resultObject = new JSONObject((Map)rObject);
// JSONObject resultObject = JSONObject.parseObject(rObject.toString());
JSONObject metrics = resultObject.getJSONObject("metric");
if (metrics != null && metrics.size() > 0) {
for (Map.Entry<String, Object> entry : metrics.entrySet())
labelMap.computeIfAbsent(entry.getKey(), k -> new HashSet<>()).add(entry.getValue().toString());
}
});
}
Optional<String> uniqueLabelKey = labelMap.entrySet().stream().filter(entry -> entry.getValue().size() == result.size()).map(Map.Entry::getKey).findFirst();
result.forEach(rObject -> {
MetricData metricData = new MetricData();
List<String> timestamps = new ArrayList<>();
List<Double> values = new ArrayList<>();
JSONObject resultObject = new JSONObject((Map)rObject);
JSONObject metrics = resultObject.getJSONObject("metric");
JSONArray jsonArray = resultObject.getJSONArray("values");
jsonArray.forEach(value -> {
JSONArray ja = JSONObject.parseArray(value.toString());
Double timestamp = ja.getDouble(0);
try {
timestamps.add(DateUtils.getTimeString((long) (timestamp * 1000)));
} catch (Exception e) {
e.printStackTrace();
}
values.add(ja.getDouble(1));
});
if (CollectionUtils.isNotEmpty(values)) {
metricData.setValues(values);
metricData.setTimestamps(timestamps);
metricData.setSeriesName(seriesName);
metricData.setInstance(instance);
uniqueLabelKey.ifPresent(s -> metricData.setUniqueLabel(metrics.getString(s)));
list.add(metricData);
}
});
}
return list;
}
public List<MetricData> queryMetric(String reportId) {
LoadTestReportWithBLOBs report = loadTestReportMapper.selectByPrimaryKey(reportId);
String testId = report.getTestId();
LoadTestWithBLOBs loadTestWithBLOBs = loadTestMapper.selectByPrimaryKey(testId);
String advancedConfiguration = loadTestWithBLOBs.getAdvancedConfiguration();
JSONObject jsonObject = JSON.parseObject(advancedConfiguration);
JSONArray monitorParams = jsonObject.getJSONArray("monitorParams");
if (monitorParams == null) {
return new ArrayList<>();
}
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();
getRequest(instance, list);
}
ReportTimeInfo reportTimeInfo = reportService.getReportTimeInfo(reportId);
MetricRequest metricRequest = new MetricRequest();
metricRequest.setMetricDataQueries(list);
try {
SimpleDateFormat df = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
Date startTime = df.parse(reportTimeInfo.getStartTime());
Date endTime = df.parse(reportTimeInfo.getEndTime());
metricRequest.setStartTime(startTime.getTime());
metricRequest.setEndTime(endTime.getTime());
} catch (Exception e) {
LogUtil.error(e, e.getMessage());
e.printStackTrace();
}
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);
});
}
}

View File

@ -1,5 +1,7 @@
package io.metersphere.performance.service;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import io.metersphere.base.domain.*;
import io.metersphere.base.mapper.*;
import io.metersphere.base.mapper.ext.ExtLoadTestMapper;
@ -17,10 +19,13 @@ import io.metersphere.controller.request.QueryScheduleRequest;
import io.metersphere.controller.request.ScheduleRequest;
import io.metersphere.dto.DashboardTestDTO;
import io.metersphere.dto.LoadTestDTO;
import io.metersphere.dto.NodeDTO;
import io.metersphere.dto.ScheduleDao;
import io.metersphere.i18n.Translator;
import io.metersphere.job.sechedule.PerformanceTestJob;
import io.metersphere.performance.base.MonitorStatus;
import io.metersphere.performance.dto.LoadTestExportJmx;
import io.metersphere.performance.dto.Monitor;
import io.metersphere.performance.engine.Engine;
import io.metersphere.performance.engine.EngineFactory;
import io.metersphere.performance.engine.producer.LoadTestProducer;
@ -44,6 +49,7 @@ import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
@Service
@ -81,6 +87,8 @@ public class PerformanceTestService {
private TestResourcePoolMapper testResourcePoolMapper;
@Resource
private LoadTestProducer loadTestProducer;
@Resource
private TestResourceMapper testResourceMapper;
public List<LoadTestDTO> list(QueryTestPlanRequest request) {
request.setOrders(ServiceUtils.getDefaultOrder(request.getOrders()));
@ -204,6 +212,36 @@ public class PerformanceTestService {
}
final LoadTestWithBLOBs loadTest = new LoadTestWithBLOBs();
String poolId = request.getTestResourcePoolId();
TestResourceExample testResourceExample = new TestResourceExample();
testResourceExample.createCriteria().andTestResourcePoolIdEqualTo(poolId);
List<TestResource> testResources = testResourceMapper.selectByExampleWithBLOBs(testResourceExample);
String advancedConfiguration = request.getAdvancedConfiguration();
JSONObject jsonObject = JSON.parseObject(advancedConfiguration);
List<Monitor> list = new ArrayList<>();
if (!CollectionUtils.isEmpty(testResources)) {
AtomicInteger index = new AtomicInteger(1);
testResources.forEach(testResource -> {
String configuration = testResource.getConfiguration();
NodeDTO nodeDTO = JSON.parseObject(configuration, NodeDTO.class);
Monitor monitor = new Monitor();
monitor.setName("名称" + index.getAndIncrement());
monitor.setDescription("默认生成");
monitor.setIp(nodeDTO.getIp());
monitor.setPort(9100);
monitor.setMonitorStatus(MonitorStatus.NORMAL.name());
list.add(monitor);
});
}
if (!CollectionUtils.isEmpty(list)) {
jsonObject.put("monitorParams", list);
}
advancedConfiguration = JSON.toJSONString(jsonObject);
loadTest.setUserId(SessionUtils.getUser().getId());
loadTest.setId(UUID.randomUUID().toString());
loadTest.setName(request.getName());
@ -212,7 +250,7 @@ public class PerformanceTestService {
loadTest.setUpdateTime(System.currentTimeMillis());
loadTest.setTestResourcePoolId(request.getTestResourcePoolId());
loadTest.setLoadConfiguration(request.getLoadConfiguration());
loadTest.setAdvancedConfiguration(request.getAdvancedConfiguration());
loadTest.setAdvancedConfiguration(advancedConfiguration);
loadTest.setStatus(PerformanceTestStatus.Saved.name());
loadTest.setNum(getNextNum(request.getProjectId()));
loadTestMapper.insert(loadTest);

View File

@ -81,6 +81,9 @@
<el-tab-pane :label="$t('report.test_log_details')">
<ms-report-log-details :report="report"/>
</el-tab-pane>
<el-tab-pane label="监控详情">
<monitor-card :report="report"/>
</el-tab-pane>
</el-tabs>
</div>
@ -117,11 +120,13 @@ import html2canvas from 'html2canvas';
import MsPerformanceReportExport from "./PerformanceReportExport";
import {Message} from "element-ui";
import SameTestReports from "@/business/components/performance/report/components/SameTestReports";
import MonitorCard from "@/business/components/performance/report/components/MonitorCard";
export default {
name: "PerformanceReportView",
components: {
MonitorCard,
SameTestReports,
MsPerformanceReportExport,
MsReportErrorLog,

View File

@ -0,0 +1,240 @@
<template>
<div v-loading="result.loading">
<el-tabs type="border-card" :stretch="true">
<el-tab-pane v-for="item in instances" :key="item" :label="item" class="logging-content">
<el-row>
<el-col :span="10" :offset="2">
<ms-chart ref="chart1" :options="getCpuOption(item)" :autoresize="true"></ms-chart>
</el-col>
<el-col :span="10" :offset="2">
<ms-chart ref="chart2" :options="getMemoryOption(item)" :autoresize="true"></ms-chart>
</el-col>
</el-row>
<el-row>
<el-col :span="10" :offset="2">
<ms-chart ref="chart3" :options="getDiskOption(item)" :autoresize="true"></ms-chart>
</el-col>
<el-col :span="10" :offset="2">
<ms-chart ref="chart4" :options="getNetInOption(item)" :autoresize="true"></ms-chart>
</el-col>
</el-row>
<el-row>
<el-col :span="10" :offset="2">
<ms-chart ref="chart3" :options="getNetOutOption(item)" :autoresize="true"></ms-chart>
</el-col>
</el-row>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script>
import MsChart from "@/business/components/common/chart/MsChart";
export default {
name: "MonitorCard",
props: ['report'],
components: {MsChart},
data() {
return {
resource: [],
logContent: [],
result: {},
id: '',
loading: false,
instances: [],
data: []
}
},
methods: {
getResource() {
this.$get("/metric/query/" + this.report.id, result => {
if (result) {
let data = result.data;
this.data = data;
let set = new Set()
data.map(d => set.add(d.instance));
this.instances = Array.from(set);
}
});
},
getCpuOption(id) {
let xAxis = [];
let yAxis = [];
this.data.map(d => {
if (d.instance === id && d.seriesName === 'cpu') {
xAxis = d.timestamps;
yAxis = d.values;
}
});
let option = {
title: {
left: 'center',
text: 'CPU',
textStyle: {
color: '#99743C'
},
},
xAxis: {
type: 'category',
data: xAxis
},
yAxis: {
type: 'value'
},
series: [{
data: yAxis,
type: 'line'
}]
};
return option;
},
getDiskOption(id) {
let xAxis = [];
let yAxis = [];
this.data.map(d => {
if (d.instance === id && d.seriesName === 'disk') {
xAxis = d.timestamps;
yAxis = d.values;
}
});
let option = {
title: {
left: 'center',
text: 'Disk',
textStyle: {
color: '#99743C'
},
},
xAxis: {
type: 'category',
data: xAxis
},
yAxis: {
type: 'value'
},
series: [{
data: yAxis,
type: 'line'
}]
};
return option;
},
getNetInOption(id) {
let xAxis = [];
let yAxis = [];
this.data.map(d => {
if (d.instance === id && d.seriesName === 'netIn') {
xAxis = d.timestamps;
yAxis = d.values;
}
});
let option = {
title: {
left: 'center',
text: 'NetIn',
textStyle: {
color: '#99743C'
},
},
xAxis: {
type: 'category',
data: xAxis
},
yAxis: {
type: 'value'
},
series: [{
data: yAxis,
type: 'line'
}]
};
return option;
},
getNetOutOption(id) {
let xAxis = [];
let yAxis = [];
this.data.map(d => {
if (d.instance === id && d.seriesName === 'netOut') {
xAxis = d.timestamps;
yAxis = d.values;
}
});
let option = {
title: {
left: 'center',
text: 'NetOut',
textStyle: {
color: '#99743C'
},
},
xAxis: {
type: 'category',
data: xAxis
},
yAxis: {
type: 'value'
},
series: [{
data: yAxis,
type: 'line'
}]
};
return option;
},
getMemoryOption(id) {
let xAxis = [];
let yAxis = [];
this.data.map(d => {
if (d.instance === id && d.seriesName === 'memory') {
xAxis = d.timestamps;
yAxis = d.values;
}
});
let option = {
title: {
left: 'center',
text: 'Memory',
textStyle: {
color: '#99743C'
},
},
xAxis: {
type: 'category',
data: xAxis
},
yAxis: {
type: 'value'
},
series: [{
data: yAxis,
type: 'line'
}]
};
return option;
}
},
watch: {
report: {
handler(val) {
if (!val.status || !val.id) {
return;
}
let status = val.status;
this.id = val.id;
if (status === "Completed" || status === "Running") {
this.getResource();
} else {
this.resource = [];
}
},
deep: true
}
},
}
</script>
<style scoped>
</style>

View File

@ -22,49 +22,19 @@
</el-option>
</el-select>
</el-form-item>
<!-- <h4 style="margin-left: 80px;">认证配置</h4>-->
<!-- <el-form-item label="IP" prop="name">-->
<!-- <el-input v-model="form.ip" autocomplete="off"/>-->
<!-- </el-form-item>-->
<!-- <el-form-item label="用户名" prop="description">-->
<!-- <el-input v-model="form.username" autocomplete="off"/>-->
<!-- </el-form-item>-->
<!-- <el-form-item label="密码" prop="description">-->
<!-- <el-input v-model="form.password" autocomplete="off"/>-->
<!-- </el-form-item>-->
<h4 style="margin-left: 80px;">监控配置</h4>
<el-form-item label="地址" prop="host">
<el-input v-model="form.host" autocomplete="off"/>
</el-form-item>
<div v-for="(item,index) in monitorList " :key="index">
<el-row>
<el-col :span="8">
<el-form-item label="指标名" prop="indicator">
<el-input v-model="item.indicator" autocomplete="off"/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="表达式" prop="expression">
<el-input v-model="item.expression" autocomplete="off"/>
</el-form-item>
</el-col>
<el-col :offset="1" :span="2">
<span class="box">
<el-button @click="addMonitorConfig" type="success" size="mini" circle>
<font-awesome-icon :icon="['fas', 'plus']"/>
</el-button>
</span>
<span class="box">
<el-button @click="delMonitorConfig" type="danger" size="mini" circle>
<font-awesome-icon :icon="['fas', 'minus']"/>
</el-button>
</span>
</el-col>
</el-row>
</div>
<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">
<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>
@ -115,19 +85,10 @@ export default {
methods: {
open(data, index) {
this.index = '';
this.monitorList = [
{
indicator: '',
expression: '',
}
];
this.dialogVisible = true;
if (data) {
const copy = JSON.parse(JSON.stringify(data));
this.form = copy;
if (copy.monitorConfig) {
this.monitorList = JSON.parse(copy.monitorConfig);
}
}
if (index !== '' && index !== undefined) {
this.index = index;
@ -140,13 +101,6 @@ export default {
update() {
this.$refs.monitorForm.validate(valid => {
if (valid) {
this.form.monitorConfig = JSON.stringify(this.monitorList);
// let authConfig = {
// "ip": this.form.ip,
// "username": this.form.username,
// "password": this.form.password,
// };
// this.form.authConfig = JSON.stringify(authConfig);
this.list.splice(this.index, 1, this.form);
this.$emit("update:list", this.list);
} else {
@ -158,13 +112,6 @@ export default {
create() {
this.$refs.monitorForm.validate(valid => {
if (valid) {
this.form.monitorConfig = JSON.stringify(this.monitorList);
// let authConfig = {
// "ip": this.form.ip,
// "username": this.form.username,
// "password": this.form.password,
// };
// this.form.authConfig = JSON.stringify(authConfig);
this.form.loadTestId = this.testId;
this.form.authStatus = CONFIG_TYPE.NOT;
this.form.monitorStatus = CONFIG_TYPE.NOT;
@ -176,26 +123,6 @@ export default {
})
this.dialogVisible = false;
},
convertConfig() {
let config = [];
if (this.form.monitorConfig) {
config = JSON.parse(this.form.monitorConfig);
}
this.monitorList = config;
},
addMonitorConfig() {
this.monitorList.push({
indicator: '',
expression: ''
});
},
delMonitorConfig(index) {
if (this.monitorList.length > 1) {
this.monitorList.splice(index, 1);
} else {
this.$warning("不能删除当前节点");
}
},
change(data) {
let env = this.environments.find(env => env.id === data);
this.form.environmentName = env ? env.name : "";