log4j MDC實現日誌追蹤

houshiqun689發表於2018-04-08

介紹:
MDC 中包含的可以被同一執行緒中執行的程式碼所訪問內容。當前執行緒的子執行緒會繼承其父執行緒中的 MDC 的內容。記錄日誌時,只需要從 MDC 中獲取所需的資訊即可。
作用:
使用MDC來記錄日誌,可以規範多開發下日誌格式。

一:新建執行緒處理類 ThreadContext

import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

/**
 * 執行緒上下文
 *
 * @date 2017年3月1日
 * @since 1.0.0
 */
public class ThreadContext {
    /**
     * 執行緒上下文變數的持有者
     */
    private final static ThreadLocal<Map<String, Object>> CTX_HOLDER = new ThreadLocal<Map<String, Object>>();

    static {
        CTX_HOLDER.set(new HashMap<String, Object>());
    }

 
    /**
     * traceID
     */
    private final static String TRACE_ID_KEY = "traceId";

    /**
     * 會話ID
     */
    private final static String SESSION_KEY = "token";

    /**
     * 操作使用者ID
     */
    private final static String VISITOR_ID_KEY = "userId";
    /**
     * 操作使用者名稱
     */
    private final static String VISITOR_NAME_KEY = "userName";

    /**
     * 客戶端IP
     */
    private static final String CLIENT_IP_KEY = "clientIp";

    /**
     * 新增內容到執行緒上下文中
     *
     * @param key
     * @param value
     */
    public final static void putContext(String key, Object value) {
        Map<String, Object> ctx = CTX_HOLDER.get();
        if (ctx == null) {
            return;
        }
        ctx.put(key, value);
    }

    /**
     * 從執行緒上下文中獲取內容
     *
     * @param key
     */
    @SuppressWarnings("unchecked")
    public final static <T extends Object> T getContext(String key) {
        Map<String, Object> ctx = CTX_HOLDER.get();
        if (ctx == null) {
            return null;
        }
        return (T) ctx.get(key);
    }

    /**
     * 獲取執行緒上下文
     */
    public final static Map<String, Object> getContext() {
        Map<String, Object> ctx = CTX_HOLDER.get();
        if (ctx == null) {
            return null;
        }
        return ctx;
    }

    /**
     * 刪除上下文中的key
     *
     * @param key
     */
    public final static void remove(String key) {
        Map<String, Object> ctx = CTX_HOLDER.get();
        if (ctx != null) {
            ctx.remove(key);
        }
    }

    /**
     * 上下文中是否包含此key
     *
     * @param key
     * @return
     */
    public final static boolean contains(String key) {
        Map<String, Object> ctx = CTX_HOLDER.get();
        if (ctx != null) {
            return ctx.containsKey(key);
        }
        return false;
    }

    /**
     * 清空執行緒上下文
     */
    public final static void clean() {
        CTX_HOLDER.remove();
    }

    /**
     * 初始化執行緒上下文
     */
    public final static void init() {
        CTX_HOLDER.set(new HashMap<String, Object>());
    }


    /**
     * 設定traceID資料
     */
    public final static void putTraceId(String traceId) {
        putContext(TRACE_ID_KEY, traceId);
    }

    /**
     * 獲取traceID資料
     */
    public final static String getTraceId() {
        return getContext(TRACE_ID_KEY);
    }

    /**
     * 設定會話的使用者ID
     */
    public final static void putUserId(Integer userId) {
        putContext(VISITOR_ID_KEY, userId);
    }

    /**
     * 設定會話的使用者ID
     */
    public final static int getUserId() {
        Integer val = getContext(VISITOR_ID_KEY);
        return val == null ? 0 : val;
    }

    /**
     * 設定會話的使用者名稱
     */
    public final static void putUserName(String userName) {
        putContext(VISITOR_NAME_KEY, userName);
    }

    /**
     * 獲取會話的使用者名稱稱
     */
    public final static String getUserName() {
        return Optional.ofNullable(getContext(VISITOR_NAME_KEY))
                .map(name -> String.valueOf(name))
                .orElse("");
    }

    /**
     * 取出IP
     *
     * @return
     */
    public static final String getClientIp() {
        return getContext(CLIENT_IP_KEY);
    }

    /**
     * 設定IP
     *
     * @param ip
     */
    public static final void putClientIp(String ip) {
        putContext(CLIENT_IP_KEY, ip);
    }

    /**
     * 設定會話ID
     *
     * @param token
     */
    public static void putSessionId(String token) {
        putContext(SESSION_KEY, token);
    }

    /**
     * 獲取會話ID
     *
     * @param token
     */
    public static String getSessionId(String token) {
        return getContext(SESSION_KEY);
    }

    /**
     * 清空會話資料
     */
    public final static void removeSessionId() {
        remove(SESSION_KEY);
    }
}

二:新增工具類TraceUtil

import java.util.UUID;
import org.slf4j.MDC;
import ThreadContext;

/**
 * trace工具
 *
 * @date 2017年3月10日
 * @since 1.0.0
 */
public class TraceUtil {

    public static void traceStart() {
        ThreadContext.init();
        String traceId = generateTraceId();
        MDC.put(`traceId`, traceId);
        ThreadContext.putTraceId(traceId);
    }

    public static void traceEnd() {
        MDC.clear();
        ThreadContext.clean();
    }

    /**
     * 生成跟蹤ID
     *
     * @return
     */
    private static String generateTraceId() {
        return UUID.randomUUID().toString();
    }
}

三:新增ContextFilter,對於每個請求隨機生成RequestID並放入MDC

import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.web.filter.OncePerRequestFilter;

import com.pingan.manpan.common.util.TraceUtil;
import com.pingan.manpan.user.dto.ThreadContext;
import com.pingan.manpan.web.common.surpport.IpUtils;

/**
 * 上下文Filter
 *
 * @date 2017/3/10
 * @since 1.0.0
 */
//@Order 標記元件的載入順序
@Order(Ordered.HIGHEST_PRECEDENCE)
public class ContextFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        try {
            ThreadContext.putClientIp(IpUtils.getClientIp(request));
            TraceUtil.traceStart();

            filterChain.doFilter(request, response);
        } finally {
            TraceUtil.traceEnd();
        }
    }
}

四:在webConfiguriation註冊filter

    /**
     * 請求上下文,應該在最外層
     *
     * @return
     */
    @Bean
    public FilterRegistrationBean requestContextRepositoryFilterRegistrationBean() {
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        filterRegistrationBean.setFilter(new ContextFilter());
        filterRegistrationBean.addUrlPatterns("/*");
        return filterRegistrationBean;
    }

五:修改log4j日誌配置檔案,設定日誌traceId


<?xml version="1.0" encoding="UTF-8"?>

<configuration>

    <jmxConfigurator/>

    <property name="LOG_LEVEL_PATTERN" value="%X{traceId:-} %5p"/>
    <property name="LOG_FILE" value="${LOG_PATH}/web.logx"/>
    <property name="LOG_FILE_SUFFIX" value=".logx"/>

    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    <include resource="org/springframework/boot/logging/logback/console-appender.xml"/>

    <appender name="FILE"
              class="ch.qos.logback.core.rolling.RollingFileAppender">
        <encoder>
            <pattern>${FILE_LOG_PATTERN}</pattern>
        </encoder>
        <file>${LOG_FILE}${LOG_FILE_SUFFIX}</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_FILE}.%d{yyyy-MM-dd}${LOG_FILE_SUFFIX}</fileNamePattern>
        </rollingPolicy>
    </appender>

    <appender name="SYSLOG" class="ch.qos.logback.classic.net.SyslogAppender">
        <syslogHost>127.0.0.1</syslogHost>
        <facility>local6</facility>
        <port>514</port>
        <suffixPattern>${FILE_LOG_PATTERN}</suffixPattern>
    </appender>
    
    <logger name="druid.sql" level="DEBUG" />

    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="FILE"/>
        <appender-ref ref="SYSLOG"/>
    </root>

</configuration>

相關文章