Implement End‑to‑End TraceId Logging Across Rest, MQ, and RPC in Java
This article walks through a practical approach to generate a unique traceId at request entry, propagate it through REST, RocketMQ, and Dubbo RPC modules, and configure Log4j2 to print the traceId so that logs from different services can be correlated into a single request chain.
Background
During maintenance of an unmaintained Java project, logs from different layers (REST, MQ, RPC) lacked a common identifier, making it impossible to reconstruct the full request flow.
Solution Overview
Introduce a globally unique traceId generated at the entry point of a request or transaction and propagate it through all downstream calls. Configure Log4j2 to output the traceId in every log line so logs from different modules can be correlated.
REST Module
For HTTP requests, extract traceId from an incoming header (e.g., X-Trace-Id) or generate one if absent. Use a custom Log4j2 pattern converter to render the identifier.
Log pattern (log4j2.xml):
[%d{yyyy-MM-dd HH:mm:ss.SSS}][%t][%level][%C:%L][%traceId] %msg%nDefine a message class that carries the trace identifier:
@Getter
@Setter
public class TraceMessage extends ParameterizedMessage {
private final String traceId;
public TraceMessage(String traceId, String messagePattern, Object... arguments) {
super(messagePattern, arguments);
this.traceId = traceId;
}
}Factory that creates TraceMessage instances by reading the current trace identifier from a thread‑local holder (e.g., MDC):
public class TraceMessageFactory extends AbstractMessageFactory {
@Override
public Message newMessage(String message, Object... params) {
String traceId = TraceContextHolder.getTraceId(); // custom holder
return new TraceMessage(traceId, message, params);
}
// other overloads delegate to this method
}Custom pattern converter that prints the identifier when the log event contains a TraceMessage:
@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("[‑]");
}
}
}MQ Module
When consuming messages from RocketMQ, the broker supplies a msgId. Propagating the original traceId through the message body is often impractical, so the consumer substitutes the msgId for the trace identifier.
An AOP aspect intercepts the RocketMQ listener, extracts the msgId, and stores it in MDC so the Log4j2 converter can pick it up:
@Slf4j
@Aspect
@Component
public class LogRocketMQAspect {
@Pointcut("execution(* org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently.consumeMessage(..))")
public void consumeMessagePointcut() {}
@Around("consumeMessagePointcut()")
public Object injectTraceId(ProceedingJoinPoint pjp) throws Throwable {
try {
List<MessageExt> msgs = (List<MessageExt>) pjp.getArgs()[0];
String combinedMsgId = msgs.stream()
.map(MessageExt::getMsgId)
.collect(Collectors.joining("-"));
MDC.put("traceId", combinedMsgId);
return pjp.proceed(pjp.getArgs());
} finally {
MDC.clear();
}
}
}The same TraceIdPatternConverter can be reused; it will read the traceId value from MDC.
RPC Module
For RPC frameworks such as Dubbo, the trace identifier is attached to the RpcContext. A filter runs on both consumer and provider sides.
@Activate(order = 99, group = {Constants.PROVIDER_PROTOCOL, Constants.CONSUMER_PROTOCOL})
public class TraceIdDubboFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
RpcContext ctx = RpcContext.getContext();
if (ctx.isConsumerSide()) {
String traceId = TraceContextHolder.getTraceId(); // obtain from upstream
if (StringUtils.isBlank(traceId)) {
traceId = UUID.randomUUID().toString();
}
ctx.setAttachment("traceId", traceId);
} else if (ctx.isProviderSide()) {
String incoming = ctx.getAttachment("traceId");
TraceContextHolder.setTraceId(incoming);
}
return invoker.invoke(invocation);
}
}Conclusion
By generating a traceId at the request entry, propagating it through HTTP headers, MDC (for MQ), or RPC attachments, and configuring Log4j2 to emit the identifier, logs from REST, MQ, and RPC modules become a single, ordered trace. This greatly simplifies debugging, performance analysis, and end‑to‑end monitoring.
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.
Architect
Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.
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.
