MDC介绍
MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 和 logback 提供的一种方便在多线程条件下记录日志的功能。某些应用程序采用多线程的方式来处理多个用户的请求。在一个用户的使用过程中,可能有多个不同的线程来进行处理。典型的例子是 Web 应用服务器。当用户访问某个页面时,应用服务器可能会创建一个新的线程来处理该请求,也可能从线程池中复用已有的线程。在一个用户的会话存续期间,可能有多个线程处理过该用户的请求。这使得比较难以区分不同用户所对应的日志。当需要追踪某个用户在系统中的相关日志记录时,就会变得很麻烦。
一种解决的办法是采用自定义的日志格式,把用户的信息采用某种方式编码在日志记录中。这种方式的问题在于要求在每个使用日志记录器的类中,都可以访问到用户相关的信息。这样才可能在记录日志时使用。这样的条件通常是比较难以满足的。MDC 的作用是解决这个问题。
MDC 可以看成是一个与当前线程绑定的哈希表,可以往其中添加键值对。MDC 中包含的内容可以被同一线程中执行的代码所访问。当前线程的子线程会继承其父线程中的 MDC 的内容。当需要记录日志时,只需要从 MDC 中获取所需的信息即可。MDC 的内容则由程序在适当的时候保存进去。对于一个 Web 应用来说,通常是在请求被处理的最开始保存这些数据。
官网地址:http://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/MDC.html
中文MDC:logback中文手册
前期准备工作
小编这次用我之前搭建的springCloud基础模板架构上演示具体架构搭建可以参考我之前的一篇文档
https://blog.csdn.net/u012783543/article/details/103406818
引入相关pom的log的包
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<exclusions>
<exclusion>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
</exclusion>
<exclusion>
<groupId>javax.jms</groupId>
<artifactId>jms</artifactId>
</exclusion>
<exclusion>
<groupId>com.sun.jdmk</groupId>
<artifactId>jmxtools</artifactId>
</exclusion>
<exclusion>
<groupId>com.sun.jmx</groupId>
<artifactId>jmxri</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
配置logback相关配置
具体的logback相关配置我这里就不做太多说明了,注意:在我配置的encoder中的pattern日志输出规则的时候里面有一个变量是MDC的变量 我定义为**%X{UUID}** 变量名称就是为UUID在日期的后面你会发现有这个变量日志打印
<?xml version="1.0" encoding="utf-8"?>
<configuration debug="false">
<!--定义日志文件的存储地址 勿在 LogBack 的配置中使用相对路径-->
<property name="LOG_HOME" value="/home"/>
<!-- 控制台输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期,%X标识MDC的唯一变量名称,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %X{UUID} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>
<!-- 按照每天生成日志文件 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--日志文件输出的文件名-->
<FileNamePattern>${LOG_HOME}/test.log.%d{yyyy-MM-dd}.log</FileNamePattern>
<!--日志文件保留天数-->
<MaxHistory>30</MaxHistory>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
<!--日志文件最大的大小-->
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>10MB</MaxFileSize>
</triggeringPolicy>
</appender>
<!-- 日志输出级别 -->
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
动态引用关联日志
package com.javacyun.wx.controller;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.javacyun.wx.base.BaseResponseResult;
import com.javacyun.wx.common.Const;
import com.javacyun.wx.entity.KWxTest;
import com.javacyun.wx.service.IKWxTestService;
import com.javacyun.wx.utils.MapData;
import lombok.AllArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import com.javacyun.wx.base.BaseController;
import org.springframework.web.servlet.ModelAndView;
import java.util.HashMap;
/**
* <p>
* 前端控制器
* </p>
*
* @author jx111
* @since 2020-01-19
*/
@Controller
@AllArgsConstructor
@RequestMapping("/k-wx-test")
public class KWxTestController extends BaseController {
private final Logger log = LoggerFactory.getLogger(KWxTestController.class);
/**
* 用于测试使用
*/
@PostMapping("/logText")
public BaseResponseResult logText(@RequestBody JSONObject values) {
String userId = values.getString("userId");
log.info("KWxTestController logText 没有加入MDC之前");
//构建日志主键唯一关联
MDC.put("UUID",userId);
log.info("KWxTestController logText 加入MDC之后");
return BaseResponseResult.ok();
}
}
尝试运行得到结果
通过postman运行
这里可以观察到运行结果日志的唯一关联已经被带入进去了,并且相关报错信息日志也带入了这个UUID
多线程场景下将MDC带入
这里我们都知道当程序开启线程的情况下相关入参如果不传入进去的话是带不进去的,所以这里小编重写了线程池,将MDC进行重新写入到日志中,直接上代码
package com.javacyun.wx.utils;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
/**
* @Desc
* @Author jx111
* @Date 2020/3/27-16:05
*/
public class ThreadPoolUtil {
private static ExecutorService executorServer;
static {
executorServer = new MdcThreadPoolExecutor(500, Executors.defaultThreadFactory());
}
@SuppressWarnings("rawtypes")
public static Future submit(Runnable run) {
if (run == null) {
return null;
}
return executorServer.submit(run);
}
public static Future submit1(Callable run) {
if (run == null) {
return null;
}
return executorServer.submit(run);
}
public static void shutdown() {
executorServer.shutdown();
}
}
package com.javacyun.wx.utils;
import org.slf4j.MDC;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
/**
* @Desc mdc线程任务重写类,用于加入全程追踪标识
* @Author jx111
* @Date 2020/3/27-16:03
*/
public class MdcThreadPoolExecutor extends ScheduledThreadPoolExecutor {
public MdcThreadPoolExecutor(int corePoolSize, ThreadFactory threadFactory) {
super(corePoolSize, threadFactory);
}
@Override
public Future<?> submit(Runnable command) {
return doExecute(command, MDC.getCopyOfContextMap());
}
@Override
public <T> Future<T> submit(Callable<T> task) {
return doExecute(task, MDC.getCopyOfContextMap());
}
private <V> Future<V> doExecute(final Callable<V> task, final Map mdcContextMap) {
return super.submit(new Callable<V>() {
@Override
public V call() throws Exception {
if(mdcContextMap!=null){
MDC.setContextMap(mdcContextMap);
}
return task.call();
}
});
}
private Future<?> doExecute(final Runnable command, final Map mdcContextMap) {
if(mdcContextMap!=null){
MDC.setContextMap(mdcContextMap);
}
return super.submit(command);
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
}
}
开启多线程时的运行结果
加入多线程后的测试类
/**
* 用于测试使用
*/
@PostMapping("/logText")
public BaseResponseResult logText(@RequestBody JSONObject values) {
String userId = values.getString("userId");
log.info("KWxTestController logText 没有加入MDC之前");
//构建日志主键唯一关联
MDC.put("UUID",userId);
log.info("KWxTestController logText 加入MDC之后");
Callable<Boolean> call = new Callable<Boolean>() {
@Override
public Boolean call(){
try {
log.info("KWxTestController logText 开启线程后");
}catch (Throwable e){
e.printStackTrace();
}
return true;
}
};
ThreadPoolUtil.submit1(call);
log.info("KWxTestController logText 结束");
return BaseResponseResult.ok();
}
执行结果
搞定,如有不妥,还请多多指教!