Java項目分布式/集群部署,接口請求日志流程跟蹤
由於公司的項目使用了SpringCloud微服務,並且各個模塊都是集群部署,日志分布在不同的服務器,當線上請求出現問題,日志查詢和跟蹤是一個很麻煩的事情,大量日志輸出導致很難篩出指定請求的全部相關日志,以及下游服務調用對應的日志。
解決思路
網關服務接收到請求,然后給每個請求分配一個唯一的請求ID(traceId),再將traceId放到請求頭當中,這樣該請求再調用下游服務時,下游服務便可以從請求頭中獲取到這個traceId,微服務之間就可以銜接起來。
日志打印
String traceId = request.getHeader("traceId");//從Header中獲取traceId
log.info("接收到請求,traceId:{}",traceId);//打印日志
使用上面的方式打印日志,雖然可以達到目的,但是這意味着打印每一條日志,都需要獲取traceId,而同一個請求的traceId是不變的,這樣操作顯然不是很好的處理方式。由於我處理日志是使用的logback,通過網上查詢資料,logback日志框架都提供了MDC功能。
MDC
MDC 介紹
MDC(Mapped Diagnostic Context,映射調試上下文)是 log4j 和 logback 提供的一種方便在多線程條件下記錄日志的功能。MDC 可以看成是一個與當前線程綁定的Map,可以往其中添加鍵值對。MDC 中包含的內容可以被同一線程中執行的代碼所訪問。當前線程的子線程會繼承其父線程中的 MDC 的內容。當需要記錄日志時,只需要從 MDC 中獲取所需的信息即可。MDC 的內容則由程序在適當的時候保存進去。對於一個 Web 應用來說,通常是在請求被處理的最開始保存這些數據。
簡而言之,MDC就是日志框架提供的一個InheritableThreadLocal
,項目代碼中可以將鍵值對放入其中,然后使用指定方式取出打印即可。
在logback 的取值方式為:
%X{traceId}
實現方案
gateway網關層
新增一個攔截器,攔截request請求,生成traceId並放入request的請求頭。
import com.caiyi.sport.core.constant.HeaderConstant;
import com.caiyi.sport.core.constant.LogConstant;
import com.caiyi.sport.core.domain.App;
import com.caiyi.sport.core.utils.IPUtils;
import com.caiyi.sport.core.utils.UniqueStrCreator;
import com.caiyi.sport.core.utils.UserSourceMapUtil;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Enumeration;
@Component
@Slf4j
@WebFilter(urlPatterns = {"/"}, filterName = "headerFilter")
public class HeaderFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HeaderMapRequestWrapper requestWrapper = new HeaderMapRequestWrapper(request);
//生成唯一的字符串traceId
String traceId = "traceId_" + UniqueStrCreator.getRandomStr();
//將traceId放到Header中
requestWrapper.addHeader(LogConstant.TRACE_ID, traceId);
//MDC加入相關參數,便於日志打印
MDC.put(LogConstant.TRACE_ID, traceId);
MDC.put("ip", IPUtils.getIpFromRequest(request));
MDC.put("uri", request.getRequestURI());
filterChain.doFilter(requestWrapper, servletResponse);
}
@Override
public void destroy() {
}
}
import lombok.extern.slf4j.Slf4j;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.util.*;
@Slf4j
public class HeaderMapRequestWrapper extends HttpServletRequestWrapper {
/**
* construct a wrapper for this request
*
* @param request
*/
public HeaderMapRequestWrapper(HttpServletRequest request) {
super(request);
}
private Map<String, String> headerMap = new HashMap<>();
/**
* add a header with given name and value
*
* @param name
* @param value
*/
public void addHeader(String name, String value) {
headerMap.put(name, value);
}
@Override
public String getHeader(String name) {
//log.info("getHeader --->{}",name);
String headerValue = super.getHeader(name);
if (headerMap.containsKey(name)) {
headerValue = headerMap.get(name);
}
return headerValue;
}
/**
* get the Header names
*/
@Override
public Enumeration<String> getHeaderNames() {
List<String> names = Collections.list(super.getHeaderNames());
for (String name : headerMap.keySet()) {
names.add(name);
}
return Collections.enumeration(names);
}
@Override
public Enumeration<String> getHeaders(String name) {
List<String> values = Collections.list(super.getHeaders(name));
if (headerMap.containsKey(name)) {
values = Arrays.asList(headerMap.get(name));
}
return Collections.enumeration(values);
}
}
下游模塊
新增一個攔截器或者切面,從request中取出traceId並放入MDC
//從Header中獲取traceId
String traceId = request.getHeader("traceId");
MDC.put(LogConstant.TRACE_ID, traceId);
模塊之間調用使用feign,需要新增一個Request攔截器
import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;
/**
* 攔截請求Request
*
*/
@Slf4j
@Configuration
public class FeignConfiguration implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
//有可能是獲取不到attributes
return;
}
HttpServletRequest request = attributes.getRequest();
Enumeration<String> headerNames = request.getHeaderNames();
if (headerNames != null) {
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
String values = request.getHeader(name);
template.header(name, values);
}
//log.info("feign interceptor header:{}", template);
}
}
}
logback配置
<appender name="INFO_LOG"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${path}/%d{yyyy-MM-dd}/info_%i.log
</FileNamePattern>
<MaxHistory>365</MaxHistory>
<timeBasedFileNamingAndTriggeringPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<!--文件達到 最大128MB時會被切割 -->
<maxFileSize>128 MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder>
<pattern>%date [%X{traceId}] [%thread] %-5level %logger{0} [%file:%line] [%X{userId}] - %msg%n
</pattern>
<charset>utf8</charset>
</encoder>
</appender>
參考文章:https://blog.csdn.net/yangcheng33/article/details/80796129