diff --git a/backend/src/main/java/io/metersphere/commons/constants/ReportKeys.java b/backend/src/main/java/io/metersphere/commons/constants/ReportKeys.java index 809d8c4936..f933097b1b 100644 --- a/backend/src/main/java/io/metersphere/commons/constants/ReportKeys.java +++ b/backend/src/main/java/io/metersphere/commons/constants/ReportKeys.java @@ -1,6 +1,6 @@ package io.metersphere.commons.constants; public enum ReportKeys { - LoadChart, ResponseTimeChart, Errors, ErrorsTop5, RequestStatistics, Overview, TimeInfo, ResultStatus + LoadChart, ResponseTimeChart, ResponseCodeChart, Errors, ErrorsTop5, RequestStatistics, Overview, TimeInfo, ResultStatus, ErrorsChart } diff --git a/backend/src/main/java/io/metersphere/performance/controller/PerformanceReportController.java b/backend/src/main/java/io/metersphere/performance/controller/PerformanceReportController.java index e1852d8bf5..2e2c5a651e 100644 --- a/backend/src/main/java/io/metersphere/performance/controller/PerformanceReportController.java +++ b/backend/src/main/java/io/metersphere/performance/controller/PerformanceReportController.java @@ -95,6 +95,16 @@ public class PerformanceReportController { return reportService.getResponseTimeChartData(reportId); } + @GetMapping("/content/error_chart/{reportId}") + public List getErrorChartData(@PathVariable String reportId) { + return reportService.getErrorChartData(reportId); + } + + @GetMapping("/content/response_code_chart/{reportId}") + public List getResponseCodeChartData(@PathVariable String reportId) { + return reportService.getResponseCodeChartData(reportId); + } + @GetMapping("/{reportId}") public LoadTestReportWithBLOBs getLoadTestReport(@PathVariable String reportId) { return reportService.getLoadTestReport(reportId); diff --git a/backend/src/main/java/io/metersphere/performance/service/ReportService.java b/backend/src/main/java/io/metersphere/performance/service/ReportService.java index 6dd37cea0e..d3a22c1b26 100644 --- a/backend/src/main/java/io/metersphere/performance/service/ReportService.java +++ b/backend/src/main/java/io/metersphere/performance/service/ReportService.java @@ -256,4 +256,16 @@ public class ReportService { List ids = reportRequest.getIds(); ids.forEach(this::deleteReport); } + + public List getErrorChartData(String id) { + checkReportStatus(id); + String content = getContent(id, ReportKeys.ErrorsChart); + return JSON.parseArray(content, ChartsData.class); + } + + public List getResponseCodeChartData(String id) { + checkReportStatus(id); + String content = getContent(id, ReportKeys.ResponseCodeChart); + return JSON.parseArray(content, ChartsData.class); + } } diff --git a/frontend/src/business/components/performance/report/components/TestOverview.vue b/frontend/src/business/components/performance/report/components/TestOverview.vue index 90dae5eab8..229160a2eb 100644 --- a/frontend/src/business/components/performance/report/components/TestOverview.vue +++ b/frontend/src/business/components/performance/report/components/TestOverview.vue @@ -4,7 +4,7 @@ - {{maxUsers}} + {{ maxUsers }} VU Max Users @@ -13,7 +13,7 @@ - {{avgThroughput}} + {{ avgThroughput }} Hits/s Avg.Throughput @@ -22,7 +22,7 @@ - {{errors}} + {{ errors }} % Errors @@ -31,7 +31,7 @@ - {{avgResponseTime}} + {{ avgResponseTime }} s Avg.Response Time @@ -40,7 +40,7 @@ - {{responseTime90}} + {{ responseTime90 }} s 90% Response Time @@ -49,7 +49,7 @@ - {{avgBandwidth}} + {{ avgBandwidth }} KiB/s Avg.Bandwidth @@ -65,6 +65,14 @@ + + + + + + + + @@ -84,353 +92,503 @@ export default { avgBandwidth: "0", loadOption: {}, resOption: {}, - id: '' - } + errorOption: {}, + resCodeOption: {}, + id: '' + } + }, + methods: { + initTableData() { + this.$get("/performance/report/content/testoverview/" + this.id).then(res => { + let data = res.data.data; + this.maxUsers = data.maxUsers; + this.avgThroughput = data.avgThroughput; + this.errors = data.errors; + this.avgResponseTime = data.avgResponseTime; + this.responseTime90 = data.responseTime90; + this.avgBandwidth = data.avgBandwidth; + }).catch(() => { + this.maxUsers = '0'; + this.avgThroughput = '0'; + this.errors = '0'; + this.avgResponseTime = '0'; + this.responseTime90 = '0'; + this.avgBandwidth = '0'; + this.$warning(this.$t('report.generation_error')); + }) + this.getLoadChart(); + this.getResChart(); + this.getErrorChart(); + this.getResponseCodeChart(); }, - methods: { - initTableData() { - this.$get("/performance/report/content/testoverview/" + this.id).then(res => { - let data = res.data.data; - this.maxUsers = data.maxUsers; - this.avgThroughput = data.avgThroughput; - this.errors = data.errors; - this.avgResponseTime = data.avgResponseTime; - this.responseTime90 = data.responseTime90; - this.avgBandwidth = data.avgBandwidth; - }).catch(() => { + getLoadChart() { + this.$get("/performance/report/content/load_chart/" + this.id).then(res => { + let data = res.data.data; + let yAxisList = data.filter(m => m.yAxis2 === -1).map(m => m.yAxis); + let yAxis2List = data.filter(m => m.yAxis === -1).map(m => m.yAxis2); + let yAxisListMax = this._getChartMax(yAxisList); + let yAxis2ListMax = this._getChartMax(yAxis2List); + + let yAxisIndex0List = data.filter(m => m.yAxis2 === -1).map(m => m.groupName); + yAxisIndex0List = this._unique(yAxisIndex0List); + let yAxisIndex1List = data.filter(m => m.yAxis === -1).map(m => m.groupName); + yAxisIndex1List = this._unique(yAxisIndex1List); + + let loadOption = { + title: { + text: 'Load', + left: 'center', + top: 20, + textStyle: { + color: '#65A2FF' + }, + }, + tooltip: { + show: true, + trigger: 'axis' + }, + legend: {}, + xAxis: {}, + yAxis: [{ + name: 'User', + type: 'value', + min: 0, + max: yAxisListMax, + splitNumber: 5, + interval: yAxisListMax / 5 + }, + { + name: 'Hits/s', + type: 'value', + splitNumber: 5, + min: 0, + max: yAxis2ListMax, + interval: yAxis2ListMax / 5 + } + ], + series: [] + }; + let setting = { + series: [ + { + name: 'users', + color: '#0CA74A', + }, + { + name: 'hits', + yAxisIndex: '1', + color: '#65A2FF', + }, + { + name: 'errors', + yAxisIndex: '1', + color: '#E6113C', + } + ] + } + yAxisIndex0List.forEach(item => { + setting["series"].splice(0, 0, {name: item, yAxisIndex: '0'}) + }) + + yAxisIndex1List.forEach(item => { + setting["series"].splice(0, 0, {name: item, yAxisIndex: '1'}) + }) + this.loadOption = this.generateOption(loadOption, data, setting); + }).catch(() => { + this.loadOption = {}; + }) + }, + getResChart() { + this.$get("/performance/report/content/res_chart/" + this.id).then(res => { + let data = res.data.data; + let yAxisList = data.filter(m => m.yAxis2 === -1).map(m => m.yAxis); + let yAxis2List = data.filter(m => m.yAxis === -1).map(m => m.yAxis2); + let yAxisListMax = this._getChartMax(yAxisList); + let yAxis2ListMax = this._getChartMax(yAxis2List); + + let yAxisIndex0List = data.filter(m => m.yAxis2 === -1).map(m => m.groupName); + yAxisIndex0List = this._unique(yAxisIndex0List); + let yAxisIndex1List = data.filter(m => m.yAxis === -1).map(m => m.groupName); + yAxisIndex1List = this._unique(yAxisIndex1List); + + let resOption = { + title: { + text: 'Response Time', + left: 'center', + top: 20, + textStyle: { + color: '#99743C' + }, + }, + tooltip: { + show: true, + trigger: 'axis', + extraCssText: 'z-index: 999;', + formatter: function (params, ticket, callback) { + let result = ""; + let name = params[0].name; + result += name + "
"; + 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] + "
"; + } + + return result; + } + }, + legend: {}, + xAxis: {}, + yAxis: [{ + name: 'User', + type: 'value', + min: 0, + max: yAxisListMax, + interval: yAxisListMax / 5 + }, + { + name: 'Response Time', + type: 'value', + min: 0, + max: yAxis2ListMax, + interval: yAxis2ListMax / 5 + } + ], + series: [] + } + let setting = { + series: [ + { + name: 'users', + color: '#0CA74A', + } + ] + } + + yAxisIndex0List.forEach(item => { + setting["series"].splice(0, 0, {name: item, yAxisIndex: '0'}) + }) + + yAxisIndex1List.forEach(item => { + setting["series"].splice(0, 0, {name: item, yAxisIndex: '1'}) + }) + + this.resOption = this.generateOption(resOption, data, setting); + }).catch(() => { + this.resOption = {}; + }) + }, + getErrorChart() { + this.$get("/performance/report/content/error_chart/" + this.id).then(res => { + let data = res.data.data; + let yAxisList = data.filter(m => m.yAxis2 === -1).map(m => m.yAxis); + let yAxisListMax = this._getChartMax(yAxisList); + + let yAxisIndex0List = data.filter(m => m.yAxis2 === -1).map(m => m.groupName); + yAxisIndex0List = this._unique(yAxisIndex0List); + + let errorOption = { + title: { + text: 'Errors', + left: 'center', + top: 20, + textStyle: { + color: '#99743C' + }, + }, + tooltip: { + show: true, + trigger: 'axis', + extraCssText: 'z-index: 999;', + formatter: function (params, ticket, callback) { + let result = ""; + let name = params[0].name; + result += name + "
"; + 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] + "
"; + } + + return result; + } + }, + legend: {}, + xAxis: {}, + yAxis: [ + { + name: 'No', + type: 'value', + min: 0, + max: yAxisListMax, + interval: yAxisListMax / 5 + } + ], + series: [] + } + let setting = { + series: [ + { + name: 'users', + color: '#0CA74A', + } + ] + } + + yAxisIndex0List.forEach(item => { + setting["series"].splice(0, 0, {name: item, yAxisIndex: '0'}) + }) + + this.errorOption = this.generateOption(errorOption, data, setting); + }).catch(() => { + this.errorOption = {}; + }) + }, + getResponseCodeChart() { + this.$get("/performance/report/content/response_code_chart/" + this.id).then(res => { + let data = res.data.data; + let yAxisList = data.filter(m => m.yAxis2 === -1).map(m => m.yAxis); + let yAxisListMax = this._getChartMax(yAxisList); + + let yAxisIndex0List = data.filter(m => m.yAxis2 === -1).map(m => m.groupName); + yAxisIndex0List = this._unique(yAxisIndex0List); + + let resCodeOption = { + title: { + text: 'Response code', + left: 'center', + top: 20, + textStyle: { + color: '#99743C' + }, + }, + tooltip: { + show: true, + trigger: 'axis', + extraCssText: 'z-index: 999;', + formatter: function (params, ticket, callback) { + let result = ""; + let name = params[0].name; + result += name + "
"; + 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] + "
"; + } + + return result; + } + }, + legend: {}, + xAxis: {}, + yAxis: [ + { + name: 'No', + type: 'value', + min: 0, + max: yAxisListMax, + interval: yAxisListMax / 5 + } + ], + series: [] + } + let setting = { + series: [ + { + name: 'users', + color: '#0CA74A', + } + ] + } + + yAxisIndex0List.forEach(item => { + setting["series"].splice(0, 0, {name: item, yAxisIndex: '0'}) + }) + + this.resCodeOption = this.generateOption(resCodeOption, data, setting); + }).catch(() => { + this.resCodeOption = {}; + }) + }, + generateOption(option, data, setting) { + let chartData = data; + let seriesArray = []; + for (let set in setting) { + if (set === "series") { + seriesArray = setting[set]; + continue; + } + this.$set(option, set, setting[set]); + } + 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] = [] + } + if (item.yAxis === -1) { + series[name].splice(xAxis.indexOf(item.xAxis), 0, [item.xAxis, item.yAxis2.toFixed(2)]); + } else { + 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 + }; + let seriesArrayNames = seriesArray.map(m => m.name); + if (seriesArrayNames.includes(name)) { + for (let j = 0; j < seriesArray.length; j++) { + let seriesObj = seriesArray[j]; + if (seriesObj['name'] === name) { + Object.assign(items, seriesObj); + } + } + } + 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)); + } + }, + watch: { + report: { + handler(val) { + if (!val.status || !val.id) { + return; + } + let status = val.status; + this.id = val.id; + if (status === "Completed" || status === "Running") { + this.initTableData(); + } else { this.maxUsers = '0'; this.avgThroughput = '0'; this.errors = '0'; this.avgResponseTime = '0'; this.responseTime90 = '0'; this.avgBandwidth = '0'; - this.$warning(this.$t('report.generation_error')); - }) - this.$get("/performance/report/content/load_chart/" + this.id).then(res => { - let data = res.data.data; - let yAxisList = data.filter(m => m.yAxis2 === -1).map(m => m.yAxis); - let yAxis2List = data.filter(m => m.yAxis === -1).map(m => m.yAxis2); - let yAxisListMax = this._getChartMax(yAxisList); - let yAxis2ListMax = this._getChartMax(yAxis2List); - - let yAxisIndex0List = data.filter(m => m.yAxis2 === -1).map(m => m.groupName); - yAxisIndex0List = this._unique(yAxisIndex0List); - let yAxisIndex1List = data.filter(m => m.yAxis === -1).map(m => m.groupName); - yAxisIndex1List = this._unique(yAxisIndex1List); - - let loadOption = { - title: { - text: 'Load', - left: 'center', - top: 20, - textStyle: { - color: '#65A2FF' - }, - }, - tooltip: { - show: true, - trigger: 'axis' - }, - legend: {}, - xAxis: {}, - yAxis: [{ - name: 'User', - type: 'value', - min: 0, - max: yAxisListMax, - splitNumber: 5, - interval: yAxisListMax / 5 - }, - { - name: 'Hits/s', - type: 'value', - splitNumber: 5, - min: 0, - max: yAxis2ListMax, - interval: yAxis2ListMax / 5 - } - ], - series: [] - }; - let setting = { - series: [ - { - name: 'users', - color: '#0CA74A', - }, - { - name: 'hits', - yAxisIndex: '1', - color: '#65A2FF', - }, - { - name: 'errors', - yAxisIndex: '1', - color: '#E6113C', - } - ] - } - yAxisIndex0List.forEach(item => { - setting["series"].splice(0, 0, {name: item, yAxisIndex: '0'}) - }) - - yAxisIndex1List.forEach(item => { - setting["series"].splice(0, 0, {name: item, yAxisIndex: '1'}) - }) - this.loadOption = this.generateOption(loadOption, data, setting); - }).catch(() => { this.loadOption = {}; - }) - this.$get("/performance/report/content/res_chart/" + this.id).then(res => { - let data = res.data.data; - let yAxisList = data.filter(m => m.yAxis2 === -1).map(m => m.yAxis); - let yAxis2List = data.filter(m => m.yAxis === -1).map(m => m.yAxis2); - let yAxisListMax = this._getChartMax(yAxisList); - let yAxis2ListMax = this._getChartMax(yAxis2List); - - let yAxisIndex0List = data.filter(m => m.yAxis2 === -1).map(m => m.groupName); - yAxisIndex0List = this._unique(yAxisIndex0List); - let yAxisIndex1List = data.filter(m => m.yAxis === -1).map(m => m.groupName); - yAxisIndex1List = this._unique(yAxisIndex1List); - - let resOption = { - title: { - text: 'Response Time', - left: 'center', - top: 20, - textStyle: { - color: '#99743C' - }, - }, - tooltip: { - show: true, - trigger: 'axis', - extraCssText: 'z-index: 999;', - formatter: function (params, ticket, callback) { - let result = ""; - let name = params[0].name; - result += name + "
"; - 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] + "
"; - } - - return result; - } - }, - legend: {}, - xAxis: {}, - yAxis: [{ - name: 'User', - type: 'value', - min: 0, - max: yAxisListMax, - interval: yAxisListMax / 5 - }, - { - name: 'Response Time', - type: 'value', - min: 0, - max: yAxis2ListMax, - interval: yAxis2ListMax / 5 - } - ], - series: [] - } - let setting = { - series: [ - { - name: 'users', - color: '#0CA74A', - } - ] - } - - yAxisIndex0List.forEach(item => { - setting["series"].splice(0, 0, {name: item, yAxisIndex: '0'}) - }) - - yAxisIndex1List.forEach(item => { - setting["series"].splice(0, 0, {name: item, yAxisIndex: '1'}) - }) - - this.resOption = this.generateOption(resOption, data, setting); - }).catch(() => { this.resOption = {}; - }) - }, - generateOption(option, data, setting) { - let chartData = data; - let seriesArray = []; - for (let set in setting) { - if (set === "series") { - seriesArray = setting[set]; - continue; - } - this.$set(option, set, setting[set]); } - 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] = [] - } - if (item.yAxis === -1) { - series[name].splice(xAxis.indexOf(item.xAxis), 0, [item.xAxis, item.yAxis2.toFixed(2)]); - } else { - 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 - }; - let seriesArrayNames = seriesArray.map(m => m.name); - if (seriesArrayNames.includes(name)) { - for (let j = 0; j < seriesArray.length; j++) { - let seriesObj = seriesArray[j]; - if (seriesObj['name'] === name) { - Object.assign(items, seriesObj); - } - } - } - 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)); - } - }, - watch: { - report: { - handler(val) { - if (!val.status || !val.id) { - return; - } - let status = val.status; - this.id = val.id; - if (status === "Completed" || status === "Running") { - this.initTableData(); - } else { - this.maxUsers = '0'; - this.avgThroughput = '0'; - this.errors = '0'; - this.avgResponseTime = '0'; - this.responseTime90 = '0'; - this.avgBandwidth = '0'; - this.loadOption = {}; - this.resOption = {}; - } - }, - deep: true - } - }, - props: ['report'] - } + deep: true + } + }, + props: ['report'] +}