How to Link Distributed Logs with TraceId Across Rest, MQ, and RPC in Java
This article explains how to generate a unique traceId at request entry, propagate it through Rest, MQ, and RPC modules using Log4j2, MDC, and custom converters, and format logs so that distributed traces become easily readable and correlated across services.
Background
When taking over a project with no maintenance, the author found that log output during debugging lacked any context, making it impossible to follow the request chain.
The project only depends on Rest and MQ modules, so the solution focuses on those (and a reference for RPC).
Solution
Introduce a unique identifier (traceId) generated at the entry point of a request or transaction and propagate it downstream. By printing traceId in every log line, logs from different services can be correlated.
1. Rest Module
Use Log4j2's LogEventPatternConverter to include traceId in the log pattern. Define a custom TraceMessage class extending ParameterizedMessage, a TraceMessageFactory that creates TraceMessage, and a TraceIdPatternConverter that appends the traceId to the log output.
[%d{yyyy-MM-dd HH:mm:ss.SSS}][%t][%level][%C:%L][%traceId] %m%n @Getter
@Setter
public class TraceMessage extends ParameterizedMessage {
private String traceId;
public TraceMessage(String traceId, String spanId, String messagePattern, Object... arguments) {
super(messagePattern, arguments);
this.traceId = traceId;
}
} public class TraceMessageFactory extends AbstractMessageFactory {
@Override
public Message newMessage(String message, Object... params) {
String traceId = "..."; // obtain traceId from upstream
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 message = event.getMessage();
if (message instanceof TraceMessage) {
TraceMessage tm = (TraceMessage) message;
toAppendTo.append("[" + ObjectUtil.defaultIfBlank(tm.getTraceId(), "") + "]");
return;
}
toAppendTo.append("~");
}
}2. MQ Module
For RocketMQ, use the built‑in msgId as a surrogate traceId. An AOP aspect extracts the msgId before the consumer processes the message and stores it in MDC, which Log4j2 then reads.
@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 {
if (pjp.getSignature().getName().equals("consumeMessage")) {
List<MessageExt> list = (List<MessageExt>) pjp.getArgs()[0];
String messageId = list.stream()
.map(MessageExt::getMsgId)
.collect(Collectors.joining("-"));
MDC.put("msgId", messageId);
}
return pjp.proceed(pjp.getArgs());
} finally {
MDC.clear();
}
}
} @Plugin(name = "TraceIdPatternConverter", category = PatternConverter.CATEGORY)
@ConverterKeys({"traceId"})
public class TraceIdPatternConverter extends LogEventPatternConverter {
// same implementation as in the Rest module, but uses MDC.get("msgId") when available
}3. RPC Module (Dubbo example)
Even though the current project does not use RPC, a Dubbo filter is shown to illustrate how to put traceId into Dubbo's RPC context attachments and retrieve it on the provider side.
@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 context = RpcContext.getContext();
if (context.isConsumerSide()) {
String traceId = "..."; // obtain from upstream or generate
if (StringUtils.isBlank(traceId)) {
traceId = UuidUtils.getUuid();
}
context.setAttachment("traceId", traceId);
} else if (context.isProviderSide()) {
LogContext.setTraceId(context.getAttachment("traceId"));
}
return invoker.invoke(invocation);
}
}Conclusion
After applying the above changes, the distributed logs are linked by a common traceId, making the request flow easy to follow.
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.
