How to retain slf4j MDC logging context in CompletableFuture?
My solution theme would be to (It would work with JDK 9+ as a couple of overridable methods are exposed since that version)
Make the complete ecosystem aware of MDC
And for that, we need to address the following scenarios:
- When all do we get new instances of CompletableFuture from within this class? → We need to return a MDC aware version of the same rather.
- When all do we get new instances of CompletableFuture from outside this class? → We need to return a MDC aware version of the same rather.
- Which executor is used when in CompletableFuture class? → In all circumstances, we need to make sure that all executors are MDC aware
For that, let's create a MDC aware version class of CompletableFuture
by extending it. My version of that would look like below
import org.slf4j.MDC;
import java.util.Map;
import java.util.concurrent.*;
import java.util.function.Function;
import java.util.function.Supplier;
public class MDCAwareCompletableFuture<T> extends CompletableFuture<T> {
public static final ExecutorService MDC_AWARE_ASYNC_POOL = new MDCAwareForkJoinPool();
@Override
public CompletableFuture newIncompleteFuture() {
return new MDCAwareCompletableFuture();
}
@Override
public Executor defaultExecutor() {
return MDC_AWARE_ASYNC_POOL;
}
public static <T> CompletionStage<T> getMDCAwareCompletionStage(CompletableFuture<T> future) {
return new MDCAwareCompletableFuture<>()
.completeAsync(() -> null)
.thenCombineAsync(future, (aVoid, value) -> value);
}
public static <T> CompletionStage<T> getMDCHandledCompletionStage(CompletableFuture<T> future,
Function<Throwable, T> throwableFunction) {
Map<String, String> contextMap = MDC.getCopyOfContextMap();
return getMDCAwareCompletionStage(future)
.handle((value, throwable) -> {
setMDCContext(contextMap);
if (throwable != null) {
return throwableFunction.apply(throwable);
}
return value;
});
}
}
The MDCAwareForkJoinPool
class would look like (have skipped the methods with ForkJoinTask
parameters for simplicity)
public class MDCAwareForkJoinPool extends ForkJoinPool {
//Override constructors which you need
@Override
public <T> ForkJoinTask<T> submit(Callable<T> task) {
return super.submit(MDCUtility.wrapWithMdcContext(task));
}
@Override
public <T> ForkJoinTask<T> submit(Runnable task, T result) {
return super.submit(wrapWithMdcContext(task), result);
}
@Override
public ForkJoinTask<?> submit(Runnable task) {
return super.submit(wrapWithMdcContext(task));
}
@Override
public void execute(Runnable task) {
super.execute(wrapWithMdcContext(task));
}
}
The utility methods to wrap would be such as
public static <T> Callable<T> wrapWithMdcContext(Callable<T> task) {
//save the current MDC context
Map<String, String> contextMap = MDC.getCopyOfContextMap();
return () -> {
setMDCContext(contextMap);
try {
return task.call();
} finally {
// once the task is complete, clear MDC
MDC.clear();
}
};
}
public static Runnable wrapWithMdcContext(Runnable task) {
//save the current MDC context
Map<String, String> contextMap = MDC.getCopyOfContextMap();
return () -> {
setMDCContext(contextMap);
try {
return task.run();
} finally {
// once the task is complete, clear MDC
MDC.clear();
}
};
}
public static void setMDCContext(Map<String, String> contextMap) {
MDC.clear();
if (contextMap != null) {
MDC.setContextMap(contextMap);
}
}
Below are some guidelines for usage:
- Use the class
MDCAwareCompletableFuture
rather than the classCompletableFuture
. - A couple of methods in the class
CompletableFuture
instantiates the self version such asnew CompletableFuture...
. For such methods (most of the public static methods), use an alternative method to get an instance ofMDCAwareCompletableFuture
. An example of using an alternative could be rather than usingCompletableFuture.supplyAsync(...)
, you can choosenew MDCAwareCompletableFuture<>().completeAsync(...)
- Convert the instance of
CompletableFuture
toMDCAwareCompletableFuture
by using the methodgetMDCAwareCompletionStage
when you get stuck with one because of say some external library which returns you an instance ofCompletableFuture
. Obviously, you can't retain the context within that library but this method would still retain the context after your code hits the application code. - While supplying an executor as a parameter, make sure that it is MDC Aware such as
MDCAwareForkJoinPool
. You could createMDCAwareThreadPoolExecutor
by overridingexecute
method as well to serve your use case. You get the idea!
With that, your code would look like
List<CompletableFuture<UpdateHotelAllotmentsRsp>> futures =
tasks.stream()
new MDCAwareCompletableFuture<UpdateHotelAllotmentsRsp>().completeAsync(
() -> businesslogic(task))
.collect(Collectors.toList());
List results = futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
public UpdateHotelAllotmentsRsp businesslogic(Task task) {
LOGGER.info("mdc fishtag context is not lost here");
}
You can find a detailed explanation of all of the above here in a post about the same.
The most readable way I solved this problem was as below -
---------------Thread utils class--------------------
public static Runnable withMdc(Runnable runnable) {
Map<String, String> mdc = MDC.getCopyOfContextMap();
return () -> {
MDC.setContextMap(mdc);
runnable.run();
};
}
public static <U> Supplier<U> withMdc(Supplier<U> supplier) {
Map<String, String> mdc = MDC.getCopyOfContextMap();
return (Supplier) () -> {
MDC.setContextMap(mdc);
return supplier.get();
};
}
---------------Usage--------------
CompletableFuture.supplyAsync(withMdc(() -> someSupplier()))
.thenRunAsync(withMdc(() -> someRunnable())
....
WithMdc in ThreadUtils would have to be overloaded to include other functional interfaces which are accepted by CompletableFuture
Please note that the withMdc() method is statically imported to improve readability.
At the end I created a Supplier
wrapper retaining the MDC
. If anyone has a better idea feel free to comment.
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor) {
return CompletableFuture.supplyAsync(new SupplierMDC(supplier), executor);
}
private static class SupplierMDC<T> implements Supplier<T> {
private final Supplier<T> delegate;
private final Map<String, String> mdc;
public SupplierMDC(Supplier<T> delegate) {
this.delegate = delegate;
this.mdc = MDC.getCopyOfContextMap();
}
@Override
public T get() {
MDC.setContextMap(mdc);
return delegate.get();
}
}