重构路由监控

This commit is contained in:
tanghc
2020-10-31 18:02:02 +08:00
parent 4ce2fc826d
commit 80003c44d3
58 changed files with 1966 additions and 215 deletions

View File

@@ -21,11 +21,6 @@
</properties>
<dependencies>
<dependency>
<groupId>com.gitee.sop</groupId>
<artifactId>sop-bridge-nacos</artifactId>
<version>4.0.3-SNAPSHOT</version>
</dependency>
<!-- easyopen starter -->
<dependency>
@@ -64,6 +59,11 @@
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
</dependency>
<!-- optional-->
<dependency>
<groupId>com.alibaba.nacos</groupId>

View File

@@ -0,0 +1,79 @@
package com.gitee.sop.adminserver.api.service;
import com.gitee.easyopen.annotation.Api;
import com.gitee.easyopen.annotation.ApiService;
import com.gitee.fastmybatis.core.query.Query;
import com.gitee.fastmybatis.core.query.Sort;
import com.gitee.fastmybatis.core.support.PageEasyui;
import com.gitee.fastmybatis.core.util.MapperUtil;
import com.gitee.sop.adminserver.api.service.param.InstanceMonitorSearchParam;
import com.gitee.sop.adminserver.api.service.param.MonitorErrorMsgParam;
import com.gitee.sop.adminserver.api.service.param.MonitorInfoErrorSolveParam;
import com.gitee.sop.adminserver.api.service.param.MonitorSearchParam;
import com.gitee.sop.adminserver.bean.RouteErrorCount;
import com.gitee.sop.adminserver.entity.MonitorInfoError;
import com.gitee.sop.adminserver.entity.MonitorSummary;
import com.gitee.sop.adminserver.mapper.MonitorInfoErrorMapper;
import com.gitee.sop.adminserver.mapper.MonitorInfoMapper;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* @author tanghc
*/
@ApiService
public class MonitorNewApi {
@Autowired
private MonitorInfoMapper monitorInfoMapper;
@Autowired
private MonitorInfoErrorMapper monitorInfoErrorMapper;
@Api(name = "monitornew.data.page")
PageInfo<Object> listMonitor(MonitorSearchParam param) {
Query query = Query.build(param);
query.orderby("errorCount", Sort.DESC)
.orderby("avgTime", Sort.DESC);
return PageHelper.offsetPage(query.getStart(), query.getLimit())
.doSelectPage(() -> monitorInfoMapper.listMonitorSummary(query))
.toPageInfo();
}
@Api(name = "monitornew.routeid.data.get")
List<MonitorSummary> listInstanceMonitor(InstanceMonitorSearchParam param) {
Query query = Query.build(param);
query.orderby("errorCount", Sort.DESC)
.orderby("avgTime", Sort.DESC);
return monitorInfoMapper.listInstanceMonitorInfo(query);
}
private Map<String, Integer> getRouteErrorCount() {
List<RouteErrorCount> routeErrorCounts = monitorInfoErrorMapper.listRouteErrorCount();
return routeErrorCounts.stream()
.collect(Collectors.toMap(RouteErrorCount::getRouteId, RouteErrorCount::getCount));
}
@Api(name = "monitornew.error.page")
PageEasyui<MonitorInfoError> listError(MonitorErrorMsgParam param) {
Query query = param.toQuery()
.orderby("gmt_modified", Sort.DESC);
return MapperUtil.queryForEasyuiDatagrid(monitorInfoErrorMapper, query);
}
@Api(name = "monitornew.error.solve")
void solve(MonitorInfoErrorSolveParam param) {
Query query = Query.build(param);
Map<String, Object> set = new HashMap<>(4);
set.put("is_deleted", 1);
set.put("count", 0);
monitorInfoErrorMapper.updateByMap(set, query);
}
}

View File

@@ -0,0 +1,15 @@
package com.gitee.sop.adminserver.api.service.param;
import com.gitee.fastmybatis.core.query.annotation.Condition;
import lombok.Getter;
import lombok.Setter;
/**
* @author tanghc
*/
@Getter
@Setter
public class InstanceMonitorSearchParam {
@Condition(column = "t.route_id")
private String routeId;
}

View File

@@ -0,0 +1,21 @@
package com.gitee.sop.adminserver.api.service.param;
import com.gitee.fastmybatis.core.query.annotation.Condition;
import com.gitee.fastmybatis.core.query.param.PageParam;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.NotBlank;
/**
* @author tanghc
*/
@Getter
@Setter
public class MonitorErrorMsgParam extends PageParam {
@NotBlank(message = "routeId不能为空")
private String routeId;
@Condition(ignoreEmptyString = true)
private String instanceId;
}

View File

@@ -0,0 +1,19 @@
package com.gitee.sop.adminserver.api.service.param;
import com.gitee.fastmybatis.core.query.annotation.Condition;
import lombok.Data;
import javax.validation.constraints.NotBlank;
/**
* @author tanghc
*/
@Data
public class MonitorInfoErrorSolveParam {
/** 错误id,md5(error_msg), 数据库字段error_id */
@NotBlank
@Condition(index = 1)
private String errorId;
}

View File

@@ -0,0 +1,23 @@
package com.gitee.sop.adminserver.api.service.param;
import com.gitee.easyopen.doc.annotation.ApiDocField;
import com.gitee.fastmybatis.core.query.Operator;
import com.gitee.fastmybatis.core.query.annotation.Condition;
import com.gitee.fastmybatis.core.query.param.PageParam;
import lombok.Getter;
import lombok.Setter;
/**
* @author tanghc
*/
@Getter
@Setter
public class MonitorSearchParam extends PageParam {
@ApiDocField(description = "服务名serviceId")
@Condition(column = "service_id", operator = Operator.like, ignoreEmptyString = true)
private String serviceId;
@ApiDocField(description = "路由id")
@Condition(column = "route_id", operator = Operator.like, ignoreEmptyString = true)
private String routeId;
}

View File

@@ -0,0 +1,26 @@
package com.gitee.sop.adminserver.api.service.result;
import lombok.Data;
import java.util.Date;
/**
* @author tanghc
*/
@Data
public class MonitorInfoErrorMsgResult {
private String routeId;
private String errorId;
/** 错误信息, 数据库字段error_msg */
private String errorMsg;
private Integer errorStatus;
private Integer count;
/** 数据库字段gmt_modified */
private Date gmtModified;
}

View File

@@ -0,0 +1,13 @@
package com.gitee.sop.adminserver.bean;
import lombok.Data;
/**
* @author tanghc
*/
@Data
public class RouteErrorCount {
private String routeId;
private Integer count;
}

View File

@@ -0,0 +1,75 @@
package com.gitee.sop.adminserver.common;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
/**
* @author tanghc
*/
@Slf4j
public class CopyUtil {
public static void copyProperties(Object from, Object to) {
BeanUtils.copyProperties(from, to);
}
public static <T> T copyBean(Object from, Supplier<T> supplier) {
Objects.requireNonNull(from);
T to = supplier.get();
BeanUtils.copyProperties(from, to);
return to;
}
public static <T> T copyBeanNullable(Object from, Supplier<T> supplier) {
if (from == null) {
return supplier.get();
}
T to = supplier.get();
BeanUtils.copyProperties(from, to);
return to;
}
public static <T> T copyBean(Object from, Supplier<T> supplier, Consumer<T> after) {
if (from == null) {
return null;
}
T to = supplier.get();
BeanUtils.copyProperties(from, to);
after.accept(to);
return to;
}
public static <T> List<T> copyList(List<?> fromList, Supplier<T> toElement) {
if (fromList == null) {
return Collections.emptyList();
}
return fromList.stream()
.map(source -> {
T target = toElement.get();
BeanUtils.copyProperties(source, target);
return target;
})
.collect(Collectors.toList());
}
public static <T> List<T> copyList(List<?> fromList, Supplier<T> toElement, Consumer<T> after) {
if (fromList == null) {
return Collections.emptyList();
}
return fromList.stream()
.map(source -> {
T target = toElement.get();
BeanUtils.copyProperties(source, target);
after.accept(target);
return target;
})
.collect(Collectors.toList());
}
}

View File

@@ -0,0 +1,66 @@
package com.gitee.sop.adminserver.entity;
import lombok.Data;
import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import java.util.Date;
/**
* 表名monitor_info
* 备注:接口监控信息
*
* @author tanghc
*/
@Table(name = "monitor_info")
@Data
public class MonitorInfo {
/** 数据库字段id */
@Id
@Column(name = "id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 路由id, 数据库字段route_id */
private String routeId;
/** 接口名, 数据库字段name */
private String name;
/** 版本号, 数据库字段version */
private String version;
/** 数据库字段service_id */
private String serviceId;
/** 数据库字段instance_id */
private String instanceId;
/** 请求耗时最长时间, 数据库字段max_time */
private Integer maxTime;
/** 请求耗时最小时间, 数据库字段min_time */
private Integer minTime;
/** 总时长,毫秒, 数据库字段total_time */
private Long totalTime;
/** 总调用次数, 数据库字段total_request_count */
private Long totalRequestCount;
/** 成功次数, 数据库字段success_count */
private Long successCount;
/** 失败次数(业务主动抛出的异常算作成功,如参数校验,未知的错误算失败), 数据库字段error_count */
private Long errorCount;
/** 数据库字段gmt_create */
private Date gmtCreate;
/** 数据库字段gmt_modified */
private Date gmtModified;
}

View File

@@ -0,0 +1,54 @@
package com.gitee.sop.adminserver.entity;
import lombok.Data;
import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import java.util.Date;
/**
* 表名monitor_info_error
*
* @author tanghc
*/
@Table(name = "monitor_info_error")
@Data
public class MonitorInfoError {
/** 数据库字段id */
@Id
@Column(name = "id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 错误id,md5Hex(instanceId + routeId + errorMsg), 数据库字段error_id */
private String errorId;
/** 实例id, 数据库字段instance_id */
private String instanceId;
/** 数据库字段route_id */
private String routeId;
/** 数据库字段error_msg */
private String errorMsg;
/** http status非200错误, 数据库字段error_status */
private Integer errorStatus;
/** 错误次数, 数据库字段count */
private Integer count;
/** 数据库字段is_deleted */
@com.gitee.fastmybatis.core.annotation.LogicDelete
private Byte isDeleted;
/** 数据库字段gmt_create */
private Date gmtCreate;
/** 数据库字段gmt_modified */
private Date gmtModified;
}

View File

@@ -0,0 +1,61 @@
package com.gitee.sop.adminserver.entity;
import lombok.Data;
import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author tanghc
*/
@Data
public class MonitorSummary {
private static final AtomicInteger i = new AtomicInteger();
private Integer id = i.incrementAndGet();
private String routeId;
private String name;
private String version;
/** 数据库字段service_id */
private String serviceId;
/** 数据库字段instance_id */
private String instanceId;
/** 请求耗时最长时间, 数据库字段max_time */
private Integer maxTime;
/** 请求耗时最小时间, 数据库字段min_time */
private Integer minTime;
/** 总时长,毫秒, 数据库字段total_time */
private Long totalTime;
/** 总调用次数, 数据库字段total_request_count */
private Long totalRequestCount;
/** 成功次数, 数据库字段success_count */
private Long successCount;
/** 失败次数(业务主动抛出的异常算作成功,如参数校验,未知的错误算失败), 数据库字段error_count */
private Long errorCount;
/** 未解决的错误数量 */
private Long unsolvedErrorCount;
private Float avgTime;
private Boolean hasChildren;
}

View File

@@ -0,0 +1,22 @@
package com.gitee.sop.adminserver.mapper;
import com.gitee.fastmybatis.core.mapper.CrudMapper;
import com.gitee.sop.adminserver.bean.RouteErrorCount;
import com.gitee.sop.adminserver.entity.MonitorInfoError;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* @author tanghc
*/
public interface MonitorInfoErrorMapper extends CrudMapper<MonitorInfoError, Long> {
@Select("SELECT route_id routeId, count(*) `count` FROM monitor_info_error \n" +
"WHERE is_deleted=0 \n" +
"GROUP BY route_id")
List<RouteErrorCount> listRouteErrorCount();
}

View File

@@ -0,0 +1,19 @@
package com.gitee.sop.adminserver.mapper;
import com.gitee.fastmybatis.core.mapper.CrudMapper;
import com.gitee.fastmybatis.core.query.Query;
import com.gitee.sop.adminserver.entity.MonitorInfo;
import com.gitee.sop.adminserver.entity.MonitorSummary;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* @author tanghc
*/
public interface MonitorInfoMapper extends CrudMapper<MonitorInfo, Long> {
List<MonitorSummary> listMonitorSummary(@Param("query") Query query);
List<MonitorSummary> listInstanceMonitorInfo(@Param("query") Query query);
}

View File

@@ -30,6 +30,7 @@ spring.datasource.hikari.pool-name=HikariCP
spring.datasource.hikari.max-lifetime=500000
# 固定不用改
mybatis.config-location=classpath:mybatis/mybatisConfig.xml
mybatis.mapper-locations=classpath:mybatis/mapper/*.xml
easyopen.show-doc=false
easyopen.ignore-validate=true

View File

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- 注意文件名必须跟Dao类名字一致因为是根据文件名做关联。 -->
<mapper namespace="com.gitee.sop.adminserver.mapper.MonitorInfoMapper">
<select id="listMonitorSummary" resultType="com.gitee.sop.adminserver.entity.MonitorSummary">
SELECT t.*, IFNULL(t2.unsolvedErrorCount, 0) unsolvedErrorCount
FROM (
SELECT
t.`service_id` serviceId,
CONCAT(t.name, t.version) routeId,
t.`name` name,
t.`version` version,
SUM(t.`max_time`) maxTime,
SUM(t.`min_time`) minTime,
SUM(t.`total_time`) totalTime,
SUM(t.`total_request_count`) totalRequestCount,
SUM(t.`success_count`) successCount,
SUM(t.`error_count`) errorCount,
SUM(t.`total_time`)/SUM(t.`total_request_count`) avgTime,
1 hasChildren
FROM
`monitor_info` t
<include refid="common.where"/>
GROUP BY t.service_id, t.name, t.version
<include refid="common.orderBy"/>
) t
LEFT JOIN
(
SELECT route_id, count(*) unsolvedErrorCount
FROM monitor_info_error WHERE is_deleted=0 GROUP BY route_id
) t2 on t.routeId = t2.route_id
</select>
<select id="listInstanceMonitorInfo" resultType="com.gitee.sop.adminserver.entity.MonitorSummary">
SELECT
t.`service_id` serviceId,
t.`instance_id` instanceId,
t.route_id routeId,
t.`name` name,
t.`version` version,
t.`max_time` maxTime,
t.`min_time` minTime,
t.`total_time` totalTime,
t.`total_request_count` totalRequestCount,
t.`success_count` successCount,
t.`error_count` errorCount,
IFNULL(t2.unsolvedErrorCount, 0) unsolvedErrorCount,
t.`total_time`/t.`total_request_count` avgTime,
0 hasChildren
FROM
`monitor_info` t
LEFT JOIN
(
SELECT instance_id, route_id, count(*) unsolvedErrorCount
FROM monitor_info_error WHERE is_deleted=0 GROUP BY instance_id, route_id
) t2 on t.route_id = t2.route_id AND t.instance_id = t2.instance_id
<include refid="common.where" />
<include refid="common.orderBy" />
</select>
</mapper>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<plugins>
<!-- com.github.pagehelper为PageHelper类所在包名 -->
<plugin interceptor="com.github.pagehelper.PageInterceptor" />
</plugins>
</configuration>

View File

@@ -76,7 +76,7 @@ export const constantRoutes = [
{
path: 'monitor',
name: 'Monitor',
component: () => import('@/views/service/monitor'),
component: () => import('@/views/service/monitorNew'),
meta: { title: '路由监控' }
},
{

View File

@@ -0,0 +1,273 @@
<template>
<div class="app-container">
<el-form :inline="true" :model="searchFormData" class="demo-form-inline" size="mini" @submit.native.prevent>
<el-form-item label="接口名">
<el-input v-model="searchFormData.routeId" :clearable="true" style="width: 250px;" />
</el-form-item>
<el-form-item label="serviceId">
<el-input v-model="searchFormData.serviceId" :clearable="true" style="width: 250px;" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" native-type="submit" @click="loadTable">查询</el-button>
</el-form-item>
</el-form>
<el-table
:data="pageInfo.list"
row-key="id"
lazy
empty-text="无数据"
:load="loadInstanceMonitorInfo"
>
<el-table-column
fixed
prop="instanceId"
label="网关实例"
width="200"
>
<template slot-scope="scope">
<span v-if="!scope.row.children">{{ scope.row.instanceId }}</span>
</template>
</el-table-column>
<el-table-column
fixed
prop="name"
label="接口名 (版本号)"
width="200"
>
<template slot-scope="scope">
{{ scope.row.name + (scope.row.version ? ' (' + scope.row.version + ')' : '') }}
</template>
</el-table-column>
<el-table-column
prop="serviceId"
label="serviceId"
width="150"
/>
<el-table-column
prop="maxTime"
label="最大耗时(ms)"
>
<template slot="header">
最大耗时(ms)
<el-tooltip content="耗时计算:签名验证成功后开始,应用返回结果后结束" placement="top">
<i class="el-icon-question" style="cursor: pointer"></i>
</el-tooltip>
</template>
</el-table-column>
<el-table-column
prop="minTime"
label="最小耗时(ms)"
/>
<el-table-column
prop="avgTime"
label="平均耗时(ms)"
>
<template slot-scope="scope">
{{ scope.row.avgTime.toFixed(1) }}
</template>
</el-table-column>
<el-table-column
prop="totalRequestCount"
label="总调用次数"
/>
<el-table-column
prop="successCount"
label="成功次数"
/>
<el-table-column
prop="errorCount"
label="失败次数"
/>
<el-table-column
prop="unsolvedErrorCount"
label="未解决错误"
>
<template slot-scope="scope">
<el-link
v-if="scope.row.unsolvedErrorCount > 0"
:underline="false"
type="danger"
style="text-decoration: underline;"
@click="onShowErrorDetail(scope.row)"
>
{{ scope.row.unsolvedErrorCount }}
</el-link>
<span v-if="scope.row.unsolvedErrorCount === 0">0</span>
</template>
</el-table-column>
</el-table>
<el-pagination
background
style="margin-top: 5px"
:current-page="searchFormData.pageIndex"
:page-size="searchFormData.pageSize"
:page-sizes="[5, 10, 20, 40]"
:total="pageInfo.total"
layout="total, sizes, prev, pager, next"
@size-change="onSizeChange"
@current-change="onPageIndexChange"
/>
<!-- dialog -->
<el-dialog
:title="errorMsgData.title"
:visible.sync="logDetailVisible"
:close-on-click-modal="false"
width="70%"
@close="onCloseErrorDlg"
>
<el-alert
title="修复错误后请标记解决"
:closable="false"
class="el-alert-tip"
/>
<el-table
:data="errorMsgData.pageInfo.rows"
empty-text="无错误日志"
>
<el-table-column
type="expand"
>
<template slot-scope="props">
<el-input v-model="props.row.errorMsg" type="textarea" :rows="8" readonly />
</template>
</el-table-column>
<el-table-column
prop="errorMsg"
label="错误内容"
>
<template slot-scope="props">
<span v-if="props.row.errorMsg.length > 50">{{ props.row.errorMsg.substring(0, 50) }}...</span>
<span v-else>{{ props.row.errorMsg }}</span>
</template>
</el-table-column>
<el-table-column
prop="instanceId"
label="实例ID"
width="150px"
/>
<el-table-column
prop="count"
label="报错次数"
width="80px"
/>
<el-table-column
prop="gmtModified"
label="报错时间"
width="160px"
/>
<el-table-column
label="操作"
width="120"
>
<template slot-scope="scope">
<el-link type="primary" @click="onSolve(scope.row)">标记解决</el-link>
</template>
</el-table-column>
</el-table>
<el-pagination
background
style="margin-top: 5px"
:current-page="errorMsgFormData.pageIndex"
:page-size="errorMsgFormData.pageSize"
:page-sizes="[5, 10, 20, 40]"
:total="errorMsgData.pageInfo.total"
layout="total, sizes, prev, pager, next"
@size-change="onSizeChange"
@current-change="onPageIndexChange"
/>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="logDetailVisible = false"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
export default {
data() {
return {
searchFormData: {
routeId: '',
serviceId: '',
pageIndex: 1,
pageSize: 20
},
pageInfo: {
list: [],
total: 0
},
logDetailVisible: false,
errorMsgFormData: {
routeId: '',
instanceId: '',
pageIndex: 1,
pageSize: 5
},
errorMsgData: {
title: '',
name: '',
version: '',
pageInfo: {
rows: [],
total: 0
}
}
}
},
created() {
this.loadTable()
},
methods: {
loadTable: function() {
this.post('monitornew.data.page', this.searchFormData, function(resp) {
this.pageInfo = resp.data
})
},
loadErrorData: function() {
this.post('monitornew.error.page', this.errorMsgFormData, function(resp) {
this.errorMsgData.pageInfo = resp.data
this.logDetailVisible = true
})
},
loadInstanceMonitorInfo(row, treeNode, resolve) {
this.post('monitornew.routeid.data.get', { routeId: row.routeId }, resp => {
const children = resp.data
row.children = children
resolve(children)
})
},
onShowErrorDetail: function(row) {
this.errorMsgData.title = `错误日志 ${row.name}${row.version}`
this.errorMsgData.name = row.name
this.errorMsgData.version = row.version
this.errorMsgFormData.routeId = row.routeId
this.errorMsgFormData.instanceId = row.instanceId
this.loadErrorData()
},
onSolve: function(row) {
this.confirm('确认标记为已解决吗?', function(done) {
this.post('monitornew.error.solve', { routeId: row.routeId, errorId: row.errorId }, function(resp) {
done()
this.loadErrorData()
})
})
},
onCloseErrorDlg: function() {
this.loadTable()
},
onSizeChange: function(size) {
this.searchFormData.pageSize = size
this.loadTable()
},
onAdd: function() {
this.dialogTitle = '新增IP'
this.dialogVisible = true
this.dialogFormData.id = 0
},
onPageIndexChange: function(pageIndex) {
this.searchFormData.pageIndex = pageIndex
this.loadTable()
}
}
}
</script>