项目背景
目前我们的告警监控相对比较完善:运维告警、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);
}
// .....
}
可实现功能效果
- 接口、定时器、同步回调、火箭回调发生异常,一分钟内,会发送企微告警
- 核心接口的 RT 一段时间内(环比)发生的巨大波动,会发送企微告警
- 可基于历史的日志,统计异常状况,和异常发生的时间段等等故障分析工作
- 特殊情况,可将配置设置成 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,