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

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; return null;
} }
public static String getMessage(Throwable th) {
return ExceptionUtils.getMessage(th);
}
public static String getRootCauseMessage(Throwable th) { public static String getRootCauseMessage(Throwable th) {
return ExceptionUtils.getRootCauseMessage(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.constant.SysErrorCodeEnum;
import cn.iocoder.common.framework.exception.ServiceException; 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.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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Async;
import org.springframework.util.Assert;
import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler; 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.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolationException; import javax.validation.ConstraintViolationException;
import java.util.Date;
@ControllerAdvice @ControllerAdvice
public class GlobalExceptionHandler { public class GlobalExceptionHandler {
private Logger logger = LoggerFactory.getLogger(getClass()); 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 @ResponseBody
@ExceptionHandler(value = ServiceException.class) @ExceptionHandler(value = ServiceException.class)
@ -47,24 +65,58 @@ public class GlobalExceptionHandler {
+ detailMessage.toString()); + detailMessage.toString());
} }
// TODO 芋艿应该还有其它的异常需要进行翻译
@ResponseBody @ResponseBody
@ExceptionHandler(value = Exception.class) @ExceptionHandler(value = Exception.class)
public CommonResult resultExceptionHandler(HttpServletRequest req, Exception e) { public CommonResult exceptionHandler(HttpServletRequest req, Exception e) {
logger.error("[resultExceptionHandler]", e); logger.error("[exceptionHandler]", e);
// 返回 // 插入异常日志
ExceptionLogAddDTO exceptionLog = new ExceptionLogAddDTO();
try { try {
addExceptionLog(); // 初始化 exceptionLog
initExceptionLog(exceptionLog, req, e);
// 执行插入 exceptionLog
addExceptionLog(exceptionLog);
} catch (Throwable th) { } 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()); 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 @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) { public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
AccessLogAddDTO accessLog = new AccessLogAddDTO(); AccessLogAddDTO accessLog = new AccessLogAddDTO();
try { 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)); accessLog.setUserId(MallUtil.getUserId(request));
if (accessLog.getUserId() == null) { if (accessLog.getUserId() == null) {
@ -71,14 +84,6 @@ public class AccessLogInterceptor extends HandlerInterceptorAdapter {
.setIp(HttpUtil.getIp(request)) .setIp(HttpUtil.getIp(request))
.setStartTime(START_TIME.get()) .setStartTime(START_TIME.get())
.setResponseTime((int) (System.currentTimeMillis() - accessLog.getStartTime().getTime())); // 默认响应时间设为 0 .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 // 异步入库 @Async // 异步入库

View File

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

View File

@ -1,7 +1,6 @@
package cn.iocoder.mall.admin.api.dto; package cn.iocoder.mall.admin.api.dto;
import cn.iocoder.common.framework.vo.CommonResult;
import lombok.Data; import lombok.Data;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
@ -20,78 +19,33 @@ public class AccessLogAddDTO implements Serializable {
* 用户编号 - * 用户编号 -
*/ */
public static final Integer USER_ID_NULL = 0; public static final Integer USER_ID_NULL = 0;
/**
* 链路追踪编号
*
* 一般来说通过链路追踪编号可以将访问日志错误日志链路追踪日志logger 打印日志等结合在一起从而进行排错
*/
@NotNull(message = "链路追踪编号不能为空") @NotNull(message = "链路追踪编号不能为空")
private String traceId; private String traceId;
/**
* 用户编号.
*
* 当管理员为空时该值为 {@link #USER_ID_NULL}
*/
@NotNull(message = "用户编号不能为空") @NotNull(message = "用户编号不能为空")
private Integer userId; private Integer userId;
/**
* 用户类型
*/
@NotNull(message = "用户类型不能为空") @NotNull(message = "用户类型不能为空")
private Integer userType; private Integer userType;
/**
* 应用名
*
* 目前读取 spring.application.name
*/
@NotNull(message = "应用名不能为空") @NotNull(message = "应用名不能为空")
private String applicationName; private String applicationName;
/**
* 访问地址
*/
@NotNull(message = "访问地址不能为空") @NotNull(message = "访问地址不能为空")
private String uri; private String uri;
/**
* 参数
*/
@NotNull(message = "请求参数不能为空") @NotNull(message = "请求参数不能为空")
private String queryString; private String queryString;
/**
* http 方法
*/
@NotNull(message = "http 请求方法不能为空") @NotNull(message = "http 请求方法不能为空")
private String method; private String method;
/**
* User Agent
*/
@NotNull(message = "User-Agent 不能为空") @NotNull(message = "User-Agent 不能为空")
private String userAgent; private String userAgent;
/**
* ip
*/
@NotNull(message = "ip 不能为空") @NotNull(message = "ip 不能为空")
private String ip; private String ip;
/**
* 请求时间
*/
@NotNull(message = "请求时间不能为空") @NotNull(message = "请求时间不能为空")
private Date startTime; private Date startTime;
/**
* 响应时长 -- 毫秒级
*/
@NotNull(message = "响应时长不能为空") @NotNull(message = "响应时长不能为空")
private Integer responseTime; private Integer responseTime;
/**
* 错误码
*
* 目前的结果是使用 {@link CommonResult#getCode()} 属性
*/
@NotNull(message = "错误码不能为空") @NotNull(message = "错误码不能为空")
private Integer errorCode; private Integer errorCode;
/** /**
* 错误提示 * 错误提示
*
* 目前的结果是使用 {@link CommonResult#getMessage()} 属性
*/ */
private String errorMessage; 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; package cn.iocoder.mall.admin.convert;
import cn.iocoder.mall.admin.api.dto.AccessLogAddDTO; 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.AccessLogDO;
import cn.iocoder.mall.admin.dataobject.ExceptionLogDO;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.Mappings; import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers; import org.mapstruct.factory.Mappers;
@ -14,4 +16,7 @@ public interface AccessLogConvert {
@Mappings({}) @Mappings({})
AccessLogDO convert(AccessLogAddDTO accessLogAddDTO); 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; 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 cn.iocoder.common.framework.vo.CommonResult;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data; import lombok.Data;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
import java.util.Date; import java.util.Date;
/** /**
* 管理员访问日志 DO * 访问日志 DO
*/ */
@Data @Data
@Accessors(chain = true) @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 @Override
public CommonResult<List<DataDictBO>> selectDataDictList() { public CommonResult<List<DataDictBO>> selectDataDictList() {
if (true) {
throw new NullPointerException("阿拉啦啦啦");
}
List<DataDictDO> dataDicts = dataDictMapper.selectList(); List<DataDictDO> dataDicts = dataDictMapper.selectList();
return CommonResult.success(DataDictConvert.INSTANCE.convert(dataDicts)); 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.common.framework.util.StringUtil;
import cn.iocoder.mall.admin.api.SystemLogService; import cn.iocoder.mall.admin.api.SystemLogService;
import cn.iocoder.mall.admin.api.dto.AccessLogAddDTO; 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.convert.AccessLogConvert;
import cn.iocoder.mall.admin.dao.AccessLogMapper; 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.AccessLogDO;
import cn.iocoder.mall.admin.dataobject.ExceptionLogDO;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -30,8 +33,11 @@ public class SystemLogServiceImpl implements SystemLogService {
@Autowired @Autowired
private AccessLogMapper accessLogMapper; private AccessLogMapper accessLogMapper;
@Autowired
private ExceptionLogMapper exceptionLogMapper;
@Override @Override
@SuppressWarnings("Duplicates")
public void addAccessLog(AccessLogAddDTO adminAccessLogAddDTO) { public void addAccessLog(AccessLogAddDTO adminAccessLogAddDTO) {
// 创建 AdminAccessLogDO // 创建 AdminAccessLogDO
AccessLogDO accessLog = AccessLogConvert.INSTANCE.convert(adminAccessLogAddDTO); AccessLogDO accessLog = AccessLogConvert.INSTANCE.convert(adminAccessLogAddDTO);
@ -50,4 +56,24 @@ public class SystemLogServiceImpl implements SystemLogService {
accessLogMapper.insert(accessLog); 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--> <!--create_time-->
<!--</sql>--> <!--</sql>-->
<insert id="insert" parameterType="AccessLogDO" useGeneratedKeys="true" keyColumn="id" keyProperty="id"> <!-- <insert id="insert" parameterType="AccessLogDO" useGeneratedKeys="true" keyColumn="id" keyProperty="id">-->
INSERT INTO access_log ( <!-- INSERT INTO access_log (-->
trace_id, user_id, user_type, uri, query_string, method, user_agent, <!-- trace_id, user_id, user_type, uri, query_string, method, user_agent,-->
ip, start_time, response_time, error_code, error_message, create_time <!-- ip, start_time, response_time, error_code, error_message, create_time-->
) VALUES ( <!-- ) VALUES (-->
#{traceId}, #{userId}, #{userType}, #{uri}, #{queryString}, #{method}, #{userAgent}, <!-- #{traceId}, #{userId}, #{userType}, #{uri}, #{queryString}, #{method}, #{userAgent},-->
#{ip}, #{startTime}, #{responseTime}, #{errorCode}, #{errorMessage}, #{createTime} <!-- #{ip}, #{startTime}, #{responseTime}, #{errorCode}, #{errorMessage}, #{createTime}-->
) <!-- )-->
</insert> <!-- </insert>-->
</mapper> </mapper>