ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot微服务全链路日志TraceId追踪方案与实战

SpringBoot微服务全链路日志TraceId追踪方案与实战 做微服务或者多模块项目的同学应该都有过这种体验一个用户请求从前端进来后端调用三个服务中间还穿插了几次Redis和数据库操作。突然线上报了个错你去翻日志发现只有一条孤零零的异常信息上下文里既没有请求参数也没有调用链路上的其他日志。你只能靠猜靠时间戳碰运气在四五个服务里来回翻日志折腾一两个小时才定位到问题。这就是我今天想聊的事——给SpringBoot项目实现全链路日志TraceId追踪。这套方案解决的就是“一次请求跨多个服务日志怎么串起来”的问题。核心思路不复杂给每个请求生成一个全局唯一的ID打进所有日志里后期靠这个ID把所有日志捞出来。但实际落地过程中线程池传递、Feign调用、日志格式改造、关键位置埋点每一环都有坑。这篇文章把这些步骤完整走一遍包含可直接复制的代码和配置适合正在做微服务拆分、以及被日志排查折磨过的兄弟参考。1. 为什么你的日志在微服务环境下越来越难用1.1 一次真实的生产事故排查经历先讲个我自己的经历。前两年我们一个订单系统做了微服务拆分拆成了订单服务、库存服务、支付服务三个模块。上线后不到一周线上出现了一次偶发性的支付超时。用户下单后前端一直转圈但最终订单状态居然还是支付成功的。我一个人从订单服务的日志开始查找到下单日志跟着用户ID翻库存扣减记录再跟着订单号翻支付回调解。单次请求涉及的日志散落在三台服务器的文件里每台服务器的时间还不完全一致要靠秒级时间戳去猜上下文。整个排查过程花了将近三个小时最后发现是支付回调里一个空指针导致回调处理提前退出后续重试机制又因为缺少幂等判断重复扣了库存。这件事之后我下决心把TraceId方案落地。思路也很直接用户每次请求进入系统时生成一个全局唯一的TraceId无论这个请求在内部调了多少个服务、异步线程怎么切换日志里都必须带着同一个TraceId。排查时只需要拿TraceId在所有服务的日志里搜一遍整条链路就清晰了。1.2 全链路日志追踪的核心目标与价值所谓全链路日志追踪标准做法是给一次业务请求分配全局唯一的标识符并在整个调用过程中传递这个标识。最常见的标识体系分两层TraceId链路ID一次业务请求的全局唯一ID从入口往下游传递整个过程保持不变。SpanId单元ID链路中每一个服务调用单元都有独立ID多个Span按顺序串起来就能还原出调用拓扑。实际工程中很多团队只做了TraceId一层也够用。但如果你后续打算接入SkyWalking这类APM系统建议一开始就把父子Span的概念设计进去后面改造成本更低。这套方案解决的核心痛点有三个快速检索一次请求的全部日志拥有同一个TraceId用grep traceIdxxx就能跨服务捞全。异常关联业务异常从底层抛到上层日志里都带TraceId可以直观看到异常在哪个服务、哪个方法触发。性能分析通过Span的起止时间能粗略判断每个环节耗时定位慢服务。一句话总结日志从“按时间猜”变成“按ID精准捞”排查效率至少提升一个数量级。2. 实现方案选型从零手写不如理解原理再动手2.1 先搞清楚TraceId必须经过哪些环节动手写代码之前先把TraceId的生命周期梳理清楚。一个典型的同步请求链路是这样的浏览器发起请求 → 网关或入口服务接收 → 内部调用订单服务 → 订单服务调用库存服务 → 返回结果给前端TraceId需要经历的环节包括生成在链路入口网关或第一个服务生成全局唯一ID。传递在服务间调用时通过HTTP Header携带TraceId传给下游。记录服务内部所有日志输出统一带上TraceId。清理请求处理完毕后删除日志上下文中的TraceId。如果链路中混入了异步线程池、MQ消息、定时任务还需要额外处理线程间传递否则子线程里日志的TraceId就断了。2.2 四种TraceId生成方案对比生成TraceId的方案不少我把常见几种列一下方案长度有序性优点缺点Java UUID36位无序生成简单无需额外依赖太长日志里占空间无序导致索引性能差短UUIDHutool32位去横杠无序简单好用社区普及内容偏随机但够用雪花算法19位左右趋势有序短小有序适合海量日志依赖时钟时钟回拨时有极小概率重复W3C Trace-Context55位左右无序标准化云厂商兼容偏长非必要场景有点重我的建议是生产环境用Hutool的IdUtil.fastSimpleUUID()或自研雪花算法实现。前者简单可靠ID长度适中唯一性完全够用后者适合对日志体积和排序有极强要求的团队。不要为了炫技引入复杂方案TraceId的核心是全局唯一不重复就行。2.3 关键技术底座MDCMapped Diagnostic Context选定了ID生成方式接下来要解决“日志怎么自动带TraceId”的问题。这里必须提到SLF4J提供的MDC机制。MDC全称Mapped Diagnostic Context是日志框架提供的一种线程绑定的KV存储本质是一个ThreadLocal。你可以往MDC里放值比如MDC.put(traceId, abc123);然后在日志配置文件的pattern里用占位符引用它pattern%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - [%X{traceId}] - %msg%n/pattern%X{traceId}就是MDC中key为traceId的值。这样所有日志输出会自动带上TraceId业务代码里不需要手动拼接。MDC既然是ThreadLocal就要注意两点子线程默认拿不到父线程的MDC值线程池复用时MDC里的旧值要记得清理。这两点后面单独说。2.4 为什么不推荐用Spring Cloud Sleuth或SkyWalking方案看到这里可能有人问现在不是有Spring Cloud Sleuth、Micrometer Tracing还有SkyWalking吗为什么还要自己写我的看法是轻量场景手写一套完全够用而且可控性更强。Sleuth 3.0之后更名为Micrometer Tracing对Spring Boot 3.x和Spring Cloud版本有强依赖升级Spring Boot时容易连带踩坑。SkyWalking的TraceId查询确实更强但它本质是APM系统落地需要部署Agent、存储后端和UI对中小团队来说运维成本偏高。手写方案把核心逻辑收敛在一个Filter和几个拦截器里代码量不超过200行不引入额外组件。等你真的需要APM能力时再在现有TraceId基础上接SkyWalking也不迟自定义链路ID是可以和SkyWalking的TraceId做映射的。3. 手把手实现Filter MDC 日志格式改造3.1 第一步编写TraceIdFilter核心拦截逻辑先创建一个过滤器作为TraceId的入口。这里有几个细节需要特别注意我直接在代码里标注了import org.slf4j.MDC; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.UUID; /** * 全链路日志TraceId过滤器 */ Component Order(Ordered.HIGHEST_PRECEDENCE) public class TraceIdFilter extends OncePerRequestFilter { /** 请求头中传递TraceId的key */ public static final String TRACE_ID_HEADER X-Trace-Id; /** MDC中存放TraceId的key */ public static final String MDC_TRACE_ID_KEY traceId; Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { // 优先取上游传递的TraceId保证链路串联 String traceId request.getHeader(TRACE_ID_HEADER); if (traceId null || traceId.trim().isEmpty()) { traceId generateTraceId(); } MDC.put(MDC_TRACE_ID_KEY, traceId); // 响应头也带上TraceId方便前端定位问题 response.setHeader(TRACE_ID_HEADER, traceId); filterChain.doFilter(request, response); } finally { // 必须清理MDC避免线程池复用导致TraceId串用 MDC.remove(MDC_TRACE_ID_KEY); } } private String generateTraceId() { return UUID.randomUUID().toString().replace(-, ); } }几个关键决策说下必须用OncePerRequestFilterSpring的普通Filter在Servlet容器里可能被调用多次但OncePerRequestFilter保证一次请求只执行一次过滤逻辑避免重复生成TraceId。Order(Ordered.HIGHEST_PRECEDENCE)过滤器执行顺序很关键必须放在所有业务Filter最前面否则后续Filter的日志就已经拿不到TraceId了。finally里清理MDC这步经常被忽略。Tomcat线程池是复用的如果不清理下一次请求可能读到上一次的TraceId日志会串链路。响应头带回TraceId这个环节是加分项。前端拿到TraceId报错时直接提交这个ID后端能立刻定位。3.2 第二步改造logback日志格式过滤器能力有了接下来要让日志真正输出TraceId。我用的是Spring Boot最常见的logback-spring.xml配置核心是修改pattern?xml version1.0 encodingUTF-8? configuration !-- 日志输出格式中加入%X{traceId} -- property nameLOG_PATTERN value%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - [%X{traceId}] - %msg%n/ appender nameCONSOLE classch.qos.logback.core.ConsoleAppender encoder pattern${LOG_PATTERN}/pattern charsetUTF-8/charset /encoder /appender !-- 异步文件Appender生产环境建议开启 -- appender nameASYNC_FILE classch.qos.logback.classic.AsyncAppender appender-ref refFILE/ /appender appender nameFILE classch.qos.logback.core.rolling.RollingFileAppender file${LOG_PATH:-logs}/app.log/file rollingPolicy classch.qos.logback.core.rolling.TimeBasedRollingPolicy fileNamePattern${LOG_PATH:-logs}/app.%d{yyyy-MM-dd}.log/fileNamePattern maxHistory30/maxHistory /rollingPolicy encoder pattern${LOG_PATTERN}/pattern charsetUTF-8/charset /encoder /appender root levelINFO appender-ref refCONSOLE/ appender-ref refASYNC_FILE/ /root /configuration配置好之后启动服务访问任意接口你会看到类似这样的输出2025-01-15 10:23:45.123 [http-nio-8080-exec-1] INFO com.example.OrderService - [a3f9d0c2e1b84a5f9d0c2e1b84a5f9] - 收到订单创建请求方括号里那一串就是TraceId。以后排查问题先拿到这串ID再在所有服务的日志目录里搜链路就出来了。3.3 第三步跨服务调用传递TraceIdFeign/RestTemplate入口服务和网关的TraceId生成了日志格式也改好了但下游服务怎么拿到同一个TraceId靠的是HTTP Header传递。这里以项目里最常用的OpenFeign为例import feign.RequestInterceptor; import feign.RequestTemplate; import org.slf4j.MDC; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; Configuration public class FeignTraceIdConfig { Bean public RequestInterceptor feignTraceIdInterceptor() { return new RequestInterceptor() { Override public void apply(RequestTemplate template) { // 从MDC中取出当前线程的TraceId String traceId MDC.get(traceId); if (traceId ! null !traceId.isEmpty()) { template.header(X-Trace-Id, traceId); } } }; } }RestTemplate同样有拦截器机制原理一致import org.springframework.http.HttpRequest; import org.springframework.http.client.ClientHttpRequestExecution; import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.http.client.ClientHttpResponse; import org.slf4j.MDC; import java.io.IOException; public class RestTemplateTraceIdInterceptor implements ClientHttpRequestInterceptor { Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { String traceId MDC.get(traceId); if (traceId ! null !traceId.isEmpty()) { request.getHeaders().add(X-Trace-Id, traceId); } return execution.execute(request, body); } }使用RestTemplate时注册这个拦截器即可RestTemplate restTemplate new RestTemplate(); restTemplate.setInterceptors(Collections.singletonList(new RestTemplateTraceIdInterceptor()));下游服务不需要单独写逻辑因为入口的TraceIdFilter会从Header读取X-Trace-Id并放入MDC。这样整条调用链的TraceId就自动串联了。3.4 第四步异步线程池场景保持链路贯通MDC基于ThreadLocal天生跨线程失效。项目中用到Async、自定义线程池、或者CompletableFuture的子线程里的日志就不会带TraceId了。这是全链路日志落地中最大的坑。解决方法有两种我推荐第二种。方法一手动传递在提交任务前手动把TraceId塞到子线程用了ThreadLocal的包装类// 任务提交处包装Runnable Runnable taskWithTraceId () - { try { MDC.put(traceId, MDC.get(traceId)); // 实际业务逻辑 } finally { MDC.remove(traceId); } }; executor.submit(taskWithTraceId);这种方法的问题很明显每个提交任务的地方都要写一遍容易漏。方法二用TaskDecorator统一包装线程池任务Spring的ThreadPoolTaskExecutor提供了TaskDecorator钩子可以统一处理所有任务提交。一次配置全局生效import org.slf4j.MDC; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.task.TaskDecorator; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.Map; Configuration public class ThreadPoolConfig { Bean public ThreadPoolTaskExecutor customTaskExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setCorePoolSize(8); executor.setMaxPoolSize(16); executor.setQueueCapacity(100); executor.setThreadNamePrefix(custom-task-); // 重点设置TaskDecorator executor.setTaskDecorator(new MdcTaskDecorator()); executor.initialize(); return executor; } /** * 将父线程的MDC上下文拷贝到子线程 */ public static class MdcTaskDecorator implements TaskDecorator { Override public Runnable decorate(Runnable runnable) { MapString, String contextMap MDC.getCopyOfContextMap(); return () - { try { if (contextMap ! null) { MDC.setContextMap(contextMap); } runnable.run(); } finally { MDC.clear(); } }; } } }MDC.getCopyOfContextMap()会复制当前线程的MDC全部内容在子线程执行前放进去执行完清理。这样即使子线程异常抛出也不会有残留。对于Async注解通过自定义TaskDecorator同样有效。配置一下AsyncConfigurer即可Configuration EnableAsync public class AsyncConfig implements AsyncConfigurer { Override public Executor getAsyncExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setCorePoolSize(8); executor.setMaxPoolSize(16); executor.setQueueCapacity(100); executor.setThreadNamePrefix(async-task-); executor.setTaskDecorator(new ThreadPoolConfig.MdcTaskDecorator()); executor.initialize(); return executor; } }这套方案可以把90%以上的异步场景覆盖掉。剩下的像new Thread()手搓线程、第三方框架内部线程池只能靠代码规范去约束了。4. 全链路日志落地的8个典型问题与排查实录4.1 问题速查表手写TraceId方案踩过的坑不少我把最高频的问题整理成一张表方便排查时按图索骥现象根因解决方案所有日志都没有TraceId没有加载logback配置文件或pattern未配置%X检查logback-spring.xml是否生效确认pattern里有%X{traceId}入口服务日志有TraceId下游没有Feign/RestTemplate没加拦截器确认服务间调用是否走的是同一套HTTP客户端封装日志TraceId串到其他请求MDC在finally中未清理在Filter的finally里调用MDC.remove异步线程日志没有TraceIdThreadLocal子线程拿不到父线程MDC线程池配置TaskDecorator统一复制MDC第一个请求没有TraceId后续请求有Filter顺序错误业务Filter先执行了给TraceIdFilter设置最高优先级前端跨域场景自定义Header被拦截浏览器CORS预检未放行自定义Header网关/后端配置Access-Control-Allow-HeadersMQ消费者日志无法关联业务链路消息体没有携带TraceId生产者在消息Header写TraceId消费者读取后塞入MDC网关入口没接入FilterTraceId一直变化请求入口在网关但网关没生成/传递TraceId在网关最外层统一接入TraceIdFilter4.2 网关入口接入注意点如果项目走了网关比如Gateway那入口Filter必须加在网关层而不是每个业务服务各来一个。否则用户每次请求到不同服务生成的TraceId都不一样链路就断了。网关的写法基本和Filter相同核心逻辑仍然是读上游Header → 没有就生成 → 放入MDC → 转发下游时带上Header。需要注意网关自身的WebFlux模型是响应式的不能用Servlet的OncePerRequestFilter要改用WebFilterimport org.slf4j.MDC; 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; Component public class GatewayTraceIdFilter implements GlobalFilter, Ordered { Override public MonoVoid filter(ServerWebExchange exchange, org.springframework.cloud.gateway.filter.GatewayFilterChain chain) { String traceId exchange.getRequest().getHeaders().getFirst(TraceIdFilter.TRACE_ID_HEADER); if (traceId null || traceId.trim().isEmpty()) { traceId UUID.randomUUID().toString().replace(-, ); } MDC.put(TraceIdFilter.MDC_TRACE_ID_KEY, traceId); // 将TraceId写入请求头转发给下游服务 ServerHttpRequest mutatedRequest exchange.getRequest() .mutate() .header(TraceIdFilter.TRACE_ID_HEADER, traceId) .build(); ServerWebExchange mutatedExchange exchange.mutate().request(mutatedRequest).build(); return chain.filter(mutatedExchange).doFinally(signalType - MDC.remove(TraceIdFilter.MDC_TRACE_ID_KEY)); } Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; } }注意点响应式场景里MDC.remove不能写在普通finally里因为Mono不一定被执行完要用doFinally确保链路结束后清理。4.3 MQ消息场景的TraceId传递异步场景里还有一块容易断链的是消息队列。比如订单服务发消息给库存服务如果消息里不带TraceId库存服务侧日志就跟主链路脱节。MQ的解决方案和HTTP类似关键点在于消息头传递。以RocketMQ为例生产者发送消息时把TraceId塞进Message的propertiesMessage message new Message(inventory_topic, body); message.putUserProperty(X-Trace-Id, MDC.get(traceId)); producer.send(message);消费者接收到消息后先从消息头里取TraceId放入MDC再执行业务逻辑Override public void onMessage(MessageExt messageExt) { String traceId messageExt.getUserProperty(X-Trace-Id); if (traceId null || traceId.isEmpty()) { traceId UUID.randomUUID().toString().replace(-, ); } MDC.put(TraceIdFilter.MDC_TRACE_ID_KEY, traceId); try { // 消费业务逻辑 } finally { MDC.remove(TraceIdFilter.MDC_TRACE_ID_KEY); } }Kafka的ConsumerRecord也有headers属性同理。RabbitMQ则是用MessageProperties的headers。无非是“取→放MDC→执行→清理”四步套路一致。4.4 埋点检测与链路演练TraceId方案上线前至少要做一次完整链路的演练。我的做法是找一台测试环境启动入口网关 订单服务 库存服务三个应用。用一个带自定义Header的请求工具比如Postman发起一次调用Header里写X-Trace-Id: test-10001。依次去三个服务的日志文件里grep这个TraceId确认每条日志都带上了。再发起一次不带Header的请求确认入口服务自动生成了新的TraceId并且下游服务跟随的是同一个值。看一条关键业务链路是否完整比如从“收到请求”到“返回响应”之间数据库操作、Redis缓存、远程调用这些关键节点是否都有日志。演练过程中如果发现某段链路日志缺失多半是以下三种原因一是代码走了AOP切面切面里自己打着玩没带MDC的占位符二是某些日志是第三方SDK内部放出来的不受logback pattern控制这种情况只能靠SDK自身支持MDC或者用日志增强插件截获后补充三是异步线程内部的嵌套任务又开了线程子线程的装饰器没生效。4.5 几个提升排查效率的额外小技巧方案主流程跑通之后我建议加几个小优化排查效率能再上一个台阶。标记完整调用链不光是异常日志打TraceId请求入口、关键业务节点、外部调用、出参返回这些位置都打一条带TraceId的日志。这样拿到TraceId就能看到整个调用过程的“时间线”比只看异常灭菌更直观。响应头里回传TraceId前面提到过前端报错时只要把浏览器Network里的X-Trace-Id值发过来后端就能直接定位。这个习惯要养起来线上沟通成本能低很多。日志文件按天切分并保留30天TraceId方案再完美日志文件被覆盖了也是白搭。logback-spring.xml里给RollingFileAppender配置maxHistory历史日志保留至少30天。如果磁盘允许保留60天。关键服务接入独立的Error日志文件可以把ERROR级别的日志单独输出到一个文件排查线上问题时先在这个文件里搜TraceId效率更高。配置方式给一段参考appender nameERROR_FILE classch.qos.logback.core.rolling.RollingFileAppender file${LOG_PATH:-logs}/error.log/file rollingPolicy classch.qos.logback.core.rolling.TimeBasedRollingPolicy fileNamePattern${LOG_PATH:-logs}/error.%d{yyyy-MM-dd}.log/fileNamePattern maxHistory30/maxHistory /rollingPolicy encoder pattern${LOG_PATTERN}/pattern charsetUTF-8/charset /encoder filter classch.qos.logback.classic.filter.LevelFilter levelERROR/level onMatchACCEPT/onMatch onMismatchDENY/onMismatch /filter /appender logger namecom.example levelINFO/ root levelINFO appender-ref refCONSOLE/ appender-ref refASYNC_FILE/ appender-ref refERROR_FILE/ /root5. 全链路日志追踪的扩展方向与我的实操体会5.1 从日志追踪走向调用链分析手写的TraceId方案解决的是“日志检索”问题但它的数据天然可以用来做调用链分析。只要你在代码的关键节点记录当前SpanId、父SpanId、方法名、耗时把日志汇总到ELK或Loki就能按TraceId聚合出一次请求的完整调用拓扑。我之前做过一个轻量版在AOP切面里拦截所有业务方法每次调用把方法名、参数、耗时、TraceId、SpanId拼成一条结构化日志输出到独立文件。然后用小脚本按TraceId聚合生成简单的调用链树。虽然比起SkyWalking差远了但解决“这个调用耗时3秒花在哪了”这种问题完全够用。如果团队有预算部署SkyWalking落地时也无需推翻现有方案。SkyWalking有原生的TraceId体系但它的搜索框也支持自定义业务TraceId关联查询相当于两条并行能力。5.2 我对这套方案的真实感受这套方案我前前后后在两个项目里落地过代码量不大收益却非常明显。最直观的感受是线上排查从“碰运气猜时间窗口”变成“拿ID说话”新人也能很快上手。以前周报里写“排查线上问题耗时X小时”现在基本都是分钟级定位。踩过几次坑之后有几个习惯我一直保持到现在所有日志一律走SLF4J接口禁止直接调用log4j2或logback的API。这样才能保证MDC随处可用将来切换日志实现也不受影响。Filter里不用try-catch吞异常只做try-finally。TraceIdFilter不干预业务异常传播只在最后清理MDC避免副作用。每个服务都要配一份TraceId上下文工具类。比如把取TraceId、生成TraceId的方法收敛到一个公共组件里避免各服务自己各写一套维护时才知道什么叫痛苦。5.3 一个可以直接抄作业的优化点最后分享一个我在项目里做的小优化当请求是外部传入时TraceId生成在网关或第一个服务但如果是一次内部定时任务发起的业务链路入口不在HTTP请求里Filter根本不会触发。解决方式是写一个通用的工具方法业务入口或定时任务启动时手动调用public class TraceIdUtil { /** 初始化TraceId适用于非HTTP入口 */ public static void initTraceId() { if (MDC.get(traceId) null) { MDC.put(traceId, UUID.randomUUID().toString().replace(-, )); } } /** 清理TraceId */ public static void clearTraceId() { MDC.remove(traceId); } }定时任务里这样用Scheduled(cron 0 0 2 * * ?) public void dailyReconciliationTask() { TraceIdUtil.initTraceId(); try { // 业务逻辑 log.info(每日对账任务开始执行); // ... } finally { TraceIdUtil.clearTraceId(); } }这样定时任务的日志也带上了TraceId和HTTP请求走的是同一套检索逻辑。排查问题时不用再做“任务日志在另一个文件里只能盲猜”这种事了。全链路日志TraceId这个东西单独看每个环节都不难难的是把所有管道都打通HTTP入口、HTTP调用、异步线程、MQ消息、定时任务、日志格式。哪一环断了排查的时候TraceId就搜不到完整链路方案就算白做。如果你正准备在项目里落地这套方案按我前文的顺序一步步来把最后那些容易断的点都处理掉基本就能做到一套代码全场景覆盖了。
返回列表