优秀的编程知识分享平台

网站首页 > 技术文章 正文

基于 SLF4J 的 MDC 实现日志链路追踪详解

nanyue 2024-12-29 04:55:22 技术文章 3 ℃

在复杂的分布式系统中,日志是故障排查和性能分析的关键工具。然而,当请求跨多个线程或服务时,保持日志的上下文信息变得困难。这时,MDC(Mapped Diagnostic Context)变得非常有用。SLF4J 提供了对 MDC 的支持,可以帮助我们在日志中保持上下文信息,实现日志链路追踪。

本文将详解如何使用 SLF4J 的 MDC 功能,实现请求、异步任务、线程池中的日志链路追踪。

一、什么是 MDC?

MDC 是一种用于在日志中添加上下文信息的机制。它允许将一些特定于当前线程的信息(如请求 ID、用户 ID 等)存储起来,并在日志输出时自动添加这些信息。这样可以帮助我们更好地追踪请求的整个生命周期。

二、基本使用

首先,确保你的项目中包含 SLF4J 及其实现的依赖。这里以 Logback 为例:

<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.30</version>
</dependency>

三、设置和清理 MDC

在处理请求的入口处(例如 Spring 的过滤器或拦截器),设置 MDC 信息:

import org.slf4j.MDC;

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    try {
        // 设置 MDC 信息
        MDC.put("requestId", generateRequestId());
        chain.doFilter(request, response);
    } finally {
        // 清理 MDC 信息
        MDC.clear();
    }
}

private String generateRequestId() {
    return UUID.randomUUID().toString();
}

在日志配置中使用 %X{key} 占位符来包含 MDC 信息,例如在 logback.xml 中:

<encoder>
    <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg %X{requestId}%n</pattern>
</encoder>

四、在异步任务中传递 MDC

在异步任务中,我们需要确保 MDC 信息能够从父线程传递到子线程。这里有几种常见的方式。

1. 使用装饰器模式

创建一个装饰器类来传递 MDC 信息:

import java.util.concurrent.Callable;

public class MDCTask<V> implements Callable<V> {
    private final Callable<V> task;
    private final Map<String, String> contextMap;

    public MDCTask(Callable<V> task) {
        this.task = task;
        this.contextMap = MDC.getCopyOfContextMap();
    }

    @Override
    public V call() throws Exception {
        if (contextMap != null) {
            MDC.setContextMap(contextMap);
        }
        try {
            return task.call();
        } finally {
            MDC.clear();
        }
    }
}

2. 使用自定义的线程池

创建一个自定义的线程池来确保 MDC 信息在任务执行时被传递:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

public class MDCExecutorService {

    private final ExecutorService executor;

    public MDCExecutorService(int poolSize) {
        this.executor = Executors.newFixedThreadPool(poolSize, r -> {
            Thread thread = Executors.defaultThreadFactory().newThread(new MDCRunnable(r));
            thread.setName("MDCThread-" + thread.getId());
            return thread;
        });
    }

    public void submit(Runnable task) {
        executor.submit(new MDCRunnable(task));
    }

    public void shutdown() {
        executor.shutdown();
    }
}

3. 使用 Spring Async 支持

如果使用 Spring 框架,可以通过配置 TaskDecorator 来实现 MDC 的传递:

import org.springframework.core.task.TaskDecorator;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;

import java.util.concurrent.Executor;

@Component
public class CustomAsyncConfigurer implements AsyncConfigurer {

    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setTaskDecorator(new MDCTaskDecorator());
        executor.initialize();
        return executor;
    }
}

class MDCTaskDecorator implements TaskDecorator {
    @Override
    public Runnable decorate(Runnable runnable) {
        Map<String, String> contextMap = MDC.getCopyOfContextMap();
        return () -> {
            if (contextMap != null) {
                MDC.setContextMap(contextMap);
            }
            try {
                runnable.run();
            } finally {
                MDC.clear();
            }
        };
    }
}

五、完整示例

结合上述内容,下面是一个完整的示例,展示如何在一个简单的 Spring Boot 应用中实现日志链路追踪。

1. 创建 Spring Boot 应用

@SpringBootApplication
public class MdcLoggingApplication {
    public static void main(String[] args) {
        SpringApplication.run(MdcLoggingApplication.class, args);
    }
}

2. 配置拦截器设置 MDC

@Component
public class MDCInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        MDC.put("requestId", UUID.randomUUID().toString());
        return true;
    }

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

3. 配置异步任务

@Component
public class AsyncService {

    private static final Logger logger = LoggerFactory.getLogger(AsyncService.class);

    @Async
    public void executeAsyncTask() {
        logger.info("Executing async task");
    }
}

4. Controller 使用异步任务

@RestController
public class MyController {

    private final AsyncService asyncService;

    @Autowired
    public MyController(AsyncService asyncService) {
        this.asyncService = asyncService;
    }

    @GetMapping("/test")
    public ResponseEntity<String> testAsync() {
        asyncService.executeAsyncTask();
        return ResponseEntity.ok("Async task executed");
    }
}

六、总结

通过 MDC,我们可以在分布式系统中实现完整的日志链路追踪,从而更好地进行故障排查和性能分析。本文展示了如何在同步和异步任务中使用 MDC,包括线程池的处理方式。希望这篇博客能够帮助你在实际项目中有效地应用 MDC,实现高效的日志管理。

最近发表
标签列表