1. 前言
众所周知,构建一个完整的日志体系是保障开发顺畅的第一步。而一个构建完整的日志体系所耗费的时间其实并不少于开发几个功能模块的时间了,那么在日常开发中为了节省这时间简单记录日志,我做出来以下一些尝试,希望对读者有所帮助。
(本文面向后端入门同学或是对鉴权欠缺研究的道友)
2. 注解+手动记录
这应该是最常见的一种做法,使用注解@Sl4j
自动创建日志类,然后通过调用log.info()
手动记录日志,这是一种相对灵活的方式,我们可以在任何想要的地方记录任何内容的日志。但是在实际开发中常常会出现大量日志穿插在业务逻辑的代码中,导致代码与日志耦合度高,具体体现在业务逻辑需要修改时得把日志一起改了,并且容易出现大量重复日志,比如请求参数或者鉴权信息等。但是并不是不推荐使用此种方式处理日志,毕竟这种方式最简单,最灵活,但是我们可以结合其他方式减少日志与业务的耦合。
3. AOP统一处理
那么首先我将介绍如何使用AOP自动记录日志,并且哪些日志用AOP会比较合适。
3.1 请求详情日志
对于请求详情日志用AOP统一处理与控制层和业务层解耦是一种很好的方式:
@Slf4j
@Aspect
@Component
public class LoggingAspect {
/* 日志输出被访问的接口url */
@Before("execution(* com.steadon.example.controller.*.*(..))")
public void beforeRequest(JoinPoint joinPoint) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
String httpMethod = request.getMethod();
String remoteAddr = request.getRemoteAddr();
String requestURI = request.getRequestURI();
String queryString = request.getQueryString();
String params = "";
if ("POST".equalsIgnoreCase(httpMethod)) {
Object[] args = joinPoint.getArgs();
if (args != null && args.length > 0) {
params = Arrays.toString(args);
}
}
log.info("Request Info: IP [{}] HTTP_METHOD [{}] URL [{}] QUERY_STRING [{}] PARAMS [{}]",
remoteAddr, httpMethod, requestURI, queryString, params);
}
}
这样一个AOP通知类就能帮我记录极其详细的请求详情日志,在遇到大部分问题都可以分析此日志得出结论(一般都是前端传参不对或者后端接收不正确),其效果如下所示:
3.2 其他
同理,你也可以用AOP切入任何切点,消息队列、Mybatis的Mapper
操作,甚至某个循环都可以织入通知,但是同时也需要考虑成本。AOP并不是性能有多好,也不是有多方便,只是有着最好的解耦效果,而是否使用AOP取决于你的日志与业务的耦合度够不够高。
4. Lark机器人 + 全局异常处理
我相信大部分读者都是知道飞书机器人,通全局异常捕获调用飞书机器人告警是一种极其实用的业务告警通知,问猜读者此时第一个想法是“这个我知道”,但是如何做到的可能并不是都清楚,其实很简单,看看我的操作:
4.1 创建告警群聊
对于简单报警我们一般不会创建一个机器人应用,而是一个群聊机器人。创建群聊可以帮助我们动态管理需要收到告警的开发人员。
4.2 创建自定义机器人
群聊 >>>> 设置 >>>> 群机器人 >>>> 添加机器人
然后我们在机器人详情页面拿到webhook
的地址(注意地址不要泄露):
下一步就在代码中引入飞书机器人告警,首先我们创建一个飞书机器人的告警方法:
/* 飞书机器人告警 */
public String sendLarkNotification(String webhookUrl, String user, String title, String messageBody) throws Exception {
URL url = new URL(webhookUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setDoOutput(true);
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/json; ");
ObjectMapper mapper = new ObjectMapper();
ObjectNode root = mapper.createObjectNode();
ObjectNode content = root.putObject("content").putObject("post").putObject("zh_cn");
content.put("title", title);
ArrayNode contentArray = content.putArray("content");
ArrayNode atUserArray = contentArray.addArray();
atUserArray.addObject().put("tag", "at").put("user_id", user);
ArrayNode messageArray = contentArray.addArray();
messageArray.addObject().put("tag", "text").put("text", messageBody);
root.put("msg_type", "post");
byte[] input = mapper.writeValueAsBytes(root);
try (OutputStream os = connection.getOutputStream()) {
os.write(input, 0, input.length);
}
try (BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
StringBuilder response = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
response.append(line);
}
return response.toString();
}
}
代码很简单,不用过于纠结其逻辑,无非是通过HTTP
向webhook
地址发送携带告警信息的POST
请求,那么如何发出这个请求,就需要我们在全局异常处理中调用该方法(为了方便我直接在全局异常处理类中写的这个方法,读者也可专门创建形如FeishuRobotUtil.java
的工具类,以下是我的全局异常处理类的逻辑:
@ControllerAdvice
public class GlobalExceptionHandler {
@Value("${notifications.larkBotEnabled}")
private boolean larkBotEnabled;
@ExceptionHandler(value = Exception.class)
public CommonResult handleException(Exception e) {
if (!larkBotEnabled) return CommonResult.fail();
// Send notification to Lark
String title = "线上BUG通报";
String user = "all";
String webhookUrl = "https://open.feishu.cn/open-apis/bot/v2/hook/f4150b6c-xxxx-xxxx-xxxx-xxxx-xxxx";
try {
String s = sendLarkNotification(webhookUrl, user, title, e.getMessage());
return CommonResult.fail(s);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
此时你一定疑惑larkBotEnabled
是干嘛的,因为全局异常捕获是不区分线上环境和开发环境的,那么显然我们应该用一个变量去控制报警事件,此时我们用线上配置文件和本地配置文件区分这个配置:
application.yml
spring:
profiles:
active: dev #决定是否使用本地配置
application:
name: app-name
notifications:
larkBotEnabled: true #自定义字段控制报警行为
application-dev.yml
notifications:
larkBotEnabled: false
这样就完美区分了不同环境是否告警了,告警效果如下所示:
这个告警十分优雅简洁,一眼能看出问题所在,而当你想要过滤掉一些低级告警(比如上图所示为前端没有传请求体),那么你可以提前捕获一些低级告警并不去调用机器人(即多级try-catch
,只有第一个捕获的会执行,以此来进行告警分级)。
那么日志类就说到这里,在大型工程中日志体系并非如此,而是需要投入大量人力和时间去构建完备的日志系统的,即使是飞书机器人告警也会使用消息分发平台通过机器人应用去拉群告警,但本篇内容用于日常开发中绰绰有余,如有误导之处,欢迎评论区斧正!