Controller 日志拦截,邮件异常告警

App

记得开启 @EnableAspectJAutoProxy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.bozhi.xiaoluo;

import com.bozhi.xiaoluo.modules.common.utils.SSLUtil;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@SpringBootApplication
@EnableCaching
// @EnableAsync // 禁止开启此注释,否则 AsyncContextAopConfig 会拦截上下文失败
@EnableAspectJAutoProxy(exposeProxy = true, proxyTargetClass = true)
// exposeProxy = true,可以 通过 AopContext.currentProxy() 获取代理类
// proxyTargetClass = true,使用 CGLIB 动态代理
public class XiaoluoApplication {

public static void main(String[] args) {
SSLUtil.ignoreSsl();
SpringApplication.run(XiaoluoApplication.class, args);
}

}

ControllerLogAspect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
package com.bozhi.xiaoluo.config.aop;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.lang.Opt;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.json.JSONUtil;
import com.bozhi.xiaoluo.common.utils.E;
import com.bozhi.xiaoluo.common.utils.R;
import com.bozhi.xiaoluo.modules.common.utils.$;
import com.bozhi.xiaoluo.modules.common.utils.ContextUtils;
import com.bozhi.xiaoluo.modules.common.utils.MDCUtil;
import com.bozhi.xiaoluo.modules.common.utils.UserUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
* AOP 日志切面
*/
@Component
@Aspect
@Slf4j
public class ControllerLogAspect {

/**
* 切入点
*/
@Pointcut("@within(org.springframework.web.bind.annotation.RestController)")
private void pointcut() {
}

/**
* 环绕通知
*/
@Around("pointcut()")
public Object aroundAdvice(ProceedingJoinPoint point) throws Throwable {
Object[] args = point.getArgs();
String uri = UserUtil.request().getRequestURI();
Long userId = Opt.ofTry(() -> ContextUtils.loginUser().getId()).get();
String nickName = Opt.ofTry(() -> ContextUtils.loginUser().nickName()).get();
String token = UserUtil.token();
String requestBody = ifExclude() ? "禁止打印" : JSONUtil.toJsonStr(args);
String requestInfo = StrUtil.format("uri=[{}] userId=[{}] nickName=[{}] token=[{}] 请求参数:{}",
uri, userId, nickName, token, requestBody
);

try {
log.info(">>>>>>>>>>>>>> Controller 前置拦截:{}", requestInfo);
long startTime = System.currentTimeMillis();
Object returnValue = point.proceed(args);
long costMs = System.currentTimeMillis() - startTime;
log.info(">>>>>>>>>>>>>> Controller 后置拦截:耗时:{}ms", costMs);
// log.info(">>>>>>>>>>>>>> Controller 后置拦截:返回结果:{}", JSONUtil.toJsonStr(returnValue));// 避免打印大对象

R.costMs(returnValue, costMs);// 耗时放入响应体
return returnValue;
} catch (Throwable t) {
// 异常:日志追踪
String exceptionMessage = exceptionMessage(t);
log.error(">>>>>>>>>>>>>> Controller 异常拦截:[grep '{}' -A 300 server.log] 异常信息:{}", MDCUtil.newTraceId(), exceptionMessage);

// 忽略:doris SQL 超时异常,不进行邮件告警
if (ifSqlException(exceptionMessage)) {
throw t;
}

// 未知异常:发邮件
if (!(t instanceof E)) {
sendEmail(requestInfo, exceptionMessage);
}
throw t;
}
}

// 忽略SQL异常(此错误对前端无感知,推荐数据调取下一页时发生)
//
// 情况一
// cn.hutool.core.thread.ThreadException: ExecutionException: org.springframework.jdbc.UncategorizedSQLException:
// ### Error querying database. Cause: java.sql.SQLException: errCode = 2, detailMessage = Query timeout
// ### SQL: ...
// 情况二
// ### Cause: java.sql.SQLIntegrityConstraintViolationException: errCode = 2, detailMessage = Column 'create_time' in field list is ambiguous
private static boolean ifSqlException(String exceptionMessage) {
return (StrUtil.contains(exceptionMessage, "SQLException") && StrUtil.contains(exceptionMessage, "Query timeout"))
|| (StrUtil.contains(exceptionMessage, "SQLIntegrityConstraintViolationException") && StrUtil.contains(exceptionMessage, "Column 'create_time' in field list is ambiguous"));
}

private static final Set<String> excludeUris = CollUtil.newHashSet(
"/common/addBuryingEventBatch",// 埋点 --- 十几分钟发送一次,可能包含上千条的事件
"占位符"
);

// 是否排除 --- 防止打印请求体过大
private boolean ifExclude() {
String uri = UserUtil.request().getRequestURI();
return excludeUris.contains(uri);
}

// 获取异常信息(排除无用堆栈打印)
private String exceptionMessage(Throwable t) {
String lineSeparator = System.lineSeparator();
String[] lines = ExceptionUtil.stacktraceToString(t, Integer.MAX_VALUE).split(lineSeparator);
return Stream.of(lines)
.filter(StrUtil::isNotBlank)
.filter(e -> e.contains("Exception") || e.contains("bozhi"))
.collect(Collectors.joining(lineSeparator));
}

// 未知异常 -> 邮件通知
private void sendEmail(String requestInfo, String exceptionMessage) {
// [1]. 邮箱:正文
String emailContent = String.join(
"\n",
StrUtil.format("请求信息:{}", requestInfo),
StrUtil.format("请求日志追踪ID:{}", MDCUtil.get()),
"-----------------------------------------------------------------------------------",
exceptionMessage,
"-----------------------------------------------------------------------------------"
);

// [2]. 邮箱:标题
String emailTitle = StrUtil.format("APP未知异常,环境:{}", SpringUtil.getActiveProfile());

// [3]. 发送邮件 - 异步
$.smsUtil.asyncSendEmailToDevGroup(emailContent, emailTitle);
}
}