194

In our software we extensively use MDC to track things like session IDs and user names for web requests. This works fine while running in the original thread.

However, there's a lot of things that need to be processed in the background. For that we use the java.concurrent.ThreadPoolExecutor and java.util.Timer classes along with some self-rolled async execution services. All these services manage their own thread pool.

This is what Logback's manual has to say about using MDC in such an environment:

A copy of the mapped diagnostic context can not always be inherited by worker threads from the initiating thread. This is the case when java.util.concurrent.Executors is used for thread management. For instance, newCachedThreadPool method creates a ThreadPoolExecutor and like other thread pooling code, it has intricate thread creation logic.

In such cases, it is recommended that MDC.getCopyOfContextMap() is invoked on the original (master) thread before submitting a task to the executor. When the task runs, as its first action, it should invoke MDC.setContextMapValues() to associate the stored copy of the original MDC values with the new Executor managed thread.

This would be fine, but it is a very easy to forget adding those calls, and there is no easy way to recognize the problem until it is too late. The only sign with Log4j is that you get missing MDC info in the logs, and with Logback you get stale MDC info (since the thread in the tread pool inherits its MDC from the first task that was ran on it). Both are serious problems in a production system.

I don't see our situation special in any way, yet I could not find much about this problem on the web. Apparently, this is not something that many people bump up against, so there must be a way to avoid it. What are we doing wrong here?

informatik01
  • 16,038
  • 10
  • 74
  • 104
Lóránt Pintér
  • 10,152
  • 14
  • 47
  • 53
  • 2
    If your application is deployed under JEE environment you can use java interceptors for setting the MDC context before the EJB invoke. – Maxim Kirilov Oct 14 '13 at 05:59
  • 2
    As of logback version 1.1.5, MDC values are no longer inherited by child threads. – Ceki Mar 17 '16 at 05:09
  • http://jira.qos.ch/browse/LOGBACK-422 solved – lyjackal May 26 '16 at 22:16
  • 3
    @Ceki The documentation needs to be updated: "A child thread automatically inherits a copy of the mapped diagnostic context of its parent." http://logback.qos.ch/manual/mdc.html – steffen Aug 02 '16 at 11:54
  • I created a pull request to slf4j that solves the issue of using MDC across threads (link https://github.com/qos-ch/slf4j/pull/150). May be, if people comment and ask for it, they will incorporate the change in the SLF4J :) – Valentin Kovalenko Jun 02 '17 at 15:21
  • Recently I reason more and more in functional terms, and I started considering logging in worker threads as an antipattern. The worker thread should do small piece of work, and return a result having no side effects, and logging is a side effect. The same you do in parallel stream processing whre you don't usually place logs. – user1708042 Nov 08 '22 at 15:40

7 Answers7

101

Yes, this is a common problem I've run into as well. There are a few workarounds (like manually setting it, as described), but ideally you want a solution that

  • Sets the MDC consistently;
  • Avoids tacit bugs where the MDC is incorrect but you don't know it; and
  • Minimizes changes to how you use thread pools (e.g. subclassing Callable with MyCallable everywhere, or similar ugliness).

Here's a solution that I use that meets these three needs. Code should be self-explanatory.

(As a side note, this executor can be created and fed to Guava's MoreExecutors.listeningDecorator(), if you use Guava's ListanableFuture.)

import org.slf4j.MDC;

import java.util.Map;
import java.util.concurrent.*;

/**
 * A SLF4J MDC-compatible {@link ThreadPoolExecutor}.
 * <p/>
 * In general, MDC is used to store diagnostic information (e.g. a user's session id) in per-thread variables, to facilitate
 * logging. However, although MDC data is passed to thread children, this doesn't work when threads are reused in a
 * thread pool. This is a drop-in replacement for {@link ThreadPoolExecutor} sets MDC data before each task appropriately.
 * <p/>
 * Created by jlevy.
 * Date: 6/14/13
 */
public class MdcThreadPoolExecutor extends ThreadPoolExecutor {

    final private boolean useFixedContext;
    final private Map<String, Object> fixedContext;

    /**
     * Pool where task threads take MDC from the submitting thread.
     */
    public static MdcThreadPoolExecutor newWithInheritedMdc(int corePoolSize, int maximumPoolSize, long keepAliveTime,
                                                            TimeUnit unit, BlockingQueue<Runnable> workQueue) {
        return new MdcThreadPoolExecutor(null, corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }

    /**
     * Pool where task threads take fixed MDC from the thread that creates the pool.
     */
    @SuppressWarnings("unchecked")
    public static MdcThreadPoolExecutor newWithCurrentMdc(int corePoolSize, int maximumPoolSize, long keepAliveTime,
                                                          TimeUnit unit, BlockingQueue<Runnable> workQueue) {
        return new MdcThreadPoolExecutor(MDC.getCopyOfContextMap(), corePoolSize, maximumPoolSize, keepAliveTime, unit,
                workQueue);
    }

    /**
     * Pool where task threads always have a specified, fixed MDC.
     */
    public static MdcThreadPoolExecutor newWithFixedMdc(Map<String, Object> fixedContext, int corePoolSize,
                                                        int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                                                        BlockingQueue<Runnable> workQueue) {
        return new MdcThreadPoolExecutor(fixedContext, corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }

    private MdcThreadPoolExecutor(Map<String, Object> fixedContext, int corePoolSize, int maximumPoolSize,
                                  long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
        this.fixedContext = fixedContext;
        useFixedContext = (fixedContext != null);
    }

    @SuppressWarnings("unchecked")
    private Map<String, Object> getContextForTask() {
        return useFixedContext ? fixedContext : MDC.getCopyOfContextMap();
    }

    /**
     * All executions will have MDC injected. {@code ThreadPoolExecutor}'s submission methods ({@code submit()} etc.)
     * all delegate to this.
     */
    @Override
    public void execute(Runnable command) {
        super.execute(wrap(command, getContextForTask()));
    }

    public static Runnable wrap(final Runnable runnable, final Map<String, Object> context) {
        return new Runnable() {
            @Override
            public void run() {
                Map previous = MDC.getCopyOfContextMap();
                if (context == null) {
                    MDC.clear();
                } else {
                    MDC.setContextMap(context);
                }
                try {
                    runnable.run();
                } finally {
                    if (previous == null) {
                        MDC.clear();
                    } else {
                        MDC.setContextMap(previous);
                    }
                }
            }
        };
    }
}
jlevy
  • 2,958
  • 2
  • 16
  • 11
  • In case the previous context is not empty, isn't it always garbage? Why do you carry it around? – djjeck Sep 19 '14 at 00:53
  • 2
    Right; it should not be set. It just seems like good hygiene, e.g. if the wrap() method were exposed and used by someone else down the road. – jlevy Jan 27 '15 at 17:42
  • Can you provide a reference as to how this MdcThreadPoolExecutor was attached or referenced by Log4J2? Is there somewhere where we need to specifically reference this class, or is it "automagically" done? I am not using Guava. I could, but I would like to know if there is some other way prior to using it. – jcb Aug 25 '16 at 19:24
  • 1
    If I understand your question correctly, the answer is yes, it's "magic" thread-local variables in SLF4J — see the implementations of MDC.setContextMap() etc. Also, by the way, this uses SLF4J, not Log4J, which is preferable as it works with Log4j, Logback, and other logging setups. – jlevy Aug 25 '16 at 19:32
  • Thanks @jlevy! I'm sorry if my question sounds basic, but if I understand correctly, as long as the MDCPoolExecutor is on the class path, the SLF4J MDC should pick it up and use it? Again, I apologize for the simple questions, but I am also trying to understand how this thread pool is used. – jcb Aug 26 '16 at 14:12
  • 1
    Since v1.7.7, SFL4J API uses generics and the context is defined as `Map`, not `Map`, so the code needs some minor change. – Pino Nov 14 '16 at 11:58
  • Is there any other way for doing above when using for example JBeret inside WildFly and you don't touch threads/ThreadPoolExecutor directly etc? – rtbf Dec 16 '16 at 15:08
  • Logging frameworks do not reference thread pool executors. They pull (via %X pattern for example) values from the (static) MDC methods. The executors just have to ensure that the assigned thread (from a pool) get a copy of the MDC from the thread that called it so that the context is conveyed down and henced logged correctly. – Darrell Teague Jul 28 '17 at 20:52
  • What's the difference between the 'submitting thread' and the 'thread that creates the pool'? (c.f JavaDocs in the two first methods) – kinbiko Jan 15 '18 at 13:11
  • 3
    Just for completeness: if you are using Spring's `ThreadPoolTaskExecutor` instead of plain Java `ThreadPoolExecutor`, you can use the `MdcTaskDecorator` described at https://moelholm.com/2017/07/24/spring-4-3-using-a-taskdecorator-to-copy-mdc-data-to-async-threads/ – Pino Feb 22 '18 at 15:09
  • @jcb I have the same question. How do I hook this class up? For example: I am using Micronaut and would love to know how to do what's mentioned in the answer above. – user770119 Dec 21 '18 at 23:23
  • Spent 3 days searching on Stack, and this post is still a solution nowadays – IQbrod Sep 30 '20 at 10:03
  • The updated link if you are using Spring's ThreadPoolTaskExecutor instead of plain Java ThreadPoolExecutor is https://moelholm.com/blog/2017/07/24/spring-43-using-a-taskdecorator-to-copy-mdc-data-to-async-threads – Bernie Lenz Apr 05 '23 at 17:16
28

We have run into a similar problem. You might want to extend ThreadPoolExecutor and override before/afterExecute methods to make the MDC calls you need before starting/stopping new threads.

valiano
  • 16,433
  • 7
  • 64
  • 79
Mark
  • 297
  • 2
  • 3
  • 12
    The methods `beforeExecute(Thread, Runnable)` and `afterExecute(Runnable, Throwable)` may be helpful in other cases but I'm not sure how this will work for setting MDCs. They are both executed under the the spawned thread. This means you need to be able to get hold of the updated map from the main thread before `beforeExecute`. – Kenston Choi Sep 28 '16 at 08:53
  • Better to set MDCs in the filter, that means when request is under processing by business logic, the context will not be updated. I don't think we should update MDC everywhere across the application – dereck Mar 15 '17 at 07:37
26

IMHO the best solution is to:

  • use ThreadPoolTaskExecutor
  • implement your own TaskDecorator
  • use it: executor.setTaskDecorator(new LoggingTaskDecorator());

The decorator can look like this:

private final class LoggingTaskDecorator implements TaskDecorator {

    @Override
    public Runnable decorate(Runnable task) {
        // web thread
        Map<String, String> webThreadContext = MDC.getCopyOfContextMap();
        return () -> {
            // work thread
            try {
                // TODO: is this thread safe?
                MDC.setContextMap(webThreadContext);
                task.run();
            } finally {
                MDC.clear();
            }
        };
    }

}
Tomáš Myšík
  • 309
  • 3
  • 5
  • Sorry, not really sure what you mean. UPDATE: I think I see now, will improve my answer. – Tomáš Myšík May 16 '18 at 06:42
  • 4
    Just to let others know: both `ThreadPoolTaskExecutor` and `TaskDecorator` are Spring classes. – kolistivra Mar 17 '21 at 10:24
  • 1
    Regarding your question about thread safety: MDC.getCopyOfContextMap() may be NOT thread safe, if used with outdated SLF4J, that just returns a mutable ThreadContext.getContext(). For newer SLF4J versions, new HashMap<>(ThreadContext.getContext()) is returned. – Andrey Jul 03 '21 at 04:54
16

This is how I do it with fixed thread pools and executors:

ExecutorService executor = Executors.newFixedThreadPool(4);
Map<String, String> mdcContextMap = MDC.getCopyOfContextMap();

In the threading part:

executor.submit(() -> {
    MDC.setContextMap(mdcContextMap);
    // my stuff
});
Amaury D
  • 475
  • 4
  • 7
  • 4
    A potential problem with this approach is: the context values are never UNSET. So subsequently if that thread is re-used in a place where the context is not overwritten, you'll get very confusing log messages. – Robert May 23 '22 at 18:39
13

In case you face this problem in a spring framework related environment where you run tasks by using @Async annotation you are able to decorate the tasks by using the TaskDecorator approach.

A sample of how to do it is provided here:

I faced this issue and the article above helped me to tackle it so that's why I am sharing it here.

informatik01
  • 16,038
  • 10
  • 74
  • 104
Soner
  • 213
  • 3
  • 9
2

Similar to the previously posted solutions, the newTaskFor methods for Runnable and Callable can be overwritten in order to wrap the argument (see accepted solution) when creating the RunnableFuture.

Note: Consequently, the executorService's submit method must be called instead of the execute method.

For the ScheduledThreadPoolExecutor, the decorateTask methods would be overwritten instead.

MyKey_
  • 837
  • 1
  • 7
  • 22
1

Another variation similar to existing answers here is to implement ExecutorService and allow a delegate to be passed to it. Then using generics, it can still expose the actual delegate in case one wants to get some stats (as long no other modification methods are used).

Reference code:

public class MDCExecutorService<D extends ExecutorService> implements ExecutorService {

    private final D delegate;

    public MDCExecutorService(D delegate) {
        this.delegate = delegate;
    }

    @Override
    public void shutdown() {
        delegate.shutdown();
    }

    @Override
    public List<Runnable> shutdownNow() {
        return delegate.shutdownNow();
    }

    @Override
    public boolean isShutdown() {
        return delegate.isShutdown();
    }

    @Override
    public boolean isTerminated() {
        return delegate.isTerminated();
    }

    @Override
    public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
        return delegate.awaitTermination(timeout, unit);
    }

    @Override
    public <T> Future<T> submit(Callable<T> task) {
        return delegate.submit(wrap(task));
    }

    @Override
    public <T> Future<T> submit(Runnable task, T result) {
        return delegate.submit(wrap(task), result);
    }

    @Override
    public Future<?> submit(Runnable task) {
        return delegate.submit(wrap(task));
    }

    @Override
    public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException {
        return delegate.invokeAll(wrapCollection(tasks));
    }

    @Override
    public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException {
        return delegate.invokeAll(wrapCollection(tasks), timeout, unit);
    }

    @Override
    public <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException {
        return delegate.invokeAny(wrapCollection(tasks));
    }

    @Override
    public <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
        return delegate.invokeAny(wrapCollection(tasks), timeout, unit);
    }

    @Override
    public void execute(Runnable command) {
        delegate.execute(wrap(command));
    }

    public D getDelegate() {
        return delegate;
    }

    /* Copied from https://github.com/project-ncl/pnc/blob/master/common/src/main/java/org/jboss/pnc/common
    /concurrent/MDCWrappers.java */

    private static Runnable wrap(final Runnable runnable) {
        final Map<String, String> context = MDC.getCopyOfContextMap();
        return () -> {
            Map previous = MDC.getCopyOfContextMap();
            if (context == null) {
                MDC.clear();
            } else {
                MDC.setContextMap(context);
            }
            try {
                runnable.run();
            } finally {
                if (previous == null) {
                    MDC.clear();
                } else {
                    MDC.setContextMap(previous);
                }
            }
        };
    }

    private static <T> Callable<T> wrap(final Callable<T> callable) {
        final Map<String, String> context = MDC.getCopyOfContextMap();
        return () -> {
            Map previous = MDC.getCopyOfContextMap();
            if (context == null) {
                MDC.clear();
            } else {
                MDC.setContextMap(context);
            }
            try {
                return callable.call();
            } finally {
                if (previous == null) {
                    MDC.clear();
                } else {
                    MDC.setContextMap(previous);
                }
            }
        };
    }

    private static <T> Consumer<T> wrap(final Consumer<T> consumer) {
        final Map<String, String> context = MDC.getCopyOfContextMap();
        return (t) -> {
            Map previous = MDC.getCopyOfContextMap();
            if (context == null) {
                MDC.clear();
            } else {
                MDC.setContextMap(context);
            }
            try {
                consumer.accept(t);
            } finally {
                if (previous == null) {
                    MDC.clear();
                } else {
                    MDC.setContextMap(previous);
                }
            }
        };
    }

    private static <T> Collection<Callable<T>> wrapCollection(Collection<? extends Callable<T>> tasks) {
        Collection<Callable<T>> wrapped = new ArrayList<>();
        for (Callable<T> task : tasks) {
            wrapped.add(wrap(task));
        }
        return wrapped;
    }
}
Kenston Choi
  • 2,862
  • 1
  • 27
  • 37
  • Why are you setting MDC again after the callable runs? I found this example but it seems to be clearing out the MDC on my calling thread: https://www.chintanradia.com/blog/pass-mdc-context-from-calling-thread-to-new-thread/ I thought MDC was thread local so anything you do on the runnable/callable is its own unique copy? – devo Aug 23 '21 at 15:05
  • @devo, if the thread originally has an MDC set, then `previous` is non-null, so in `finally` we just restore that back. In most cases, `previous` is `null` so `MDC.clear` is performed similar to the other example you found. Yes, MDC is thread-local. – Kenston Choi Aug 24 '21 at 03:37