首页 > 其他 > 详细

系统中的异常追踪设计

时间:2019-11-09 15:04:00      阅读:85      评论:0      收藏:0      [点我收藏+]

        系统在日常的运行过程中,难免遇到各种错误响应,或者是用户的操作不当导致,或者是系统中未察觉的BUG。当系统发生错误的响应时,就需要开发人员对各种错误响应进行定位、追踪。好的系统设计可以让开发人员在很短的时间内定位到问题,而避免查询日志甚至翻阅代码带来的时间损耗和烂心情。

        系统的异常追踪可以分为四个层面:异常、响应码、日志、用户行为表。

        在系统中异常和错误码共同组成了系统的异常响应值,在收到用户的报错反馈后,研发人员根据异常和错误码可以轻易的分辨出系统中的问题,或对用户提示信息加以优化,或对BUG进行修复。
  • 异常

        异常作为系统的第一级对外响应,在Web系统中根据异常的类别返回不同的HTTP响应码,异常可以分为对外异常和对内异常;

        对外异常用于在统一异常处理中区分不同的HTTP响应,常见的对外异常有:

        1) RequestException           400     请求错误
        2) AuthorizationException      401     认证错误
        3) AccessException               403     禁止访问
        4) ConflictException       407     访问冲突(唯一性、幂等性校验)
        5) SystemException               500     服务器内部异常
 
        在统一异常处理中逻辑如下:
@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。

  常见的对内异常有(根据实际场景创建):

        1) RpcException
        2) UserException
        3) RedisException
        4) DatabaseException
        5) NotSupportedException
        6) BusinessException
   
  • 错误码

        用户在使用系统的过程中如果遇到错误响应,大多数的做法是将报错信息截图、反馈。为了避免过多的沟通成本,如何在简短的用户提示信息中包含更多的信息成为关键。        

        错误码的设计用来区分详细的报错场景,给用户友好的文本提示,同时研发人员也可以仅仅凭借错误码快速的判断出发生异常的原因,而避免查日志甚至翻阅代码的时间损耗。

        错误码的设计应该和异常结合起来,例如用户在查询数据的过程中抛出了一个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值进行匹配。例如:

    [{}]用户[{}]已存在,请勿重复创建  转换后  [1000011]用户[drana]已存在,请勿重复创建
 
  • 日志

        日志是系统中排查问题的重要依据,接口关键节点中的日志打印为查询问题也提供了极大的帮助。

        举一个案例: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

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!