网站核心接口监控方案

项目背景

目前我们的告警监控相对比较完善:运维告警、ELK告警、ARMS告警等等,但是告警太多,且噪音较大,无效告警没有得到很好的治理,可能导致对核心接口的异常忽略。

本告警的核心目标,就是关注核心接口是否发生异常、接口的RT响应时间,是否会突然增大(环比或同比对比接口的平均响应时间,判断是否发生较大的变化)

实现方案

通过打印指定格式的日志,然后通过elk日志服务监控。

基于 spring aop 环绕通知,先记录核心接口的整个过程,输出到 log 日志文件中,包括异常和 rt(响应时间)

监控指标VO



public class LogMonitorVO extends BaseVO {

    /**
     * 当前时间
     */
    private String currentTime;

    /**
     * 链路ID
     */
    private String traceId;

    /**
     * 功能名称:接口url、定时器handler名称、同步回调、火箭推送
     */
    private String functionName;

    /**
     * 项目名称
     */
    private String projectName;

    /**
     * 入口类型:监控的入口类型
     */
    private String entryType;

    /**
     * 是否成功:1:成功,0:失败。
     */
    private Long success = 1L;

    /**
     * 接口响应时间
     */
    private Long rt;

    /**
     * 客户ID
     */
    private String userId;

    /**
     * 当errorCode不为空,则代表当前核心接口出现了异常
     * 异常编码:错误异常码,方便统计,如 SYS_ERROR
     */
    private String errorCode;

    /**
     * 异常信息
     */
    private String errorMsg;

    /**
     * 切面处理时间
     */
    private Long aspectTime;

    /**
     * 接口请求参数
     */
    private JSONObject reqParam;

    public void end(long start) {
        this.rt = System.currentTimeMillis() - start;
        this.currentTime = DateUtil.now();
    }

    public void markFail() {
        this.success = 0L;
    }
}

监控基类

// 抽象类
public abstract class AbstractLogMonitorAspect {

    // 正则表达式,用于截取异常信息,匹配以 "com.xx" 开头且以 "数字+)" 结尾的整行内容,并排除包含 “aop” 的行。
    private static final Pattern PATTERN = Pattern.compile("^(?!.*aop.*).*?com\\.xx.*?(\\d+\\)).*$", Pattern.MULTILINE);

    /**
     * 执行环绕通知
     *
     * @param point 切点
     * @return 切点返回结果值
     * @throws Throwable 异常
     */
    protected Object doAround(ProceedingJoinPoint point) throws Throwable {
        long start = System.currentTimeMillis();

        String functionName = this.getFunctionName(point);
        // 判断URL或者xxlJobHandler是否在核心接口配置中,不在配置中则不记录日志
        if (!this.match(functionName)) {
            return point.proceed();
        }

        LogMonitorVO logMonitorVO = new LogMonitorVO();
        logMonitorVO.setTraceId(Optional.ofNullable(MDC.get("EagleEye-TraceID")).orElse("-"));
        logMonitorVO.setEntryType(this.getEntryType());
        logMonitorVO.setFunctionName(functionName);
        logMonitorVO.setProjectName(this.getProjectName());
        logMonitorVO.setUserId(this.getCustomer());

        long businessBeforeTime = System.currentTimeMillis() - start;
        try {
            // 处理业务
            Object result = point.proceed();
            long businessAfter = System.currentTimeMillis();

            // 判断返回结果是否异常,处理没有抛异常,但是返回的结果非200,或者包含系统异常等情况
            Pair<Boolean, Boolean> resultFailBool = this.judgeResultIsFail(result);
            if (resultFailBool.getKey()) {
                // 把返回值记录下来
                logMonitorVO.setErrorMsg(StringEscapeUtils.unescapeJava(JSON.toJSONString(result)));
                // 当发生异常时,记录请求参数
                logMonitorVO.setReqParam(this.getParam(point));
                logMonitorVO.setErrorCode(MonitorErrorCodeEnum.BUSINESS_EXCEPTION.getCode());
                // 如果返回的msg中包含,系统异常、系统繁忙等内容,则视为系统运行时异常
                if (resultFailBool.getValue()) {
                    logMonitorVO.setErrorCode(MonitorErrorCodeEnum.SYS_ERROR.getCode());
                }
                logMonitorVO.markFail();
            }

            // 记录rt和截止时间
            logMonitorVO.end(start);
            // 记录切面处理时间
            logMonitorVO.setAspectTime((System.currentTimeMillis() - businessAfter) + businessBeforeTime);

            // 核心接口,记录每一次访问过程
            this.doLog(logMonitorVO);

            return result;
        } catch (Throwable e) {
            try {

                long proceedAfter = System.currentTimeMillis();

                // 区别业务异常和系统运行异常,告警时可选择性排除业务异常,目前只区分这两种
                // 后面对异常进行治理,按照规则抛异常,包含异常码,可以方便后续做接口的异常统计,如:RPC_TIMEOUT, DISCOUNT_INVALID
                if (e instanceof BusinessTipException && !StrUtil.containsAny(e.getMessage(), "系统异常", "系统繁忙")) {
                    logMonitorVO.setErrorCode(MonitorErrorCodeEnum.BUSINESS_EXCEPTION.getCode());
                } else {
                    logMonitorVO.setErrorCode(MonitorErrorCodeEnum.SYS_ERROR.getCode());
                }

                logMonitorVO.markFail();
                // 异常堆栈信息
                logMonitorVO.setErrorMsg(this.getSimpleErrorMsg(e));
                // 当发生异常时,记录请求参数
                logMonitorVO.setReqParam(this.getParam(point));
                // 记录rt和截止时间
                logMonitorVO.end(start);
                // 记录切面处理时间
                logMonitorVO.setAspectTime((System.currentTimeMillis() - proceedAfter) + businessBeforeTime);

                this.doLog(logMonitorVO);

            } catch (Exception ex) {
                log.error("核心请求日志监控切面执行异常", ex);
            }

            throw e;
        }
    }

     /**
      * 获取简易异常信息
     * @param e 异常
     * @return 简易异常信息
     */
    protected String getSimpleErrorMsg(Throwable e) {
        try {
            // 记录异常信息
            // eg: 异常信息:[ArithmeticException: / by zero][at com.xxx.base.cipher.constant.test.main(test.java:21)]
            return "[" + ExceptionUtil.getRootCauseMessage(e) + "]" +
                    "[" + StringUtils.trim(this.findFirstMatchingLine(ExceptionUtil.stacktraceToString(e)) + "]");
        } catch (Throwable ex) {
            log.error("核心请求日志监控切面执行异常", ex);
            return null;
        }
    }

    /**
     * 获取请求入参
     */
    protected JSONObject getParam(ProceedingJoinPoint joinPoint) {
        try {
            Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
            // 获取参数名称
            LocalVariableTableParameterNameDiscoverer paramNames = new LocalVariableTableParameterNameDiscoverer();
            String[] params = paramNames.getParameterNames(method);
            // 获取参数
            Object[] args = joinPoint.getArgs();

            if (params == null || args == null) {
                return null;
            }

            // 创建一个映射,用于存储过滤后的参数和它们的名称
            Map<String, Object> filteredArgsMap = new HashMap<>();

            // 使用IntStream遍历参数的索引
            IntStream.range(0, args.length)
                    .forEach(i -> {
                        // 只有当参数不是HttpServletRequest和HttpServletResponse时,才将它们添加到映射中
                        if (!(args[i] instanceof HttpServletRequest) && !(args[i] instanceof HttpServletResponse)) {
                            filteredArgsMap.put(params[i], args[i]);
                        }
                    });

            // 如果过滤后的参数为空,则返回null
            if (MapUtils.isEmpty(filteredArgsMap)) {
                return null;
            }

            // 使用过滤后的参数映射创建JSONObject
            JSONObject rqsParams = new JSONObject(filteredArgsMap);
            return rqsParams;
        } catch (Throwable e) {
            log.error("核心请求日志监控切面执行异常", e);
            return null;
        }
    }

    /**
     * 查找并返回第一个以 "com.xxx" 开头且以 ")" 结尾的内容所在的整行。
     * @param input 要搜索的字符串
     * @return 匹配的字符串,如果没有找到则返回 null
     */
    public String findFirstMatchingLine(String input) {
        Matcher matcher = PATTERN.matcher(input);
        if (matcher.find()) {
            return matcher.group(0); // 返回整行内容
        }
        return null; // 如果没有找到匹配项,则返回 null
    }

    /**
     * 按照SLS日志规则记录日志,子类可以重写,再找运维改正则匹配规则
     * @param logMonitorVO 日志内容
     */
    protected void doLog(LogMonitorVO logMonitorVO) {
        log.error("[aliyun_log_monitor]::{}::[aliyun_log_monitor]", JSON.toJSONString(logMonitorVO));
    }
}

监控实现类

// controller 接口监控
public class ControllerLogMonitorAspect extends AbstractLogMonitorAspect {

    @Around("(execution(* com.xxx..controller..*(..)) && !execution(* com.xxx..BaseController.*(..)))")
    public Object aspect(ProceedingJoinPoint point) throws Throwable {
        return super.doAround(point);
    }

    @Override
    protected String getEntryType() {
        return EntryType.CONTROLLER.getDescription();
    }

    @Override
    protected String getFunctionName(ProceedingJoinPoint point) {
        return getCurrentHttpRequestUrl();
    }

    @Override
    protected boolean match(String function) {
        String monitorUrl = PropertyConfig.getConfigValue("aliyun_slslog_monitor_url");
        if (StringUtils.isBlank(monitorUrl)) {
            return false;
        }
        if (StringUtils.equals("ALL", monitorUrl)) {
            return true;
        }
        return Sets.newHashSet(monitorUrl.split(",")).contains(function);
    }

    @Override
    protected Pair<Boolean, Boolean> judgeResultIsFail(Object result) {
        return super.judgeResultVOIsFail(result);
    }
}

// 同步计划回调监控
public class SyncCallbackLogMonitorAspect extends AbstractLogMonitorAspect {

    @Around("@annotation(com.xxx.callback.annotation.SyncCallback)")
    public Object aspect(ProceedingJoinPoint point) throws Throwable {
        return super.doAround(point);
    }
    @Override
    protected String getEntryType() {
        String currentHttpRequestUrl = getCurrentHttpRequestUrl();
        if (StringUtils.contains(currentHttpRequestUrl, "rocket")) {
            return EntryType.ROCKET_HANDLER.getDescription();
        }
        return EntryType.SYNC_CALLBACK.getDescription();
    }

    @Override
    protected String getFunctionName(ProceedingJoinPoint point) {
        try {
            MethodSignature signature = (MethodSignature) point.getSignature();
            Method method = signature.getMethod();

            // 获取类上的@SyncCallback注解
            SyncCallback syncCallbackAnnotation = method.getAnnotation(SyncCallback.class);
            if (syncCallbackAnnotation != null) {
                // 获取注解的value值,即定时器的名字
                String planCode = syncCallbackAnnotation.value();
                if (StringUtils.isNotBlank(planCode)) {
                    return planCode;
                }
            }
        } catch (Throwable e) {
            log.info("同步回调请求日志监控切面执行异常", e);
        }

        return "-";
    }
    @Override
    protected boolean match(String function) {
        String monitorUrl = PropertyConfig.getConfigValue("aliyun_slslog_monitor_sync_callback");
        if (StringUtils.isBlank(monitorUrl)) {
            return false;
        }
        if (StringUtils.equals("ALL", monitorUrl)) {
            return true;
        }
        return Sets.newHashSet(monitorUrl.split(",")).contains(function);
    }

    @Override
    protected Pair<Boolean, Boolean> judgeResultIsFail(Object result) {
        return super.judgeResultVOIsFail(result);
    }
}

// 火箭推送回调监控
public class RocketNoSyncHandlerLogMonitorAspect extends AbstractLogMonitorAspect {

    @Around("@annotation(com.xxx.rocketsync.annotation.RocketHandler)")
    public Object aspect(ProceedingJoinPoint point) throws Throwable {
        return super.doAround(point);
    }
    // .....
}

// Xxljob 监控
public class XxlJobLogMonitorAspect extends AbstractLogMonitorAspect {

    @Around("execution(* com.xxx..timer..execute(..)) " +
            "|| execution(* com.xxx..jobhandler..execute(..))")
    public Object aspect(ProceedingJoinPoint point) throws Throwable {
        return super.doAround(point);
    }
    // .....
}

可实现功能效果

  1. 接口、定时器、同步回调、火箭回调发生异常,一分钟内,会发送企微告警
  2. 核心接口的 RT 一段时间内(环比)发生的巨大波动,会发送企微告警
  3. 可基于历史的日志,统计异常状况,和异常发生的时间段等等故障分析工作
  4. 特殊情况,可将配置设置成 ALL 监控所有异常情况,比如系统大范围重构上线等,可及时监控生产异常情况

日志打印效果

【原始日志】:

***********************
[aliyun_log_monitor]::{"aspectTime":0,"currentTime":"2024-05-27 15:48:01","entryType":"Controller","errorCode":"SYS_ERROR","errorMsg":"[BusinessTipException: 系统繁忙][at com.XXXX(XXXImpl.java:360)]","functionName":"XX/invoice/save","projectName":"xxx","reqParam":{},"rt":30,"success":0,"traceId":"-","userId":"-"}::[aliyun_log_monitor]
***********************

阿里云SLS日志平台解析

找运维配置 SLS 日志解析,通过正则表达式 【\[aliyun_log_monitor]::(.*?)::\[aliyun_log_monitor]】来采集监控的数据,

配置告警监控

(1)核心接口异常告警

当存在 data_errorCode = SYS_ERROR 时,则发起告警,一分钟扫描一次(合并发送)

(2)核心接口RT 波动告警

查询 data_success = 1 的日志,并且根据 projectName, functionName 字段联合 group by,判断环比 data_rt 增长 10% 则发起告警。

说明:这里主要说一下SUCCESS RT,这个指标是可以最准确的反馈出最近RT是否存在波动

(3)基于SLS日志,进行数据统计

统计异常的类型次数,统计异常发生时间线曲线图、统计接口 SUCCESS RT 的波动曲线,等等

增加Apollo配置

# 有配置才会开启,其中 ALL 表示监控所有
aliyun_slslog_monitor_url = ALL

# 有配置才会开启,其中 ALL 表示监控所有
aliyun_slslog_monitor_xxljob = xxxHandler

# 有配置才会开启,其中 ALL 表示监控所有
aliyun_slslog_monitor_sync_callback = update_XX,

# 有配置才会开启,其中 ALL 表示监控所有
aliyun_slslog_monitor_rocket_handler = insertXX,

  • 5
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值