写在最前

在日常开发中,日志虽然随处可见,但很多时候我们却难以快速定位问题根因。在没有统一的 traceId 和用户信息时,日志就像“无头苍蝇”,排查问题既费时又费力。因此本篇章就来详细讲讲如何优雅地实现日志 traceId 和用户信息的自动贯穿与记录。

1.操作流程

1.1 LogContextUtil

这个工具类 LogContextUtil 是用来在日志中添加用户相关信息的,比如当前请求的用户 ID、用户名、邮箱,以及一个唯一的 traceId。它通过 Spring Security 获取当前登录用户的信息(要求是 OIDC 登录),然后放到日志的上下文(MDC)里,这样日志输出时可以自动带上这些信息,方便排查问题。处理完后要记得调用 clearContext() 清除 MDC,避免数据污染其他请求。常用于拦截器或过滤器中。


import org.slf4j.MDC;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;

import java.util.UUID;

public class LogContextUtil {
    public static final String TRACE_ID = "traceId";
    public static final String USER_ID = "userId";
    public static final String USERNAME = "username";
    public static final String EMAIL = "email";

    public static void setContext() {
        // 设置跟踪ID
        MDC.put(TRACE_ID, UUID.randomUUID().toString());

        // 获取当前认证信息
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication != null && authentication.getPrincipal() instanceof OidcUser) {
            OidcUser oidcUser = (OidcUser) authentication.getPrincipal();

            // 设置用户信息到MDC
            MDC.put(USER_ID, oidcUser.getSubject());
            MDC.put(USERNAME, oidcUser.getPreferredUsername());
            MDC.put(EMAIL, oidcUser.getEmail());
        }
    }

    public static void clearContext() {
        MDC.clear();
    }
}

1.2 TraceIdInterceptor

TraceIdInterceptor 是一个 Spring MVC 拦截器,用于在每个 HTTP 请求开始时通过 LogContextUtil.setContext() 生成唯一的 traceId,并将当前登录用户信息(如 userId、username、email)写入日志上下文(MDC),在请求结束后通过 clearContext() 清除这些信息,既方便日志追踪,又避免线程复用带来的数据污染问题。

每次 HTTP 请求到来时,TraceIdInterceptor 的 preHandle 调用 LogContextUtil.setContext() 生成 traceId 并放入 MDC。请求线程内的所有日志(包括 Controller、Service、ExceptionHandler)都会自动携带 traceId、user、userId,无需手动传递。MDC 为线程级上下文,请求结束时 afterCompletion 会清理 MDC,防止 traceId 泄漏。不建议在业务代码中手动调用 setContext(),避免覆盖 traceId。

import com.tanqidi.survey.common.utils.LogContextUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;

public class TraceIdInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        LogContextUtil.setContext();
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        LogContextUtil.clearContext();
    }
}

1.3 log4j.xml

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" monitorInterval="30">
    <Properties>
        <Property name="LOG_PATTERN">
            %d{yyyy-MM-dd HH:mm:ss.SSS} %highlight{%-5level} [%style{%t}{bright,blue}] [traceId=%X{traceId}] [user=%X{username}] [userId=%X{userId}] %style{%C{1.}}{bright,yellow}: %msg%n%throwable
        </Property>
        <Property name="FILE_PATH">logs</Property>
        <Property name="FILE_NAME">survey-service</Property>
    </Properties>

    <Appenders>
        <Console name="ConsoleAppender" target="SYSTEM_OUT">
            <PatternLayout pattern="${LOG_PATTERN}"/>
        </Console>

        <RollingFile name="FileAppender"
                     fileName="${FILE_PATH}/${FILE_NAME}.log"
                     filePattern="${FILE_PATH}/${FILE_NAME}-%d{yyyy-MM-dd}-%i.log">
            <PatternLayout pattern="${LOG_PATTERN}"/>
            <Policies>
                <SizeBasedTriggeringPolicy size="10MB"/>
                <TimeBasedTriggeringPolicy interval="1" modulate="true"/>
            </Policies>
            <DefaultRolloverStrategy max="10"/>
        </RollingFile>

        <!-- 异步日志配置 -->
        <Async name="AsyncAppender" bufferSize="512">
            <AppenderRef ref="FileAppender"/>
        </Async>
    </Appenders>

    <Loggers>
        <!-- 应用日志配置 -->
        <Logger name="com.tanqidi.survey" level="INFO" additivity="false">
            <AppenderRef ref="ConsoleAppender"/>
            <AppenderRef ref="AsyncAppender"/>
        </Logger>

        <!-- Spring框架日志配置 -->
        <Logger name="org.springframework" level="INFO" additivity="false">
            <AppenderRef ref="ConsoleAppender"/>
            <AppenderRef ref="AsyncAppender"/>
        </Logger>

        <!-- MyBatis日志配置 -->
        <Logger name="org.apache.ibatis" level="INFO" additivity="false">
            <AppenderRef ref="ConsoleAppender"/>
            <AppenderRef ref="AsyncAppender"/>
        </Logger>

        <!-- Mapper SQL日志配置 -->
        <Logger name="com.tanqidi.survey.mapper" level="INFO" additivity="false">
            <AppenderRef ref="ConsoleAppender"/>
            <AppenderRef ref="AsyncAppender"/>
        </Logger>

        <!-- 默认日志配置 -->
        <Root level="WARN">
            <AppenderRef ref="ConsoleAppender"/>
            <AppenderRef ref="AsyncAppender"/>
        </Root>
    </Loggers>
</Configuration>

1.4 实现效果

这样一来,任何请求的参数和日志都会自动携带用户信息,即使到了Service层,也能通过MDC轻松获取用户数据。借助INFO和DEBUG灵活控制日志输出,后续还可将日志通过Kafka传输,利用Logstash写入Elasticsearch,最终在Kibana中精准搜索用户信息,快速定位所需日志。

2025-06-01 07:43:59.486 INFO  [http-nio-8080-exec-8] [traceId=4919f35f-7f1a-4f50-b6c7-c0efcf858339] [user=test1] [userId=81367518-e5ca-4e26-a057-89925d5952e7] c.t.s.c.BaseController: 开始执行: 查询所有实体 - 参数: []
2025-06-01 07:43:59.486 INFO  [http-nio-8080-exec-8] [traceId=4919f35f-7f1a-4f50-b6c7-c0efcf858339] [user=test1] [userId=81367518-e5ca-4e26-a057-89925d5952e7] c.t.s.s.i.SysUserServiceImpl: 查询所有用户信息
2025-06-01 07:43:59.489 INFO  [http-nio-8080-exec-8] [traceId=4919f35f-7f1a-4f50-b6c7-c0efcf858339] [user=test1] [userId=81367518-e5ca-4e26-a057-89925d5952e7] c.t.s.c.BaseController: 结束执行: 查询所有实体 - 耗时: 4ms

2. 拓展部分

后续可以把log4j的日志推送到Kafka,Logstash再从Kafka拉取日志,处理后写进Elasticsearch,最后用Kibana做直观的可视化展示,方便实时监控和分析。