MDC简介
MDC ( Mapped Diagnostic Contexts ) 是一个线程安全的存放诊断日志的容器。
MDC应用
使用日志组件输出日志后,我们可以方便的查看各种线上问题,但是由于服务端并发处理大量请求,导致同一请求的日志会分散在日志输出中,如果没有唯一的标识很难定位某次请求的执行流程。
因此,我们可能需要对一个用户的操作流程进行归类标记,比如打印用户的ID,为每次请求生成一个唯一标识并在这次请求中输出的日志都打印这个唯一标识等等。这样我们就可以从大量日志中直接定位到用户的操作流程,其目的是为了方便我们诊断线上问题而出现的工具类。
MDC是slf4j提供的适配其他具体日志实现包的工具类,目前只有logback和log4j支持此功能
MDC实现原理
MDC的代码如下所示
MDC使用单例模式并通过StaticMDCBinder
类动态绑定具体的MDC实现类来操作具体的日志组件的MDC实现
public class MDC {
static final String NULL_MDCA_URL = "http://www.slf4j.org/codes.html#null_MDCA";
static final String NO_STATIC_MDC_BINDER_URL = "http://www.slf4j.org/codes.html#no_static_mdc_binder";
static MDCAdapter mdcAdapter;
private MDC() {
}
static {
try {
mdcAdapter = StaticMDCBinder.SINGLETON.getMDCA();
} catch (NoClassDefFoundError ncde) {
mdcAdapter = new NOPMDCAdapter();
String msg = ncde.getMessage();
if (msg != null && msg.indexOf("StaticMDCBinder") != -1) {
Util.report("Failed to load class \"org.slf4j.impl.StaticMDCBinder\".");
Util.report("Defaulting to no-operation MDCAdapter implementation.");
Util
.report("See " + NO_STATIC_MDC_BINDER_URL + " for further details.");
} else {
throw ncde;
}
} catch (Exception e) {
// we should never get here
Util.report("MDC binding unsuccessful.", e);
}
}
以log4j2为例简介MDC的实现原理
JVM加载MDC类时执行static代码,StaticMDCBinder
动态加载具体日志组件提供的同包同名的类
log4j2的实现
public final class StaticMDCBinder {
/**
* The unique instance of this class.
*/
public static final StaticMDCBinder SINGLETON = new StaticMDCBinder();
private StaticMDCBinder() {
}
/**
* Currently this method always returns an instance of {@link StaticMDCBinder}.
* @return an MDC adapter
*/
public MDCAdapter getMDCA() {
return new Log4jMDCAdapter();
}
/**
* Retrieve the adapter class name.
* @return The adapter class name.
*/
public String getMDCAdapterClassStr() {
return Log4jMDCAdapter.class.getName();
}
}
MDCAdapter
是MDC的适配接口具体的日志组件实现该接口,MDC
中通过MDCAdapter
的实例进行操作Log4j2的动态绑定实现返回了Log4jMDCAdapter
实例
Log4jMDCAdapter
public class Log4jMDCAdapter implements MDCAdapter {
@Override
public void put(final String key, final String val) {
ThreadContext.put(key, val);
}
@Override
public String get(final String key) {
return ThreadContext.get(key);
}
@Override
public void remove(final String key) {
ThreadContext.remove(key);
}
@Override
public void clear() {
ThreadContext.clearMap();
}
@Override
public Map<String, String> getCopyOfContextMap() {
return ThreadContext.getContext();
}
@Override
@SuppressWarnings("unchecked") // nothing we can do about this, restricted by SLF4J API
public void setContextMap(@SuppressWarnings("rawtypes") final Map map) {
ThreadContext.clearMap();
for (final Map.Entry<String, String> entry : ((Map<String, String>) map).entrySet()) {
ThreadContext.put(entry.getKey(), entry.getValue());
}
}
}
MDCAdapter
使用适配器模式对具体日志组件中的ThreadLocal进行操作 ,log4j2中通过org.apache.logging.log4j.ThreadContext
封装了对ThreadLocal存储元数据和操作。
MDC的使用
拦截器配置
public class TraceMdcFilter implements Filter {
public static final String TRACE_NAME = "traceId";
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
//对每个Request生成一个UUID并绑定到MDC中
TraceUtil.setRequestId();
MDC.put(TRACE_NAME, TraceUtil.getRequestId());
chain.doFilter(request, response);
//将TraceId放到response的Header中
setResponseHeader(response, TraceUtil.getRequestId());
//清空MDC防止内存泄露
MDC.clear();
TraceUtil.remove();
}
@Override
public void destroy() {
}
private void setResponseHeader(ServletResponse response, String traceId) {
((HttpServletResponse) response).setHeader(RequestUtil.REQUEST_ID, traceId);
}
}
日志格式配置
通过%X{traceId}
输出TraceId
<Console name="ConsoleLogger" target="SYSTEM_OUT">
<PatternLayout pattern="[%d{yy-MM-dd HH:mm:ss:SSS}][traceId:%X{traceId}][%level][%logger{1.}:%L] %m%n"/>
</Console>
这样每次请求就很方便在日志中区分出来了