feat(性能测试): 测试报告支持自定义图表

This commit is contained in:
Captain.B 2021-06-04 17:58:35 +08:00 committed by 刘瑞斌
parent cc24a21e48
commit 051552c0da
8 changed files with 369 additions and 7 deletions

View File

@ -1,6 +1,24 @@
package io.metersphere.commons.constants; package io.metersphere.commons.constants;
public enum ReportKeys { public enum ReportKeys {
LoadChart, ResponseTimeChart, ResponseCodeChart, Errors, ErrorsTop5, RequestStatistics, Overview, TimeInfo, ResultStatus, ErrorsChart LoadChart,
ResponseTimeChart,
ResponseTimePercentilesChart,
ConnectTimeChart,
BytesThroughputChart,
DistributedActiveThreads,
HitsChart,
LatencyChart,
ResponseCodeChart,
TransactionsChart,
TotalTransactionsChart,
ResponseTimeVsRequestChart,
LatencyVsRequestChart,
ErrorsChart,
Errors,
ErrorsTop5,
RequestStatistics,
Overview,
TimeInfo,
ResultStatus,
} }

View File

@ -68,6 +68,11 @@ public class PerformanceReportController {
return performanceReportService.getReportErrors(reportId); return performanceReportService.getReportErrors(reportId);
} }
@GetMapping("/content/{reportKey}/{reportId}")
public List<ChartsData> getReportChart(@PathVariable String reportKey, @PathVariable String reportId) {
return performanceReportService.getReportChart(reportKey, reportId);
}
@GetMapping("/content/errors_top5/{reportId}") @GetMapping("/content/errors_top5/{reportId}")
public List<ErrorsTop5> getReportErrorsTop5(@PathVariable String reportId) { public List<ErrorsTop5> getReportErrorsTop5(@PathVariable String reportId) {
return performanceReportService.getReportErrorsTOP5(reportId); return performanceReportService.getReportErrorsTOP5(reportId);

View File

@ -392,4 +392,14 @@ public class PerformanceReportService {
} }
return null; return null;
} }
public List<ChartsData> getReportChart(String reportKey, String reportId) {
checkReportStatus(reportId);
try {
String content = getContent(reportId, ReportKeys.valueOf(reportKey));
return JSON.parseArray(content, ChartsData.class);
} catch (Exception e) {
return new ArrayList<>();
}
}
} }

View File

@ -26,7 +26,8 @@
@click="rerun(testId)"> @click="rerun(testId)">
{{ $t('report.test_execute_again') }} {{ $t('report.test_execute_again') }}
</el-button> </el-button>
<el-button :disabled="isReadOnly" type="info" plain size="mini" @click="handleExport(reportName)" v-permission="['PROJECT_PERFORMANCE_REPORT:READ+EXPORT']"> <el-button :disabled="isReadOnly" type="info" plain size="mini" @click="handleExport(reportName)"
v-permission="['PROJECT_PERFORMANCE_REPORT:READ+EXPORT']">
{{ $t('test_track.plan_view.export_report') }} {{ $t('test_track.plan_view.export_report') }}
</el-button> </el-button>
<el-button :disabled="report.status !== 'Completed'" type="default" plain <el-button :disabled="report.status !== 'Completed'" type="default" plain
@ -84,6 +85,9 @@
<el-tab-pane :label="$t('report.test_overview')"> <el-tab-pane :label="$t('report.test_overview')">
<ms-report-test-overview :report="report" ref="testOverview"/> <ms-report-test-overview :report="report" ref="testOverview"/>
</el-tab-pane> </el-tab-pane>
<el-tab-pane :label="$t('report.test_details')">
<ms-report-test-details :report="report" ref="testDetails"/>
</el-tab-pane>
<el-tab-pane :label="$t('report.test_request_statistics')"> <el-tab-pane :label="$t('report.test_request_statistics')">
<ms-report-request-statistics :report="report" ref="requestStatistics"/> <ms-report-request-statistics :report="report" ref="requestStatistics"/>
</el-tab-pane> </el-tab-pane>
@ -122,6 +126,7 @@
import MsReportErrorLog from './components/ErrorLog'; import MsReportErrorLog from './components/ErrorLog';
import MsReportLogDetails from './components/LogDetails'; import MsReportLogDetails from './components/LogDetails';
import MsReportRequestStatistics from './components/RequestStatistics'; import MsReportRequestStatistics from './components/RequestStatistics';
import MsReportTestDetails from './components/TestDetails';
import MsReportTestOverview from './components/TestOverview'; import MsReportTestOverview from './components/TestOverview';
import MsPerformancePressureConfig from "./components/PerformancePressureConfig"; import MsPerformancePressureConfig from "./components/PerformancePressureConfig";
import MsContainer from "../../common/components/MsContainer"; import MsContainer from "../../common/components/MsContainer";
@ -147,6 +152,7 @@ export default {
MsReportTestOverview, MsReportTestOverview,
MsContainer, MsContainer,
MsMainContainer, MsMainContainer,
MsReportTestDetails,
MsPerformancePressureConfig MsPerformancePressureConfig
}, },
data() { data() {

View File

@ -0,0 +1,299 @@
<template>
<div>
<el-row>
<el-col :span="6">
<el-collapse v-model="activeNames" class="test-detail">
<el-collapse-item :title="$t('load_test.report.ActiveThreadsChart')" name="users">
<el-checkbox-group v-model="checkList['ActiveThreadsChart']"
@change="handleChecked('ActiveThreadsChart')">
<el-checkbox v-for="name in checkOptions['ActiveThreadsChart']" :key="name" :label="name"/>
</el-checkbox-group>
</el-collapse-item>
<el-collapse-item :title="$t('load_test.report.TransactionsChart')" name="transactions">
<el-checkbox-group v-model="checkList['TransactionsChart']" @change="handleChecked('TransactionsChart')">
<el-checkbox v-for="name in checkOptions['TransactionsChart']" :key="name" :label="name"/>
</el-checkbox-group>
</el-collapse-item>
<el-collapse-item :title="$t('load_test.report.ResponseTimeChart')" name="responseTime">
<el-checkbox-group v-model="checkList['ResponseTimeChart']" @change="handleChecked('ResponseTimeChart')">
<el-checkbox v-for="name in checkOptions['ResponseTimeChart']" :key="name" :label="name"/>
</el-checkbox-group>
</el-collapse-item>
<el-collapse-item :title="$t('load_test.report.ResponseTimePercentilesChart')" name="responseTimePercentiles">
<el-checkbox-group v-model="checkList['ResponseTimePercentilesChart']"
@change="handleChecked('ResponseTimePercentilesChart')">
<el-checkbox v-for="name in checkOptions['ResponseTimePercentilesChart']" :key="name" :label="name"/>
</el-checkbox-group>
</el-collapse-item>
<el-collapse-item :title="$t('load_test.report.ResponseCodeChart')" name="responseCode">
<el-checkbox-group v-model="checkList['ResponseCodeChart']" @change="handleChecked('ResponseCodeChart')">
<el-checkbox v-for="code in checkOptions['ResponseCodeChart']" :key="code" :label="code"/>
</el-checkbox-group>
</el-collapse-item>
<el-collapse-item :title="$t('load_test.report.LatencyChart')" name="latency">
<el-checkbox-group v-model="checkList['LatencyChart']" @change="handleChecked('LatencyChart')">
<el-checkbox v-for="name in checkOptions['LatencyChart']" :key="name" :label="name"/>
</el-checkbox-group>
</el-collapse-item>
<el-collapse-item :title="$t('load_test.report.BytesThroughputChart')" name="bytes">
<el-checkbox-group v-model="checkList['BytesThroughputChart']"
@change="handleChecked('BytesThroughputChart')">
<el-checkbox v-for="code in checkOptions['BytesThroughputChart']" :key="code" :label="code"/>
</el-checkbox-group>
</el-collapse-item>
<el-collapse-item :title="$t('load_test.report.ErrorsChart')" name="errors">
<el-checkbox-group v-model="checkList['ErrorsChart']" @change="handleChecked('ErrorsChart')">
<el-checkbox v-for="name in checkOptions['ErrorsChart']" :key="name" :label="name"/>
</el-checkbox-group>
</el-collapse-item>
</el-collapse>
</el-col>
<el-col :span="18">
<ms-chart ref="chart2" :options="totalOption" class="chart-config" :autoresize="true"></ms-chart>
</el-col>
</el-row>
</div>
</template>
<script>
import MsChart from "@/business/components/common/chart/MsChart";
const CHART_MAP = [
'ActiveThreadsChart',
'TransactionsChart',
'ResponseTimePercentilesChart',
'ResponseTimeChart',
'ResponseCodeChart',
'ErrorsChart',
'LatencyChart',
'BytesThroughputChart',
];
export default {
name: "TestDetails",
components: {MsChart},
props: ['report', 'export'],
data() {
return {
activeNames: 'users',
loadOption: {},
resOption: {},
totalOption: {},
responseCodes: [],
checkList: CHART_MAP.reduce((result, curr) => {
result[curr] = [];
return result;
}, {}),
checkOptions: {},
defaultProps: {
children: 'children',
label: 'label'
},
init: false,
baseOption: {
title: {
text: 'Test Details',
left: 'center',
top: 20,
textStyle: {
color: '#99743C'
},
},
tooltip: {
show: true,
trigger: 'axis',
// extraCssText: 'z-index: 999;',
confine: true,
formatter: function (params, ticket, callback) {
let result = "";
let name = params[0].name;
result += name + "<br/>";
for (let i = 0; i < params.length; i++) {
let seriesName = params[i].seriesName;
if (seriesName.length > 100) {
seriesName = seriesName.substring(0, 100);
}
let value = params[i].value;
let marker = params[i].marker;
result += marker + seriesName + ": " + value[1] + "<br/>";
}
return result;
}
},
legend: {},
xAxis: {},
series: []
},
chartData: [],
};
},
methods: {
handleChecked(name) {
this.getTotalChart();
},
initTableData() {
this.init = true;
for (const name of CHART_MAP) {
this.getCheckOptions(name);
}
this.checkList['ActiveThreadsChart'] = ['ALL'];
this.checkList['TransactionsChart'] = ['ALL'];
this.checkList['ResponseTimeChart'] = ['ALL'];
this.getTotalChart();
},
getCheckOptions(reportKey) {
this.$get("/performance/report/content/" + reportKey + "/" + this.id)
.then(res => {
let data = res.data.data;
if (!data) {
return;
}
let yAxisIndex0List = data.filter(m => m.yAxis2 === -1).map(m => m.groupName);
yAxisIndex0List = this._unique(yAxisIndex0List);
this.checkOptions[reportKey] = ['ALL'].concat(yAxisIndex0List);
});
},
getTotalChart() {
this.totalOption = {};
this.chartData = [];
for (let name in this.checkList) {
if (this.checkList[name].length > 0) {
this.getChart(name, this.checkList[name]);
}
}
},
getChart(reportKey, checkList) {
this.$get("/performance/report/content/" + reportKey + "/" + this.id)
.then(res => {
let data = res.data.data;
if (!data || data.length === 0) {
this.init = false;
}
if (checkList) {
data = data.filter(item => {
if (checkList.indexOf('ALL') > -1) {
return true;
}
if (checkList.indexOf(item.groupName) > -1) {
return true;
}
});
}
// prefix
data.forEach(item => {
item.groupName = this.$t('load_test.report.' + reportKey) + ': ' + item.groupName;
});
this.chartData = this.chartData.concat(data);
let yAxisList = data.filter(m => m.yAxis2 === -1).map(m => m.yAxis);
let yAxisListMax = this._getChartMax(yAxisList);
this.baseOption.yAxis = [
{
name: 'Value',
type: 'value',
min: 0,
}
];
this.totalOption = this.generateOption(this.baseOption, this.chartData);
})
.catch(() => {
this.totalOption = {};
});
},
generateOption(option, data) {
let chartData = data;
let legend = [], series = {}, xAxis = [], seriesData = [];
chartData.forEach(item => {
if (!xAxis.includes(item.xAxis)) {
xAxis.push(item.xAxis);
}
xAxis.sort();
let name = item.groupName;
if (!legend.includes(name)) {
legend.push(name);
series[name] = [];
}
series[name].splice(xAxis.indexOf(item.xAxis), 0, [item.xAxis, item.yAxis.toFixed(2)]);
});
this.$set(option.legend, "data", legend);
this.$set(option.legend, "type", "scroll");
this.$set(option.legend, "bottom", "10px");
this.$set(option.xAxis, "data", xAxis);
for (let name in series) {
let d = series[name];
d.sort((a, b) => a[0].localeCompare(b[0]));
let items = {
name: name,
type: 'line',
data: d,
smooth: true,
sampling: 'lttb',
animation: !this.export,
};
seriesData.push(items);
}
this.$set(option, "series", seriesData);
return option;
},
_getChartMax(arr) {
const max = Math.max(...arr);
return Math.ceil(max / 4.5) * 5;
},
_unique(arr) {
return Array.from(new Set(arr));
}
},
created() {
this.id = this.$route.path.split('/')[4];
this.initTableData();
},
watch: {
'$route'(to) {
if (to.name === "perReportView") {
this.id = to.path.split('/')[4];
this.init = false;
this.initTableData();
}
},
report: {
handler(val) {
if (!val.status || !val.id) {
return;
}
let status = val.status;
this.id = val.id;
if (this.init) {
return;
}
if (status === "Completed" || status === "Running") {
this.initTableData();
}
},
deep: true
}
},
};
</script>
<style scoped>
.chart-config {
width: 100%;
height: 450px;
}
.test-detail {
height: calc(100vh - 320px);
overflow: auto;
}
</style>

View File

@ -646,7 +646,15 @@ export default {
project_file_exist: "The file already exists in the project, please import it directly", project_file_exist: "The file already exists in the project, please import it directly",
project_file_update_type_error: 'Updated file types must be consistent', project_file_update_type_error: 'Updated file types must be consistent',
report: { report: {
diff: "Compare" diff: "Compare",
ActiveThreadsChart: 'Users',
TransactionsChart: 'Requests/Transactions',
ErrorsChart: 'Error',
ResponseTimeChart: 'Response Time',
ResponseTimePercentilesChart: 'Response time percentage',
ResponseCodeChart: 'Response Code',
LatencyChart: 'Latency',
BytesThroughputChart: 'Bytes',
}, },
}, },
api_test: { api_test: {
@ -847,7 +855,7 @@ export default {
wait_controller: "Wait controller", wait_controller: "Wait controller",
if_controller: "If controller", if_controller: "If controller",
loop_controller: "Loop Controller", loop_controller: "Loop Controller",
transcation_controller:"Transcation controller", transcation_controller: "Transcation controller",
scenario_import: "Scenario import", scenario_import: "Scenario import",
customize_script: "Customize script", customize_script: "Customize script",
customize_req: "Customize req", customize_req: "Customize req",

View File

@ -644,7 +644,15 @@ export default {
load_api_automation_jmx: '引用接口自动化场景', load_api_automation_jmx: '引用接口自动化场景',
project_file_exist: "项目中已存在该文件,请直接引用", project_file_exist: "项目中已存在该文件,请直接引用",
report: { report: {
diff: "对比" diff: "对比",
ActiveThreadsChart: '用户数',
TransactionsChart: '请求/事务数',
ErrorsChart: '错误',
ResponseTimeChart: '响应时间',
ResponseTimePercentilesChart: '响应时间百分比',
ResponseCodeChart: '响应码',
LatencyChart: '延迟时间',
BytesThroughputChart: '字节数',
}, },
project_file_update_type_error: '更新的文件类型必须一致', project_file_update_type_error: '更新的文件类型必须一致',
}, },

View File

@ -644,7 +644,15 @@ export default {
load_api_automation_jmx: '引用接口自動化場景', load_api_automation_jmx: '引用接口自動化場景',
project_file_exist: "項目中已存在該文件,請直接引用", project_file_exist: "項目中已存在該文件,請直接引用",
report: { report: {
diff: "對比" diff: "對比",
ActiveThreadsChart: '用戶數',
TransactionsChart: '請求/事務數',
ErrorsChart: '錯誤',
ResponseTimeChart: '響應時間',
ResponseTimePercentilesChart: '響應時間百分比',
ResponseCodeChart: '響應碼',
LatencyChart: '延遲時間',
BytesThroughputChart: '字節數',
}, },
project_file_update_type_error: '更新的文件類型必須一致', project_file_update_type_error: '更新的文件類型必須一致',
}, },