Slf4j MDC實現(xiàn)原理分析

MDC ( Mapped Diagnostic Contexts ) 有了日志之后,我們就可以追蹤各種線上問題腌零。但是捍壤,在分布式系統(tǒng)中,各種無關(guān)日志穿行其中栈妆,導(dǎo)致我們可能無法直接定位整個操作流程胁编。因此,我們可能需要對一個用戶的操作流程進行歸類標(biāo)記鳞尔,比如使用線程+時間戳嬉橙,或者用戶身份標(biāo)識等;如此寥假,我們可以從大量日志信息中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

對應(yīng)的輸出日志 可以看到輸出了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ù)扫腺。

當(dāng)必須將變量(如用戶 ID 和 事務(wù) ID)中維護的每線程屬性(per-thread-attribute)自動傳送給創(chuàng)建的所有子線程時岗照,應(yī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");
                }
            }
        }
    }
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末笆环,一起剝皮案震驚了整個濱河市攒至,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌咧织,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,204評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件籍救,死亡現(xiàn)場離奇詭異习绢,居然都是意外死亡,警方通過查閱死者的電腦和手機蝙昙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評論 3 395
  • 文/潘曉璐 我一進店門闪萄,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人奇颠,你說我怎么就攤上這事败去。” “怎么了烈拒?”我有些...
    開封第一講書人閱讀 164,548評論 0 354
  • 文/不壞的土叔 我叫張陵圆裕,是天一觀的道長。 經(jīng)常有香客問我荆几,道長吓妆,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,657評論 1 293
  • 正文 為了忘掉前任吨铸,我火速辦了婚禮行拢,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘诞吱。我一直安慰自己舟奠,他們只是感情好竭缝,可當(dāng)我...
    茶點故事閱讀 67,689評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著沼瘫,像睡著了一般抬纸。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上晕鹊,一...
    開封第一講書人閱讀 51,554評論 1 305
  • 那天松却,我揣著相機與錄音,去河邊找鬼溅话。 笑死晓锻,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的飞几。 我是一名探鬼主播砚哆,決...
    沈念sama閱讀 40,302評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼屑墨!你這毒婦竟也來了躁锁?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,216評論 0 276
  • 序言:老撾萬榮一對情侶失蹤卵史,失蹤者是張志新(化名)和其女友劉穎战转,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體以躯,經(jīng)...
    沈念sama閱讀 45,661評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡槐秧,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,851評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了忧设。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片刁标。...
    茶點故事閱讀 39,977評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖址晕,靈堂內(nèi)的尸體忽然破棺而出膀懈,到底是詐尸還是另有隱情,我是刑警寧澤谨垃,帶...
    沈念sama閱讀 35,697評論 5 347
  • 正文 年R本政府宣布启搂,位于F島的核電站,受9級特大地震影響刘陶,放射性物質(zhì)發(fā)生泄漏狐血。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,306評論 3 330
  • 文/蒙蒙 一易核、第九天 我趴在偏房一處隱蔽的房頂上張望匈织。 院中可真熱鬧,春花似錦、人聲如沸缀匕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽乡小。三九已至阔加,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間满钟,已是汗流浹背胜榔。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留湃番,地道東北人夭织。 一個月前我還...
    沈念sama閱讀 48,138評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像吠撮,于是被迫代替她去往敵國和親尊惰。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,927評論 2 355

推薦閱讀更多精彩內(nèi)容