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.

Architect
Architect
Architect
Implement End‑to‑End TraceId Logging Across Rest, MQ, and RPC in Java

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%n

Define 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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaMicroservicesloggingDistributed Tracinglog4j2traceidmdc
Architect
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.