基于SLF4J MDC機(jī)制實(shí)現(xiàn)日志的鏈路追蹤

問題描述

最近經(jīng)常做線上問題的排查,而排查問題用得最多的方式是查看日志留荔,但是在現(xiàn)有系統(tǒng)中府树,各種無關(guān)日志穿行其中,導(dǎo)致我沒辦法快速的找出用戶在一次請求中所有的日志偎蘸。

問題分析

我們沒辦法快速定位用戶在一次請求中對應(yīng)的所有日志庄蹋,或者說是定位某個(gè)用戶操作的所有日志,那是因?yàn)槲覀冊谳敵龅娜罩镜臅r(shí)候沒把請求的唯一標(biāo)示或者說是用戶身份標(biāo)示輸出到我們的日志中迷雪,導(dǎo)致我們沒辦法根據(jù)一個(gè)請求或者用戶身份標(biāo)示來做日志的過濾限书。所以我們在記錄日志的是后把請求的唯一標(biāo)示(sessionId)或者身份標(biāo)示(userId) 記錄到日志中這個(gè)問題就可以得到很好的解決了。

解決方案

  1. 在每次請求的時(shí)候章咧,獲取到請求的sessionId(或者自己生成一個(gè)偽sessionId)倦西,并在每次輸出log的時(shí)候?qū)⑦@個(gè)sessionId輸出到日志中。這個(gè)方式實(shí)現(xiàn)簡單赁严,代碼侵入型強(qiáng)扰柠,每次輸出都會(huì)多輸出一個(gè)sessionId參數(shù),工作量大疼约,但是可控粒度高卤档。

  2. 我們使用Logback的MDC機(jī)制,日志模板中加入sessionId格式程剥。在日志輸出格式中指定輸出sessionId劝枣。如:

%d{yyyy-MM-dd HH:mm:ss.SSS}  [%X{sessionId}] -%5p ${PID:-} [%15.15t] %-40.40logger{39} : %m%n

這種方式工作量小,代碼侵入小,易擴(kuò)展哨免,但是可控粒度低茎活。

方案說明

第一種方案很簡單,也很容易實(shí)現(xiàn)琢唾,就是在輸出日志的時(shí)候多輸出一個(gè)參數(shù)载荔,如:

logger.info("sessionId: {}, message: {}", sessionId, "日志信息");

我們這里主要說一下第二種方式的實(shí)現(xiàn)。

實(shí)現(xiàn)思路采桃,這里以Spring MVC為例:

  1. 新建一個(gè)日志攔截器懒熙,在攔截所有請求,在處理請求前將sessionId放到MDC中普办,在處理完請求后清除MDC的內(nèi)容工扎。這里就解決了80%的問題。

  2. 在原來版本中新起線程時(shí)MDC會(huì)自動(dòng)將父線程的MDC內(nèi)容復(fù)制給子線程衔蹲,因?yàn)镸DC內(nèi)部使用的是InheritableThreadLocal肢娘,但是因?yàn)樾阅軉栴}在最新的版本中被取消了,所以子線程不會(huì)自動(dòng)獲取到父線程的MDC內(nèi)容舆驶。官方建議我們在父線程新建子線程之前調(diào)用MDC.getCopyOfContextMap()方法將MDC內(nèi)容取出來傳給子線程橱健,子線程在執(zhí)行操作前先調(diào)用MDC.setContextMap()方法將父線程的MDC內(nèi)容設(shè)置到子線程。

  3. 設(shè)置日志輸出格式

%d{yyyy-MM-dd HH:mm:ss.SSS}  [%X{sessionId}] -%5p ${PID:-} [%15.15t] %-40.40logger{39} : %m%n

攔截器 LogInterceptor

package com.xiaolyuh.interceptors;

import org.slf4j.MDC;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;

/**
 * 日志攔截器組件沙廉,在輸出日志中加上sessionId
 *
 * @author yuhao.wang3
 */
public class LogInterceptor extends HandlerInterceptorAdapter {
    /**
     * 會(huì)話ID
     */
    private final static String SESSION_KEY = "sessionId";

    @Override
    public void afterCompletion(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, Exception arg3)
            throws Exception {

        // 刪除SessionId
        MDC.remove(SESSION_KEY);
    }

    @Override
    public void postHandle(HttpServletRequest arg0, HttpServletResponse arg1,
                           Object arg2, ModelAndView arg3) throws Exception {
    }

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response, Object handler) throws Exception {

        // 設(shè)置SessionId
        String token = UUID.randomUUID().toString().replace("-", "");
        MDC.put(SESSION_KEY, token);
        return true;
    }
}

注冊攔截器

/**
 * WEB MVC配置類
 *
 * @author yuhao.wang3
 */
@Configuration
public class WebMvcConfigurer extends WebMvcConfigurerAdapter {

    /**
     * 把我們的攔截器注入為bean
     *
     * @return
     */
    @Bean
    public HandlerInterceptor logInterceptor() {
        return new LogInterceptor();
    }

    /**
     * 注冊攔截器
     *
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // addPathPatterns 用于添加攔截規(guī)則, 這里假設(shè)攔截 /url 后面的全部鏈接
        // excludePathPatterns 用戶排除攔截
        registry.addInterceptor(logInterceptor()).addPathPatterns("/**");
        super.addInterceptors(registry);
    }
}

擴(kuò)展ThreadPoolTaskExecutor線程池

擴(kuò)展ThreadPoolTaskExecutor線程池的主要目的是實(shí)現(xiàn)將父線程的MDC內(nèi)容復(fù)制給子線程拘荡。

package com.xiaolyuh.utils;

import org.slf4j.MDC;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.Map;

/**
 * 這是{@link ThreadPoolTaskExecutor}的一個(gè)簡單替換,可以在每個(gè)任務(wù)之前設(shè)置子線程的MDC數(shù)據(jù)撬陵。
 * <p/>
 * 在記錄日志的時(shí)候珊皿,一般情況下我們會(huì)使用MDC來存儲(chǔ)每個(gè)線程的特有參數(shù),如身份信息等巨税,以便更好的查詢?nèi)罩尽? * 但是Logback在最新的版本中因?yàn)樾阅軉栴}蟋定,不會(huì)自動(dòng)的將MDC的內(nèi)存?zhèn)鹘o子線程。所以Logback建議在執(zhí)行異步線程前
 * 先通過MDC.getCopyOfContextMap()方法將MDC內(nèi)存獲取出來垢夹,再傳給線程溢吻。
 * 并在子線程的執(zhí)行的最開始調(diào)用MDC.setContextMap(context)方法將父線程的MDC內(nèi)容傳給子線程。
 * <p>
 * https://logback.qos.ch/manual/mdc.html
 *
 * @author yuhao.wang3
 */
public class MdcThreadPoolTaskExecutor extends ThreadPoolTaskExecutor {

    /**
     * 所有線程都會(huì)委托給這個(gè)execute方法果元,在這個(gè)方法中我們把父線程的MDC內(nèi)容賦值給子線程
     * https://logback.qos.ch/manual/mdc.html#managedThreads
     *
     * @param runnable
     */
    @Override
    public void execute(Runnable runnable) {
        // 獲取父線程MDC中的內(nèi)容促王,必須在run方法之前,否則等異步線程執(zhí)行的時(shí)候有可能MDC里面的值已經(jīng)被清空了而晒,這個(gè)時(shí)候就會(huì)返回null
        Map<String, String> context = MDC.getCopyOfContextMap();
        super.execute(() -> run(runnable, context));
    }

    /**
     * 子線程委托的執(zhí)行方法
     *
     * @param runnable {@link Runnable}
     * @param context  父線程MDC內(nèi)容
     */
    private void run(Runnable runnable, Map<String, String> context) {
        // 將父線程的MDC內(nèi)容傳給子線程
        MDC.setContextMap(context);
        try {
            // 執(zhí)行異步操作
            runnable.run();
        } finally {
            // 清空MDC內(nèi)容
            MDC.clear();
        }
    }
}

擴(kuò)展Hystrix

擴(kuò)展Hystrix線程池隔離支持日志鏈路跟蹤

/**
 * Hystrix線程池隔離支持日志鏈路跟蹤
 *
 * @author yuhao.wang3
 */
public class MdcHystrixConcurrencyStrategy extends HystrixConcurrencyStrategy {

    @Override
    public <T> Callable<T> wrapCallable(Callable<T> callable) {
        return new MdcAwareCallable(callable, MDC.getCopyOfContextMap());
    }

    private class MdcAwareCallable<T> implements Callable<T> {

        private final Callable<T> delegate;

        private final Map<String, String> contextMap;

        public MdcAwareCallable(Callable<T> callable, Map<String, String> contextMap) {
            this.delegate = callable;
            this.contextMap = contextMap != null ? contextMap : new HashMap();
        }

        @Override
        public T call() throws Exception {
            try {
                MDC.setContextMap(contextMap);
                return delegate.call();
            } finally {
                MDC.clear();
            }
        }
    }
}

配置Hystrix

@Configuration
public class HystrixConfig {

    //用來攔截處理HystrixCommand注解
    @Bean
    public HystrixCommandAspect hystrixAspect() {
        return new HystrixCommandAspect();
    }

    @PostConstruct
    public void init() {
        HystrixPlugins.getInstance().registerConcurrencyStrategy(new MdcHystrixConcurrencyStrategy());
    }

}

Logback配置

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <property name="CONSOLE_LOG_PATTERN"
              value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} [%X{sessionId}] %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
    <property name="FILE_LOG_PATTERN"
              value="${FILE_LOG_PATTERN:-%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{sessionId}] ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%t] %-40.40logger{39} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
    <include resource="org/springframework/boot/logging/logback/base.xml"/>

    <appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
        <!-- 不丟失日志.默認(rèn)的,如果隊(duì)列的80%已滿,則會(huì)丟棄TRACT蝇狼、DEBUG、INFO級別的日志 -->
        <discardingThreshold>0</discardingThreshold>
        <!-- 更改默認(rèn)的隊(duì)列的深度,該值會(huì)影響性能.默認(rèn)值為256 -->
        <queueSize>2048</queueSize>
        <includeCallerData>true</includeCallerData>
        <!-- 添加附加的appender,最多只能添加一個(gè) -->
        <appender-ref ref="FILE"/>
    </appender>

    <logger name="com.xiaolyuh" level="debug" additivity="true"/>
</configuration>  

測試類

@Service
public class LogService {
    Logger logger = LoggerFactory.getLogger(LogService.class);

    public void log() {
        logger.debug("==============================================");
        ThreadTaskUtils.run(() -> run());
        FutureTask<String> futureTask = new FutureTask<String>(() -> call());
        ThreadTaskUtils.run(futureTask);
        try {
            logger.debug("===================: {}", futureTask.get());
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
        logger.debug("==============================================");
    }

    private String call() {
        logger.debug("11111111111");
        return "3333";
    }

    public void run() {
        logger.debug("222222222222222");
    }
}

日志輸出示例

2018-06-02 23:11:02.376 [a114db8f4be942d891407d57ff74276d] DEBUG 12828 --- [http-nio-80-exec-9] com.xiaolyuh.service.LogService          : ==============================================
2018-06-02 23:11:02.376 [a114db8f4be942d891407d57ff74276d] DEBUG 12828 --- [MdcThreadPoolTaskExecutor-4] com.xiaolyuh.service.LogService          : 222222222222222
2018-06-02 23:11:02.377 [a114db8f4be942d891407d57ff74276d] DEBUG 12828 --- [MdcThreadPoolTaskExecutor-1] com.xiaolyuh.service.LogService          : 11111111111
2018-06-02 23:11:02.377 [a114db8f4be942d891407d57ff74276d] DEBUG 12828 --- [http-nio-80-exec-9] com.xiaolyuh.service.LogService          : ===================: 3333
2018-06-02 23:11:02.377 [a114db8f4be942d891407d57ff74276d] DEBUG 12828 --- [http-nio-80-exec-9] com.xiaolyuh.service.LogService          : ==============================================
2018-06-02 23:11:02.536 [8657ed4f5267489aa323f9422974002b] DEBUG 12828 --- [http-nio-80-exec-2] com.xiaolyuh.service.LogService          : ==============================================
2018-06-02 23:11:02.536 [8657ed4f5267489aa323f9422974002b] DEBUG 12828 --- [MdcThreadPoolTaskExecutor-5] com.xiaolyuh.service.LogService          : 222222222222222
2018-06-02 23:11:02.536 [8657ed4f5267489aa323f9422974002b] DEBUG 12828 --- [MdcThreadPoolTaskExecutor-3] com.xiaolyuh.service.LogService          : 11111111111
2018-06-02 23:11:02.536 [8657ed4f5267489aa323f9422974002b] DEBUG 12828 --- [http-nio-80-exec-2] com.xiaolyuh.service.LogService          : ===================: 3333
2018-06-02 23:11:02.536 [8657ed4f5267489aa323f9422974002b] DEBUG 12828 --- [http-nio-80-exec-2] com.xiaolyuh.service.LogService          : ==============================================
2018-06-02 23:11:02.728 [e85380fb1554463ca156318b0a3ff7c2] DEBUG 12828 --- [http-nio-80-exec-3] com.xiaolyuh.service.LogService          : ==============================================
2018-06-02 23:11:02.728 [e85380fb1554463ca156318b0a3ff7c2] DEBUG 12828 --- [MdcThreadPoolTaskExecutor-2] com.xiaolyuh.service.LogService          : 222222222222222
2018-06-02 23:11:02.729 [e85380fb1554463ca156318b0a3ff7c2] DEBUG 12828 --- [MdcThreadPoolTaskExecutor-4] com.xiaolyuh.service.LogService          : 11111111111
2018-06-02 23:11:02.729 [e85380fb1554463ca156318b0a3ff7c2] DEBUG 12828 --- [http-nio-80-exec-3] com.xiaolyuh.service.LogService          : ===================: 3333
2018-06-02 23:11:02.729 [e85380fb1554463ca156318b0a3ff7c2] DEBUG 12828 --- [http-nio-80-exec-3] com.xiaolyuh.service.LogService          : ==============================================

源碼

https://github.com/wyh-spring-ecosystem-student/spring-boot-student/tree/releases

spring-boot-student-log 工程

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末倡怎,一起剝皮案震驚了整個(gè)濱河市迅耘,隨后出現(xiàn)的幾起案子贱枣,更是在濱河造成了極大的恐慌,老刑警劉巖颤专,帶你破解...
    沈念sama閱讀 206,602評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件纽哥,死亡現(xiàn)場離奇詭異,居然都是意外死亡栖秕,警方通過查閱死者的電腦和手機(jī)春塌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來簇捍,“玉大人只壳,你說我怎么就攤上這事∈钏埽” “怎么了吼句?”我有些...
    開封第一講書人閱讀 152,878評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長事格。 經(jīng)常有香客問我惕艳,道長,這世上最難降的妖魔是什么分蓖? 我笑而不...
    開封第一講書人閱讀 55,306評論 1 279
  • 正文 為了忘掉前任尔艇,我火速辦了婚禮尔许,結(jié)果婚禮上么鹤,老公的妹妹穿的比我還像新娘。我一直安慰自己味廊,他們只是感情好蒸甜,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,330評論 5 373
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著余佛,像睡著了一般柠新。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上辉巡,一...
    開封第一講書人閱讀 49,071評論 1 285
  • 那天恨憎,我揣著相機(jī)與錄音,去河邊找鬼郊楣。 笑死憔恳,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的净蚤。 我是一名探鬼主播钥组,決...
    沈念sama閱讀 38,382評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼今瀑!你這毒婦竟也來了程梦?” 一聲冷哼從身側(cè)響起点把,我...
    開封第一講書人閱讀 37,006評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎屿附,沒想到半個(gè)月后郎逃,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,512評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡挺份,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,965評論 2 325
  • 正文 我和宋清朗相戀三年衣厘,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片压恒。...
    茶點(diǎn)故事閱讀 38,094評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡影暴,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出探赫,到底是詐尸還是另有隱情型宙,我是刑警寧澤,帶...
    沈念sama閱讀 33,732評論 4 323
  • 正文 年R本政府宣布伦吠,位于F島的核電站妆兑,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏毛仪。R本人自食惡果不足惜搁嗓,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,283評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望箱靴。 院中可真熱鬧腺逛,春花似錦、人聲如沸衡怀。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽抛杨。三九已至够委,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間怖现,已是汗流浹背茁帽。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留屈嗤,地道東北人潘拨。 一個(gè)月前我還...
    沈念sama閱讀 45,536評論 2 354
  • 正文 我出身青樓瞧毙,卻偏偏與公主長得像寡具,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子版姑,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,828評論 2 345

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