Search code examples
javatomcatlog4j2logback

Can a JAR shared by two web applications log to the same file in Tomcat?


Let's say we have two web applications and a Tomcat instance loading shared JARs (depedencies for both applications) from an external directory (via the means of shared.loader defined in catalina.properties). Therefore, these dependencies are not packaged into the WAR files.

Let's also say that:

  • Both web applications depend on a particular shared JAR file, which uses a logging framework (log4j2 at the moment but that's not required).
  • Both web applications use a logging framework of their own (we don't care whether they are identical or not, as long as things work as expected), and different logging configurations.

What we would like to achieve is for the shared JAR to reliably log to the same file, regardless of which web application its methods are called. To our understanding, both web applications have different logging contexts and having two such contexts log to the same file is either not possible or at least dangerous. If that's not true or doesn't have to be true, please elaborate.

The question: is it possible to achieve the above scenario with a single logging context? If so, could you please provide an example to make it working (the crucial bits will perfectly suffice), using lo4j2 or logback? Are there any catches?

Please note that we would like to avoid setting up a special servlet in one of the web applications for this (so the other web application would call it instead of logging directly to a file). Using (e.g.) syslog instead might be a solution perhaps but still, let's keep this question focused on the described scenario please.


Solution

  • After some research, trial and error, we managed to satisfy our requirements:

    • Each web application logs to its own files.
    • The shared JAR always logs to its own file too.

    At least with Log4j2, the problem appears to revolve around class loaders. In our case, classes from the shared JAR file, and all of its dependencies, were always loaded using a class loader dedicated to (shared.loader). This means that Log4j2's JAR files need to be there beside the shared JAR file and if we tried to remove them, we would see ClassNotFoundExceptions.

    Now, we can move to the web applications:

    1. If a web application DOESN'T package Log4j2 for its own use, its loggers are also loaded using the class loader for shared.loader (as a fallback), and the web application's logging configuration overwrites the configuration applied previously (in our case, we had to call explicit initialization or reconfiguration, so that's why). It wouldn't work as expected.
    2. If a web application DOES package Log4j2 for its own use, its loggers are loaded using the class loader for the web application (since dependencies packaged within WEB-INF take precedence), which is a completely different 'context' (class loader) than in approach #1, and the web application's logging configuration does NOT overwrite previously applied configurations (since the shared JAR and all web applications have their own 'context').

    That is the behaviour we observed. During approach #1, each web application overwrote logging configuration for the applications initialized/started previously, because the Log4j2 'context' (class loader) was shared. During approach #2, the shared JAR's Log4j2 context was initialized by the web application that called its initialization (only one of them), and the configuration was not touched ever since. In our case, the configuration file was provided by the web application (it was not packaged within the shared JAR). Note that in practice, one of the web applications will always have to initialize the shared JAR, either implicitly or explicitly.

    For absolute certainty, we listed open file descriptors to the shared JAR's log files and with approach #2, there was indeed only one. With approach #2, there can still be multiple descriptors open if some of the web applications' configuration files also reference the shared JAR's log files (configuration duplicity). That is precisely the sort of situation you'd normally want to avoid.

    Pitfalls that we discovered:

    1. Notice that the shared JAR's Log4j2 context was initialized by the web application that called its initialization (only one of them) remark is a bit tedious. Unless we copy or include the shared JAR's configuration within configuration of each web application (and for the above stated reasons, we want to avoid that), some of the logging messages may end up elsewhere or get lost during servlet container's start. In general, it may not be possible to guarantee the order in which web applications are started. I may be wrong but from what I saw in Tomcat logs, Tomcat starts the web applications (WAR files) in alphabetic order.
    2. If the shared JAR and any of the web applications happen to share some more dependencies (beside the logging framework), these dependencies must, also, be placed within shared.loader. But, if the shared JAR's logging configuration redirects logging messages of any of those dependencies into its own logs, they can not end up in the web application's logs, unless that dependency is also packaged within the web application (same principle as approach #2 above). But in practice, separation of logging API and backing implementations makes this difficult. For example, you can read here on StackOverflow that selection of SLF4J bindings is rather "random" (JVM-dependent). If you put different bindings to shared.loader and into one of the web application's WEB-INF folder, you may not have certainly about which binding is going to get selected.

    I certainly can not recommend the scenario we are trying to achieve but the described "solution" definitely works as expected (despite the pitfalls).