java 如何實現(xiàn)日志追蹤MDC
java 日志追蹤MDC
MDC ( Mapped Diagnostic Contexts ) 有了日志之后,我們就可以追蹤各種線上問題。
但是,在分布式系統(tǒng)中,各種無關(guān)日志穿行其中,導致我們可能無法直接定位整個操作流程。
因此,我們可能需要對一個用戶的操作流程進行歸類標記,比如使用線程+時間戳,或者用戶身份標識等;如此,我們可以從大量日志信息中g(shù)rep出某個用戶的操作流程,或者某個時間的流轉(zhuǎn)記錄。其目的是為了便于我們診斷線上問題而出現(xiàn)的方法工具類。
雖然,Slf4j 是用來適配其他的日志具體實現(xiàn)包的,但是針對 MDC功能,目前只有l(wèi)ogback 以及 log4j 支持。 MDC
package org.slf4j;
import java.io.Closeable;
import java.util.Map;
import org.slf4j.helpers.NOPMDCAdapter;
import org.slf4j.helpers.BasicMDCAdapter;
import org.slf4j.helpers.Util;
import org.slf4j.impl.StaticMDCBinder;
import org.slf4j.spi.MDCAdapter;
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;
public static class MDCCloseable implements Closeable {
private final String key;
private MDCCloseable(String key) {
this.key = key;
}
public void close() {
MDC.remove(this.key);
}
}
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);
}
}
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);
}
public static MDCCloseable putCloseable(String key, String val) throws IllegalArgumentException {
put(key, val);
return new MDCCloseable(key);
}
public static String get(String key) 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);
}
return mdcAdapter.get(key);
}
public static void remove(String key) 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.remove(key);
}
public static void clear() {
if (mdcAdapter == null) {
throw new IllegalStateException("MDCAdapter cannot be null. See also " + NULL_MDCA_URL);
}
mdcAdapter.clear();
}
public static Map<String, String> getCopyOfContextMap() {
if (mdcAdapter == null) {
throw new IllegalStateException("MDCAdapter cannot be null. See also " + NULL_MDCA_URL);
}
return mdcAdapter.getCopyOfContextMap();
}
public static void setContextMap(Map<String, String> contextMap) {
if (mdcAdapter == null) {
throw new IllegalStateException("MDCAdapter cannot be null. See also " + NULL_MDCA_URL);
}
mdcAdapter.setContextMap(contextMap);
}
public static MDCAdapter getMDCAdapter() {
return mdcAdapter;
}
}
簡單的demo
package com.alibaba.otter.canal.common;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
public class LogTest {
private static final Logger logger = LoggerFactory.getLogger(LogTest.class);
public static void main(String[] args) {
MDC.put("THREAD_ID", String.valueOf(Thread.currentThread().getId()));
logger.info("純字符串信息的info級別日志");
}
}
logback.xml 配置
<configuration scan="true" scanPeriod=" 5 seconds">
<jmxConfigurator />
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{56} %X{THREAD_ID} - %msg%n
</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
</configuration
對應的輸出日志 可以看到輸出了THREAD_ID
2016-12-08 14:59:32.855 [main] INFO com.alibaba.otter.canal.common.LogTest THREAD_ID 1 - 純字符串信息的info級別日志
slf4j只是起到適配的作用 故查看實現(xiàn)類LogbackMDCAdapter屬性
final InheritableThreadLocal<Map<String, String>> copyOnInheritThreadLocal = new InheritableThreadLocal<Map<String, String>>();
InheritableThreadLocal 該類擴展了 ThreadLocal,為子線程提供從父線程那里繼承的值:在創(chuàng)建子線程時,子線程會接收所有
可繼承的線程局部變量的初始值,以獲得父線程所具有的值。通常,子線程的值與父線程的值是一致的;但是,通過重寫這個類中的 childValue 方法,子線程的值可以作為父線程值的一個任意函數(shù)。
當必須將變量(如用戶 ID 和 事務 ID)中維護的每線程屬性(per-thread-attribute)自動傳送給創(chuàng)建的所有子線程時,應盡可能地采用可繼承的線程局部變量,而不是采用普通的線程局部變量
驗證一下
package com.alibaba.otter.canal.parse.driver.mysql;
import org.junit.Test;
public class TestInheritableThreadLocal {
@Test
public void testThreadLocal() {
final ThreadLocal<String> local = new ThreadLocal<String>();
work(local);
}
@Test
public void testInheritableThreadLocal() {
final ThreadLocal<String> local = new InheritableThreadLocal<String>();
work(local);
}
private void work(final ThreadLocal<String> local) {
local.set("a");
System.out.println(Thread.currentThread() + "," + local.get());
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread() + "," + local.get());
local.set("b");
System.out.println(Thread.currentThread() + "," + local.get());
}
});
t.start();
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "," + local.get());
}
}
分別運行得到的輸出結(jié)果
ThreadLocal 存貯輸出結(jié)果
Thread[main,5,main],a
Thread[Thread-0,5,main],null
Thread[Thread-0,5,main],b
Thread[main,5,main],a
InheritableThreadLocal存貯輸出結(jié)果
Thread[main,5,main],a
Thread[Thread-0,5,main],a
Thread[Thread-0,5,main],b
Thread[main,5,main],a
輸出結(jié)果說明一切 對于參數(shù)傳遞十分有用 我知道 canal的源碼中用到了MDC
在 CanalServerWithEmbedded 中的 start 和stop等方法中都有用到
public void start(final String destination) {
final CanalInstance canalInstance = canalInstances.get(destination);
if (!canalInstance.isStart()) {
try {
MDC.put("destination", destination);
canalInstance.start();
logger.info("start CanalInstances[{}] successfully", destination);
} finally {
MDC.remove("destination");
}
}
}
public void stop(String destination) {
CanalInstance canalInstance = canalInstances.remove(destination);
if (canalInstance != null) {
if (canalInstance.isStart()) {
try {
MDC.put("destination", destination);
canalInstance.stop();
logger.info("stop CanalInstances[{}] successfully", destination);
} finally {
MDC.remove("destination");
}
}
}
}
MDC的介紹及使用
1、MDC是什么?
MDC是(Mapped Diagnostic Context,映射調(diào)試上下文)是 log4j 和 logback 支持的一種方便在多線程條件下記錄追蹤日志的功能。通常打印出的日志會有線程號等信息來標志當前日志屬于哪個線程,然而由于線程是可以重復使用的,所以并不能很清晰的確認一個請求的日志范圍。處理這種情況一般有兩種處理方式:
1)手動生成一個唯一序列號打印在日志中;
2)使用日志控件提供的MDC功能,生成一個唯一序列標記一個線程的日志;
兩種方法的區(qū)別在于:
方法一只能標記一條日志,線程內(nèi)其他日志需要人肉去篩選;
方法二標記整個線程的所有日志,方便grep命令查詢;
對比可見,使用MDC功能更好。
2、MDC的原理
MDC 可以看成是一個與當前線程綁定的哈希表,可以往其中添加鍵值對。MDC 中包含的內(nèi)容可以被同一線程中執(zhí)行的代碼所訪問。當前線程的子線程會繼承其父線程中的 MDC 的內(nèi)容。當需要記錄日志時,只需要從 MDC 中獲取所需的信息即可。MDC 的內(nèi)容則由程序在適當?shù)臅r候保存進去。對于一個 Web 應用來說,通常是在請求被處理的最開始保存這些數(shù)據(jù)。
@RunWith(SpringRunner.class)
@SpringBootTest(classes=CreditAppApplication.class)
publicclassMDCTest{
@Test
publicvoidmdcTest1(){
MDC.put("first","thefirst1");
Loggerlogger=LoggerFactory.getLogger(MDCTest.class);
MDC.put("last","thelast1");
logger.info("checkenclosed.");
logger.debug("themostbeautifultwowordsinenglish.");
MDC.put("first","thefirst2");
MDC.put("last","thelast2");
logger.info("iamnotacrook.");
logger.info("AttributedtotheformerUSpresident.17Nov1973.");
}
}
logback的配置:

3、MDC的使用
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
publicclassGlobalLogTagConfigextendsOncePerRequestFilter{
privatestaticfinalStringGLOBAL_LOG_TAG="GLOG_TAG";
privatestaticStringgenerateSeqNo(){
returnUUID.randomUUID().toString().replace("-","").substring(0,12);
}
@Override
protectedvoiddoFilterInternal(HttpServletRequesthttpServletRequest,HttpServletResponsehttpServletResponse,FilterChainfilterChain)throwsServletException,IOException{
try{
StringseqNo;
if(httpServletRequest!=null){
seqNo=httpServletRequest.getHeader(GLOBAL_LOG_TAG);
if(StringUtils.isEmpty(seqNo)){
seqNo=generateSeqNo();
}
}else{
seqNo=generateSeqNo();
}
MDC.put(GLOBAL_LOG_TAG,seqNo);
filterChain.doFilter(httpServletRequest,httpServletResponse);
}finally{
MDC.remove(GLOBAL_LOG_TAG);
}
}
}
注意:
OncePerRequestFilter的作用是為了讓每個請求只經(jīng)過這個過濾器一次(因為web container的不同,有些過濾器可能被多次執(zhí)行)
logback配置:

以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Spring MVC Mybatis多數(shù)據(jù)源的使用實例解析
項目需要從其他網(wǎng)站獲取數(shù)據(jù),因為是臨時加的需求,這篇文章主要介紹了Spring MVC Mybatis多數(shù)據(jù)源的使用實例解析,需要的朋友可以參考下2016-12-12
SpringBootAdmin+actuator實現(xiàn)服務監(jiān)控
這篇文章主要為大家詳細介紹了SpringBootAdmin+actuator實現(xiàn)服務監(jiān)控,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-01-01
Java新特性之Nashorn_動力節(jié)點Java學院整理
這篇文章主要介紹了Java新特性之Nashorn的相關(guān)資料,需要的朋友可以參考下2017-06-06
IntelliJ IDEA下載GitHub私有倉庫到本地的方法(新版)
這篇文章主要介紹了IntelliJ IDEA下載GitHub私有倉庫到本地(新版),本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-05-05
IDEA導入外部項目報Error:java: 無效的目標發(fā)行版: 11的解決方法
這篇文章主要介紹了IDEA導入外部項目報Error:java: 無效的目標發(fā)行版: 11,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-09-09

