系统在日常的运行过程中,难免遇到各种错误响应,或者是用户的操作不当导致,或者是系统中未察觉的BUG。当系统发生错误的响应时,就需要开发人员对各种错误响应进行定位、追踪。好的系统设计可以让开发人员在很短的时间内定位到问题,而避免查询日志甚至翻阅代码带来的时间损耗和烂心情。
系统的异常追踪可以分为四个层面:异常、响应码、日志、用户行为表。
异常作为系统的第一级对外响应,在Web系统中根据异常的类别返回不同的HTTP响应码,异常可以分为对外异常和对内异常;
对外异常用于在统一异常处理中区分不同的HTTP响应,常见的对外异常有:
@RestControllerAdvice @Slf4j public class GlobalExceptionHandler { @Autowired private BizResultCodeBuilder bizResultCodeBuilder; @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(value = { MethodArgumentNotValidException.class, ConstraintViolationException.class, DranaRequestException.class }) public DranaResponse handleRequestValidationException(Exception ex) { String objErrorMsg = parseRequestException(ex); log.warn("Request invalid caused by [{}]", objErrorMsg); DranaRequestException wrapperException; if (ex instanceof DranaRequestException) { wrapperException = (DranaRequestException) ex; } else { wrapperException = new DranaRequestException(objErrorMsg, GenericResultCode.REQUEST_INVALID); } return DranaResponse.fail(bizResultCodeBuilder.build(wrapperException.getResultCode(), wrapperException)); } @ResponseStatus(HttpStatus.FORBIDDEN) @ExceptionHandler(DranaAccessException.class) public DranaResponse handleForbiddenException(DranaAccessException ex) { log.warn("Forbidden caused by [{}]", ex.getMessage(), ex); return DranaResponse.fail(bizResultCodeBuilder.build(ex.getResultCode(), ex)); } @ResponseStatus(HttpStatus.UNAUTHORIZED) @ExceptionHandler(DranaAuthorizationException.class) public DranaResponse handleAuthorizationException(DranaAuthorizationException ex) { log.warn("Authorization caused by [{}]", ex.getMessage(), ex); return DranaResponse.fail(bizResultCodeBuilder.build(ex.getResultCode(), ex)); } }
对内异常多是业务异常或者内部系统异常,用于在不同的业务场景中进行再次封装,例如根据用户ID查询用户的方法,在用户不存在时抛出UserException,在登录认证的流程中,对UserException进行再次封装为AccessException;在添加用户的流程中再次封装为RequestException;在审批流获取用户信息的流程中,再次封装为SystemException。
常见的对内异常有(根据实际场景创建):
用户在使用系统的过程中如果遇到错误响应,大多数的做法是将报错信息截图、反馈。为了避免过多的沟通成本,如何在简短的用户提示信息中包含更多的信息成为关键。
错误码的设计用来区分详细的报错场景,给用户友好的文本提示,同时研发人员也可以仅仅凭借错误码快速的判断出发生异常的原因,而避免查日志甚至翻阅代码的时间损耗。
错误码的设计应该和异常结合起来,例如用户在查询数据的过程中抛出了一个AccessException异常,对应的HTTP响应码是403(禁止访问)。那么究竟是什么原因导致的禁止访问,用户如果想要访问这部分数据,管理员要做怎么样的处理呢?结合用户的操作场景和日志分析当然可以,但是我此时有其它工作要做,压根懒得分心,此时错误码便派上了用场。例如在AccessException异常中加上NeedAdmin(4000011, "需要管理员权限")错误码,标识这部分数据只有管理员才能查询。或者加上LackAuthority(4000012, "无权访问")错误码,标识要查询这部分数据必须要管理员给你赋权才行,走流程申请去吧。
错误码的设计多是包含一个用于标识的Code和一个提示性的文本信息Desc:
public enum GenericResultCode implements IResultCode { SYSTEM_ERROR(100000,"系统异常"), ARGUMENT_ERROR(1000001,"参数错误"), NOT_SUPPORTED(1000002,"尚未支持"), CONFLICT(1000003,"资源冲突"), REQUEST_INVALID(4000000,"非法请求"), UNAUTHORIZED(4000001,"认证失败"), FORBIDDEN(4000003,"禁止访问"), USER_NOT_EXIST(5000001,"用户不存在"); }
错误码的Desc描述分为两类:一类是系统内部描述,简短即可,用于打印日志;一类是外部描述,用于显示给用户,外部描述可以在分布式配置系统中配置文本格式,根据异常和错误码Code值进行匹配。例如:
日志是系统中排查问题的重要依据,接口关键节点中的日志打印为查询问题也提供了极大的帮助。
举一个案例:A系统为企业内部的员工提供线上数据查询的服务,员工需要查询数据必须申请成为A系统的用户,并登陆验证才行。在用户登录验证过程中,根据从passport解析出来的员工工号去系统中查询,如果员工还不是A系统的用户,则返回409(GONE)错误码。在实际的运行过程中我们发现有一部分员工尝试登录了系统,发现还不是我们系统的用户,根据提示信息找管理员申请通过后,再次登录,依然报409的错误。但是此时去系统中查询,发现这一部分员工已经是系统中的用户了。此时根据异常响应和错误码都无法发现问题,这里便需要日志登场了,通过查询接口的入口日志,我们发现,随着用户点击按钮触发登录操作,系统中并没有打印入口日志。那便应该是登录接口之前的环节出现了问题,后来经过排查我们发现是Chrome浏览器会对409的错误码做缓存导致的。
日志设计的第一步是选用合适的日志框架,常用的log4j2和logback都可以。重点在于如果并发度比较高,导致日志量很大的话,日志的打印要开启异步模式(之前我们就遇到过由于日志IO导致的接口堵塞问题)。日志一般在接口的开始和结束位置处打印,在关键节点处打印即可。过多的日志一是导致查询比较麻烦,二是对系统也会有压力(此处一个案例就是在用BLK收集日志时,过多的日志量把kafka队列阻塞,造成部分日志缺失)。
分布式系统在kibana或者hubble上查看日志时,需要按照接口内的逻辑顺序将日志排列起来。这里可以在接口的起始位置创建一个日志ID,保存到ThreadLocal中,在查询日志时,根据日志ID串联即可。
用户的行为监控大致也可以分为两类:一是敏感行为监控,二是用于分析目的的操作记录监控;
用户敏感行为监控更多的是用于追责,比如某用户不小心把一张很重要的表清空了,还死不承认,此时便可以查询敏感行为监控表把他揪出来暴打一顿。用户的操作记录监控为用户行为分析提供了数据支撑,可以在用户行为监控记录表的基础上做出各种报表,进行图形化的展示,进行更直观的分析。例如:一个电商平台发现某一个商品被查询的频次较其它商品更高,那么平台就可以针对这一类商品加大库存量。
这里分享一套基于注解支持的异步保存用户行为记录的代码:
@Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) public @interface EditingMonitor { /** * 编辑类型 * * @return editingType */ EditingTypeEnum editingType(); /** * 业务类型 * * @return bizType */ BizTypeEnum bizType(); /** * 操作标注 * * @return string */ String remark() default ""; enum BizTypeEnum { } enum EditingTypeEnum { POST, PUT, DELETE; }
@Aspect @Service @Slf4j public class MonitorAspect { @Autowired private ApplicationEventPublisher applicationEventPublisher; @Pointcut("@annotation(com.XXX.biz.service.monitor.annotation.EditingMonitor)") public void monitorPointCut() { } @Around("monitorPointCut()") public Object recordMonitorInfo(ProceedingJoinPoint pjp) throws Throwable { Object result = pjp.proceed(); MonitorModel monitorModel = null; if (result instanceof DranaResponse) { Object responseData = ((DranaResponse) result).getData(); if (responseData instanceof MonitorModel) { monitorModel = (MonitorModel) responseData; } } else if (result instanceof MonitorModel) { monitorModel = (MonitorModel) result; } if (monitorModel != null) { int id = monitorModel.getId(); String name = monitorModel.getName(); if (id < 1 || StringUtils.isEmpty(name)) { log.warn("MonitorModel填充错误!Id=[{}],Name=[{}]", id, name); return result; } publishMonitorEvent(monitorModel, pjp); } return result; } private void publishMonitorEvent(MonitorModel monitorModel, ProceedingJoinPoint pjp) { Method method = resolveMethod(pjp); EditingMonitor editingMonitor = method.getAnnotation(EditingMonitor.class); String editType = editingMonitor.editingType().name(); String bizType = editingMonitor.bizType().name(); TableEditRecordPO tableEditRecordPO = new TableEditRecordPO(); tableEditRecordPO.setTableId(monitorModel.getId()); tableEditRecordPO.setName(monitorModel.getName()); tableEditRecordPO.setBizType(bizType); tableEditRecordPO.setEditType(editType); tableEditRecordPO.setRemark(editingMonitor.remark()); // 此处是基于Spring Security实现,获取用户信息。将用户信息保存到ThreadLocal中即可 AbstractAuthenticationToken authenticationToken = (AbstractAuthenticationToken) SecurityContextHolder .getContext().getAuthentication(); UserDetail userDetail = (UserDetail) authenticationToken.getPrincipal(); tableEditRecordPO.setOperatorJobNo(userDetail.getJobNo()); tableEditRecordPO.setOperatorName(userDetail.getUsername()); // 此处使用了Spring Event。定义一个EventListener用于接收事件,再向BeanFactory注册一个线程池即可 applicationEventPublisher.publishEvent(new MonitorEvent(tableEditRecordPO)); } private Method resolveMethod(ProceedingJoinPoint joinPoint) { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Class<?> targetClass = joinPoint.getTarget().getClass(); Method method = getDeclaredMethodFor(targetClass, signature.getName(), signature.getMethod().getParameterTypes()); if (method == null) { throw new IllegalStateException("Cannot resolve target method: " + signature.getMethod().getName()); } return method; } /** * Get declared method with provided name and parameterTypes in given class and its super classes. All parameters * should be valid. * * @param clazz class where the method is located * @param name method name * @param parameterTypes method parameter type list * @return resolved method, null if not found */ private Method getDeclaredMethodFor(Class<?> clazz, String name, Class<?>... parameterTypes) { try { return clazz.getDeclaredMethod(name, parameterTypes); } catch (NoSuchMethodException e) { Class<?> superClass = clazz.getSuperclass(); if (superClass != null) { return getDeclaredMethodFor(superClass, name, parameterTypes); } } return null; } }
原文:https://www.cnblogs.com/dranawhite/p/11825480.html