1. 背景
隨著分布式的發(fā)展,開發(fā)問(wèn)題排查過(guò)程的日志分析逐漸復(fù)雜,現(xiàn)在一般通過(guò)ELK系統(tǒng)整合分布式系統(tǒng)的所有日志進(jìn)行分析,此時(shí)需要一個(gè)全局的日志id進(jìn)行鏈路跟蹤。
2. 解決思路
2.1 系統(tǒng)內(nèi)部
Log4j和Logback提供MDC,可以實(shí)現(xiàn)基于ThreadLocal級(jí)別的數(shù)據(jù)存儲(chǔ),從而實(shí)現(xiàn)系統(tǒng)內(nèi)部同一線程日志打印的traceId一致性
備注:如需在系統(tǒng)內(nèi)的多線程實(shí)現(xiàn)日志一致性,則可以通過(guò)進(jìn)行參數(shù)傳遞方式傳輸traceId
2.2 跨系統(tǒng)
-
Feign調(diào)用SpringBoot項(xiàng)目
由于Feign訪問(wèn)其他服務(wù)時(shí)Hystrix會(huì)使用線程池方式進(jìn)行調(diào)用,故需要實(shí)現(xiàn)traceId跨線程、跨系統(tǒng)傳輸。跨系統(tǒng)傳輸通過(guò)請(qǐng)求頭方式傳輸,跨線程則使用
-
異構(gòu)系統(tǒng)
直接通過(guò)請(qǐng)求頭傳輸traceId
3. 實(shí)現(xiàn)思路

3.1 服務(wù)內(nèi)部日志跟蹤
- 網(wǎng)關(guān)生成traceId
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class AuthorizeFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 獲取請(qǐng)求對(duì)象request
ServerHttpRequest request = exchange.getRequest();
// 獲取響應(yīng)對(duì)象response
ServerHttpResponse response = exchange.getResponse();
// 請(qǐng)求頭加入traceId
long nanoTime = System.nanoTime();
request.mutate().header("traceId", String.valueOf(nanoTime)).build();
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}
- 服務(wù)過(guò)濾器攔截請(qǐng)求頭加入MDC
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import org.slf4j.MDC;
import org.springframework.context.annotation.Configuration;
import com.alibaba.fastjson.JSON;
import com.xxx.common.utils.LogUtils;
@Configuration
public class RequestHeaderFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
String sTraceId = req.getHeader("traceId");
MDC.put("traceId", sTraceId);
chain.doFilter(request, response);
}
}
日志配置xml中使用[%X{traceId}]引用日志id
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<property name="log_dir" value="/data/logs/demo"/>
<property name="maxHistory" value="30" />
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} -%msg%n</pattern>
</encoder>
</appender>
<appender name="INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log_dir}/%d{yyyy-MM-dd}/info-log.log</fileNamePattern>
<maxHistory>${maxHistory}</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%green(%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n)</pattern>
</encoder>
</appender>
<appender name="demoLog" 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.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${log_dir}/%d{yyyy-MM-dd}/demo.%i.log</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>30GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} [%X{traceId}] - %msg%n</pattern>
</encoder>
</appender>
<logger name="demoLog" additivity="false" level="INFO">
<appender-ref ref="demoLog"/>
</logger>
<root level="INFO">
<appender-ref ref="INFO"/>
</root>
</configuration>
當(dāng)前服務(wù)中相同線程打印日志都會(huì)帶上日志id,如需要使用線程池或異步線程處理業(yè)務(wù),請(qǐng)自行傳入?yún)?shù)或進(jìn)行線程間的數(shù)據(jù)傳輸
3.2 跨服務(wù)Feign調(diào)用跟蹤
先使用過(guò)濾器從請(qǐng)求頭獲取traceId,存儲(chǔ)到HystrixRequestVariableDefault中,
然后攔截器中獲取到traceId,加入RequestTemplate的請(qǐng)求頭中
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.netflix.hystrix.strategy.concurrency.HystrixRequestContext;
import com.netflix.hystrix.strategy.concurrency.HystrixRequestVariableDefault;
import feign.RequestInterceptor;
import feign.RequestTemplate;
@Configuration
public class FeignConfigContext {
private static final HystrixRequestVariableDefault<String> varTraceId = new HystrixRequestVariableDefault<>();
public static HystrixRequestVariableDefault<String> getVariableInstance() {
return varTraceId;
}
@Bean
public RequestInterceptor hystrixInterceptor() {
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate requestTemplate) {
String traceId = FeignConfigContext.getVariableInstance().get();
requestTemplate.header("traceId", traceId);
}
};
}
@Bean
public FilterRegistrationBean<Filter> hystrixFilter() {
FilterRegistrationBean<Filter> filter = new FilterRegistrationBean<>();
filter.setFilter(new Filter() {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
HttpServletRequest req = (HttpServletRequest) request;
String traceId = req.getHeader("traceId");
if (!HystrixRequestContext.isCurrentThreadInitialized()) {
HystrixRequestContext.initializeContext();
}
HystrixRequestVariableDefault<String> variable = FeignConfigContext.getVariableInstance();
variable.set(traceId);
try {
chain.doFilter(request, response);
} catch (Exception e) {
if (HystrixRequestContext.isCurrentThreadInitialized()) {
HystrixRequestContext.getContextForCurrentThread().shutdown();
}
}
}
});
filter.addUrlPatterns("/*");
return filter;
}
}
4. 總結(jié)
本教程通過(guò)過(guò)濾器、攔截器結(jié)合HystrixRequestVariableDefault實(shí)現(xiàn)系統(tǒng)內(nèi)和跨系統(tǒng)的日志跟蹤。