slf4j MDC是個好東西
阿新 • • 發佈:2020-12-20
slf4j MDC是個好東西
簡介
MDC 全拼 Mapped Diagnostic Contexts,是SLF4J類日誌系統中實現分散式多執行緒日誌資料傳遞的重要工具。
同時,使用者也可利用MDC將一些執行時的上下文資料打印出來。
什麼意思呢?
常規情況下,寫打日誌的程式碼時,一般都是log.info、log.warn、log.error將想要打的日誌進行拼裝和格式化,打到日誌輸出中。MDC能幹什麼呢?能在不改動log.xxx打日誌程式碼的情況下,在最終的日誌輸出的指定位置列印額外的資訊。而這,就是靠MDC進行傳遞實現的。
應用場景
在日誌中自動列印框架/元件方面的資訊
例如:
- 全鏈路日誌traceId
- 使用者請求的IP地址、user-agent
程式碼示例
一般配合AOP / Filter / Interceptor使用
@Around(value = "execution(* com.xx.xx.facade.impl.*.*(..))", argNames="pjp")
public Object validator(ProceedingJoinPoint pjp) throws Throwable {
try {
String traceId = TraceUtils. begin();
MDC.put("mdc_trace_id", traceId);
Object obj = pjp.proceed(args);
return obj;
} catch(Throwable e) {
//TODO 處理錯誤
} finally {
TraceUtils.endTrace();
}
}
程式碼通過AOP記錄了每次請求的traceId,並使用變數"mdc_trace_id"記錄到MDC內。
在日誌配置檔案裡需要設定變數才能將"mdc_trace_id"輸出到日誌檔案中。以logback配置檔案為例,看日誌第10行%X{mdc_trace_id}:
<appender name="ALL" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${CATALINA_BASE}/logs/all.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- daily rollover -->
<fileNamePattern>${CATALINA_BASE}/logs/all.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- keep 30 days' worth of history -->
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder charset="UTF-8">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - traceId:[%X{mdc_trace_id}] - %msg%n</pattern>
</encoder>
</appender>
可見的優勢
1、如果你的系統早已上線,突然有一天老闆說我們增加一些使用者資料到日誌裡分析一下。如果沒有MDC,你不得不在N個工程裡翻天覆地的“傳引數+改打日誌的程式碼”,你肯定很崩潰,不懂技術的老闆也很無奈(就多加幾點資訊,這麼大動靜嗎?)。而MDC能讓你很從容的完成此事。
- 筆者團隊就有這樣的情況,但提出在日誌里加內容的是我們自已的優化想法:將pinpoint的鏈路標識打到應用日誌裡去。
2、使程式碼簡潔、日誌風格統一、變更靈活。
對MDC原始碼的窺探
MDC所在的jar包
此處以 Logback中的實現為例。為了方便講解,我們只分析MDC的put()方法:
public class MDC {
public static void put(String key, String val)
throws IllegalArgumentException {
if (key == null) {
throw new IllegalArgumentException("key parameter cannot be null");
}
if (mdcAdapter == null) {
throw new IllegalStateException("MDCAdapter cannot be null. See also "
+ NULL_MDCA_URL);
}
mdcAdapter.put(key, val);
}
MDC的put()方法利用MDCAdapter實現。
Logback中的具體實現
既然一般都是結合AOP使用MDC,那麼還是要考慮內部方法實現時的支撐情況,例如:多執行緒
下面看一下Logback中MDCAdapter的實現LogbackMDCAdapter:
public final class LogbackMDCAdapter implements MDCAdapter {
final InheritableThreadLocal<Map<String, String>> copyOnInheritThreadLocal = new InheritableThreadLocal();
public void put(String key, String val) throws IllegalArgumentException {
if (key == null) {
throw new IllegalArgumentException("key cannot be null");
} else {
Map<String, String> oldMap = (Map)this.copyOnInheritThreadLocal.get();
Integer lastOp = this.getAndSetLastOperation(1);
if (!this.wasLastOpReadOrNull(lastOp) && oldMap != null) {
oldMap.put(key, val);
} else {
Map<String, String> newMap = this.duplicateAndInsertNewMap(oldMap);
newMap.put(key, val);
}
}
}
}
- MDC只有一種用法:
MDC.put(X,Y)
。那麼,在MDC的具體實現包中,肯定會有個Map作為儲存容器。如上,LogbackMDCAdapter中也有Map<String, String>。 - MDC內的key-value要能在呼叫鏈路中都能列印,那麼Map肯定是儲存在ThreadLocal中傳遞。
- Map<String, String>儲存在InheritableThreadLocal中,即AOP內真正的業務方法內部若進行了子執行緒的建立,MDC內的key-value也能正常的列印到日誌中。但,內部若是執行緒池的方式執行細分業務,則執行緒池任務內列印的日誌則不會有此內容(執行緒池的ThreadLocal傳遞需要用TransmittableThreadLocal)。比較遺憾,logback沒有預留這一點的SPI。