1. 程式人生 > 其它 >AOP/Filter+MDC實現traceId日誌追蹤

AOP/Filter+MDC實現traceId日誌追蹤

技術標籤:系統架構日誌traceId日誌追蹤

AOP/Filter+MDC實現traceId日誌追蹤

在應用日誌查詢時,我們常常希望可以有個關鍵字可以查詢某個業務的整生命週期,log4j 和 logback提供了MDC(Mapped Diagnostic Context,對映除錯上下文)功能,可以在多執行緒條件下記錄日誌。

在微服務、分散式中更是希望可以進行鏈路追蹤。


一、AOP+MDC簡單實現


/**
 * ************************************************************
 * Copyright © 2021 cnzz Inc.All rights reserved.  *    **
 * ************************************************************
 *
 * @program: Unknown
 * @description: 業務日誌追蹤
 * @author: cnzz
 * @create: 2021-01-12 13:49
 **/
@Slf4j
@Component
@Aspect
public class TraceIdHandler {
    private static final String TRACE_ID = "traceId";


   /* 引數部分允許使用萬用字元:
            *  匹配任意字元,但只能匹配一個元素
            .. 匹配任意字元,可以匹配任意多個元素,表示類時,必須和*聯合使用
            +  必須跟在類名後面,如Horseman+,表示類本身和繼承或擴充套件指定類的所有類
  */
    @Before(value = "execution(* com.ytkj.feec..*(..))")
    public void excuteBefore() {
        if (StringUtils.isBlank(MDC.get(TRACE_ID))) {
            String traceId = UUID.randomUUID().toString().replace("-", "");
            MDC.put(TRACE_ID, traceId);
        }
    }
}

log日誌檔案xml配置 輸出格式Pattern 新增

%X{traceId}
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>

    <!-- 屬性檔案:在properties檔案中找到對應的配置項 -->
    <springProperty scope="context" name="logging.path" source="logging.path"/>

    <!--logger上下文名稱,區分不同應用程式-->
    <contextName>rcbcloud-settle</contextName>
    <property name="LOG_PATH" value="/data/logs/paycentre" />
    <property name="project_name" value="zuul" />
    <property name="LOG_HOME" value="${LOG_PATH}/%d{yyyyMMdd}/${project_name}/${project_name}"/>

    <appender name="consoleLog" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--格式化輸出(配色):%d表示日期,%thread表示執行緒名,%-5level:級別從左顯示5個字元寬度%msg:日誌訊息,%n是換行符-->
            <pattern>%yellow(%d{yyyy-MM-dd HH:mm:ss.SSS }) %magenta([%thread]) - %red([%-5level]) %cyan(%c [%L]) - %blue([%X{corrId}-%X{traceId}]) - %highlight(%msg) %n

            </pattern>
            <charset class="java.nio.charset.Charset">UTF-8</charset>
        </encoder>
    </appender>

    <!--根據日誌級別分離日誌,分別輸出到不同的檔案-->
    <appender name="fileInfoLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <filter class="ch.qos.logback.classic.filter.LevelFilter">

            <level>INFO</level>

          <!--  <level>ERROR</level>
            <onMatch>DENY</onMatch>
            <onMismatch>ACCEPT</onMismatch>-->
        </filter>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>
                %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
            </pattern>
            <charset>UTF-8</charset>
        </encoder>
        <!--滾動策略-->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--按時間儲存日誌 修改格式可以按小時、按天、月來儲存-->
            <fileNamePattern>${LOG_HOME}.info.%d{yyyy-MM-dd}.log</fileNamePattern>
            <!--儲存時長-天數  MaxHistory指的是檔案數量,超過MaxHistory數量才會刪除,只有當每天生成且只生成一個檔案時才表示保留天數-->
            <MaxHistory>90</MaxHistory>
            <cleanHistoryOnStart>true</cleanHistoryOnStart>
            <!--檔案大小-->
            <totalSizeCap>1GB</totalSizeCap>

        </rollingPolicy>
    </appender>

    <appender name="fileErrorLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>ERROR</level>
        </filter>
        <encoder>
            <pattern>
                %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
            </pattern>
        </encoder>
        <!--滾動策略-->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--路徑-->
            <fileNamePattern>${LOG_HOME}.error.%d{yyyy-MM-dd}.log</fileNamePattern>
            <MaxHistory>90</MaxHistory>
        </rollingPolicy>
    </appender>
    <root level="debug">
        <appender-ref ref="consoleLog"/>
        <appender-ref ref="fileInfoLog"/>
        <appender-ref ref="fileErrorLog"/>
    </root>
</configuration>

二、使用filter+MDC實現



/**
 * ************************************************************
 * Copyright © 2020 cnzz Inc.All rights reserved.  *    **
 * ************************************************************
 *
 * @program: demo
 * @description: api過濾器
 * @author: cnzz
 * @create: 2020-12-17 11:31
 * <p>
 * 對介面api進行簽名驗證
 **/
@Configuration
@Slf4j
public class ApiHeaderFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        long startTime=System.currentTimeMillis();
        HttpServletRequest request = (HttpServletRequest) servletRequest;

        //引數偷換,直留data
        //HttpServletRequest沒有提供相關的set方法來修改body,所以需要用修飾類
        servletRequest = new BodyRequestWrapper2((HttpServletRequest) request);
        filterChain.doFilter(servletRequest, servletResponse);

        //交易響應時長
        String servletPath = request.getServletPath();
        log.info("響應時長--time={}ms,servletPath={}",System.currentTimeMillis()-startTime,servletPath);
    }

    @Override
    public void destroy() {

    }

}



/**
 * ************************************************************
 * Copyright © 2020 cnzz Inc.All rights reserved.  *    **
 * ************************************************************
 *
 * @program: demo
 * @description: 重新請求物件
 * @author: cnzz
 * @create: 2020-12-17 14:03
 * <p>
 * 整理請求引數
 **/
@Slf4j
public class BodyRequestWrapper2 extends HttpServletRequestWrapper {
    private byte[] body;
    private static final String TRACE_ID = "traceId";
    public BodyRequestWrapper2(HttpServletRequest request){
        super(request);

//        StreamUtil.readBytes(request.getReader(), "utf-8");
//        //由於request並沒有提供現成的獲取json字串的方法,所以我們需要將body中的流轉為字串
//        String json = new String(StreamUtil.readBytes(request.getReader(), "utf-8"));
        String data = HttpUtil.getDataFromRequest2(request);
        body = data.getBytes();

        //1、獲取請求頭
        SysHeader header = getSysHeader(request, data);

        //2、MDC + trace_id新增
        String traceId = StringUtils.isNotBlank(header.getCorrId()) ? header.getCorrId() : UUID.randomUUID().toString().replace("-", "");
        MDC.put(TRACE_ID, traceId);

        //3、servletPath+userInfo+ipAddr+userAgent
        String ipAddr = IpUtils.getIpAddr(request);
        String servletPath = request.getServletPath();
        String userAgent = request.getHeader("User-Agent");
        log.info("【請求filter】servletPath={},ipAddr={},userAgent={}", servletPath, ipAddr,userAgent);
        if (!ApiRouter.isApiRouter(servletPath)) {

            log.info("【需要驗籤】{}", servletPath);
            //校驗
            ValidSysReqUtil2.validSysHeader(header);
        }

    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }

    /**
     * 在使用@RequestBody註解的時候,其實框架是呼叫了getInputStream()方法,所以我們要重寫這個方法
     *
     * @return
     * @throws IOException
     */
    @Override
    public ServletInputStream getInputStream() throws IOException {
        final ByteArrayInputStream bais = new ByteArrayInputStream(body);

        return new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener readListener) {

            }

            @Override
            public int read() throws IOException {
                return bais.read();
            }
        };
    }


    private SysHeader getSysHeader(HttpServletRequest request, String data) {
        return new SysHeader()
                .setTimestamp(request.getHeader("timestamp"))
                .setSign(request.getHeader("sign"))
                .setSignType(request.getHeader("signType"))
                .setCorrId(request.getHeader("corrId"))
                .setData(data);
    }

}

效果展示