Implement Simple Request Tracing in SpringBoot 3.2 with SLF4J and MDC
This tutorial shows how to build a lightweight request‑tracing solution in SpringBoot 3.2 using SLF4J, MDC, custom filters, logback configuration, and a global ResponseBodyAdvice to log and return trace IDs for improved debugging and observability.
Environment: SpringBoot 3.2.0
1. Introduction
Combining SpringBoot with SLF4J enables easy implementation of request tracing, improving maintainability and diagnosability. Tracing records the request chain via logs, allowing developers to quickly locate and resolve issues.
Instead of using external systems like Zipkin or SkyWalking, we build a simple tracing mechanism that stores a unique trace ID in MDC (Mapped Diagnostic Context) using ThreadLocal, ensuring thread safety.
Benefits of tracing
Improved maintainability and diagnosability : Understand service interactions and latency.
Optimized performance : Analyze dependencies and resource usage.
Enhanced security and reliability : Monitor runtime status and exceptions.
Assisted design and development : Reveal architecture and flow for better scalability.
2. Hands‑on implementation
We create a custom filter that generates or extracts a traceId from the request header and stores it in MDC.
<code>@WebFilter("/**")
@Component
public class TraceXFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(TraceXFilter.class);
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
String traceId = req.getHeader("x-trace");
if (!StringUtils.hasLength(traceId)) {
traceId = UUID.randomUUID().toString().replace("-", "").toUpperCase();
}
MDC.put(TRACE_KEY, traceId);
logger.info("请求: {}", req.getServletPath());
chain.doFilter(request, response);
}
}
</code>Configure logback‑spring.xml to output the trace ID:
<code><?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true">
<property name="DEFAULT_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %logger Line:%-3L - %msg%n"/>
<property name="TRACEX_PATTERN" value="%green(%d{yyyy-MM-dd HH:mm:ss.SSS}) %highlight(%-5level) [%yellow(%thread)] %logger Line:%-3L traceId:【%red(%X{traceXId})】 - %msg%n"/>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${DEFAULT_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<appender name="TRACEX" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${TRACEX_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<logger name="com.pack.tracex.test" additivity="false" level="INFO">
<appender-ref ref="TRACEX"/>
</logger>
<springProfile name="dev">
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</springProfile>
</configuration>
</code>Define a simple controller to test the tracing:
<code>private static final Logger logger = LoggerFactory.getLogger(TraceController.class);
@Resource
private HttpServletRequest request;
@Resource
private UsersService usersService;
@GetMapping("")
public List<Users> index() throws Exception {
logger.info("start uri: {}", "/tracex");
List<Users> list = this.usersService.list();
logger.info("end uri: {}", "/tracex");
return list;
}
</code>Running the endpoint produces log entries that include the same traceId for the request, the controller, and the SQL statements, enabling end‑to‑end correlation.
Two further improvements are suggested:
Log SQL bind parameters by adding a TRACE‑level logger for org.hibernate.orm.jdbc.bind .
Return the traceId to the client using a ResponseBodyAdvice implementation.
Adding the following logger configuration prints SQL parameters:
<code><logger name="org.hibernate.orm.jdbc.bind" level="TRACE">
<appender-ref ref="TRACEX"/>
</logger>
</code>Implementing a global ResponseBodyAdvice injects the trace ID into every response:
<code>@RestControllerAdvice
public class PackResponseBodyAdvice implements ResponseBodyAdvice<Object> {
@Resource
private ObjectMapper objectMapper;
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return !returnType.getParameterType().equals(R.class);
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
if (body instanceof String) {
try {
return objectMapper.writeValueAsString(R.success(response, MDC.get(TraceX.TRACE_KEY)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
if (body instanceof ResponseEntity<?> entity) {
return R.success(entity.getBody(), MDC.get(TraceX.TRACE_KEY));
}
return R.success(body, MDC.get(TraceX.TRACE_KEY));
}
}
</code>After this change, the API response contains the traceId , allowing developers to match client‑side errors with server logs.
Example response (image):
The article concludes that the simple tracing setup, together with the two optimizations, provides a practical way to improve debugging and observability in SpringBoot applications.
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.