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,它是基于字节码增强的实现,不需要业务的代码改动

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐