1. 背景
随着分布式的发展,开发问题排查过程的日志分析逐渐复杂,现在一般通过ELK系统整合分布式系统的所有日志进行分析,此时需要一个全局的日志id进行链路跟踪。
2. 解决思路
2.1 系统内部
Log4j和Logback提供MDC,可以实现基于ThreadLocal级别的数据存储,从而实现系统内部同一线程日志打印的traceId一致性
备注:如需在系统内的多线程实现日志一致性,则可以通过进行参数传递方式传输traceId
2.2 跨系统
-
Feign调用SpringBoot项目
由于Feign访问其他服务时Hystrix会使用线程池方式进行调用,故需要实现traceId跨线程、跨系统传输。跨系统传输通过请求头方式传输,跨线程则使用
-
异构系统
直接通过请求头传输traceId
3. 实现思路
微信图片_20211211091435.png
3.1 服务内部日志跟踪
- 网关生成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) {
// 获取请求对象request
ServerHttpRequest request = exchange.getRequest();
// 获取响应对象response
ServerHttpResponse response = exchange.getResponse();
// 请求头加入traceId
long nanoTime = System.nanoTime();
request.mutate().header("traceId", String.valueOf(nanoTime)).build();
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}
- 服务过滤器拦截请求头加入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>
当前服务中相同线程打印日志都会带上日志id,如需要使用线程池或异步线程处理业务,请自行传入参数或进行线程间的数据传输
3.2 跨服务Feign调用跟踪
先使用过滤器从请求头获取traceId,存储到HystrixRequestVariableDefault中,
然后拦截器中获取到traceId,加入RequestTemplate的请求头中
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. 总结
本教程通过过滤器、拦截器结合HystrixRequestVariableDefault实现系统内和跨系统的日志跟踪。