写在前面

本文只讨论非分布式体系的日志追踪,如果项目使用了微服务、RPC等分布式框架的话,日志追踪有很成熟的框架,Google的Dapper,阿里的EagleEye,Twitter基于Google的Dapper论文开发的Zipkin

通过本文将了解到什么是MDC、MDC应用中存在的问题、如何解决存在的问题

MDC介绍

简介:

MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 、logback及log4j2 提供的一种方便在多线程条件下记录日志的功能。MDC 可以看成是一个与当前线程绑定的哈希表,可以往其中添加键值对。MDC 中包含的内容可以被同一线程中执行的代码所访问。当前线程的子线程会继承其父线程中的 MDC 的内容。当需要记录日志时,只需要从 MDC 中获取所需的信息即可。MDC 的内容则由程序在适当的时候保存进去。对于一个 Web 应用来说,通常是在请求被处理的最开始保存这些数据

API说明:

  • clear() => 移除所有MDC
  • get (String key) => 获取当前线程MDC中指定key的值
  • getContext() => 获取当前线程MDC的MDC
  • put(String key, Object o) => 往当前线程的MDC中存入指定的键值对
  • remove(String key) => 删除当前线程MDC中指定的键值对

优点:

  • 代码简洁,日志风格统一,不需要在log打印中手动拼写traceId,即LOGGER.info("traceId:{} ", traceId)

这里使用log4j2打印日志

SpringBoot中使用log4j2可以参考SpringBoot log4j2日志之旅

pom.xml

       <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <artifactId>spring-boot-starter-logging</artifactId>
                    <groupId>org.springframework.boot</groupId>
                </exclusion>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-tomcat</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jetty</artifactId>
        </dependency>
        <!-- log4j2 start -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j2</artifactId>
        </dependency>
        <!-- log4j2 end -->

log4j2.xml

这里的关键是

<!-- 添加日志追踪traceId -->
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{traceId}] [%p] [%t] %c(%F:%L) - %m%n" />

%X{traceId}对应的就是MDC.put(Constants.TRACE_ID, traceId);
 

<?xml version="1.0" encoding="UTF-8"?>
<Configuration monitorinterval="10" status="debug">
  <Properties>
    <Property name="infoFile">./logs/info.log</Property>
    <Property name="infoFilePattern">./logs/info.log.%d{yyyy-MM-dd}_%i</Property>
    <Property name="errorFile">./logs/error.log</Property>
    <Property name="errorFilePattern">./logs/error.log.%d{yyyy-MM-dd}_%i</Property>
    <Property name="layoutPattern">%d{yyyy-MM-dd HH:mm:ss.SSS} [%p] [%t] %c(%F:%L) - %m%n</Property>
  </Properties>

  <Appenders>

    <Console name="stdout">
<!--      <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%p] [%t] %c(%F:%L) - %m%n"/>-->
      <!-- 添加日志追踪traceId -->
      <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{traceId}] [%p] [%t] %c(%F:%L) - %m%n" />
    </Console>

    <RollingFile fileName="${infoFile}" filePattern="${infoFilePattern}" name="infoAppender">

      <!--显示INFO级别日志-->
      <ThresholdFilter level="INFO" onMatch="ACCEPT"  onMismatch="DENY"/>
      <PatternLayout pattern="${layoutPattern}"/>

      <!--每个文件最多只能2G-->
      <Policies>
        <SizeBasedTriggeringPolicy size="2 GB" />
        <TimeBasedTriggeringPolicy modulate="true"/>
      </Policies>

      <!--删除超过15天的日志-->
      <DefaultRolloverStrategy>
        <Delete basePath="./logs" maxDepth="2">
          <IfLastModified age="30d" />
        </Delete>
      </DefaultRolloverStrategy>
    </RollingFile>


    <RollingFile fileName="${errorFile}" filePattern="${errorFilePattern}" name="errorAppender">

      <ThresholdFilter level="ERROR" onMatch="ACCEPT"  onMismatch="DENY"/>
      <PatternLayout pattern="${layoutPattern}"/>
      <Policies>
        <SizeBasedTriggeringPolicy size="2 GB" />
        <TimeBasedTriggeringPolicy modulate="true"/>
      </Policies>
    </RollingFile>
  </Appenders>

  <Loggers>

    <!--additivity="false"表示在该logger中输出的日志不会再延伸到父层logger。这里如果改为true,则会延伸到Root Logger,遵循Root Logger的配置也输出一次。-->
    <Logger name="org.springframework.boot.autoconfigure" level="DEBUG" additivity="true">
      <AppenderRef ref="stdout"/>
    </Logger>
    <!-- druid配置 start -->
    <logger name="druid.sql.Statement" level="debug" additivity="false">
      <appender-ref ref="stdout"/>
    </logger>
    <logger name="druid.sql.ResultSet" level="debug" additivity="false">
      <appender-ref ref="stdout"/>
    </logger>
    <!-- druid配置 end -->

    <Root level="INFO">
      <AppenderRef ref="stdout"/>
      <AppenderRef ref="infoAppender"/>
      <AppenderRef ref="errorAppender"/>
    </Root>
  </Loggers>

</Configuration>

接口的日志追踪,traceId可以在拦截器中添加,也可以在过滤器中添加

{

    @Bean
    public RequestContextListener requestContextListener() {
        return new RequestContextListener();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 方式一:拦截器中添加traceId
        // registry.addInterceptor(new LogInterceptor())
        // .addPathPatterns("/**")
        // .excludePathPatterns("/static/**", "/templates/**");
    }

    @Bean
    public FilterRegistrationBean<LogTraceFilter> logTraceFilter() {
        // 方式二:过滤器中添加traceId
        final FilterRegistrationBean<LogTraceFilter> filterRegistrationBean = new FilterRegistrationBean<>();
        final LogTraceFilter logTraceFilter = new LogTraceFilter();
        filterRegistrationBean.setFilter(logTraceFilter);
        filterRegistrationBean.setName("logTraceFilter");
        filterRegistrationBean.addUrlPatterns("/*");
        filterRegistrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
        return filterRegistrationBean;
    }

}

方式一:拦截器添加traceId

package com.foo.bar.interceptor;

import com.foo.bar.constant.Constants;
import com.foo.bar.utils.TraceIdUtil;
import org.slf4j.MDC;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


/**
 * 日志追踪拦截器
 * @author foo
 */
public class LogInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //如果有上层调用就用上层的ID
        String traceId = request.getHeader(Constants.TRACE_ID);
        if (traceId == null) {
            traceId = TraceIdUtil.getTraceId();
        }

        MDC.put(Constants.TRACE_ID, traceId);
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
            throws Exception {
        // Do nothing because of no business
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
            throws Exception {
        //调用结束后删除
        MDC.remove(Constants.TRACE_ID);
    }
}

方式二:过滤器添加traceId

package com.foo.bar.filter;

import com.foo.bar.constant.Constants;
import com.foo.bar.utils.TraceIdUtil;
import org.slf4j.MDC;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author: foo
 * @create: 2020-09-04 11:02
 */
public class LogTraceFilter extends OncePerRequestFilter {


    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        preHandle(request);
        filterChain.doFilter(request, response);
        afterCompletion();
    }

    /**
     * 过滤前处理
     * @param request 请求
     * @return
     */
    public void preHandle(HttpServletRequest request) {
        //如果有上层调用就用上层的ID
        String traceId = request.getHeader(Constants.TRACE_ID);
        if (traceId == null) {
            traceId = TraceIdUtil.getTraceId();
        }

        MDC.put(Constants.TRACE_ID, traceId);
    }

    /**
     * 过滤后处理
     */
    public void afterCompletion() {
        //调用结束后删除
        MDC.remove(Constants.TRACE_ID);
    }
}

constant.java 

package com.foo.bar.constant;

/**
 * 常量类
 * @author foo
 */
public class Constants {

    private Constants(){
        throw new UnsupportedOperationException();
    }

    /**
     * 日志追踪ID
     */
    public static final String TRACE_ID = "traceId";

}

TraceIdUtil.java 

package com.foo.bar.utils;

import java.util.UUID;

/**
 * traceId生成工具类
 * @author foo
 */
public class TraceIdUtil {

    private TraceIdUtil() {
        throw new UnsupportedOperationException("Utility class");
    }

    /**
     * 获取traceId
     * @return
     */
    public static String getTraceId() {
        return UUID.randomUUID().toString().replace("-", "").toUpperCase();
    }
}

 

MDC 存在的问题

  • 子线程中打印日志丢失traceId
  • kafka等中间件打印日志丢失traceId

子线程中打印日志丢失traceId

开启异步注解@EnableAsync

package com.foo.bar;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;

/**
 * @author foo
 */
@EnableAsync
@SpringBootApplication
public class FooBarApplication {

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

}

配置异步线程池

package com.foo.bar.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * 线程池配置
 *
 * @author foo
 */
@Slf4j
@Configuration
public class ExecutorConfig {

    @Bean
    @Primary
    public Executor asyncServiceExecutor() {
        log.info("start asyncServiceExecutor");
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        //配置核心线程数
        executor.setCorePoolSize(10);
        //配置最大线程数
        executor.setMaxPoolSize(200);
        //配置队列大小
        executor.setQueueCapacity(99999);
        //配置线程池中的线程的名称前缀
        executor.setThreadNamePrefix("async-service-");

        // 设置拒绝策略:当pool已经达到max size的时候,如何处理新任务
        // CALLER_RUNS:不在新线程中执行任务,而是有调用者所在的线程来执行
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        //执行初始化
        executor.initialize();
        return executor;
    }

}

测试Controller

package com.foo.bar.controller;

import com.foo.bar.service.HelloService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author: foo
 * @create: 2020-09-04 14:15
 */
@RestController
@Slf4j
@RequiredArgsConstructor
public class HelloController {

    private final HelloService helloService;

    @GetMapping("/hello")
    public String hello(){
        log.info("Hello World! hello无异步追踪!");
        helloService.hello();
        return "Hello World!";
    }

    @GetMapping("/hello-async")
    public String helloAsync(){
        log.info("Hello World! hello-async异步追踪!");
        helloService.helloAsync();
        return "Hello World!";
    }
}

测试Service

package com.foo.bar.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

/**
 * @author: foo
 * @create: 2020-09-04 14:21
 */
@Service
@Slf4j
public class HelloService {

    public void hello(){
        log.info("进入hello业务方法");
    }

    @Async
    public void helloAsync(){
        log.info("进入helloAsync业务方法");
    }
}

测试结果:异步线程丢失了traceId

解决子线程日志打印丢失traceId问题

子线程在打印日志的过程中traceId将丢失,解决方式为重写线程池

  • 线程池封装类:ThreadPoolExecutorMdcWrapper.java
package com.foo.bar.config;

import com.foo.bar.utils.ThreadMdcUtil;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.util.concurrent.ListenableFuture;

import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * spring线程池封装类
 * @author foo
 */
@Slf4j
public class ThreadPoolTaskExecutorWrapper extends ThreadPoolTaskExecutor {

    private void showThreadPoolInfo(String prefix){
        ThreadPoolExecutor threadPoolExecutor = getThreadPoolExecutor();

        if(null==threadPoolExecutor){
            return;
        }

        log.info("{}, {},taskCount [{}], completedTaskCount [{}], activeCount [{}], queueSize [{}]",
                this.getThreadNamePrefix(),
                prefix,
                threadPoolExecutor.getTaskCount(),
                threadPoolExecutor.getCompletedTaskCount(),
                threadPoolExecutor.getActiveCount(),
                threadPoolExecutor.getQueue().size());
    }

    @Override
    public void execute(Runnable task) {
        showThreadPoolInfo("1. do execute");
        super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));

    }

    @Override
    public void execute(Runnable task, long startTimeout) {
        showThreadPoolInfo("2. do execute");
        super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()), startTimeout);

    }

    @Override
    public Future<?> submit(Runnable task) {
        showThreadPoolInfo("1. do submit");
        return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }

    @Override
    public <T> Future<T> submit(Callable<T> task) {
        showThreadPoolInfo("2. do submit");
        return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }

    @Override
    public ListenableFuture<?> submitListenable(Runnable task) {
        showThreadPoolInfo("1. do submitListenable");
        return super.submitListenable(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }

    @Override
    public <T> ListenableFuture<T> submitListenable(Callable<T> task) {
        showThreadPoolInfo("2. do submitListenable");
        return super.submitListenable(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }
}
  •  线程traceId封装工具类:ThreadMdcUtil.java
package com.foo.bar.utils;

import com.foo.bar.constant.Constants;
import org.slf4j.MDC;

import java.util.Map;
import java.util.concurrent.Callable;


/**
 * 线程MDC工具类
 * @author foo
 */
public class ThreadMdcUtil {

    public static void setTraceIdIfAbsent() {
        if (MDC.get(Constants.TRACE_ID) == null) {
            MDC.put(Constants.TRACE_ID, TraceIdUtil.getTraceId());
        }
    }

    public static <T> Callable<T> wrap(final Callable<T> callable, final Map<String, String> context) {
        return () -> {
            if (context == null) {
                MDC.clear();
            } else {
                MDC.setContextMap(context);
            }
            setTraceIdIfAbsent();
            try {
                return callable.call();
            } finally {
                MDC.clear();
            }
        };
    }

    public static Runnable wrap(final Runnable runnable, final Map<String, String> context) {
        return () -> {
            if (context == null) {
                MDC.clear();
            } else {
                MDC.setContextMap(context);
            }
            setTraceIdIfAbsent();
            try {
                runnable.run();
            } finally {
                MDC.clear();
            }
        };
    }
}

说明【以封装Runnable为例】:

  • 判断当前线程对应MDC的Map是否存在,存在则设置
  • 设置MDC中的traceId值,不存在则新生成,针对不是子线程的情况,如果是子线程,MDC中traceId不为null
  • 执行run方法

代码等同于以下写法,会更直观

 

public static Runnable wrap(final Runnable runnable, final Map<String, String> context) {
        return new Runnable() {
            @Override
            public void run() {
                if (context == null) {
                    MDC.clear();
                } else {
                    MDC.setContextMap(context);
                }
                setTraceIdIfAbsent();
                try {
                    runnable.run();
                } finally {
                    MDC.clear();
                }
            }
        };
    }

重新返回的是包装后的Runnable,在该任务执行之前【runnable.run()】先将主线程的Map设置到当前线程中【 即MDC.setContextMap(context)】,这样子线程和主线程MDC对应的Map就是一样的了

修改ExecutorConfig.java的asyncServiceExecutor方法,将ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor()改为ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutorWrapper(),如下所示:

package com.foo.bar.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * 线程池配置
 *
 * @author foo
 */
@Slf4j
@Configuration
public class ExecutorConfig {

    @Bean
    @Primary
    public Executor asyncServiceExecutor() {
        log.info("start asyncServiceExecutor");
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutorWrapper();
        //配置核心线程数
        executor.setCorePoolSize(10);
        //配置最大线程数
        executor.setMaxPoolSize(200);
        //配置队列大小
        executor.setQueueCapacity(99999);
        //配置线程池中的线程的名称前缀
        executor.setThreadNamePrefix("async-service-");

        // 设置拒绝策略:当pool已经达到max size的时候,如何处理新任务
        // CALLER_RUNS:不在新线程中执行任务,而是有调用者所在的线程来执行
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        //执行初始化
        executor.initialize();
        return executor;
    }

}

重新测试异步线程日志追踪,异步子线程中的traceId与父线程程一致了,OK。

  • kafka等中间件打印日志丢失traceId

解决方式可以通过AOP切入onMessage这个方法

package com.foo.bar.aspect;


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.slf4j.MDC;
import org.springframework.stereotype.Component;

import com.foo.bar.constant.Constants;
import com.foo.bar.utils.TraceIdUtil;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
 * MDC日志kafka切面
 *
 * @author foo
 * @date 2020/9/7 14:36
 */
@Aspect
@Component
@Slf4j
@RequiredArgsConstructor
public class MdcKafkaAspect {

    /**
     * 定义切入点
     * 通过@Pointcut注解声明频繁使用的切点表达式
     */
    @Pointcut("execution(* onMessage(..)) && target(org.springframework.kafka.listener.MessageListener)")
    public void messageListener() {
        // This func is intentionally empty. Nothing special is needed here.
    }

    @Around(value = "messageListener()")
    public void wrapOnMessage(ProceedingJoinPoint joinPoint) throws Throwable {
        try {
            MDC.put(Constants.TRACE_ID, TraceIdUtil.getTraceId());
            joinPoint.proceed();
        } finally {
            MDC.clear();
        }
    }
}

参考:

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐