Backend Development 12 min read

Traceable Logging in Spring Boot: Using Logback, Interceptors, MDC and Custom Thread Pools

This article demonstrates how to implement end‑to‑end traceable logging in a Spring Boot application by configuring Logback, creating a request interceptor that injects a TRACE_ID, propagating the ID across thread pools with custom executors and MDC utilities, and verifying the solution with sample controller code and log output.

Architect
Architect
Architect
Traceable Logging in Spring Boot: Using Logback, Interceptors, MDC and Custom Thread Pools

The author starts by describing a common problem in large business call chains where logs from multiple services interleave, making it hard to follow a single request.

To solve this, the article proposes concatenating logs that belong to the same business call by assigning a unique TRACE_ID to each request and ensuring the ID is present in every log entry.

Project structure

The sample project includes the following key parts:

1. <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.16.10</version> </dependency> </dependencies> – basic Spring Boot dependencies and Lombok.

2. <?xml version="1.0" encoding="UTF-8"?> <configuration debug="false"> <property name="log" value="D:/test/log"/> <appender name="console" class="ch.qos.logback.core.ConsoleAppender"> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <pattern>[%X{TRACE_ID}] %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern> </encoder> </appender> <appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender"> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <FileNamePattern>${log}/%d{yyyy-MM-dd}.log</FileNamePattern> <MaxHistory>30</MaxHistory> </rollingPolicy> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <pattern>[%X{TRACE_ID}] %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern> </encoder> <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy"> <MaxFileSize>10MB</MaxFileSize> </triggeringPolicy> </appender> <root level="INFO"> <appender-ref ref="console"/> <appender-ref ref="file"/> </root> </configuration> – a simple Logback configuration that prints the TRACE_ID in every log line.

3. application.yml server: port: 8826 logging: config: classpath:logback-spring.xml – sets the server port and points Spring Boot to the Logback config.

4. public class LogInterceptor implements HandlerInterceptor { private static final String TRACE_ID = "TRACE_ID"; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String tid = UUID.randomUUID().toString().replace("-", ""); if (!StringUtils.isEmpty(request.getHeader("TRACE_ID"))) { tid = request.getHeader("TRACE_ID"); } MDC.put(TRACE_ID, tid); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) { MDC.remove(TRACE_ID); } } – a Spring MVC interceptor that generates a TRACE_ID (or reuses one from the request header) and stores it in MDC.

5. @Configuration public class WebConfigurerAdapter implements WebMvcConfigurer { @Bean public LogInterceptor logInterceptor() { return new LogInterceptor(); } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(logInterceptor()); } } – registers the interceptor with the Spring MVC framework.

6. To handle asynchronous execution, a custom thread pool is defined:

@Configuration @EnableAsync public class ThreadPoolConfig { @Bean("MyExecutor") public Executor asyncExecutor() { MyThreadPoolTaskExecutor executor = new MyThreadPoolTaskExecutor(); executor.setCorePoolSize(5); executor.setMaxPoolSize(5); executor.setQueueCapacity(500); executor.setKeepAliveSeconds(60); executor.setThreadNamePrefix("asyncJCccc"); executor.initialize(); return executor; } }

7. public final class MyThreadPoolTaskExecutor extends ThreadPoolTaskExecutor { @Override public void execute(Runnable task) { super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap())); } @Override public Future submit(Callable task) { return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap())); } @Override public Future submit(Runnable task) { return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap())); } } – overrides execution methods to copy the MDC context to child threads.

8. public final class ThreadMdcUtil { private static final String TRACE_ID = "TRACE_ID"; public static String generateTraceId() { return UUID.randomUUID().toString(); } public static void setTraceIdIfAbsent() { if (MDC.get(TRACE_ID) == null) { MDC.put(TRACE_ID, generateTraceId()); } } public static Callable wrap(final Callable callable, final Map context) { return () -> { if (context == null) { MDC.clear(); } else { MDC.setContextMap(context); } setTraceIdIfAbsent(); try { return callable.call(); } finally { MDC.clear(); } }; } public static Runnable wrap(final Runnable runnable, final Map context) { return () -> { if (context == null) { MDC.clear(); } else { MDC.setContextMap(context); } setTraceIdIfAbsent(); try { runnable.run(); } finally { MDC.clear(); } }; } } – utility that propagates MDC (including TRACE_ID) to asynchronous tasks.

A simple test controller is added to illustrate the effect:

@PostMapping("doTest") public String doTest(@RequestParam("name") String name) throws InterruptedException { log.info("入参 name={}", name); testTrace(); log.info("调用结束 name={}", name); return "Hello," + name; } private void testTrace() { log.info("这是一行info日志"); log.error("这是一行error日志"); testTrace2(); } private void testTrace2() { log.info("这也是一行info日志"); }

Running the application shows that each log line contains the same TRACE_ID, even when the request spawns asynchronous tasks. The author also demonstrates that without the MDC‑aware thread pool, the child thread loses the TRACE_ID, and then shows the corrected output after applying the custom executor.

Finally, the article includes a series of promotional notes and links, but the technical core provides a complete, reproducible solution for traceable logging in Spring Boot back‑end services.

asynchronousloggingSpring BootInterceptorthread pooltrace IDMDC
Architect
Written by

Architect

Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.