diff --git a/backend/src/main/java/io/metersphere/api/controller/ApiMonitorController.java b/backend/src/main/java/io/metersphere/api/controller/ApiMonitorController.java new file mode 100644 index 0000000000..2384d0b7f2 --- /dev/null +++ b/backend/src/main/java/io/metersphere/api/controller/ApiMonitorController.java @@ -0,0 +1,55 @@ +package io.metersphere.api.controller; + +import io.metersphere.api.dto.ApiMonitorSearch; +import io.metersphere.api.dto.ApiResponseCodeMonitor; +import io.metersphere.api.dto.ApiResponseTimeMonitor; +import io.metersphere.api.service.APIMonitorService; +import io.metersphere.commons.constants.RoleConstants; +import org.apache.shiro.authz.annotation.Logical; +import org.apache.shiro.authz.annotation.RequiresRoles; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; + +@RestController +@RequestMapping(value = "/api/monitor") +@RequiresRoles(value = {RoleConstants.TEST_MANAGER, RoleConstants.TEST_USER, RoleConstants.TEST_VIEWER}, logical = Logical.OR) +public class ApiMonitorController { + + @Resource + private APIMonitorService apiMonitorService; + + /** + * 查询所有接口 + */ + @GetMapping("/list") + public List apiList() { + return apiMonitorService.list(); + } + + + /** + * 查询响应时间 + */ + @GetMapping("/getResponseTime") + public List responseTimeData(@RequestHeader("apiUrl") String url, String startTime, String endTime) { + return apiMonitorService.getApiResponseTimeData(url, startTime, endTime); + } + + /** + * 查询状态码 + */ + @GetMapping("/getResponseCode") + public List responseCodeData(@RequestHeader("apiUrl") String url, String startTime, String endTime) { + return apiMonitorService.getApiResponseCodeData(url, startTime, endTime); + } + + /** + * 查询reportId + */ + @GetMapping("/getReportId") + public String searchReportId(@RequestHeader("apiUrl") String url, @RequestParam("startTime") String startTime) { + return apiMonitorService.getReportId(url, startTime); + } +} diff --git a/backend/src/main/java/io/metersphere/api/dto/ApiMonitorSearch.java b/backend/src/main/java/io/metersphere/api/dto/ApiMonitorSearch.java new file mode 100644 index 0000000000..9ed3e0fc31 --- /dev/null +++ b/backend/src/main/java/io/metersphere/api/dto/ApiMonitorSearch.java @@ -0,0 +1,10 @@ +package io.metersphere.api.dto; + +import lombok.Data; + +@Data +public class ApiMonitorSearch { + + private String url; + +} \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/api/dto/ApiResponseCodeMonitor.java b/backend/src/main/java/io/metersphere/api/dto/ApiResponseCodeMonitor.java new file mode 100644 index 0000000000..8fc6c1bd10 --- /dev/null +++ b/backend/src/main/java/io/metersphere/api/dto/ApiResponseCodeMonitor.java @@ -0,0 +1,20 @@ +package io.metersphere.api.dto; + +import lombok.Data; + +@Data +public class ApiResponseCodeMonitor { + + private String id; + + private String reportId; + + private String url; + + private String apiName; + + private String startTime; + + private String responseCode; + +} diff --git a/backend/src/main/java/io/metersphere/api/dto/ApiResponseTimeMonitor.java b/backend/src/main/java/io/metersphere/api/dto/ApiResponseTimeMonitor.java new file mode 100644 index 0000000000..80ba42b930 --- /dev/null +++ b/backend/src/main/java/io/metersphere/api/dto/ApiResponseTimeMonitor.java @@ -0,0 +1,20 @@ +package io.metersphere.api.dto; + +import lombok.Data; + +@Data +public class ApiResponseTimeMonitor { + + private String id; + + private String reportId; + + private String url; + + private String apiName; + + private String startTime; + + private String responseTime; + +} diff --git a/backend/src/main/java/io/metersphere/api/service/APIMonitorService.java b/backend/src/main/java/io/metersphere/api/service/APIMonitorService.java new file mode 100644 index 0000000000..d8e942a01a --- /dev/null +++ b/backend/src/main/java/io/metersphere/api/service/APIMonitorService.java @@ -0,0 +1,35 @@ +package io.metersphere.api.service; + +import io.metersphere.api.dto.ApiMonitorSearch; +import io.metersphere.api.dto.ApiResponseCodeMonitor; +import io.metersphere.api.dto.ApiResponseTimeMonitor; +import io.metersphere.base.mapper.ApiDataViewMapper; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.List; + +@Service +@Transactional(rollbackFor = Exception.class) +public class APIMonitorService { + + @Resource + private ApiDataViewMapper apiDataViewMapper; + + public List list() { + return apiDataViewMapper.selectAll(); + } + + public List getApiResponseTimeData(String apiUrl, String startTime, String endTime) { + return apiDataViewMapper.selectResponseTimeByUrl(apiUrl, startTime, endTime); + } + + public List getApiResponseCodeData(String apiUrl, String startTime, String endTime) { + return apiDataViewMapper.selectResponseCodeByUrl(apiUrl, startTime, endTime); + } + + public String getReportId(String apiUrl, String startTime) { + return apiDataViewMapper.selectReportIdByUrlAndStartTime(apiUrl, startTime); + } +} diff --git a/backend/src/main/java/io/metersphere/api/service/APIReportService.java b/backend/src/main/java/io/metersphere/api/service/APIReportService.java index cc4367eb8b..a8ce783493 100644 --- a/backend/src/main/java/io/metersphere/api/service/APIReportService.java +++ b/backend/src/main/java/io/metersphere/api/service/APIReportService.java @@ -1,17 +1,21 @@ package io.metersphere.api.service; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import io.metersphere.api.dto.APIReportResult; import io.metersphere.api.dto.DeleteAPIReportRequest; import io.metersphere.api.dto.QueryAPIReportRequest; import io.metersphere.api.jmeter.TestResult; import io.metersphere.base.domain.*; +import io.metersphere.base.mapper.ApiDataViewMapper; import io.metersphere.base.mapper.ApiTestReportDetailMapper; import io.metersphere.base.mapper.ApiTestReportMapper; import io.metersphere.base.mapper.ext.ExtApiTestReportMapper; import io.metersphere.commons.constants.APITestStatus; import io.metersphere.commons.constants.ReportTriggerMode; import io.metersphere.commons.exception.MSException; +import io.metersphere.commons.utils.LogUtil; import io.metersphere.commons.utils.ServiceUtils; import io.metersphere.dto.DashboardTestDTO; import io.metersphere.i18n.Translator; @@ -21,8 +25,11 @@ import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Date; import java.util.List; import java.util.UUID; @@ -36,6 +43,8 @@ public class APIReportService { private ApiTestReportDetailMapper apiTestReportDetailMapper; @Resource private ExtApiTestReportMapper extApiTestReportMapper; + @Resource + private ApiDataViewMapper apiDataViewMapper; public List list(QueryAPIReportRequest request) { request.setOrders(ServiceUtils.getDefaultOrder(request.getOrders())); @@ -63,6 +72,8 @@ public class APIReportService { public void delete(DeleteAPIReportRequest request) { apiTestReportDetailMapper.deleteByPrimaryKey(request.getId()); apiTestReportMapper.deleteByPrimaryKey(request.getId()); + apiDataViewMapper.deleteByReportId(request.getId()); + } public void deleteByTestId(String testId) { @@ -89,6 +100,8 @@ public class APIReportService { // report report.setUpdateTime(System.currentTimeMillis()); if (!StringUtils.equals(report.getStatus(), APITestStatus.Debug.name())) { + //新增每一条接口记录到api_data_view表中 + creatApiDataView(new String(detail.getContent(), StandardCharsets.UTF_8), report.getId()); if (result.getError() > 0) { report.setStatus(APITestStatus.Error.name()); } else { @@ -99,6 +112,44 @@ public class APIReportService { apiTestReportMapper.updateByPrimaryKeySelective(report); } + private void creatApiDataView(String jsonString, String reportId) { + List listApiDataView = new ArrayList<>(); + JSONObject jsonObject = JSON.parseObject(jsonString, JSONObject.class); + JSONArray jsonArray = jsonObject.getJSONArray("scenarios"); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + try { + for (int i = 0; i < jsonArray.size(); i++) { + JSONObject jsonInArray = jsonArray.getJSONObject(i); + JSONArray jsonRequestResults = jsonInArray.getJSONArray("requestResults"); + for (int j = 0; j < jsonRequestResults.size(); j++) { + JSONObject jsonInResponseResult = jsonRequestResults.getJSONObject(j).getJSONObject("responseResult"); + String responseTime = jsonInResponseResult.getString("responseTime"); + String responseCode = jsonInResponseResult.getString("responseCode"); + String startTime = jsonRequestResults.getJSONObject(j).getString("startTime"); + String name = jsonRequestResults.getJSONObject(j).getString("name"); + String url = jsonRequestResults.getJSONObject(j).getString("url"); + if (StringUtils.isBlank(url)){ + //如果非http请求不入库 + continue; + } + ApiDataView apiDataView = new ApiDataView(); + apiDataView.setId(UUID.randomUUID().toString()); + apiDataView.setReportId(reportId); + apiDataView.setApiName(name); + apiDataView.setUrl(StringUtils.substringBefore(url,"?")); + apiDataView.setResponseTime(responseTime); + apiDataView.setStartTime(sdf.format(new Date(Long.parseLong(startTime)))); + apiDataView.setResponseCode(responseCode); + listApiDataView.add(apiDataView); + } + } + } catch (Exception e) { + LogUtil.error(e); + } + apiDataViewMapper.insertListApiData(listApiDataView); + + } + public String create(ApiTest test, String triggerMode) { ApiTestReport running = getRunningReport(test.getId()); if (running != null) { diff --git a/backend/src/main/java/io/metersphere/base/domain/ApiDataView.java b/backend/src/main/java/io/metersphere/base/domain/ApiDataView.java new file mode 100644 index 0000000000..8399077381 --- /dev/null +++ b/backend/src/main/java/io/metersphere/base/domain/ApiDataView.java @@ -0,0 +1,24 @@ +package io.metersphere.base.domain; + +import lombok.Data; + +import java.io.Serializable; + +@Data +public class ApiDataView implements Serializable { + private String id; + + private String reportId; + + private String url; + + private String apiName; + + private String startTime; + + private String responseCode; + + private String responseTime; + + private static final long serialVersionUID = 1L; +} diff --git a/backend/src/main/java/io/metersphere/base/mapper/ApiDataViewMapper.java b/backend/src/main/java/io/metersphere/base/mapper/ApiDataViewMapper.java new file mode 100644 index 0000000000..48cf86c598 --- /dev/null +++ b/backend/src/main/java/io/metersphere/base/mapper/ApiDataViewMapper.java @@ -0,0 +1,23 @@ +package io.metersphere.base.mapper; + +import io.metersphere.api.dto.ApiMonitorSearch; +import io.metersphere.api.dto.ApiResponseCodeMonitor; +import io.metersphere.api.dto.ApiResponseTimeMonitor; +import io.metersphere.base.domain.ApiDataView; + +import java.util.List; + +public interface ApiDataViewMapper { + + List selectAll(); + + List selectResponseTimeByUrl(String url,String startTime,String endTime); + + List selectResponseCodeByUrl(String url,String startTime,String endTime); + + Integer insertListApiData(List list); + + Integer deleteByReportId(String reportId); + + String selectReportIdByUrlAndStartTime(String apiUrl,String startTime); +} \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/base/mapper/ApiDataViewMapper.xml b/backend/src/main/java/io/metersphere/base/mapper/ApiDataViewMapper.xml new file mode 100644 index 0000000000..671a97fdb5 --- /dev/null +++ b/backend/src/main/java/io/metersphere/base/mapper/ApiDataViewMapper.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + delete from api_data_view where report_id = #{reportId,jdbcType=VARCHAR} + + + + + + + + + + + insert into api_data_view(id, report_id, api_name,url, response_code, start_time,response_time) + values + + ( + #{item.id,jdbcType=VARCHAR}, + #{item.reportId,jdbcType=VARCHAR}, + #{item.apiName,jdbcType=VARCHAR}, + #{item.url,jdbcType=VARCHAR}, + #{item.responseCode,jdbcType=VARCHAR}, + #{item.startTime,jdbcType=VARCHAR}, + #{item.responseTime,jdbcType=VARCHAR} + ) + + + \ No newline at end of file diff --git a/backend/src/main/resources/db/migration/V47__api_data_view.sql b/backend/src/main/resources/db/migration/V47__api_data_view.sql new file mode 100644 index 0000000000..74ff59be4f --- /dev/null +++ b/backend/src/main/resources/db/migration/V47__api_data_view.sql @@ -0,0 +1,12 @@ +CREATE TABLE IF NOT EXISTS `api_data_view` +( + id varchar(50) NOT NULL primary key, + report_id varchar(255) NOT NULL, + api_name varchar(200) NULL, + url varchar(255) NULL, + response_code varchar(100) NULL, + start_time varchar(20) NULL, + response_time varchar(20) default '0' NULL, + create_time timestamp default CURRENT_TIMESTAMP NOT NULL, + update_time timestamp default CURRENT_TIMESTAMP NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; \ No newline at end of file diff --git a/frontend/src/business/components/api/head/ApiHeaderMenus.vue b/frontend/src/business/components/api/head/ApiHeaderMenus.vue index 5e1e6f1d1f..f398905d6f 100644 --- a/frontend/src/business/components/api/head/ApiHeaderMenus.vue +++ b/frontend/src/business/components/api/head/ApiHeaderMenus.vue @@ -32,6 +32,11 @@ + + + + {{ $t('commons.monitor') }} + diff --git a/frontend/src/business/components/api/monitor/ApiMonitor.vue b/frontend/src/business/components/api/monitor/ApiMonitor.vue new file mode 100644 index 0000000000..e5dab060db --- /dev/null +++ b/frontend/src/business/components/api/monitor/ApiMonitor.vue @@ -0,0 +1,168 @@ + + + + + diff --git a/frontend/src/business/components/api/monitor/ApiMonitorChart.vue b/frontend/src/business/components/api/monitor/ApiMonitorChart.vue new file mode 100644 index 0000000000..c7e2b1a3e8 --- /dev/null +++ b/frontend/src/business/components/api/monitor/ApiMonitorChart.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/frontend/src/business/components/api/monitor/ApiMonitorSearch.vue b/frontend/src/business/components/api/monitor/ApiMonitorSearch.vue new file mode 100644 index 0000000000..d97a9a2d33 --- /dev/null +++ b/frontend/src/business/components/api/monitor/ApiMonitorSearch.vue @@ -0,0 +1,100 @@ + + + + + diff --git a/frontend/src/business/components/api/monitor/components/ApiErrorMonitorChart.vue b/frontend/src/business/components/api/monitor/components/ApiErrorMonitorChart.vue new file mode 100644 index 0000000000..ca7903e471 --- /dev/null +++ b/frontend/src/business/components/api/monitor/components/ApiErrorMonitorChart.vue @@ -0,0 +1,93 @@ + + + + + diff --git a/frontend/src/business/components/api/monitor/components/ApiResponseTimeMonitorChart.vue b/frontend/src/business/components/api/monitor/components/ApiResponseTimeMonitorChart.vue new file mode 100644 index 0000000000..dccd45060e --- /dev/null +++ b/frontend/src/business/components/api/monitor/components/ApiResponseTimeMonitorChart.vue @@ -0,0 +1,64 @@ + + + + + diff --git a/frontend/src/business/components/api/monitor/components/CommonMonitorChart.vue b/frontend/src/business/components/api/monitor/components/CommonMonitorChart.vue new file mode 100644 index 0000000000..868373a31e --- /dev/null +++ b/frontend/src/business/components/api/monitor/components/CommonMonitorChart.vue @@ -0,0 +1,33 @@ + + + + + + + diff --git a/frontend/src/business/components/api/router.js b/frontend/src/business/components/api/router.js index 672d9faf7e..dbb73ae0c0 100644 --- a/frontend/src/business/components/api/router.js +++ b/frontend/src/business/components/api/router.js @@ -38,6 +38,11 @@ export default { path: "report/view/:reportId", name: "ApiReportView", component: () => import('@/business/components/api/report/ApiReportView'), - } + }, + { + path: 'monitor/view', + name: 'ApiMonitor', + component: () => import('@/business/components/api/monitor/ApiMonitor'), + }, ] } diff --git a/frontend/src/common/js/ajax.js b/frontend/src/common/js/ajax.js index 3482f67ebd..9acc2d2c6c 100644 --- a/frontend/src/common/js/ajax.js +++ b/frontend/src/common/js/ajax.js @@ -70,12 +70,12 @@ export default { } } - Vue.prototype.$$get = function (url, data, success) { + Vue.prototype.$$get = function (url, data, header, success) { let result = {loading: true}; if (!success) { - return axios.get(url, {params: data}); + return axios.get(url, {params: data, headers: header}); } else { - axios.get(url, {params: data}).then(response => { + axios.get(url, {params: data, headers: header}).then(response => { then(success, response, result); }).catch(error => { exception(error, result); diff --git a/frontend/src/common/js/chart.js b/frontend/src/common/js/chart.js index f2ec0b5317..5e26e1bcc6 100644 --- a/frontend/src/common/js/chart.js +++ b/frontend/src/common/js/chart.js @@ -4,6 +4,8 @@ import 'echarts/lib/chart/bar' import 'echarts/lib/chart/pie' import 'echarts/lib/component/tooltip' import 'echarts/lib/component/title' +import 'echarts/lib/component/toolbox'; +import 'echarts/lib/component/dataZoom'; import 'zrender/lib/svg/svg' export default { diff --git a/frontend/src/common/js/format-utils.js b/frontend/src/common/js/format-utils.js index 1c1206b6fd..e207b6df90 100644 --- a/frontend/src/common/js/format-utils.js +++ b/frontend/src/common/js/format-utils.js @@ -120,6 +120,42 @@ export function formatXml(text) { return outputText.replace(/\s+$/g, '').replace(/\r/g, '\r\n'); } +/** + * @param time 时间 + * @param cFormat 格式 + * @returns {string|null} 字符串 + * @example formatTime('2018-1-29', '{y}/{m}/{d} {h}:{i}:{s}') // -> 2018/01/29 00:00:00 + */ +export function formatTime(time, cFormat) { + if (arguments.length === 0) return null; + if ((time + '').length === 10) { + time = +time * 1000; + } + let format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}', date; + if (typeof time === 'object') { + date = time; + } else { + date = new Date(time); + } + let formatObj = { + y: date.getFullYear(), + m: date.getMonth() + 1, + d: date.getDate(), + h: date.getHours(), + i: date.getMinutes(), + s: date.getSeconds(), + a: date.getDay() + }; + return format.replace(/{([ymdhisa])+}/g, (result, key) => { + let value = formatObj[key]; + if (key === 'a') return ['一', '二', '三', '四', '五', '六', '日'][value - 1]; + if (result.length > 0 && value < 10) { + value = '0' + value; + } + return value || 0; + }); +} + function getPrefix(prefixIndex) { var span = ' '; var output = []; diff --git a/frontend/src/i18n/en-US.js b/frontend/src/i18n/en-US.js index bfc2f4a616..7e69aaaffc 100644 --- a/frontend/src/i18n/en-US.js +++ b/frontend/src/i18n/en-US.js @@ -162,7 +162,8 @@ export default { between: "Between", current_user: "Current user" } - } + }, + monitor:"monitor" }, license: { title: 'Authorization management', @@ -707,6 +708,16 @@ export default { running: "The test is reporting", not_exist: "Test report does not exist", }, + api_monitor: { + to:"to", + start_time:"Start Time", + end_time:"End Time", + today:"Today", + this_week:"This Week", + this_mouth:"This Mouth", + please_search:"Please Search", + date:"Date" + }, test_track: { test_track: "Track", confirm: "Confirm", diff --git a/frontend/src/i18n/zh-CN.js b/frontend/src/i18n/zh-CN.js index 6974ad127a..45403e5574 100644 --- a/frontend/src/i18n/zh-CN.js +++ b/frontend/src/i18n/zh-CN.js @@ -162,7 +162,8 @@ export default { between: "之间", current_user: "是当前用户" } - } + }, + monitor: "监控" }, license: { title: '授权管理', @@ -709,6 +710,16 @@ export default { running: "测试报告导出中", not_exist: "测试报告不存在", }, + api_monitor: { + to:"至", + start_time:"开始日期", + end_time:"结束日期", + today:"今日", + this_week:"本周", + this_mouth:"本月", + please_search:"请搜索", + date:"日期" + }, test_track: { test_track: "测试跟踪", confirm: "确 定", diff --git a/frontend/src/i18n/zh-TW.js b/frontend/src/i18n/zh-TW.js index 62a1a3e3c7..9af605db4a 100644 --- a/frontend/src/i18n/zh-TW.js +++ b/frontend/src/i18n/zh-TW.js @@ -162,7 +162,8 @@ export default { between: "之間", current_user: "是當前用戶" } - } + }, + monitor:"監控" }, license: { title: '授權管理', @@ -710,6 +711,16 @@ export default { running: "測試報告導出中", not_exist: "測試報告不存在", }, + api_monitor: { + to:"到", + start_time:"開始時間", + end_time:"結束時間", + today:"今天", + this_week:"本週", + this_mouth:"本月", + please_search:"請搜索", + date:"日期" + }, test_track: { test_track: "測試跟蹤", confirm: "確 定",