AOP/Filter+MDC實現traceId日誌追蹤
阿新 • • 發佈:2021-01-25
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);
}
}
效果展示