- 后端:增加异常日志的记录

This commit is contained in:
YunaiV 2019-05-11 02:46:34 +08:00
parent 25ecc25139
commit 6697f4f723
13 changed files with 331 additions and 89 deletions

View File

@ -47,8 +47,16 @@ public class ExceptionUtil {
return null;
}
public static String getMessage(Throwable th) {
return ExceptionUtils.getMessage(th);
}
public static String getRootCauseMessage(Throwable th) {
return ExceptionUtils.getRootCauseMessage(th);
}
public static String getStackTrace(Throwable th) {
return ExceptionUtils.getStackTrace(th);
}
}

View File

@ -2,10 +2,21 @@ package cn.iocoder.mall.spring.boot.web.handler;
import cn.iocoder.common.framework.constant.SysErrorCodeEnum;
import cn.iocoder.common.framework.exception.ServiceException;
import cn.iocoder.common.framework.util.ExceptionUtil;
import cn.iocoder.common.framework.util.HttpUtil;
import cn.iocoder.common.framework.util.MallUtil;
import cn.iocoder.common.framework.vo.CommonResult;
import cn.iocoder.mall.admin.api.SystemLogService;
import cn.iocoder.mall.admin.api.dto.AccessLogAddDTO;
import cn.iocoder.mall.admin.api.dto.ExceptionLogAddDTO;
import com.alibaba.fastjson.JSON;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.dubbo.config.annotation.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.util.Assert;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
@ -13,12 +24,19 @@ import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolationException;
import java.util.Date;
@ControllerAdvice
public class GlobalExceptionHandler {
private Logger logger = LoggerFactory.getLogger(getClass());
@Value("${spring.application.name}")
private String applicationName;
@Reference(validation = "true", version = "${dubbo.consumer.AdminAccessLogService.version:1.0.0}")
private SystemLogService adminAccessLogService;
// 逻辑异常
@ResponseBody
@ExceptionHandler(value = ServiceException.class)
@ -47,24 +65,58 @@ public class GlobalExceptionHandler {
+ detailMessage.toString());
}
// TODO 芋艿应该还有其它的异常需要进行翻译
@ResponseBody
@ExceptionHandler(value = Exception.class)
public CommonResult resultExceptionHandler(HttpServletRequest req, Exception e) {
logger.error("[resultExceptionHandler]", e);
// 返回
public CommonResult exceptionHandler(HttpServletRequest req, Exception e) {
logger.error("[exceptionHandler]", e);
// 插入异常日志
ExceptionLogAddDTO exceptionLog = new ExceptionLogAddDTO();
try {
addExceptionLog();
// 初始化 exceptionLog
initExceptionLog(exceptionLog, req, e);
// 执行插入 exceptionLog
addExceptionLog(exceptionLog);
} catch (Throwable th) {
// TODO
logger.error("[exceptionHandler][插入访问日志({}) 发生异常({})", JSON.toJSONString(exceptionLog), ExceptionUtils.getRootCauseMessage(th));
}
// 返回 ERROR CommonResult
return CommonResult.error(SysErrorCodeEnum.SYS_ERROR.getCode(), SysErrorCodeEnum.SYS_ERROR.getMessage());
}
// TODO 芋艿应该还有其它的异常需要进行翻译
private void initExceptionLog(ExceptionLogAddDTO exceptionLog, HttpServletRequest request, Exception e) {
// 设置用户编号
exceptionLog.setUserId(MallUtil.getUserId(request));
if (exceptionLog.getUserId() == null) {
exceptionLog.setUserId(AccessLogAddDTO.USER_ID_NULL);
}
exceptionLog.setUserType(MallUtil.getUserType(request));
// 设置异常字段
exceptionLog.setExceptionName(e.getClass().getName());
exceptionLog.setExceptionMessage(ExceptionUtil.getMessage(e));
exceptionLog.setExceptionRootCauseMessage(ExceptionUtil.getRootCauseMessage(e));
exceptionLog.setExceptionStackTrace(ExceptionUtil.getStackTrace(e));
StackTraceElement[] stackTraceElements = e.getStackTrace();
Assert.notEmpty(stackTraceElements, "异常 stackTraceElements 不能为空");
StackTraceElement stackTraceElement = stackTraceElements[0];
exceptionLog.setExceptionClassName(stackTraceElement.getClassName());
exceptionLog.setExceptionFileName(stackTraceElement.getFileName());
exceptionLog.setExceptionMethodName(stackTraceElement.getMethodName());
exceptionLog.setExceptionLineNumber(stackTraceElement.getLineNumber());
// 设置其它字段
exceptionLog.setTraceId(MallUtil.getTraceId())
.setApplicationName(applicationName)
.setUri(request.getRequestURI()) // TODO 提升如果想要优化可以使用 Swagger @ApiOperation 注解
.setQueryString(HttpUtil.buildQueryString(request))
.setMethod(request.getMethod())
.setUserAgent(HttpUtil.getUserAgent(request))
.setIp(HttpUtil.getIp(request))
.setExceptionTime(new Date());
}
@Async
public void addExceptionLog() {
public void addExceptionLog(ExceptionLogAddDTO exceptionLog) {
adminAccessLogService.addExceptionLog(exceptionLog);
}
}

View File

@ -50,6 +50,19 @@ public class AccessLogInterceptor extends HandlerInterceptorAdapter {
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
AccessLogAddDTO accessLog = new AccessLogAddDTO();
try {
// 初始化 accessLog
initAccessLog(accessLog, request);
// 执行插入 accessLog
addAccessLog(accessLog);
// TODO 提升暂时不考虑 ELK 的方案而是基于 MySQL 存储如果访问日志比较多需要定期归档
} catch (Throwable th) {
logger.error("[afterCompletion][插入访问日志({}) 发生异常({})", JSON.toJSONString(accessLog), ExceptionUtils.getRootCauseMessage(th));
} finally {
clear();
}
}
private void initAccessLog(AccessLogAddDTO accessLog, HttpServletRequest request) {
// 设置用户编号
accessLog.setUserId(MallUtil.getUserId(request));
if (accessLog.getUserId() == null) {
@ -71,14 +84,6 @@ public class AccessLogInterceptor extends HandlerInterceptorAdapter {
.setIp(HttpUtil.getIp(request))
.setStartTime(START_TIME.get())
.setResponseTime((int) (System.currentTimeMillis() - accessLog.getStartTime().getTime())); // 默认响应时间设为 0
// 执行插入
addAccessLog(accessLog);
// TODO 提升暂时不考虑 ELK 的方案而是基于 MySQL 存储如果访问日志比较多需要定期归档
} catch (Throwable th) {
logger.error("[afterCompletion][插入访问日志({}) 发生异常({})", JSON.toJSONString(accessLog), ExceptionUtils.getRootCauseMessage(th));
} finally {
clear();
}
}
@Async // 异步入库

View File

@ -1,6 +1,7 @@
package cn.iocoder.mall.admin.api;
import cn.iocoder.mall.admin.api.dto.AccessLogAddDTO;
import cn.iocoder.mall.admin.api.dto.ExceptionLogAddDTO;
/**
* 系统日志 Service 接口
@ -11,4 +12,6 @@ public interface SystemLogService {
void addAccessLog(AccessLogAddDTO accessLogAddDTO);
void addExceptionLog(ExceptionLogAddDTO exceptionLogAddDTO);
}

View File

@ -1,7 +1,6 @@
package cn.iocoder.mall.admin.api.dto;
import cn.iocoder.common.framework.vo.CommonResult;
import lombok.Data;
import lombok.experimental.Accessors;
@ -20,78 +19,33 @@ public class AccessLogAddDTO implements Serializable {
* 用户编号 -
*/
public static final Integer USER_ID_NULL = 0;
/**
* 链路追踪编号
*
* 一般来说通过链路追踪编号可以将访问日志错误日志链路追踪日志logger 打印日志等结合在一起从而进行排错
*/
@NotNull(message = "链路追踪编号不能为空")
private String traceId;
/**
* 用户编号.
*
* 当管理员为空时该值为 {@link #USER_ID_NULL}
*/
@NotNull(message = "用户编号不能为空")
private Integer userId;
/**
* 用户类型
*/
@NotNull(message = "用户类型不能为空")
private Integer userType;
/**
* 应用名
*
* 目前读取 spring.application.name
*/
@NotNull(message = "应用名不能为空")
private String applicationName;
/**
* 访问地址
*/
@NotNull(message = "访问地址不能为空")
private String uri;
/**
* 参数
*/
@NotNull(message = "请求参数不能为空")
private String queryString;
/**
* http 方法
*/
@NotNull(message = "http 请求方法不能为空")
private String method;
/**
* User Agent
*/
@NotNull(message = "User-Agent 不能为空")
private String userAgent;
/**
* ip
*/
@NotNull(message = "ip 不能为空")
private String ip;
/**
* 请求时间
*/
@NotNull(message = "请求时间不能为空")
private Date startTime;
/**
* 响应时长 -- 毫秒级
*/
@NotNull(message = "响应时长不能为空")
private Integer responseTime;
/**
* 错误码
*
* 目前的结果是使用 {@link CommonResult#getCode()} 属性
*/
@NotNull(message = "错误码不能为空")
private Integer errorCode;
/**
* 错误提示
*
* 目前的结果是使用 {@link CommonResult#getMessage()} 属性
*/
private String errorMessage;

View File

@ -0,0 +1,58 @@
package cn.iocoder.mall.admin.api.dto;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotNull;
import java.util.Date;
/**
* 异常日志添加 DTO
*/
@Data
@Accessors(chain = true)
public class ExceptionLogAddDTO {
/**
* 用户编号 -
*/
public static final Integer USER_ID_NULL = 0;
@NotNull(message = "链路追踪编号不能为空")
private String traceId;
@NotNull(message = "用户编号不能为空")
private Integer userId;
@NotNull(message = "用户类型不能为空")
private Integer userType;
@NotNull(message = "应用名不能为空")
private String applicationName;
@NotNull(message = "访问地址不能为空")
private String uri;
@NotNull(message = "请求参数不能为空")
private String queryString;
@NotNull(message = "http 请求方法不能为空")
private String method;
@NotNull(message = "User-Agent 不能为空")
private String userAgent;
@NotNull(message = "ip 不能为空")
private String ip;
@NotNull(message = "异常时间不能为空")
private Date exceptionTime;
@NotNull(message = "异常名不能为空")
private String exceptionName;
@NotNull(message = "异常发生的类全名不能为空")
private String exceptionClassName;
@NotNull(message = "异常发生的类文件不能为空")
private String exceptionFileName;
@NotNull(message = "异常发生的方法名不能为空")
private String exceptionMethodName;
@NotNull(message = "异常发生的方法所在行不能为空")
private Integer exceptionLineNumber;
@NotNull(message = "异常的栈轨迹不能为空")
private String exceptionStackTrace;
@NotNull(message = "异常导致的根消息不能为空")
private String exceptionRootCauseMessage;
@NotNull(message = "异常导致的消息不能为空")
private String exceptionMessage;
}

View File

@ -1,7 +1,9 @@
package cn.iocoder.mall.admin.convert;
import cn.iocoder.mall.admin.api.dto.AccessLogAddDTO;
import cn.iocoder.mall.admin.api.dto.ExceptionLogAddDTO;
import cn.iocoder.mall.admin.dataobject.AccessLogDO;
import cn.iocoder.mall.admin.dataobject.ExceptionLogDO;
import org.mapstruct.Mapper;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;
@ -14,4 +16,7 @@ public interface AccessLogConvert {
@Mappings({})
AccessLogDO convert(AccessLogAddDTO accessLogAddDTO);
@Mappings({})
ExceptionLogDO convert(ExceptionLogAddDTO exceptionLogAddDTO);
}

View File

@ -0,0 +1,9 @@
package cn.iocoder.mall.admin.dao;
import cn.iocoder.mall.admin.dataobject.ExceptionLogDO;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springframework.stereotype.Repository;
@Repository
public interface ExceptionLogMapper extends BaseMapper<ExceptionLogDO> {
}

View File

@ -1,18 +1,20 @@
package cn.iocoder.mall.admin.dataobject;
import cn.iocoder.common.framework.dataobject.DeletableDO;
import cn.iocoder.common.framework.dataobject.BaseDO;
import cn.iocoder.common.framework.vo.CommonResult;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.Date;
/**
* 管理员访问日志 DO
* 访问日志 DO
*/
@Data
@Accessors(chain = true)
public class AccessLogDO extends DeletableDO {
@TableName("access_log")
public class AccessLogDO extends BaseDO {
/**
* 编号

View File

@ -0,0 +1,117 @@
package cn.iocoder.mall.admin.dataobject;
import cn.iocoder.common.framework.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.Date;
/**
* 异常日志 DO
*/
@Data
@Accessors(chain = true)
@TableName("exception_log")
public class ExceptionLogDO extends BaseDO {
/**
* 编号
*/
private Integer id;
/**
* 链路追踪编号
*
* 一般来说通过链路追踪编号可以将访问日志错误日志链路追踪日志logger 打印日志等结合在一起从而进行排错
*/
private String traceId;
/**
* 用户编号.
*
* 当管理员为空时该值为 {@link cn.iocoder.mall.admin.api.dto.AccessLogAddDTO#USER_ID_NULL}
*/
private Integer userId;
/**
* 用户类型
*/
private Integer userType;
/**
* 应用名
*
* 目前读取 spring.application.name
*/
private String applicationName;
/**
* 访问地址
*/
private String uri;
/**
* 参数
*/
private String queryString;
/**
* http 方法
*/
private String method;
/**
* userAgent
*/
private String userAgent;
/**
* ip
*/
private String ip;
/**
* 异常发生时间
*/
private Date exceptionTime;
/**
* 异常名
*
* {@link Throwable#getClass()} 的类全名
*/
private String exceptionName;
/**
* 异常导致的消息
*
* {@link cn.iocoder.common.framework.util.ExceptionUtil#getMessage(Throwable)}
*/
private String exceptionMessage;
/**
* 异常导致的根消息
*
* {@link cn.iocoder.common.framework.util.ExceptionUtil#getRootCauseMessage(Throwable)}
*/
private String exceptionRootCauseMessage;
/**
* 异常的栈轨迹
*
* {@link cn.iocoder.common.framework.util.ExceptionUtil#getServiceException(Exception)}
*/
private String exceptionStackTrace;
/**
* 异常发生的类全名
*
* {@link StackTraceElement#getClassName()}
*/
private String exceptionClassName;
/**
* 异常发生的类文件
*
* {@link StackTraceElement#getFileName()}
*/
private String exceptionFileName;
/**
* 异常发生的方法名
*
* {@link StackTraceElement#getMethodName()}
*/
private String exceptionMethodName;
/**
* 异常发生的方法所在行
*
* {@link StackTraceElement#getLineNumber()}
*/
private Integer exceptionLineNumber;
}

View File

@ -32,6 +32,9 @@ public class DataDictServiceImpl implements DataDictService {
@Override
public CommonResult<List<DataDictBO>> selectDataDictList() {
if (true) {
throw new NullPointerException("阿拉啦啦啦");
}
List<DataDictDO> dataDicts = dataDictMapper.selectList();
return CommonResult.success(DataDictConvert.INSTANCE.convert(dataDicts));
}

View File

@ -3,9 +3,12 @@ package cn.iocoder.mall.admin.service;
import cn.iocoder.common.framework.util.StringUtil;
import cn.iocoder.mall.admin.api.SystemLogService;
import cn.iocoder.mall.admin.api.dto.AccessLogAddDTO;
import cn.iocoder.mall.admin.api.dto.ExceptionLogAddDTO;
import cn.iocoder.mall.admin.convert.AccessLogConvert;
import cn.iocoder.mall.admin.dao.AccessLogMapper;
import cn.iocoder.mall.admin.dao.ExceptionLogMapper;
import cn.iocoder.mall.admin.dataobject.AccessLogDO;
import cn.iocoder.mall.admin.dataobject.ExceptionLogDO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@ -30,8 +33,11 @@ public class SystemLogServiceImpl implements SystemLogService {
@Autowired
private AccessLogMapper accessLogMapper;
@Autowired
private ExceptionLogMapper exceptionLogMapper;
@Override
@SuppressWarnings("Duplicates")
public void addAccessLog(AccessLogAddDTO adminAccessLogAddDTO) {
// 创建 AdminAccessLogDO
AccessLogDO accessLog = AccessLogConvert.INSTANCE.convert(adminAccessLogAddDTO);
@ -50,4 +56,24 @@ public class SystemLogServiceImpl implements SystemLogService {
accessLogMapper.insert(accessLog);
}
@Override
@SuppressWarnings("Duplicates")
public void addExceptionLog(ExceptionLogAddDTO exceptionLogAddDTO) {
// 创建 AdminAccessLogDO
ExceptionLogDO exceptionLog = AccessLogConvert.INSTANCE.convert(exceptionLogAddDTO);
exceptionLog.setCreateTime(new Date());
// 截取最大长度
if (exceptionLog.getUri().length() > URI_MAX_LENGTH) {
exceptionLog.setUri(StringUtil.substring(exceptionLog.getUri(), URI_MAX_LENGTH));
}
if (exceptionLog.getQueryString().length() > QUERY_STRING_MAX_LENGTH) {
exceptionLog.setQueryString(StringUtil.substring(exceptionLog.getQueryString(), QUERY_STRING_MAX_LENGTH));
}
if (exceptionLog.getUserAgent().length() > USER_AGENT_MAX_LENGTH) {
exceptionLog.setUserAgent(StringUtil.substring(exceptionLog.getUserAgent(), USER_AGENT_MAX_LENGTH));
}
// 插入
exceptionLogMapper.insert(exceptionLog);
}
}

View File

@ -7,14 +7,14 @@
<!--create_time-->
<!--</sql>-->
<insert id="insert" parameterType="AccessLogDO" useGeneratedKeys="true" keyColumn="id" keyProperty="id">
INSERT INTO access_log (
trace_id, user_id, user_type, uri, query_string, method, user_agent,
ip, start_time, response_time, error_code, error_message, create_time
) VALUES (
#{traceId}, #{userId}, #{userType}, #{uri}, #{queryString}, #{method}, #{userAgent},
#{ip}, #{startTime}, #{responseTime}, #{errorCode}, #{errorMessage}, #{createTime}
)
</insert>
<!-- <insert id="insert" parameterType="AccessLogDO" useGeneratedKeys="true" keyColumn="id" keyProperty="id">-->
<!-- INSERT INTO access_log (-->
<!-- trace_id, user_id, user_type, uri, query_string, method, user_agent,-->
<!-- ip, start_time, response_time, error_code, error_message, create_time-->
<!-- ) VALUES (-->
<!-- #{traceId}, #{userId}, #{userType}, #{uri}, #{queryString}, #{method}, #{userAgent},-->
<!-- #{ip}, #{startTime}, #{responseTime}, #{errorCode}, #{errorMessage}, #{createTime}-->
<!-- )-->
<!-- </insert>-->
</mapper>