Controller 日志拦截,邮件异常告警 发表于 2025-05-20 | 分类于 ---SpringBoot | App 记得开启 @EnableAspectJAutoProxy 12345678910111213141516171819202122package 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); }} ControllerLogAspect123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136package 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@Slf4jpublic 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); }}