可观测性落地:如何在 Java 项目中统一埋点 Trace ID?(一)

前言

在分布式或并发环境下,当用户反馈接口报错时,单靠时间线查找日志无异于大海捞针。由于请求是并发的,不同请求的日志会交错在一起,我们很难还原某个特定请求的完整执行链路。

为了解决这个问题,我们需要引入 Trace ID(链路追踪标识)

实现思路

  • 唯一标识:为每一个进入系统的请求分配一个全局唯一的 ID。
  • 全链路串联:通过 Trace ID,可以将从 Controller 到 Service、再到 DAO 甚至异步线程的所有日志汇聚在一起。
  • 快速排查:前端在报错时只需提供这个 ID,开发者即可在成千上万行日志中瞬间定位到问题的始末。

我们通常利用日志框架提供的 MDC(Mapped Diagnostic Context) 机制来实现。在请求进入系统时(如拦截器或过滤器中)生成 ID 并存入 MDC,后续该请求触发的所有日志都会自动携带此标识,无需手动修改原有的 Log 代码。

MDC

MDC 是一个"基于线程的日志上下文容器",它的核心原理是(以 loggack 为例):

java 复制代码
public class MDC {
    private static final ThreadLocal<Map<String, String>> contextMap;
}

每一个请求都是独立的一份 MDC 容器,并发请求不会互相污染。

常用方法

  • 设置:MDC.put("traceId", traceId);
  • 清理:MDC.remove("traceId");MDC.clear()

日志配置

通过 MDC 配置的内容想要在日志中打印,还需要我们在日志文件中进行配置,关键配置如下:

xml 复制代码
<pattern>
    [%X{traceId:--}] %d{HH:mm:ss.SSS} %-5level %logger - %msg%n
</pattern>
  • %X{key}: key 为在 MDC 中设定的key,这样在打印日志时,就会从 MDC 工具中相应的值了。
  • %X{traceId:--}: :- 后面是默认值,这里表示 traceId 不存在时打印-。(注意" :- "后面还有个" - ")

请求链路追踪过滤器

配置文件

使用配置文件,我们可以很方便的控制链路追踪的启停,同时更规范的管理使用到的 key:

java 复制代码
@Data
@ConfigurationProperties(prefix = "x-polaris.web")
public class PolarisWebProperties {

    /**
     * 链路追踪配置
     */
    private Trace trace = new Trace();

    @Data
    public static class Trace {
        /**
         * 是否开启链路追踪,默认 true
         */
        private boolean enabled = true;

        /**
         * 链路追踪 ID 的 Header 名称,默认 X-Trace-Id
         */
        private String headerName = "X-Trace-Id";

        /**
         * MDC 中的 Key 名称,默认 traceId
         */
        private String mdcKey = "traceId";
    }
}

过滤器

java 复制代码
@RequiredArgsConstructor
public class TraceIdFilter extends OncePerRequestFilter {

    private final PolarisWebProperties properties;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        
        PolarisWebProperties.Trace traceConfig = properties.getTrace();
        if (!traceConfig.isEnabled()) {
            // 未启用时,直接放行
            filterChain.doFilter(request, response);
            return;
        }

        // 获取请求头已有的 traceId,如果不存在才新建一个
        String traceId = request.getHeader(traceConfig.getHeaderName());
        if (!StringUtils.hasText(traceId)) {
            traceId = UUID.randomUUID().toString().replace("-", "");
        }

        try {
            // 记录 traceId 到 MDC,方便后续日志打印
            MDC.put(traceConfig.getMdcKey(), traceId);
            response.setHeader(traceConfig.getHeaderName(), traceId);
            filterChain.doFilter(request, response);
        } finally {
            // 移除 MDC 中的 traceId,防止内存泄漏
            MDC.remove(traceConfig.getMdcKey());
        }
    }
}

日志配置(logback)

下面是使用 logback 的简单的日志配置:

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- 定义日志格式,增加 Trace ID [%X{traceId}] -->
    <property name="CONSOLE_LOG_PATTERN" value="%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr([%X{traceId:--}]){yellow} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/>

    <!-- 引入 Spring Boot 官方预定义的 Logback 配置 -->
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    <include resource="org/springframework/boot/logging/logback/console-appender.xml"/>

    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
    </root>
</configuration>

简单测试

现在我们启动项目,此时可以看到控制台打印了 [-],我们请求接口(接口中有日志打印的话)就能看到对应的 traceId 了,类似 [d7dc27eba473456cafcf6dfe1ff2836f]

问题

MDC 是线程独立的,那么如果请求中开启了新的线程,又怎么处理呢??

相关推荐
天天摸鱼的java工程师2 小时前
volatile 关键字底层原理:为什么它不能保证原子性?
java·后端
leikooo2 小时前
SpringAI 多轮对话报错 400 Bad Request
后端·ai编程
小杨同学492 小时前
C 语言实战:堆内存存储字符串 + 多种递归方案计算字符串长度
数据库·后端·算法
golang学习记2 小时前
Go 中防止敏感数据意外泄露的几种姿势
后端
czlczl200209252 小时前
Spring Boot 构建 SaaS 多租户架构
spring boot·后端·架构
小码编匠2 小时前
完美替代 Navicat,一款开源免费、集成了 AIGC 能力的多数据库客户端工具!
数据库·后端·aigc
顺流2 小时前
从零实现一个数据结构可视化调试器(一)
后端
掘金者阿豪2 小时前
Redis键值对批量删除全攻略:安全高效删除包含特定模式的键
后端
星浩AI2 小时前
深入理解 LlamaIndex:RAG 框架核心概念与实践
人工智能·后端·python