一个讨厌八股的博主:关注我每周学习项目实战调优小技巧,拒绝CV,拒绝屎山
前言
看到标题以后,是不有人会联想到前两年出现的log4j的漏洞,很多人把日志理解为一个简单的日志输出,甚至很多新手没有记录日志的习惯。其实一个好的日志记录习惯,可以更方便我们来调试程序,甚至是排查线上bug。那么问题来了,在我们的实际生产项目中,如果并发很高的情况下,会有以下几个问题:
- 日志输出太快了,往往都找不到自己想找的
- 哪怕是根据关键词找到了日志,该怎么知道这条日志是不是我想要的呢
- 在我们的微服务架构当中,有没有一种方案能够从流量入口到流量完结都能够找到日志呢,比如说A调用B,通过A服务输出的日志,也可以知道B服务输出的日志
今天主角:在微服务架构下,全链路日志处理方案要登场了。
一、基础理论知识
1.1 MDC 技术
MDC(Mapped Diagnostic Context)是一种日志记录技术,常用于Java应用程序中,特别是在基于日志框架如Logback、Log4j等的应用中。MDC 技术允许在应用程序的不同执行线程中共享上下文信息,使得在日志输出时能够包含更多的上下文信息,方便日志的追踪、调试和分析。
MDC 技术的核心思想是在每个线程中维护一个上下文信息的映射表(通常是一个类似于Map的数据结构),在需要的时候将关键信息存储在这个映射表中。当线程执行日志输出操作时,日志框架会自动将当前线程的 MDC 上下文信息包含到日志中,从而使得日志中能够包含更多的上下文信息。
使用 MDC 技术,可以在日志中添加诸如用户ID、会话ID、请求ID等关键信息,使得在分布式系统中更容易追踪某个请求或者操作的完整执行路径。
这里需要注意两个点:1个是每个线程,一个是上下文,也就是说MDC是一个线程安全的记录技术,每个线程都有独享的信息。
二、具体实现方案
现在市面上实现全链路日志的方案太多太多了,但是万变不离其宗(学好底层,走遍天下不用怕)
可以利用AOP来做,可以用开源组件来做,也可以通过logback。本文主要介绍一下最常用的logback+MDC。
1.定义切点
切点怎么定义?
首先我们所有微服务的流量入口无非就这么几个:
- HTTP接口
- RPC
- 定时任务
- MQ
最大的流量入口是HTTP(本文先介绍单体应用该怎么实现全链路日志,下篇会介绍在微服务场景下该怎么实现日志的闭环)
代码如下(示例):
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.MDC;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
@Component
@Aspect
@Order(Ordered.HIGHEST_PRECEDENCE)
@AllArgsConstructor
@Slf4j
public class MdcAspect {
public static final String logIdKey = "logId";
private final HttpServletRequest httpServletRequest;
@Around(
"@annotation(org.springframework.web.bind.annotation.GetMapping) " +
"|| @annotation(org.springframework.web.bind.annotation.PostMapping) " +
"|| @annotation(org.springframework.web.bind.annotation.PutMapping) " +
"|| @annotation(org.springframework.web.bind.annotation.DeleteMapping) "
)
public Object injectLogId(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
String logId = httpServletRequest.getHeader(MdcAspect.logIdKey);
logId = StrUtil.isNotBlank(logId) ? (logId + "_") : "" + IdUtil.fastUUID();
MDC.put(logIdKey, logId);
try {
return proceedingJoinPoint.proceed();
} finally {
MDC.remove(logIdKey);
}
}
从上面代码可以看出,我拦截了所有的get,post,put,delete请求,然后生成了一个uuid,放到了MDC中。MDC就可以理解是一个线程独享的map。
2.修改logback配置文件
代码如下(示例):
<!--每隔15s 重新加载日志配置文件-->
<configuration scan="true" scanPeriod="15 seconds">
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!-- %X{var} 注入var变量的内容-->
<pattern>%d{HH:mm:ss} [%thread] %-5level %logger{36} [%X{logId}] - %msg%n</pattern>
</encoder>
</appender>
<!-- 定义变量-->
<property name="LOG_DIR" value="logs" />
<property name="LOG_FILE" value="detail.log" />
<!-- 注意不要用错class -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_DIR}/${LOG_FILE}</file>
<!-- 压缩归档策略-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- daily rollover -->
<!-- 按分钟归档 -->
<fileNamePattern>${LOG_DIR}/${LOG_FILE}.%d{yyyy-MM-dd-HH-mm}.gz</fileNamePattern>
<!-- keep 30 days' worth of history capped at 3GB total size -->
<maxHistory>30</maxHistory>
<totalSizeCap>1GB</totalSizeCap>
</rollingPolicy>
<append>true</append>
<encoder>
<pattern>%d{HH:mm:ss} [%thread] %-5level %logger{36} [%X{logId}] - %msg%n</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="STDOUT" />
<appender-ref ref="FILE" />
</root>
</configuration>
该配置文件当中
<pattern>%d{HH:mm:ss} [%thread] %-5level %logger{36} [%X{logId}] - %msg%n</pattern>
中的logId就是我们的全链路日志(一个http请求一个独有的日志id)
总结
非常简单,两步就做好一个单体的链路日志,但是还有很多问题,比如说:
- 异步问题怎么获取到这个logId?
- 微服务架构怎么做到这个logId的传递?
- 以上方案只是解决了http接口的,那我的RPC接口,MQ里的日志输出中的logId从哪来?
下一篇博客继续…
一个讨厌八股的博主:关注我每周学习项目实战调优小技巧,拒绝CV,拒绝屎山