Implementing TraceId-Based Distributed Logging for Rest, MQ, and RPC Modules with Log4j2
This article explains how to introduce a unique traceId for request tracing across Rest, MQ, and RPC modules using Log4j2, custom message factories, converters, and AOP, enabling clear, linked logs throughout the service chain.
Background The author inherited a project lacking maintenance and needed to perform major functional changes. During debugging, the console logs were unstructured, making it impossible to follow the request chain.
Solution Introduce a unique identifier called traceId that is generated at the entry point of each request or transaction and propagated downstream. By printing this traceId in every log entry, logs from upstream and downstream become correlated.
1. Rest Module
For the Rest module, the traceId can be added to the log output using Log4j2’s LogEventPatternConverter . The log pattern is configured as:
[%d{yyyy-MM-dd HH:mm:ss.SSS}][%t][%level][%C:%L][%traceId] %m%nDefine a custom TraceMessage class extending ParameterizedMessage :
@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;
}
}Create a TraceMessageFactory that overrides newMessage to produce TraceMessage instances:
public class TraceMessageFactory extends AbstractMessageFactory {
@Override
public Message newMessage(String message, Object... params) {
// obtain traceId from upstream
String traceId = "...";
return new TraceMessage(traceId, message, params);
}
// other overloads omitted for brevity
}Implement a Log4j2 converter to output the traceId :
@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
Using RocketMQ as an example, the consumer side receives a msgId from the message. An AOP aspect stores this msgId in MDC so that Log4j2 can treat it as the traceId :
@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 proceedingJoinPoint) throws Throwable {
try {
if (proceedingJoinPoint.getSignature().getName().equals("consumeMessage")) {
List
list = (List
) proceedingJoinPoint.getArgs()[0];
String messageId = list.stream().map(MessageExt::getMsgId).collect(Collectors.joining("-"));
MDC.put("msgId", messageId);
}
return proceedingJoinPoint.proceed(proceedingJoinPoint.getArgs());
} finally {
MDC.clear();
}
}
}The same TraceIdPatternConverter is reused, but it now prefers the MDC msgId when present:
@Plugin(name = "TraceIdPatternConverter", category = PatternConverter.CATEGORY)
@ConverterKeys({"traceId"})
public class TraceIdPatternConverter extends LogEventPatternConverter {
// constructor omitted
@Override
public void format(LogEvent event, StringBuilder toAppendTo) {
Message message = event.getMessage();
if (message instanceof TraceMessage) {
TraceMessage tm = (TraceMessage) message;
String id = StringUtils.isBlank(msgId) ? tm.getTraceId() : msgId;
toAppendTo.append("[" + id + "]");
return;
}
toAppendTo.append("~");
}
}3. RPC Module
Although the current project does not contain RPC, an example for Dubbo shows how to propagate traceId via 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 context = RpcContext.getContext();
if (context.isConsumerSide()) {
String traceId = "...";
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 these changes, the distributed logs are linked by the same traceId , making the request flow easy to follow.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
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.