Linking Distributed Logs with a Unified traceId in Spring Boot
This guide explains how to generate a unique traceId for each request, propagate it through REST, MQ, and RPC modules, and configure Log4j2 to include the traceId in log output, enabling clear end‑to‑end request tracing in a Spring Boot project.
Background
When taking over an unmaintained Java project, console logs lacked any request‑level correlation, making it impossible to trace a request across the REST, MQ and (potential) RPC layers.
Approach
Generate a globally unique traceId at the entry point of each request or transaction, propagate it downstream, and configure Log4j2 to output the identifier in every log line. The identifier is stored in MDC (or a custom thread‑local context) so that Log4j2 pattern converters can retrieve it.
Implementation
1. REST layer
Extract the incoming traceId from HTTP headers (or generate one if missing) and use a custom Log4j2 Message and pattern converter.
Create a TraceMessage that extends ParameterizedMessage and holds a traceId.
Implement TraceMessageFactory (extends AbstractMessageFactory) that builds TraceMessage instances, pulling the current traceId from MDC.
Register a Log4j2 plugin TraceIdPatternConverter that formats the traceId for the log pattern.
@Getter
@Setter
public class TraceMessage extends ParameterizedMessage {
private String traceId;
public TraceMessage(String traceId, String pattern, Object... args) {
super(pattern, args);
this.traceId = traceId;
}
}
public class TraceMessageFactory extends AbstractMessageFactory {
@Override
public Message newMessage(String message, Object... params) {
String traceId = MDC.get("traceId"); // obtain from MDC or other context
return new TraceMessage(traceId, message, params);
}
// other overloads omitted for brevity
}
@Plugin(name = "TraceIdPatternConverter", category = PatternConverter.CATEGORY)
@ConverterKeys({"traceId"})
public class TraceIdPatternConverter extends LogEventPatternConverter {
private TraceIdPatternConverter(String name, String style) {
super(name, style);
}
public static TraceIdPatternConverter newInstance() {
return new TraceIdPatternConverter("TraceIdPatternConverter", "TraceIdPatternConverter");
}
@Override
public void format(LogEvent event, StringBuilder toAppendTo) {
Message msg = event.getMessage();
if (msg instanceof TraceMessage) {
String id = ((TraceMessage) msg).getTraceId();
toAppendTo.append('[').append(StringUtils.defaultIfBlank(id, "")).append(']');
} else {
toAppendTo.append("~");
}
}
}Log4j2 pattern example (add to log4j2.xml or log4j2.properties):
[%d{yyyy-MM-dd HH:mm:ss.SSS}][%t][%level][%C:%L][%traceId] %m%n2. MQ layer (RocketMQ example)
When a consumer receives a batch of MessageExt objects, concatenate their msgId values, store the result in MDC under the key msgId, and let the same TraceIdPatternConverter render it.
@Slf4j
@Aspect
@Component
public class LogRocketMQAspect {
@Pointcut("execution(* org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently.consumeMessage(..))")
public void pointCut() {}
@Around("pointCut()")
public Object injectTraceId(ProceedingJoinPoint pjp) throws Throwable {
try {
List<MessageExt> msgs = (List<MessageExt>) pjp.getArgs()[0];
String combinedId = msgs.stream()
.map(MessageExt::getMsgId)
.collect(Collectors.joining("-"));
MDC.put("msgId", combinedId);
return pjp.proceed(pjp.getArgs());
} finally {
MDC.clear();
}
}
}The converter checks MDC for msgId first; if present it prints that value, otherwise it falls back to the traceId from the REST side.
3. RPC layer (Dubbo example)
Even though the current project does not contain RPC code, the following filter demonstrates how to propagate the identifier via Dubbo’s RpcContext attachments.
@Activate(order = 99, group = {Constants.PROVIDER_PROTOCOL, Constants.CONSUMER_PROTOCOL})
public class LogAttachmentFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
RpcContext ctx = RpcContext.getContext();
if (ctx.isConsumerSide()) {
String traceId = MDC.get("traceId");
if (StringUtils.isBlank(traceId)) {
traceId = UuidUtils.getUuid(); // generate if missing
}
ctx.setAttachment("traceId", traceId);
} else if (ctx.isProviderSide()) {
LogContext.setTraceId(ctx.getAttachment("traceId"));
}
return invoker.invoke(invocation);
}
}Result
After adding trace‑id generation, propagation, and Log4j2 formatting to the REST, MQ and optional RPC modules, every log entry contains a consistent identifier. Developers can now reconstruct the full request flow across services by filtering on traceId (or msgId for MQ).
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.
Java Backend Technology
Focus on Java-related technologies: SSM, Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading. Occasionally cover DevOps tools like Jenkins, Nexus, Docker, and ELK. Also share technical insights from time to time, committed to Java full-stack development!
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.
