Java服务日志追踪

背景

在服务中打印日志是一个非常好的习惯,在生产环境中,我们没法对代码进行debug,这时候日志对排查问题来说就非常重要,规范的打印日志可以帮助我们快速的定位问题。

在我们排查日志的时候,往往会出现以下三个问题

1:这条日志是属于哪个请求的?

2:这个请求有哪些日志?

3:前面的日志和后面的日志是否是属于同一个请求?

如果你的日志中,没有请求的唯一性标识,是很容易出现日志混乱的现象,特别是在并发量比较高的情况下,更加难区分和筛选,形成无效日志

思想

针对以上问题,我们希望用一个trackId来标识一个http请求,这个标识必须贯穿整个http请求处理的生命周期,将这次请求处理过程中的所有日志串联在一起,以实现轻量级日志追踪

MDC简介

MDC是java日志框架中的一个组件,其核心功能是保存线程MDC变量,并自动将当前线程存放在MDC中的变量,在日志中打印出来(需在打印日志格式的地方配置此变量)。MDC的原理就是使用ThreadLocal线程变量,对ThreadLocal进行进一步的封装,提供便捷的api,方便在打印日志的时候使用

MDC的常用方法

public static void put(String key, String val) 存入一个值,key-value结构

public static String get(String key)  获取一个值

public static void remove(String key) 移除一个值

public static void clear() 清除当前线程MDC中的所有值

public static Map<String, String> getCopyOfContextMap() 获取当前线程MDC所有值

public static void setContextMap(Map<String, String> contextMap) 设置MDC的值,
此方法一般和上一个getCopyOfContextMap方法一起使用,通常用户在线程场景切换状态时,
获取旧线程的MDC值,重新设置到新线程的MDC中,实现多线程环境MDC值传递

MDC接入SpringBoot

MDC是属于日志框架中的一个组件,只要是项目中包含任何日志框架既可,springboot项目有自带的日志框架

完整代码地址在文章末尾

maven

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.83</version>
        </dependency>
    </dependencies>

定义日志格式

在logback.xml文件中定义日志输出格式,设定trackId是每个请求的唯一标识,在日志输出格式中将trackId添加上,每条日志和请求的关系就能对应上。这里设置的比较简单,每个公司都有不同的标准,大家按自己的来就行,只需要在输出日志格式上加上trackId就行

<configuration>
    <!-- 定义日志输出格式 -->
<!--    <conversionRule conversionWord="sessionId" converterClass="com.example.SessionIdConverter" />-->
    <!-- 定义日志输出器 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%X{trackId} %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg %n</pattern>
        </encoder>
    </appender>
    <!-- 设置日志级别 -->
    <root level="INFO">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

MDC存取TrackId

过滤器,在请求进入服务时给MDC添加上trackId,方便后续获取

@Component
@WebFilter
@Log4j2
public class DMCFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        try {
            // DMC过滤器,向请求头中获取trackId(此作用是当多个java服务之前的http调用时,可以做多服务追踪)
            HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
            String trackId = httpServletRequest.getHeader(BuConstant.TRACK_ID);
            if(StringUtils.isEmpty(trackId)){
                trackId = UUID.randomUUID().toString().replaceAll("-", "");
            }
            MDC.put(BuConstant.TRACK_ID,trackId);
            filterChain.doFilter(servletRequest,servletResponse);
        }finally {
            MDC.clear();
        }
    }
}

编写一个测试接口

@RequestMapping("/hello")
@RestController
@Log4j2
public class HelloController {

    @GetMapping("/hi")
    public Object hi(){
        log.info("/hello/hi ....");
        log.info("业务处理中...");
        return "hi";
    }
}

运行日志结果

在日志的最开始打印出trackId,我们可以根据这个来判断是否是同一个请求中的日志

异步多线程场景

由于MDC的实现原理是ThreadLocal线程变量,所以也存在子线程无法获取到父线程的MDC变量信息

MDC提供了两个方法, 可以让我们快速转存线程的MDC信息,实现线程间的MDC信息传递

getCopyOfContextMap()和 setContextMap(Map<String, String> contextMap)方法

新建线程方式

@RequestMapping("/hello")
@RestController
@Log4j2
public class HelloController {

    @GetMapping("/hi")
    public Object hi(){
        log.info("/hello/hi ....");

        // MDC的底层原理是ThreadLocal,在线程改变后,不会传递到子线程,需要重新传递,注意清理
        // 如果是线程池,线程池中的线程不是重新创建线程,需要重置MDC

        Map<String, String> copyOfContextMap = MDC.getCopyOfContextMap();
        new Thread(()->{
            try
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值