SpringBoot Global Logging & Request Tracing with AOP & MDC
This article demonstrates how to build a production-ready global logging system in SpringBoot using MDC for trace IDs, a Filter for request interception, AOP for automatic request/response logging, and custom Jackson serializers for sensitive data masking, plus best practices for async logging and thread-pool context propagation.
Why Global Logging and Trace IDs Are Needed
In microservices and high-concurrency architectures, debugging a failed order requires correlating logs across services. Without a unique identifier, finding the relevant request among concurrent traffic is like finding a needle in a haystack. When multiple services are involved, logs must be stitched across machines. The solution requires:
TraceId : A unique ID generated per request that flows through the entire call chain, linking all related log entries.
Global logging : Automatic capture of input parameters, output results, and latency, eliminating manual log.info(...) calls.
Core Component: MDC (Mapped Diagnostic Context)
MDC is an SLF4J mechanism backed by a thread-safe ThreadLocal<Map>. Key-value pairs (e.g., TraceId) placed into MDC can be rendered in Logback patterns via %X{key}.
1. TraceId Utility Class
public class TraceIdUtil {
private static final String TRACE_ID_KEY = "traceId";
public static String getTraceId() {
String traceId = MDC.get(TRACE_ID_KEY);
return StringUtils.isEmpty(traceId) ? "" : traceId;
}
public static void setTraceId(String traceId) {
MDC.put(TRACE_ID_KEY, traceId);
}
public static void removeTraceId() {
MDC.remove(TRACE_ID_KEY);
}
}The removeTraceId method is critical to prevent data leakage when threads are reused from a pool.
2. Filter to Set TraceId on Incoming Requests
@Slf4j
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class TraceIdFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
try {
String traceId = httpRequest.getHeader("X-Trace-Id");
if (StringUtils.isEmpty(traceId)) {
traceId = UUID.randomUUID().toString().replace("-", "");
}
TraceIdUtil.setTraceId(traceId);
chain.doFilter(request, response);
} finally {
TraceIdUtil.removeTraceId();
}
}
}The filter runs first ( @Order(Ordered.HIGHEST_PRECEDENCE)), reads an existing X-Trace-Id header (useful when a gateway forwards the ID), or generates a new UUID without hyphens. The finally block guarantees cleanup.
3. Logback Pattern Configuration
<configuration>
<property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] %-5level %logger{36} - %msg%n"/>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>Output example:
2023-10-01 12:00:00.001 [http-nio-8080-exec-1] [a1b2c3d4e5f6...] INFO c.e.controller.UserController - User created successfullyAOP for Global Interface Logging
MDC links logs, but we still need automatic capture of request/response payloads and latency. AOP is the ideal fit.
1. Enable AOP
Add spring-boot-starter-aop dependency.
2. Logging Aspect Implementation
@Slf4j
@Aspect
@Component
public class ControllerLogAspect {
@Pointcut("execution(public * com.example..controller..*.*(..))")
public void controllerLog() {} @Around("controllerLog()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
LogInfo logInfo = new LogInfo();
logInfo.setTraceId(TraceIdUtil.getTraceId());
logInfo.setMethod(request.getMethod());
logInfo.setUrl(request.getRequestURI());
logInfo.setIp(request.getRemoteAddr());
logInfo.setClassMethod(joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
Object[] args = joinPoint.getArgs();
Object[] logArgs = Arrays.stream(args)
.filter(arg -> !(arg instanceof HttpServletRequest) && !(arg instanceof HttpServletResponse))
.toArray();
logInfo.setArgs(logArgs);
log.info("Request Info: {}", JSON.toJSONString(logInfo));
Object result;
try {
result = joinPoint.proceed();
} catch (Exception e) {
log.error("Controller Exception: ", e);
throw e;
}
long costTime = System.currentTimeMillis() - startTime;
logInfo.setCostTime(costTime);
logInfo.setResult(result);
log.info("Response Info ({}ms): {}", costTime, JSON.toJSONString(logInfo));
return result;
}
@Data
public static class LogInfo {
private String traceId;
private String method;
private String url;
private String ip;
private String classMethod;
private Object[] args;
private Object result;
private Long costTime;
}
}Key details:
Pointcut targets all public methods under com.example..controller. RequestContextHolder provides access to the current HttpServletRequest. HttpServletRequest / HttpServletResponse are filtered out because they are not serializable.
Latency is measured with System.currentTimeMillis() before and after joinPoint.proceed().
Exceptions are logged and re-thrown so the global exception handler can still process them.
Sensitive Data Masking
Production logs must never contain plaintext passwords, ID numbers, or phone numbers. The article shows a custom Jackson JsonSerializer<String> that masks values by keeping the first 3 and last 4 characters, replacing the middle with asterisks.
public class SensitiveDataSerializer extends JsonSerializer<String> {
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
if (StringUtils.isEmpty(value)) {
gen.writeNull();
return;
}
if (value.length() > 8) {
gen.writeString(value.substring(0, 3) + "****" + value.substring(value.length() - 4));
} else {
gen.writeString("****");
}
}
}Usage on a DTO field:
@JsonSerialize(using = SensitiveDataSerializer.class)
private String phone;Alternatively, a regex-based replacement on the full JSON string inside the AOP can be applied.
Best Practices
AsyncAppender : Wrap Console and File appenders with AsyncAppender in Logback to avoid blocking business threads under heavy log volume.
Thread-pool MDC propagation : @Async or custom thread pools do not automatically inherit MDC context. Implement TaskDecorator or wrap ThreadPoolTaskExecutor to copy MDC on task submission and restore after execution.
Avoid logging large objects : Filter out MultipartFile or huge collections in the aspect to prevent log bloat.
Log level control : Use DEBUG in development, INFO/WARN in production; ERROR must trigger alerts.
Summary
The combination of MDC + Filter + AOP delivers:
End-to-end tracing : Every request carries a unique TraceId.
Automatic auditing : Input/output parameters and latency recorded without touching business code.
Data security : Sensitive fields masked to meet compliance requirements.
This stack is standard for production-grade SpringBoot observability.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Xiaolin Talks Programming
Focuses on sharing original technical insights. Senior architect at a top tech company with years of experience in technical architecture and management, and extensive interview experience. Offers one-on-one technical coaching, guiding you from beginner to architecture design to technical management. Follow for free learning resources. Free one-on-one interview coaching to help you land offers quickly.
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.
