Search code examples
javaspringlog4j2junit5maven-surefire-plugin

Log4j2 RoutingAppender with ListAppenders across Multiple Threads


(This is a follow-up from my original question: Log4j2 Custom Appender in Maven Surefire: Possible to Reuse Across Threads?)

I'm attempting to unit test the output of any logging coming from Log4j2 in my application Spring application via JUnit5. To help with this, I've created a JUnit5 extension that creates a WriterAppender before each test that I can use to verify any logged messages.

Maven is being used along with the Surefire plugin. By default, Surefire forks multiple threads to speed up testing. However, it's been pointed out that each of these test appenders will also contain the output of other tests that run at the same time.

(I've verified this to be the case for myself.)

It's been suggested to use a RoutingAppender that creates ListAppenders based on the value stored in ThreadContext which I'm attempting to do.

I've reviewed multiple posts, threads and articles about creating such a setup. It looks to be as though the best way to do it is via an XML configuration, and not to do so programmatically as I'm not sure it fits in conceptually with what's being attempted.

I've tried the following XML configuration (and variations thereof):

log4j2.xml

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" name="RoutingForTests">
    <Appenders>
        <Routing name="Routing">
            <Routes pattern="$${ctx:appenderName}">
                <Route>
                    <List name="$${ctx:appenderName}" />
                </Route>
            </Routes>
        </Routing>
    </Appenders>
    <Loggers>
        <Root level="info">
            <AppenderRef ref="Routing" />
        </Root>
    </Loggers>
</Configuration>

The ThreadContext is setup (correctly or incorrectly) via a JUnit5 registered extension, e.g.

LoggerExtension.java

public class LoggerExtension implements BeforeEachCallback, AfterEachCallback {
    // ...

    @Override
    public void beforeEach(ExtensionContext context) {
        // ...
        ThreadContext.put("appenderName", context.getUniqueId()); // I've even tried hardcoding this value
        // ...
    }

    public void verifyMessage(String message) {
        ListAppender appender = ListAppender.getListAppender(ThreadContext.get("appenderName")); // appender ends up null
        // ...
    }
}

I've also tried using a hardcoded ListAppender name, and tried using the same lookup variable but with a single $ instead of two (but I think that would resolve to a blank/null value since I don't think the ThreadContext has been properly setup at that point, though I can't be sure). I've also tried setting the key for the Route, but to no avail.

I even tried using ThreadId from event but any attempts to lookup the ListAppender have failed as well.

I'm guessing that I'm either missing something or misunderstanding how this is to be applied.

I also tried doing this programmatically but it turned out something of a mess.

Any help would be appreciated, and I apologize ahead of time if it's something simple that I've somehow managed to overlook.

Thank you in advance!

Sources:


Solution

  • The routing appender is a little bit peculiar, when it comes to property substitution:

    • The pattern attributes of the Routes element is evaluated twice (at configuration time and each time an event is logged) and therefore requires escaping the $ as you did,
    • The contents of the Route element, however, are evaluated only once, whenever pattern evaluates to a new value. Therefore it does not require escaping the $.

    You should use:

            <Routing name="Routing">
                <Routes pattern="$${ctx:appenderName}">
                    <Route>
                        <List name="${ctx:appenderName}" />
                    </Route>
                </Routes>
            </Routing>
    

    To retrieve the appender specific to a test use:

    LoggerContext context = LoggerContext.getContext(false);
    RoutingAppender routing = context.getConfiguration().getAppender("Routing");
    AppenderControl appenderRef = routing.getAppenders().get(extensionContext.getUniqueId());
    ListAppender list = (ListAppender) appenderRef.getAppender();
    

    Note: the list appenders are created lazily, if no message was logged, no appender will be present.