1.整体思路
通过SLF4J 的日志上下文MDC保存traceId,并通过springboot请求拦截器在每次请求中从请求头中获取到traceId,并将其保存进上下文中。
2.所需依赖
xml
体验AI代码助手
代码解读
复制代码
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
3.完整实现代码
3.1 traceId拦截切面
java
体验AI代码助手
代码解读
复制代码
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.MDC;
import org.springframework.lang.NonNull;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;
/**
* 日志拦截器
*
*/
@Slf4j
public class LogInterceptor implements HandlerInterceptor {
public static final String TRACE_ID = "traceId";
@Override
public boolean preHandle(HttpServletRequest request, @NonNull HttpServletResponse response,
@NonNull Object handler) {
String traceId = request.getHeader(TRACE_ID);
if (StringUtils.isBlank(traceId)) {
traceId = createTraceId();
}
MDC.put(TRACE_ID, traceId);
return true;
}
@Override
public void afterCompletion(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response,
@NonNull Object handler, Exception ex) {
MDC.remove(TRACE_ID);
}
public static String createTraceId() {
return UUID.randomUUID().toString().replaceAll("-", "");
}
}
注册到SpringMvc中
java
体验AI代码助手
代码解读
复制代码
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
private int connectTimeout = 500;
private int readTimeout = 50000;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册
registry.addInterceptor(new LogInterceptor()).addPathPatterns("/**");
}
@Bean
RestTemplate restTemplate() {
SimpleClientHttpRequestFactory httpRequestFactory = new SimpleClientHttpRequestFactory();
httpRequestFactory.setConnectTimeout(connectTimeout);
httpRequestFactory.setReadTimeout(readTimeout);
return new RestTemplate(httpRequestFactory);
}
}
3.2 网关代码修改
在网关中添加全局过滤器来自动添加traceId
java
体验AI代码助手
代码解读
复制代码
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.MDC;
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.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.UUID;
/**
* traceId 全局过滤器
*/
@Component
@Slf4j
public class TraceIdGlobalFilter implements GlobalFilter, Ordered {
private static final String TRACE_ID = "traceId";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 获取请求头中的 traceId 字段
String traceId = exchange.getRequest().getHeaders().getFirst(TRACE_ID);
traceId = StringUtils.isNotBlank(traceId) ? traceId : createTraceId();
// 将 traceId 存储到 MDC 中
MDC.put(TRACE_ID, traceId);
ServerHttpRequest mutableReq = exchange.getRequest().mutate().header(TRACE_ID, traceId).build();
ServerWebExchange mutableExchange = exchange.mutate().request(mutableReq).build();
// 转发请求
return chain.filter(mutableExchange).doFinally(s -> {
// 从 MDC 中清除 traceId
MDC.remove(TRACE_ID);
});
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
public static String createTraceId() {
return UUID.randomUUID().toString().replaceAll("-", "").toLowerCase();
}
}
3.3服务间请求调用带上traceId
下面简单给出OpenFeign的实现
java
体验AI代码助手
代码解读
复制代码
import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.util.CollectionUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* 自定义的Feign拦截器
*
*/
@Slf4j
public class FeignRequestInterceptor implements RequestInterceptor {
/**
* 这里可以实现对请求的拦截,对请求添加一些额外信息之类的
*
* @param requestTemplate
*/
@Override
public void apply(RequestTemplate requestTemplate) {
// 1. obtain request
final ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
// 2. 兼容hystrix限流后,获取不到ServletRequestAttributes的问题(使拦截器直接失效)
if (Objects.isNull(attributes)) {
log.warn("OpenFeignRequestInterceptor is invalid!");
return;
}
HttpServletRequest request = attributes.getRequest();
// 2. 透传请求头
Enumeration<String> headerNames = request.getHeaderNames();
if (Objects.nonNull(headerNames)) {
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
// 跳过 content-length
if (name.equals("content-length")) {
continue;
}
String value = request.getHeader(name);
requestTemplate.header(name, value);
}
}
// 添加traceId到请求头
Map<String, String> copyOfContextMap = MDC.getCopyOfContextMap();
if (CollectionUtils.isEmpty(copyOfContextMap)) {
copyOfContextMap = new HashMap<>();
copyOfContextMap.put(LogInterceptor.TRACE_ID, LogInterceptor.createTraceId());
}
for (Map.Entry<String, String> entry : copyOfContextMap.entrySet()) {
requestTemplate.header(entry.getKey(), entry.getValue());
}
}
}
注册到容器中
java
体验AI代码助手
代码解读
复制代码
import feign.RequestInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OpenFeignConfiguration {
@Bean
public RequestInterceptor requestInterceptor() {
return new FeignRequestInterceptor();
}
}
3.4 多线程间同步日志上下文
由于traceId存放于MDC,而MDC的实现是基于ThreadLocal,它是线程隔离的,所以在开启多线程时要在线程间同步MDC
java
体验AI代码助手
代码解读
复制代码
import org.slf4j.MDC;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.util.Map;
import java.util.concurrent.Callable;
/**
* MDC日志上下文包装器
* 例 executorService.submit(MdcWrapper.createCallable(() -> "hello world");
*
*/
public class MdcWrapper {
public static RunnableWrapper createRunable(Runnable runable) {
return new RunnableWrapper(runable);
}
public static <V> CallableWrapper<V> createCallable(Callable<V> callable) {
return new CallableWrapper<V>(callable);
}
static class RunnableWrapper implements Runnable {
private final Map<String, String> parentMdc;
private final Runnable runnable;
private ServletRequestAttributes attributes;
public RunnableWrapper(Runnable runnable) {
this.parentMdc = MDC.getCopyOfContextMap();
this.runnable = runnable;
this.attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
}
@Override
public void run() {
try {
// 将父线程的 MDC 数据传递给子线程
if (parentMdc != null) {
MDC.setContextMap(parentMdc);
}
// 将父线程的请求头也复制给子线程
RequestContextHolder.setRequestAttributes(attributes);
// 执行线程任务
runnable.run();
} finally {
// 清除子线程的 MDC 数据
MDC.clear();
RequestContextHolder.resetRequestAttributes();
}
}
}
static class CallableWrapper<V> implements Callable<V> {
private final Map<String, String> parentMdc;
private final Callable<V> callable;
private ServletRequestAttributes attributes;
public CallableWrapper(Callable<V> callable) {
this.parentMdc = MDC.getCopyOfContextMap();
this.callable = callable;
this.attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
}
@Override
public V call() throws Exception {
try {
// 将父线程的 MDC 数据传递给子线程
if (parentMdc != null) {
MDC.setContextMap(parentMdc);
}
// 将父线程的请求头也复制给子线程
RequestContextHolder.setRequestAttributes(attributes);
// 执行任务
return callable.call();
} finally {
// 清除子线程的 MDC 数据
MDC.clear();
RequestContextHolder.resetRequestAttributes();
}
}
}
}
3.5 logback.xml配置
xml
体验AI代码助手
代码解读
复制代码
<appender name="INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<FileNamePattern>${catalina.home:-.}/logs/${appName}/${appName}_%d{yyyy-MM-dd}_info_%i.log.gz</FileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>2GB</totalSizeCap>
<cleanHistoryOnStart>true</cleanHistoryOnStart>
</rollingPolicy>
<encoder>
<!-- 通过{traceId}引用MDC中的traceId-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [traceId:%X{traceId}] %-5level %logger - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
4完结撒花
这是一个简单的traceId方案,适合于服务只有五个以内的简单业务,对于服务数超过五的,调用链路上的复杂业务最好还是使用成熟的方案 例如 skywalking,它是基于字节码增强的实现,不需要业务的代码改动