Log4j Jakarta Web

Using Log4j Core in web applications

Using a logging backend like Log4j Core in a Jakarta EE application requires particular care. Since the lifecycle of a container or web application is independent of the lifecycle of the JVM, it’s important for logging resources to be properly cleaned up (database connections closed, files closed, etc.) when the container or web application shuts down.

The purpose of the log4j-jakarta-web is to register the appropriate hooks into a container’s lifecycle.

To avoid problems, some Log4j API and Log4j Core features are automatically disabled, when running in a Jakarta EE environment. Most notably:

  • the usage of ThreadLocal and garbage-free logging,

  • the JVM shutdown hook.

Installation

In order to install log4j-jakarta-web in your Web application, you need to add it as runtime dependency:

<dependency>
  <groupId>org.apache.logging.log4j</groupId>
  <artifactId>log4j-jakarta-web</artifactId>
  <version>3.0.0-SNAPSHOT</version>
  <scope>runtime</scope>
</dependency>

Lifecycle hooks and configuration

In a Jakarta EE 9 environment, Log4j Jakarta Web automatically registers a ServletContainerInitializer that takes care of the Log4j Core startup, configuration and shutdown. The initializer can be configured through these servlet context initialization parameters, which must be specified in a web.xml deployment descriptor to be effective:

Table 1. Servlet context initialization parameters
Parameter Default Description

isLog4jAutoInitializationDisabled

false

If set to true disables the ServletContainerInitializer.

isLog4jAutoShutdownDisabled

false

If set to true disables the shutdown of the logger context.

log4j.stop.timeout.timeunit

SECONDS

The TimeUnit used to express the shutdown timeout.

log4j.stop.timeout

30

The shutdown timeout.

log4jContextName

Specifies the name of the logger context to use.

It must be explicitly specified if JndiContextSelector is used.

The default value is:

  1. the servlet context name, if present,

  2. the servlet context path, otherwise.

isLog4jContextSelectorNamed

false

Must be set to true if Log4j Core uses a JndiContextSelector.

log4jConfiguration

The location of a Log4j Core configuration file. If no value is provided:

  1. Log4j Jakarta Web looks for a file named /WEB-INFlog4j2-<name>.<extension>, where <name> is the name of the logger context,

  2. if no such file exists it looks for a file named /WEB-INF/log4j2.<extension>,

  3. otherwise Log4j Core uses the automatic configuration procedure.

The Log4j Jakarta Web JAR file is a web-fragment configured to run before any other web fragments in your application.

While this guarantees that Log4j Core starts before all other initializers and listeners, the Servlet 5.0 Specification (cf. section 8.2.3) does not give guarantees that Log4j Core will shutdown after all servlet context listeners.

If the shutdown order is important, users must create a web.xml descriptor and add the Log4jServletContextListener explicitly:

<listener>
  <listener-class>
    org.apache.logging.log4j.web.Log4jServletContextListener
  </listener-class>
</listener>

JNDI configuration

Log4j Core allows the usage of JNDI to coordinate the usage of logger contexts in a Jakarta EE application server. In order to use this feature, you need to:

  1. Install the log4j-jndi module.

  2. Switch to the JndiContextSelector, by setting the log4j2.*.LoggerContext.selector Log4j property to org.apache.logging.log4j.jndi.selector.JndiContextSelector,

  3. For security reasons you need to enable the selector, by setting the log4j2.*.JNDI.contextSelector Log4j property to true,

  4. Each web application needs to configure the servlet context parameter isLog4jContextSelectorNamed to true and provide a value for the log4jContextName servlet context parameter and java:comp/env/log4j/context-name JNDI environment entry:

<web-app>
    <context-param>
        <param-name>isLog4jContextSelectorNamed</param-name>
        <param-value>true</param-value>
    </context-param>
    <context-param>
        <param-name>log4jContextName</param-name>
        <param-value>your_application_name</param-value>
    </context-param>
    <env-entry>
        <env-entry-name>log4j/context-name</env-entry-name>
        <env-entry-value>your_application_name</env-entry-value>
        <env-entry-type>java.lang.String</env-entry-type>
    </env-entry>
</web-app>

Web lookup

Log4j Jakarta Web also provides a ${web:…​} lookup

Table 2. Web lookup supported keys
Key Description

attr.<key>

Value of the <key> servlet context attribute.

contextPathName

The first fragment of the servlet context path.

contextPath

The entire servlet context path.

cookie.<key>

Value of the <key> HTTP cookie.

effectiveMajorVersion

The major version of the Servlet specification supported by the application.

effectiveMinorVersion

The minor version of the Servlet specification supported by the application.

header.<key>

Value of the <key> request HTTP header.

initParam.<key>

Value of the <key> servlet context initialization parameter.

majorVersion

The major version of the Servlet specification supported by the server.

minorVersion

The minor version of the Servlet specification supported by the server.

request.attr.<key>

Value of the <key> servlet request attribute.

request.method

HTTP request method.

request.parameter.<key>

Value of the <key> servlet request parameter.

request.principal

Servlet request principal.

request.remoteAddress

Servlet request remote address.

request.remoteHost

Servlet request remote host.

request.remotePort

Servlet request remote port.

request.uri

Servlet request URI.

request.url

Servlet request URL.

rootDir

The root directory of the servlet context.

serverInfo

The server info.

servletContextName

The servlet context name.

session.attr.<key>

Value of the <key> servlet session attribute.

session.id

Servlet session id.

<key>

The value of <key> is searched as context attribute or context initialization parameter.

Asynchronous Requests and Threads

The handling of asynchronous requests is tricky, and regardless of Servlet container version or configuration Log4j cannot handle everything automatically. When standard requests, forwards, includes, and error resources are processed, the Log4jServletFilter binds the LoggerContext to the thread handling the request. After request processing completes, the filter unbinds the LoggerContext from the thread.

Similarly, when an internal request is dispatched using a javax.servlet.AsyncContext, the Log4jServletFilter also binds the LoggerContext to the thread handling the request and unbinds it when request processing completes. However, this only happens for requests dispatched through the AsyncContext. There are other asynchronous activities that can take place other than internal dispatched requests.

For example, after starting an AsyncContext you could start up a separate thread to process the request in the background, possibly writing the response with the ServletOutputStream. Filters cannot intercept the execution of this thread. Filters also cannot intercept threads that you start in the background during non-asynchronous requests. This is true whether you use a brand new thread or a thread borrowed from a thread pool. So what can you do for these special threads?

You may not need to do anything. If you didn’t use the isLog4jContextSelectorNamed context parameter, there is no need to bind the LoggerContext to the thread. Log4j can safely locate the LoggerContext on its own. In these cases, the filter provides only very modest performance gains, and only when creating new Logger instances. However, if you did specify the isLog4jContextSelectorNamed context parameter with the value "true", you will need to manually bind the LoggerContext to asynchronous threads. Otherwise, Log4j will not be able to locate it.

Thankfully, Log4j provides a simple mechanism for binding the LoggerContext to asynchronous threads in these special circumstances. The simplest way to do this is to wrap the Runnable instance that is passed to the AsyncContext.start() method.

import java.io.IOException;
import javax.servlet.AsyncContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.web.WebLoggerContextUtils;

public class TestAsyncServlet extends HttpServlet {

    @Override
    protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
        final AsyncContext asyncContext = req.startAsync();
        asyncContext.start(WebLoggerContextUtils.wrapExecutionContext(this.getServletContext(), new Runnable() {
            @Override
            public void run() {
                final Logger logger = LogManager.getLogger(TestAsyncServlet.class);
                logger.info("Hello, servlet!");
            }
        }));
    }

    @Override
    protected void doPost(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
        final AsyncContext asyncContext = req.startAsync();
        asyncContext.start(new Runnable() {
            @Override
            public void run() {
                final Log4jWebSupport webSupport =
                    WebLoggerContextUtils.getWebLifeCycle(TestAsyncServlet.this.getServletContext());
                webSupport.setLoggerContext();
                // do stuff
                webSupport.clearLoggerContext();
            }
        });
    }
}

This can be slightly more convenient when using Java 1.8 and lambda functions as demonstrated below.

import java.io.IOException;
import javax.servlet.AsyncContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.web.WebLoggerContextUtils;

public class TestAsyncServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        final AsyncContext asyncContext = req.startAsync();
        asyncContext.start(WebLoggerContextUtils.wrapExecutionContext(this.getServletContext(), () -> {
            final Logger logger = LogManager.getLogger(TestAsyncServlet.class);
            logger.info("Hello, servlet!");
        }));
    }
}

Alternatively, you can obtain the Log4jWebLifeCycle instance from the ServletContext attributes, call its setLoggerContext method as the very first line of code in your asynchronous thread, and call its clearLoggerContext method as the very last line of code in your asynchronous thread. The following code demonstrates this. It uses the container thread pool to execute asynchronous request processing, passing an anonymous inner Runnable to the start method.

import java.io.IOException;
import javax.servlet.AsyncContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.web.Log4jWebLifeCycle;
import org.apache.logging.log4j.web.WebLoggerContextUtils;

public class TestAsyncServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
         final AsyncContext asyncContext = req.startAsync();
        asyncContext.start(new Runnable() {
            @Override
            public void run() {
                final Log4jWebLifeCycle webLifeCycle =
                    WebLoggerContextUtils.getWebLifeCycle(TestAsyncServlet.this.getServletContext());
                webLifeCycle.setLoggerContext();
                try {
                    final Logger logger = LogManager.getLogger(TestAsyncServlet.class);
                    logger.info("Hello, servlet!");
                } finally {
                    webLifeCycle.clearLoggerContext();
                }
            }
        });
   }
}

Note that you must call clearLoggerContext once your thread is finished processing. Failing to do so will result in memory leaks. If using a thread pool, it can even disrupt the logging of other web applications in your container. For that reason, the example here shows clearing the context in a finally block, which will always execute.

Servlet Appender

Log4j Jakarta Web provides a Servlet Appender that uses the servlet context as the log target. For example:

<Configuration status="WARN" name="ServletTest">

    <Appenders>
        <Servlet name="Servlet">
            <PatternLayout pattern="%m%n%ex{none}"/>
        </Servlet>
    </Appenders>

    <Loggers>
        <Root level="debug">
            <AppenderRef ref="Servlet"/>
        </Root>
    </Loggers>

</Configuration>

To avoid double logging of exceptions to the servlet context, you must use %ex{none} in your PatternLayout as shown in the example. The exception will be omitted from the message text but it is passed to the servlet context as the actual Throwable object.