Spring Boot 自定義logback參數之MDC

什麼是logback

logback 繼承自 log4j,它建立在有十年工業經驗的日誌系統之上。它比其它所有的日誌系統更快並且更小,包含了許多獨特並且有用的特性。

什麼是MDC

(1)概念

logback 設計的目標之一是審計與調試複雜的分佈式應用。大部分的分佈式系統需要同時處理多個客戶端。在一個系統典型的多線程實現中,不同的線程處理不同的客戶端。一種可能但是不建議的方式是在每個客戶端實例化一個新的且獨立的 logger,來區分一個客戶端與另一個客戶端的日誌輸出。這種方式會導致 logger 急劇增加並且會增加維護成本。

一種輕量級的技術是給每個爲客戶端服務的 logger 打一個標記。Neil Harrison 在 Patterns for Logging Diagnostic Messages in Pattern Languages of Program Design 3, edited by R. Martin, D. Riehle, and F. Buschmann (Addison-Wesley, 1997) 這本書中描述了這種方法。logback 在 SLF4J API 利用了這種技術的變體:診斷上下文映射 (MDC)。

爲了給每個請求打上唯一的標記,用戶需要將上下文信息放到 MDC (Mapped Diagnostic Context 的縮寫) 中。下面列出了 MDC 類中主要的部分。完成的方法列表請查看 MDC javadocs

package org.slf4j;

public class MDC {
  // 將上下文的值作爲 MDC 的 key 放到線程上下的 map 中
  public static void put(String key, String val);

  // 通過 key 獲取上下文標識
  public static String get(String key);

  // 通過 key 移除上下文標識
  public static void remove(String key);

  // 清除 MDC 中所有的 entry
  public static void clear();
}

MDC 在客戶端服務器架構中最爲重要。通常,服務器上的多個線程爲多個客戶端提供服務。儘管 MDC 中的方法是靜態的,但是是以每個線程爲基礎來進行管理的,允許每個服務線程都打上一個 MDC 標記。MDC中的 put() 與 get() 操作僅僅只影響當前線程中的 MDC。其它線程中 MDC 不會受到影響。給定的 MDC 信息在每個線程的基礎上進行管理。每個線程都有一份 MDC 的拷貝。因此,在對 MDC 進行編程時,開發人員沒有必要去考慮線程安全或者同步問題。它自己會安全的處理這些問題。 

(2)線程安全原因

線程安全的原因,我繼續看MDC進行put的實現時,用的時ThreadLocal,上篇文章我們講過ThreadLocal,大家可以翻開看一下。源碼如下:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package ch.qos.logback.classic.util;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.slf4j.spi.MDCAdapter;

public class LogbackMDCAdapter implements MDCAdapter {
    final ThreadLocal<Map<String, String>> copyOnThreadLocal = new ThreadLocal();
    private static final int WRITE_OPERATION = 1;
    private static final int MAP_COPY_OPERATION = 2;
    final ThreadLocal<Integer> lastOperation = new ThreadLocal();

    public LogbackMDCAdapter() {
    }

    private Integer getAndSetLastOperation(int op) {
        Integer lastOp = (Integer)this.lastOperation.get();
        this.lastOperation.set(op);
        return lastOp;
    }

    private boolean wasLastOpReadOrNull(Integer lastOp) {
        return lastOp == null || lastOp.intValue() == 2;
    }

    private Map<String, String> duplicateAndInsertNewMap(Map<String, String> oldMap) {
        Map<String, String> newMap = Collections.synchronizedMap(new HashMap());
        if (oldMap != null) {
            synchronized(oldMap) {
                newMap.putAll(oldMap);
            }
        }

        this.copyOnThreadLocal.set(newMap);
        return newMap;
    }

    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.copyOnThreadLocal.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);
            }

        }
    }

    public void remove(String key) {
        if (key != null) {
            Map<String, String> oldMap = (Map)this.copyOnThreadLocal.get();
            if (oldMap != null) {
                Integer lastOp = this.getAndSetLastOperation(1);
                if (this.wasLastOpReadOrNull(lastOp)) {
                    Map<String, String> newMap = this.duplicateAndInsertNewMap(oldMap);
                    newMap.remove(key);
                } else {
                    oldMap.remove(key);
                }

            }
        }
    }

    public void clear() {
        this.lastOperation.set(Integer.valueOf(1));
        this.copyOnThreadLocal.remove();
    }

    public String get(String key) {
        Map<String, String> map = (Map)this.copyOnThreadLocal.get();
        return map != null && key != null ? (String)map.get(key) : null;
    }

    public Map<String, String> getPropertyMap() {
        this.lastOperation.set(Integer.valueOf(2));
        return (Map)this.copyOnThreadLocal.get();
    }

    public Set<String> getKeys() {
        Map<String, String> map = this.getPropertyMap();
        return map != null ? map.keySet() : null;
    }

    public Map<String, String> getCopyOfContextMap() {
        Map<String, String> hashMap = (Map)this.copyOnThreadLocal.get();
        return hashMap == null ? null : new HashMap(hashMap);
    }

    public void setContextMap(Map<String, String> contextMap) {
        this.lastOperation.set(Integer.valueOf(1));
        Map<String, String> newMap = Collections.synchronizedMap(new HashMap());
        newMap.putAll(contextMap);
        this.copyOnThreadLocal.set(newMap);
    }
}

 

MDC使用

(1)使用場景

 在項目開發過程中,我們需要將一個請求打印出一串唯一標識(TraceId ),方便日誌追蹤(TraceId也是鏈路追蹤的基礎),方便我們日誌查詢。我們就需要將生成的TraceId放到MDC裏,然後打印出來。這樣我們通過TraceId就可以過濾出從請求進入系統到響應全過程的日誌,處理線上問題非常方便。

(2)使用

  • 配置攔截器
package com.vipcode.config.aspect;

import com.vipcode.common.utils.RandomUtil;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;

import java.io.UnsupportedEncodingException;

@Aspect
@Component
public class ReqIdLogAspect {
    private static final String REQID_KEY = "reqId";

    @Pointcut("execution(public * com.aldeo.modules.*.mq..*.*(..))|| execution(public * com.aldeo.modules.*.controller..*.*(..))")
    public void controllerCall() {
    }

    @Before("controllerCall()")
    public void logInfoBefore(JoinPoint jp) throws UnsupportedEncodingException {
        MDC.put(REQID_KEY, RandomUtil.MixString(10));
    }

    @AfterReturning(returning = "req", pointcut = "controllerCall()")
    public void logInfoAfter(JoinPoint jp, Object req) throws Exception {
        MDC.remove(REQID_KEY);
    }
}

AOP或Filter攔截需要加入TraceId的入口,這裏以AOP爲例,下面攔截的是controller和mq的日誌。

TraceId生成規則可以參考:https://tech.antfin.com/docs/2/46947

  • 日誌輸出配置

logbak-spring.xml文件輸出格式中加上:%X{reqId} 即可

  • 輸出結果

彩蛋時間

今年是不是壓力很大,和去年對比,發現自己的髮際線又上移了

壓力好大(海綿寶寶派大星表情包)_大星_好大_海綿_寶寶表情程序,搞起來很輕鬆的,就是頭冷(脫髮) - 程序員表情包系列_程序員_碼農表情

馬上聖誕了,你想實現什麼願望呢

聖誕老人送禮物 我不想努力了 - 近期鬥圖表情包精選-2019/12/19_鬥圖表情

 

發佈了240 篇原創文章 · 獲贊 370 · 訪問量 37萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章