You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by rg...@apache.org on 2021/11/16 03:13:07 UTC

[logging-log4j2] branch master updated: migrate chagnes from release-2.x

This is an automated email from the ASF dual-hosted git repository.

rgoers pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git


The following commit(s) were added to refs/heads/master by this push:
     new f37a3b5  migrate chagnes from release-2.x
f37a3b5 is described below

commit f37a3b5b30ef2c15f60dfcbe1db401b3d4f140da
Author: Ralph Goers <rg...@apache.org>
AuthorDate: Mon Nov 15 20:12:51 2021 -0700

    migrate chagnes from release-2.x
---
 .../org/apache/logging/log4j/util/Constants.java   |   3 +-
 .../core/net/SocketAppenderReconnectTest.java      |   2 +-
 {log4j-smtp => log4j-jakarta-web}/pom.xml          | 102 ++++--
 log4j-jakarta-web/revapi.json                      |  14 +
 .../web/Log4jServletContainerInitializer.java      |  69 ++++
 .../log4j/web/Log4jServletContextListener.java     |  93 +++++
 .../logging/log4j/web/Log4jServletFilter.java      |  87 +++++
 .../Log4jShutdownOnContextDestroyedListener.java   |  80 ++++
 .../logging/log4j/web/Log4jWebInitializerImpl.java | 297 +++++++++++++++
 .../logging/log4j/web/Log4jWebLifeCycle.java       |  43 +++
 .../apache/logging/log4j/web/Log4jWebSupport.java  |  95 +++++
 .../log4j/web/ServletRequestThreadContext.java     |  45 +++
 .../logging/log4j/web/WebLoggerContextUtils.java   | 143 ++++++++
 .../org/apache/logging/log4j/web/WebLookup.java    | 115 ++++++
 .../log4j/web/appender/ServletAppender.java        | 130 +++++++
 .../org/apache/logging/log4j/web/package-info.java |  24 ++
 .../javax.servlet.ServletContainerInitializer      |  22 ++
 .../src/main/resources/META-INF/web-fragment.xml   |  33 ++
 log4j-jakarta-web/src/site/markdown/index.md       |  30 ++
 log4j-jakarta-web/src/site/site.xml                |  52 +++
 .../web/Log4jServletContainerInitializerTest.java  | 184 ++++++++++
 .../log4j/web/Log4jServletContextListenerTest.java | 109 ++++++
 .../logging/log4j/web/Log4jServletFilterTest.java  | 111 ++++++
 ...og4jShutdownOnContextDestroyedListenerTest.java |  83 +++++
 .../log4j/web/Log4jWebInitializerImplTest.java     | 403 +++++++++++++++++++++
 .../org/apache/logging/log4j/web/PropertyTest.java |  42 +++
 .../logging/log4j/web/ServletAppenderTest.java     |  58 +++
 .../apache/logging/log4j/web/TestAsyncServlet.java |  54 +++
 .../apache/logging/log4j/web/WebLookupTest.java    |  98 +++++
 .../resources/WEB-INF/classes/log4j-servlet.xml    |  33 ++
 .../resources/WEB-INF/classes/log4j-webvar.xml     |  29 ++
 .../src/test/resources/log4j2-combined.xml         |  31 ++
 .../src/test/resources/log4j2-override.xml         |  23 ++
 log4j-smtp/pom.xml                                 |   7 +
 pom.xml                                            |   6 +
 src/changes/changes.xml                            |   3 +
 src/site/asciidoc/manual/webapp.adoc               |   5 +
 src/site/site.xml                                  |   1 +
 38 files changed, 2718 insertions(+), 41 deletions(-)

diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/util/Constants.java b/log4j-api/src/main/java/org/apache/logging/log4j/util/Constants.java
index 1d47b43..06c462d 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/util/Constants.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/util/Constants.java
@@ -28,7 +28,8 @@ public final class Constants {
      * is present in the classpath.
      */
     public static final boolean IS_WEB_APP = PropertiesUtil.getProperties().getBooleanProperty(
-            "log4j2.is.webapp", isClassAvailable("javax.servlet.Servlet"));
+            "log4j2.is.webapp", isClassAvailable("javax.servlet.Servlet")
+                    || isClassAvailable("jakarta.servlet.Servlet"));
 
     /**
      * Kill switch for object pooling in ThreadLocals that enables much of the LOG4J2-1270 no-GC behaviour.
diff --git a/log4j-core/src/test/java/org/apache/logging/log4j/core/net/SocketAppenderReconnectTest.java b/log4j-core/src/test/java/org/apache/logging/log4j/core/net/SocketAppenderReconnectTest.java
index 827ab06..052c34d 100644
--- a/log4j-core/src/test/java/org/apache/logging/log4j/core/net/SocketAppenderReconnectTest.java
+++ b/log4j-core/src/test/java/org/apache/logging/log4j/core/net/SocketAppenderReconnectTest.java
@@ -210,7 +210,7 @@ class SocketAppenderReconnectTest {
             timeoutSeconds = 15;
         } else {
             // Universally sensible values.
-            pollIntervalMillis = 100;
+            pollIntervalMillis = 1000;
             timeoutSeconds = 3;
         }
         await()
diff --git a/log4j-smtp/pom.xml b/log4j-jakarta-web/pom.xml
similarity index 65%
copy from log4j-smtp/pom.xml
copy to log4j-jakarta-web/pom.xml
index 18ccb7a..64364d4 100644
--- a/log4j-smtp/pom.xml
+++ b/log4j-jakarta-web/pom.xml
@@ -1,67 +1,86 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<!-- ~ Licensed to the Apache Software Foundation (ASF) under one or more ~ contributor license agreements. See the NOTICE 
-  file distributed with ~ this work for additional information regarding copyright ownership. ~ The ASF licenses this file 
-  to You under the Apache License, Version 2.0 ~ (the "License"); you may not use this file except in compliance with ~ the 
-  License. You may obtain a copy of the License at ~ ~ http://www.apache.org/licenses/LICENSE-2.0 ~ ~ Unless required by applicable 
-  law or agreed to in writing, software ~ distributed under the License is distributed on an "AS IS" BASIS, ~ WITHOUT WARRANTIES 
-  OR CONDITIONS OF ANY KIND, either express or implied. ~ See the License for the specific language governing permissions and 
-  ~ limitations under the License. -->
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF licenses this file to You under the Apache License, Version 2.0
+  ~ (the "License"); you may not use this file except in compliance with
+  ~ the License.  You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
 
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <parent>
-    <groupId>org.apache.logging.log4j</groupId>
     <artifactId>log4j</artifactId>
-    <version>3.0.0-SNAPSHOT</version>
+    <groupId>org.apache.logging.log4j</groupId>
+    <version>2.15.0-SNAPSHOT</version>
   </parent>
   <modelVersion>4.0.0</modelVersion>
 
-  <artifactId>log4j-smtp</artifactId>
-  <name>Apache Log4j SMTP</name>
-  <description>
-    Apache Log4j Simple Mail Transfer Protocol (SMTP) Appender.
-  </description>
+  <artifactId>log4j-jakarta-web</artifactId>
+  <packaging>jar</packaging>
+  <name>Apache Log4j Jakarta Web</name>
+  <description>The Apache Log4j support for Jakarta EE 9+ web servlet containers</description>
+
   <properties>
     <log4jParentDir>${basedir}/..</log4jParentDir>
-    <docLabel>Log4j SMTP Appender Documentation</docLabel>
-    <projectDir>/log4j-smtp</projectDir>
-    <module.name>org.apache.logging.log4j.smtp</module.name>
+    <docLabel>Web Documentation</docLabel>
+    <projectDir>/log4j-jakarta-web</projectDir>
+    <module.name>org.apache.logging.log4j.web</module.name>
     <maven.doap.skip>true</maven.doap.skip>
   </properties>
 
   <dependencies>
     <dependency>
       <groupId>org.apache.logging.log4j</groupId>
+      <artifactId>log4j-api</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.logging.log4j</groupId>
       <artifactId>log4j-core</artifactId>
     </dependency>
-    <!-- Required for SMTPAppender -->
     <dependency>
-      <groupId>com.sun.mail</groupId>
-      <artifactId>javax.mail</artifactId>
+      <groupId>jakarta.servlet</groupId>
+      <artifactId>jakarta.servlet-api</artifactId>
+      <version>5.0.0</version>
+      <scope>provided</scope>
+    </dependency>
+
+    <!-- Test dependencies -->
+    <dependency>
+      <groupId>org.apache.logging.log4j</groupId>
+      <artifactId>log4j-core</artifactId>
+      <type>test-jar</type>
+      <scope>test</scope>
     </dependency>
-    <!-- Test Dependencies -->
     <dependency>
-      <groupId>org.junit.vintage</groupId>
-      <artifactId>junit-vintage-engine</artifactId>
+    	<groupId>org.hamcrest</groupId>
+    	<artifactId>hamcrest</artifactId>
     </dependency>
     <dependency>
       <groupId>org.junit.jupiter</groupId>
       <artifactId>junit-jupiter-engine</artifactId>
     </dependency>
     <dependency>
-      <groupId>org.hamcrest</groupId>
-      <artifactId>hamcrest</artifactId>
-      <scope>test</scope>
-    </dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-junit-jupiter</artifactId>
+	</dependency>
     <dependency>
-      <groupId>org.apache.logging.log4j</groupId>
-      <artifactId>log4j-api</artifactId>
-      <type>test-jar</type>
+      <groupId>org.springframework</groupId>
+      <artifactId>spring-test</artifactId>
+      <scope>test</scope>
     </dependency>
     <dependency>
-      <groupId>org.apache.logging.log4j</groupId>
-      <artifactId>log4j-core</artifactId>
-      <type>test-jar</type>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-core</artifactId>
+      <scope>test</scope>
     </dependency>
   </dependencies>
 
@@ -72,8 +91,10 @@
         <artifactId>maven-bundle-plugin</artifactId>
         <configuration>
           <instructions>
-            <Fragment-Host>org.apache.logging.log4j.core.appender.mom.jeromq</Fragment-Host>
-            <Export-Package>*</Export-Package>
+            <!-- we require 5.0 minimum -->
+            <Fragment-Host>org.apache.logging.log4j.core</Fragment-Host>
+            <Import-Package>jakarta.servlet;version="[5.0,4)",*</Import-Package>
+            <Export-Package>org.apache.logging.log4j.web</Export-Package>
           </instructions>
         </configuration>
       </plugin>
@@ -118,10 +139,13 @@
           <bottom><![CDATA[<p align="center">Copyright &#169; {inceptionYear}-{currentYear} {organizationName}. All Rights Reserved.<br />
             Apache Logging, Apache Log4j, Log4j, Apache, the Apache feather logo, the Apache Logging project logo,
             and the Apache Log4j logo are trademarks of The Apache Software Foundation.</p>]]></bottom>
-          <!-- module link generation is completely broken in the javadoc plugin for a multi-module non-aggregating project -->
+          <!-- module link generation is completely broken in the javadoc plugin for a multi-module non-aggregating
+               project -->
           <detectOfflineLinks>false</detectOfflineLinks>
           <linksource>true</linksource>
-          <source>8</source>
+          <links>
+            <link>http://docs.oracle.com/javaee/6/api/</link>
+          </links>
         </configuration>
         <reportSets>
           <reportSet>
diff --git a/log4j-jakarta-web/revapi.json b/log4j-jakarta-web/revapi.json
new file mode 100644
index 0000000..f523355
--- /dev/null
+++ b/log4j-jakarta-web/revapi.json
@@ -0,0 +1,14 @@
+[
+  {
+    "extension": "revapi.java",
+    "configuration": {
+      "filter": {
+        "classes": {
+          "exclude": [
+            "org\\.apache\\.logging\\.log4j\\.web\\.Log4jWebLifeCycle"
+          ]
+        }
+      }
+    }
+  }
+]
\ No newline at end of file
diff --git a/log4j-jakarta-web/src/main/java/org/apache/logging/log4j/web/Log4jServletContainerInitializer.java b/log4j-jakarta-web/src/main/java/org/apache/logging/log4j/web/Log4jServletContainerInitializer.java
new file mode 100644
index 0000000..3897db7
--- /dev/null
+++ b/log4j-jakarta-web/src/main/java/org/apache/logging/log4j/web/Log4jServletContainerInitializer.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.web;
+
+import java.util.EnumSet;
+import java.util.Set;
+import jakarta.servlet.DispatcherType;
+import jakarta.servlet.FilterRegistration;
+import jakarta.servlet.ServletContainerInitializer;
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.ServletException;
+
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.status.StatusLogger;
+
+/**
+ * In a Servlet 3.0 or newer environment, this initializer is responsible for starting up Log4j logging before anything
+ * else happens in application initialization. For consistency across all containers, if the effective Servlet major
+ * version of the application is less than 3.0, this initializer does nothing.
+ */
+public class Log4jServletContainerInitializer implements ServletContainerInitializer {
+
+    @Override
+    public void onStartup(final Set<Class<?>> classes, final ServletContext servletContext) throws ServletException {
+        if (servletContext.getMajorVersion() > 2 && servletContext.getEffectiveMajorVersion() > 2 &&
+                !"true".equalsIgnoreCase(servletContext.getInitParameter(
+                        Log4jWebSupport.IS_LOG4J_AUTO_INITIALIZATION_DISABLED
+                ))) {
+            final Logger LOGGER = StatusLogger.getLogger();
+
+            LOGGER.debug("Log4jServletContainerInitializer starting up Log4j in Servlet 3.0+ environment.");
+
+            final FilterRegistration.Dynamic filter =
+                    servletContext.addFilter("log4jServletFilter", Log4jServletFilter.class);
+            if (filter == null) {
+                LOGGER.warn("WARNING: In a Servlet 3.0+ application, you should not define a " +
+                    "log4jServletFilter in web.xml. Log4j 2 normally does this for you automatically. Log4j 2 " +
+                    "web auto-initialization has been canceled.");
+                return;
+            }
+
+            final Log4jWebLifeCycle initializer = WebLoggerContextUtils.getWebLifeCycle(servletContext);
+            initializer.start();
+            initializer.setLoggerContext(); // the application is just now starting to start up
+
+            if (!"true".equalsIgnoreCase(servletContext.getInitParameter(
+                    Log4jWebSupport.IS_LOG4J_AUTO_SHUTDOWN_DISABLED))) {
+                servletContext.addListener(new Log4jServletContextListener());
+            }
+
+            filter.setAsyncSupported(true); // supporting async when the user isn't using async has no downsides
+            filter.addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class), false, "/*");
+        }
+    }
+}
diff --git a/log4j-jakarta-web/src/main/java/org/apache/logging/log4j/web/Log4jServletContextListener.java b/log4j-jakarta-web/src/main/java/org/apache/logging/log4j/web/Log4jServletContextListener.java
new file mode 100644
index 0000000..f0f5881
--- /dev/null
+++ b/log4j-jakarta-web/src/main/java/org/apache/logging/log4j/web/Log4jServletContextListener.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.web;
+
+import java.util.Locale;
+import java.util.concurrent.TimeUnit;
+
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.ServletContextEvent;
+import jakarta.servlet.ServletContextListener;
+
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.core.LifeCycle2;
+import org.apache.logging.log4j.status.StatusLogger;
+import org.apache.logging.log4j.util.Strings;
+
+/**
+ * In environments older than Servlet 3.0, this initializer is responsible for starting up Log4j logging before anything
+ * else happens in application initialization. In all environments, this shuts down Log4j after the application shuts
+ * down.
+ */
+public class Log4jServletContextListener implements ServletContextListener {
+
+	private static final int DEFAULT_STOP_TIMEOUT = 30;
+    private static final TimeUnit DEFAULT_STOP_TIMEOUT_TIMEUNIT = TimeUnit.SECONDS;
+
+	private static final String KEY_STOP_TIMEOUT = "log4j.stop.timeout";
+	private static final String KEY_STOP_TIMEOUT_TIMEUNIT = "log4j.stop.timeout.timeunit";
+
+	private static final Logger LOGGER = StatusLogger.getLogger();
+
+    private ServletContext servletContext;
+    private Log4jWebLifeCycle initializer;
+
+    @Override
+    public void contextInitialized(final ServletContextEvent event) {
+        this.servletContext = event.getServletContext();
+        LOGGER.debug("Log4jServletContextListener ensuring that Log4j starts up properly.");
+        
+        if ("true".equalsIgnoreCase(servletContext.getInitParameter(
+                Log4jWebSupport.IS_LOG4J_AUTO_SHUTDOWN_DISABLED))) {
+        	throw new IllegalStateException("Do not use " + getClass().getSimpleName() + " when "
+        			+ Log4jWebSupport.IS_LOG4J_AUTO_SHUTDOWN_DISABLED + " is true. Please use "
+        			+ Log4jShutdownOnContextDestroyedListener.class.getSimpleName() + " instead of "
+        			+ getClass().getSimpleName() + ".");
+        }
+
+        this.initializer = WebLoggerContextUtils.getWebLifeCycle(this.servletContext);
+        try {
+            this.initializer.start();
+            this.initializer.setLoggerContext(); // the application is just now starting to start up
+        } catch (final IllegalStateException e) {
+            throw new IllegalStateException("Failed to initialize Log4j properly.", e);
+        }
+    }
+
+    @Override
+	public void contextDestroyed(final ServletContextEvent event) {
+		if (this.servletContext == null || this.initializer == null) {
+			LOGGER.warn("Context destroyed before it was initialized.");
+			return;
+		}
+		LOGGER.debug("Log4jServletContextListener ensuring that Log4j shuts down properly.");
+
+		this.initializer.clearLoggerContext(); // the application is finished
+		// shutting down now
+		if (initializer instanceof LifeCycle2) {
+			final String stopTimeoutStr = servletContext.getInitParameter(KEY_STOP_TIMEOUT);
+			final long stopTimeout = Strings.isEmpty(stopTimeoutStr) ? DEFAULT_STOP_TIMEOUT
+					: Long.parseLong(stopTimeoutStr);
+			final String timeoutTimeUnitStr = servletContext.getInitParameter(KEY_STOP_TIMEOUT_TIMEUNIT);
+			final TimeUnit timeoutTimeUnit = Strings.isEmpty(timeoutTimeUnitStr) ? DEFAULT_STOP_TIMEOUT_TIMEUNIT
+					: TimeUnit.valueOf(timeoutTimeUnitStr.toUpperCase(Locale.ROOT));
+			((LifeCycle2) this.initializer).stop(stopTimeout, timeoutTimeUnit);
+		} else {
+			this.initializer.stop();
+		}
+	}
+}
diff --git a/log4j-jakarta-web/src/main/java/org/apache/logging/log4j/web/Log4jServletFilter.java b/log4j-jakarta-web/src/main/java/org/apache/logging/log4j/web/Log4jServletFilter.java
new file mode 100644
index 0000000..8a59dae
--- /dev/null
+++ b/log4j-jakarta-web/src/main/java/org/apache/logging/log4j/web/Log4jServletFilter.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.web;
+
+import java.io.IOException;
+import jakarta.servlet.Filter;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.FilterConfig;
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.status.StatusLogger;
+
+/**
+ * This is responsible for the following:
+ * <ul>
+ *     <li>Clearing the logger context when the application has finished starting up.</li>
+ *     <li>Setting the logger context before processing a request and clearing it after processing a request.</li>
+ *     <li>Setting the logger context when the application is starting to shut down.</li>
+ * </ul>
+ * This filter is a once-per-request filter. It is capable of filtering all the different types of requests
+ * (standard, asynchronous, error, etc.) but will not apply processing if the filter matches multiple times on the same
+ * logical request.
+ */
+public class Log4jServletFilter implements Filter {
+
+    private static final Logger LOGGER = StatusLogger.getLogger();
+
+    static final String ALREADY_FILTERED_ATTRIBUTE = Log4jServletFilter.class.getName() + ".FILTERED";
+
+    private ServletContext servletContext;
+    private Log4jWebLifeCycle initializer;
+
+    @Override
+    public void init(final FilterConfig filterConfig) throws ServletException {
+        this.servletContext = filterConfig.getServletContext();
+        LOGGER.debug("Log4jServletFilter initialized.");
+
+        this.initializer = WebLoggerContextUtils.getWebLifeCycle(this.servletContext);
+        this.initializer.clearLoggerContext(); // the application is mostly finished starting up now
+    }
+
+    @Override
+    public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain)
+            throws IOException, ServletException {
+        if (request.getAttribute(ALREADY_FILTERED_ATTRIBUTE) != null) {
+            chain.doFilter(request, response);
+        } else {
+            request.setAttribute(ALREADY_FILTERED_ATTRIBUTE, Boolean.TRUE);
+
+            try {
+                this.initializer.setLoggerContext();
+
+                chain.doFilter(request, response);
+            } finally {
+                this.initializer.clearLoggerContext();
+            }
+        }
+    }
+
+    @Override
+    public void destroy() {
+        if (this.servletContext == null || this.initializer == null) {
+            throw new IllegalStateException("Filter destroyed before it was initialized.");
+        }
+        LOGGER.debug("Log4jServletFilter destroyed.");
+
+        this.initializer.setLoggerContext(); // the application is just now starting to shut down
+    }
+}
diff --git a/log4j-jakarta-web/src/main/java/org/apache/logging/log4j/web/Log4jShutdownOnContextDestroyedListener.java b/log4j-jakarta-web/src/main/java/org/apache/logging/log4j/web/Log4jShutdownOnContextDestroyedListener.java
new file mode 100644
index 0000000..910670a
--- /dev/null
+++ b/log4j-jakarta-web/src/main/java/org/apache/logging/log4j/web/Log4jShutdownOnContextDestroyedListener.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.web;
+
+import java.util.Locale;
+import java.util.concurrent.TimeUnit;
+
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.ServletContextEvent;
+import jakarta.servlet.ServletContextListener;
+
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.core.LifeCycle2;
+import org.apache.logging.log4j.status.StatusLogger;
+import org.apache.logging.log4j.util.Strings;
+
+public class Log4jShutdownOnContextDestroyedListener implements ServletContextListener {
+
+    private static final int DEFAULT_STOP_TIMEOUT = 30;
+    private static final TimeUnit DEFAULT_STOP_TIMEOUT_TIMEUNIT = TimeUnit.SECONDS;
+
+    private static final String KEY_STOP_TIMEOUT = "log4j.stop.timeout";
+    private static final String KEY_STOP_TIMEOUT_TIMEUNIT = "log4j.stop.timeout.timeunit";
+
+    private static final Logger LOGGER = StatusLogger.getLogger();
+
+    private ServletContext servletContext;
+    private Log4jWebLifeCycle initializer;
+
+    @Override
+    public void contextInitialized(final ServletContextEvent event) {
+        LOGGER.debug(Log4jShutdownOnContextDestroyedListener.class.getSimpleName() + 
+        		" ensuring that Log4j started up properly.");
+        servletContext = event.getServletContext();
+        if (null == servletContext.getAttribute(Log4jWebSupport.SUPPORT_ATTRIBUTE)) {
+        	throw new IllegalStateException(
+        			"Context did not contain required Log4jWebLifeCycle in the " 
+        			+ Log4jWebSupport.SUPPORT_ATTRIBUTE + " attribute.");
+        }
+        this.initializer = WebLoggerContextUtils.getWebLifeCycle(servletContext);
+    }
+
+    @Override
+    public void contextDestroyed(final ServletContextEvent event) {
+        if (this.servletContext == null || this.initializer == null) {
+            LOGGER.warn("Context destroyed before it was initialized.");
+            return;
+        }
+        LOGGER.debug(Log4jShutdownOnContextDestroyedListener.class.getSimpleName() +
+        		" ensuring that Log4j shuts down properly.");
+
+        this.initializer.clearLoggerContext(); // the application is finished
+        // shutting down now
+        if (initializer instanceof LifeCycle2) {
+            final String stopTimeoutStr = servletContext.getInitParameter(KEY_STOP_TIMEOUT);
+            final long stopTimeout = Strings.isEmpty(stopTimeoutStr) ? DEFAULT_STOP_TIMEOUT
+                    : Long.parseLong(stopTimeoutStr);
+            final String timeoutTimeUnitStr = servletContext.getInitParameter(KEY_STOP_TIMEOUT_TIMEUNIT);
+            final TimeUnit timeoutTimeUnit = Strings.isEmpty(timeoutTimeUnitStr) ? DEFAULT_STOP_TIMEOUT_TIMEUNIT
+                    : TimeUnit.valueOf(timeoutTimeUnitStr.toUpperCase(Locale.ROOT));
+            ((LifeCycle2) this.initializer).stop(stopTimeout, timeoutTimeUnit);
+        } else {
+            this.initializer.stop();
+        }
+    }
+}
diff --git a/log4j-jakarta-web/src/main/java/org/apache/logging/log4j/web/Log4jWebInitializerImpl.java b/log4j-jakarta-web/src/main/java/org/apache/logging/log4j/web/Log4jWebInitializerImpl.java
new file mode 100644
index 0000000..3ecbe14
--- /dev/null
+++ b/log4j-jakarta-web/src/main/java/org/apache/logging/log4j/web/Log4jWebInitializerImpl.java
@@ -0,0 +1,297 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.web;
+
+import java.net.URI;
+import java.net.URL;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+
+import jakarta.servlet.ServletContext;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.core.AbstractLifeCycle;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.async.AsyncLoggerContext;
+import org.apache.logging.log4j.core.config.Configurator;
+import org.apache.logging.log4j.core.impl.ContextAnchor;
+import org.apache.logging.log4j.core.impl.Log4jContextFactory;
+import org.apache.logging.log4j.core.lookup.Interpolator;
+import org.apache.logging.log4j.core.lookup.StrSubstitutor;
+import org.apache.logging.log4j.core.selector.ContextSelector;
+import org.apache.logging.log4j.core.selector.NamedContextSelector;
+import org.apache.logging.log4j.core.util.Loader;
+import org.apache.logging.log4j.core.util.NetUtils;
+import org.apache.logging.log4j.core.util.SetUtils;
+import org.apache.logging.log4j.spi.LoggerContextFactory;
+import org.apache.logging.log4j.util.LoaderUtil;
+
+/**
+ * This class initializes and deinitializes Log4j no matter how the initialization occurs.
+ */
+final class Log4jWebInitializerImpl extends AbstractLifeCycle implements Log4jWebLifeCycle {
+
+    private static final String WEB_INF = "/WEB-INF/";
+
+    static {
+        if (Loader.isClassAvailable("org.apache.logging.log4j.core.web.JNDIContextFilter")) {
+            throw new IllegalStateException("You are using Log4j 2 in a web application with the old, extinct "
+                    + "log4j-web artifact. This is not supported and could cause serious runtime problems. Please"
+                    + "remove the log4j-web JAR file from your application.");
+        }
+    }
+
+    private final Map<String, String> map = new ConcurrentHashMap<>();
+    private final StrSubstitutor substitutor = new StrSubstitutor(new Interpolator(map));
+    private final ServletContext servletContext;
+
+    private String name;
+    private NamedContextSelector namedContextSelector;
+    private LoggerContext loggerContext;
+
+    private Log4jWebInitializerImpl(final ServletContext servletContext) {
+        this.servletContext = servletContext;
+        this.map.put("hostName", NetUtils.getLocalHostname());
+    }
+
+    /**
+     * Initializes the Log4jWebLifeCycle attribute of a ServletContext. Those who wish to obtain this object should use
+     * the {@link org.apache.logging.log4j.web.WebLoggerContextUtils#getWebLifeCycle(jakarta.servlet.ServletContext)}
+     * method instead.
+     *
+     * @param servletContext
+     *        the ServletContext to initialize
+     * @return a new Log4jWebLifeCycle
+     * @since 2.0.1
+     */
+    protected static Log4jWebInitializerImpl initialize(final ServletContext servletContext) {
+        final Log4jWebInitializerImpl initializer = new Log4jWebInitializerImpl(servletContext);
+        servletContext.setAttribute(SUPPORT_ATTRIBUTE, initializer);
+        return initializer;
+    }
+
+    @Override
+    public synchronized void start() {
+        if (this.isStopped() || this.isStopping()) {
+            throw new IllegalStateException("Cannot start this Log4jWebInitializerImpl after it was stopped.");
+        }
+
+        // only do this once
+        if (this.isInitialized()) {
+            super.setStarting();
+
+            this.name = this.substitutor.replace(this.servletContext.getInitParameter(LOG4J_CONTEXT_NAME));
+            final String location = this.substitutor.replace(this.servletContext
+                    .getInitParameter(LOG4J_CONFIG_LOCATION));
+            final boolean isJndi = "true".equalsIgnoreCase(this.servletContext
+                    .getInitParameter(IS_LOG4J_CONTEXT_SELECTOR_NAMED));
+
+            if (isJndi) {
+                this.initializeJndi(location);
+            } else {
+                this.initializeNonJndi(location);
+            }
+            if (this.loggerContext instanceof AsyncLoggerContext) {
+                ((AsyncLoggerContext) this.loggerContext).setUseThreadLocals(false);
+            }
+
+            this.servletContext.setAttribute(CONTEXT_ATTRIBUTE, this.loggerContext);
+            super.setStarted();
+        }
+    }
+
+    private void initializeJndi(final String location) {
+        final URI configLocation = getConfigURI(location);
+
+        if (this.name == null) {
+            throw new IllegalStateException("A log4jContextName context parameter is required");
+        }
+
+        LoggerContext context;
+        final LoggerContextFactory factory = LogManager.getFactory();
+        if (factory instanceof Log4jContextFactory) {
+            final ContextSelector selector = ((Log4jContextFactory) factory).getSelector();
+            if (selector instanceof NamedContextSelector) {
+                this.namedContextSelector = (NamedContextSelector) selector;
+                context = this.namedContextSelector.locateContext(this.name,
+                        WebLoggerContextUtils.createExternalEntry(this.servletContext), configLocation);
+                ContextAnchor.THREAD_CONTEXT.set(context);
+                if (context.isInitialized()) {
+                    context.start();
+                }
+                ContextAnchor.THREAD_CONTEXT.remove();
+            } else {
+                LOGGER.warn("Potential problem: Selector is not an instance of NamedContextSelector.");
+                return;
+            }
+        } else {
+            LOGGER.warn("Potential problem: LoggerContextFactory is not an instance of Log4jContextFactory.");
+            return;
+        }
+        this.loggerContext = context;
+        LOGGER.debug("Created logger context for [{}] using [{}].", this.name, context.getClass().getClassLoader());
+    }
+
+    private void initializeNonJndi(final String location) {
+        if (this.name == null) {
+            this.name = this.servletContext.getServletContextName();
+            LOGGER.debug("Using the servlet context name \"{}\".", this.name);
+        }
+        if (this.name == null) {
+            this.name = this.servletContext.getContextPath();
+            LOGGER.debug("Using the servlet context context-path \"{}\".", this.name);
+        }
+        if (this.name == null && location == null) {
+            LOGGER.error("No Log4j context configuration provided. This is very unusual.");
+            this.name = new SimpleDateFormat("yyyyMMdd_HHmmss.SSS").format(new Date());
+        }
+        if (location != null && location.contains(",")) {
+            final List<URI> uris = getConfigURIs(location);
+            this.loggerContext = Configurator.initialize(this.name, this.getClassLoader(), uris,
+                    WebLoggerContextUtils.createExternalEntry(this.servletContext));
+            return;
+        }
+
+        final URI uri = getConfigURI(location);
+        this.loggerContext = Configurator.initialize(this.name, this.getClassLoader(), uri,
+                WebLoggerContextUtils.createExternalEntry(this.servletContext));
+    }
+
+    private List<URI> getConfigURIs(final String location) {
+        final String[] parts = location.split(",");
+        final List<URI> uris = new ArrayList<>(parts.length);
+        for (final String part : parts) {
+            final URI uri = getConfigURI(part);
+            if (uri != null) {
+                uris.add(uri);
+            }
+        }
+        return uris;
+    }
+
+    private URI getConfigURI(final String location) {
+        try {
+            String configLocation = location;
+            if (configLocation == null) {
+                final String[] paths = SetUtils.prefixSet(servletContext.getResourcePaths(WEB_INF), WEB_INF + "log4j2");
+                LOGGER.debug("getConfigURI found resource paths {} in servletContext at [{}]", Arrays.toString(paths), WEB_INF);
+                if (paths.length == 1) {
+                    configLocation = paths[0];
+                } else if (paths.length > 1) {
+                    final String prefix = WEB_INF + "log4j2-" + this.name + ".";
+                    boolean found = false;
+                    for (final String str : paths) {
+                        if (str.startsWith(prefix)) {
+                            configLocation = str;
+                            found = true;
+                            break;
+                        }
+                    }
+                    if (!found) {
+                        configLocation = paths[0];
+                    }
+                }
+            }
+            if (configLocation != null) {
+                final URL url = servletContext.getResource(configLocation);
+                if (url != null) {
+                    final URI uri = url.toURI();
+                    LOGGER.debug("getConfigURI found resource [{}] in servletContext at [{}]", uri, configLocation);
+                    return uri;
+                }
+            }
+        } catch (final Exception ex) {
+            // Just try passing the location.
+        }
+        if (location != null) {
+            try {
+                final URI correctedFilePathUri = NetUtils.toURI(location);
+                LOGGER.debug("getConfigURI found [{}] in servletContext at [{}]", correctedFilePathUri, location);
+                return correctedFilePathUri;
+            } catch (final Exception e) {
+                LOGGER.error("Unable to convert configuration location [{}] to a URI", location, e);
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public synchronized boolean stop(final long timeout, final TimeUnit timeUnit) {
+        if (!this.isStarted() && !this.isStopped()) {
+            throw new IllegalStateException("Cannot stop this Log4jWebInitializer because it has not started.");
+        }
+
+        // only do this once
+        if (this.isStarted()) {
+            this.setStopping();
+            if (this.loggerContext != null) {
+                LOGGER.debug("Removing LoggerContext for [{}].", this.name);
+                this.servletContext.removeAttribute(CONTEXT_ATTRIBUTE);
+                if (this.namedContextSelector != null) {
+                    this.namedContextSelector.removeContext(this.name);
+                }
+                this.loggerContext.stop(timeout, timeUnit);
+                this.loggerContext.setExternalContext(null);
+                this.loggerContext = null;
+            }
+            this.setStopped();
+        }
+        return super.stop(timeout, timeUnit);
+    }
+
+    @Override
+    public void setLoggerContext() {
+        if (this.loggerContext != null) {
+            ContextAnchor.THREAD_CONTEXT.set(this.loggerContext);
+        }
+    }
+
+    @Override
+    public void clearLoggerContext() {
+        ContextAnchor.THREAD_CONTEXT.remove();
+    }
+
+    @Override
+    public void wrapExecution(final Runnable runnable) {
+        this.setLoggerContext();
+
+        try {
+            runnable.run();
+        } finally {
+            this.clearLoggerContext();
+        }
+    }
+
+    private ClassLoader getClassLoader() {
+        try {
+            // if container is Servlet 3.0, use its getClassLoader method
+            // this may look odd, but the call below will throw NoSuchMethodError if user is on Servlet 2.5
+            // we compile against 3.0 to support Log4jServletContainerInitializer, but we don't require 3.0
+            return this.servletContext.getClassLoader();
+        } catch (final Throwable ignore) {
+            // LOG4J2-248: use TCCL if possible
+            return LoaderUtil.getThreadContextClassLoader();
+        }
+    }
+
+}
diff --git a/log4j-jakarta-web/src/main/java/org/apache/logging/log4j/web/Log4jWebLifeCycle.java b/log4j-jakarta-web/src/main/java/org/apache/logging/log4j/web/Log4jWebLifeCycle.java
new file mode 100644
index 0000000..e8cba90
--- /dev/null
+++ b/log4j-jakarta-web/src/main/java/org/apache/logging/log4j/web/Log4jWebLifeCycle.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.web;
+
+import org.apache.logging.log4j.core.LifeCycle;
+
+/**
+ * Specifies an interface for initializing and deinitializing Log4j in a Java EE web application. The default and only
+ * implementation is {@link Log4jWebInitializerImpl}. The initializer is based on an interface to improve testability.
+ * The methods here are contained in a package-private sub-interface because general application code should not have
+ * access to them.
+ */
+interface Log4jWebLifeCycle extends Log4jWebSupport, LifeCycle {
+
+    /**
+     * Starts up Log4j in the web application. Calls {@link #setLoggerContext()} after initialization is complete.
+     *
+     * @throws IllegalStateException if a JNDI config location is specified but no name is specified.
+     */
+    @Override
+    void start();
+
+    /**
+     * Shuts down Log4j in the web application. Calls {@link #clearLoggerContext()} immediately before deinitialization
+     * begins.
+     */
+    @Override
+    void stop();
+}
diff --git a/log4j-jakarta-web/src/main/java/org/apache/logging/log4j/web/Log4jWebSupport.java b/log4j-jakarta-web/src/main/java/org/apache/logging/log4j/web/Log4jWebSupport.java
new file mode 100644
index 0000000..1f3c434
--- /dev/null
+++ b/log4j-jakarta-web/src/main/java/org/apache/logging/log4j/web/Log4jWebSupport.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.web;
+
+import org.apache.logging.log4j.spi.LoggerContext;
+
+/**
+ * Specifies an interface for setting and clearing a thread-bound {@link LoggerContext} in a Java EE web application.
+ * Also defines constants for context parameter and attribute names. In most cases you will never need to use this
+ * directly because the Log4j filter handles this task automatically. However, in async operations you should wrap
+ * code that executes in separate threads with {@link #setLoggerContext} and {@link #clearLoggerContext}.
+ *
+ * <p>
+ * You can obtain the instance of this for your web application by retrieving the {@link jakarta.servlet.ServletContext}
+ * attribute named {@code org.apache.logging.log4j.core.web.Log4jWebSupport.INSTANCE}. If needed, you can also obtain
+ * the {@link LoggerContext} instance for your web application by retrieving the {@code ServletContext} attribute named
+ * {@code org.apache.logging.log4j.spi.LoggerContext.INSTANCE}.
+ * </p>
+ */
+public interface Log4jWebSupport {
+    /**
+     * The {@link jakarta.servlet.ServletContext} parameter name for the name of the
+     * {@link org.apache.logging.log4j.core.LoggerContext}.
+     */
+    String LOG4J_CONTEXT_NAME = "log4jContextName";
+
+    /**
+     * The {@link jakarta.servlet.ServletContext} parameter name for the location of the configuration.
+     */
+    String LOG4J_CONFIG_LOCATION = "log4jConfiguration";
+
+    /**
+     * The {@link jakarta.servlet.ServletContext} parameter name for the JNDI flag.
+     */
+    String IS_LOG4J_CONTEXT_SELECTOR_NAMED = "isLog4jContextSelectorNamed";
+
+    /**
+     * The {@link jakarta.servlet.ServletContext} parameter name for the flag that disables Log4j's auto-initialization
+     * in Servlet 3.0+ web applications. Set a context parameter with this name to "true" to disable
+     * auto-initialization.
+     */
+    String IS_LOG4J_AUTO_INITIALIZATION_DISABLED = "isLog4jAutoInitializationDisabled";
+
+    /**
+     * The {@link jakarta.servlet.ServletContext} parameter name for the flag that disables Log4j's auto-shutdown
+     * in Servlet 3.0+ web applications. Set a context parameter with this name to "true" to disable
+     * auto-shutdown.
+     */
+    String IS_LOG4J_AUTO_SHUTDOWN_DISABLED = "isLog4jAutoShutdownDisabled";
+
+    /**
+     * The attribute key for the {@link jakarta.servlet.ServletContext} attribute that the singleton support instance
+     * is stored in.
+     */
+    String SUPPORT_ATTRIBUTE = Log4jWebSupport.class.getName() + ".INSTANCE";
+
+    /**
+     * The attribute key for the {@link jakarta.servlet.ServletContext} attribute that the {@link LoggerContext}
+     * is stored in.
+     */
+    String CONTEXT_ATTRIBUTE = LoggerContext.class.getName() + ".INSTANCE";
+
+    /**
+     * Sets the logger context so that code executing afterwards can easily and quickly access loggers via
+     * {@link org.apache.logging.log4j.LogManager#getLogger}.
+     */
+    void setLoggerContext();
+
+    /**
+     * Clears the logger context set up in {@link #setLoggerContext}.
+     */
+    void clearLoggerContext();
+
+    /**
+     * Sets the logger context by calling {@link #setLoggerContext}, executes the runnable argument, then clears the
+     * logger context by calling {@link #clearLoggerContext}.
+     *
+     * @param runnable The runnable to execute wrapped with a configured logger context
+     */
+    void wrapExecution(Runnable runnable);
+}
diff --git a/log4j-jakarta-web/src/main/java/org/apache/logging/log4j/web/ServletRequestThreadContext.java b/log4j-jakarta-web/src/main/java/org/apache/logging/log4j/web/ServletRequestThreadContext.java
new file mode 100644
index 0000000..bf871c8
--- /dev/null
+++ b/log4j-jakarta-web/src/main/java/org/apache/logging/log4j/web/ServletRequestThreadContext.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.web;
+
+import java.util.Objects;
+
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.http.HttpServletRequest;
+
+import org.apache.logging.log4j.ThreadContext;
+
+public class ServletRequestThreadContext {
+
+    public static void put(final String key, final ServletRequest servletRequest) {
+        put(key, "RemoteAddr", servletRequest.getRemoteAddr());
+        put(key, "RemoteHost", servletRequest.getRemoteHost());
+        put(key, "RemotePort", servletRequest.getRemotePort());
+    }
+
+    public static void put(final String key, final String field, final Object value) {
+        put(key + "." + field, Objects.toString(value));
+    }
+
+    public static void put(final String key, final String value) {
+        ThreadContext.put(key, value);
+    }
+
+    public static void put(final String key, final HttpServletRequest servletRequest) {
+        put(key, (ServletRequest) servletRequest);
+    }
+}
diff --git a/log4j-jakarta-web/src/main/java/org/apache/logging/log4j/web/WebLoggerContextUtils.java b/log4j-jakarta-web/src/main/java/org/apache/logging/log4j/web/WebLoggerContextUtils.java
new file mode 100644
index 0000000..32dd981
--- /dev/null
+++ b/log4j-jakarta-web/src/main/java/org/apache/logging/log4j/web/WebLoggerContextUtils.java
@@ -0,0 +1,143 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.web;
+
+import java.util.AbstractMap;
+import java.util.Map;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+import jakarta.servlet.ServletContext;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.impl.ContextAnchor;
+
+/**
+ * Convenience methods for retrieving the {@link org.apache.logging.log4j.core.LoggerContext} associated with a
+ * particular ServletContext. These methods are most particularly useful for asynchronous servlets where the
+ * Thread Context ClassLoader (TCCL) is potentially different from the TCCL used by the
+ * Servlet container that bootstrapped Log4j.
+ *
+ * @since 2.0.1
+ */
+public final class WebLoggerContextUtils {
+    private WebLoggerContextUtils() {
+    }
+
+    private static final Lock WEB_SUPPORT_LOOKUP = new ReentrantLock();
+    private static final String SERVLET_CONTEXT = "__SERVLET_CONTEXT__";
+
+    /**
+     * Finds the main {@link org.apache.logging.log4j.core.LoggerContext} configured for the given ServletContext.
+     *
+     * @param servletContext the ServletContext to locate a LoggerContext for
+     * @return the LoggerContext for the given ServletContext
+     * @since 2.0.1
+     */
+    public static LoggerContext getWebLoggerContext(final ServletContext servletContext) {
+        return (LoggerContext) servletContext.getAttribute(Log4jWebSupport.CONTEXT_ATTRIBUTE);
+    }
+
+    /**
+     * Finds the main {@link org.apache.logging.log4j.core.LoggerContext} configured for the given ServletContext.
+     *
+     * @param servletContext the ServletContext to locate a LoggerContext for
+     * @return the LoggerContext for the given ServletContext or {@code null} if none was set
+     * @throws java.lang.IllegalStateException if no LoggerContext could be found on the given ServletContext
+     * @since 2.0.1
+     */
+    public static LoggerContext getRequiredWebLoggerContext(final ServletContext servletContext) {
+        final LoggerContext loggerContext = getWebLoggerContext(servletContext);
+        if (loggerContext == null) {
+            throw new IllegalStateException(
+                "No LoggerContext found in ServletContext attribute " + Log4jWebSupport.CONTEXT_ATTRIBUTE);
+        }
+        return loggerContext;
+    }
+
+    /**
+     * Finds or initializes the {@link org.apache.logging.log4j.web.Log4jWebLifeCycle} singleton for the given
+     * ServletContext.
+     *
+     * @param servletContext the ServletContext to get the Log4jWebLifeCycle for
+     * @return the Log4jWebLifeCycle for the given ServletContext
+     * @since 2.0.1
+     */
+    public static Log4jWebLifeCycle getWebLifeCycle(final ServletContext servletContext) {
+        WEB_SUPPORT_LOOKUP.lock();
+        try {
+            Log4jWebLifeCycle webLifeCycle = (Log4jWebLifeCycle) servletContext.getAttribute(
+                Log4jWebSupport.SUPPORT_ATTRIBUTE);
+            if (webLifeCycle == null) {
+                webLifeCycle = Log4jWebInitializerImpl.initialize(servletContext);
+            }
+            return webLifeCycle;
+        } finally {
+            WEB_SUPPORT_LOOKUP.unlock();
+        }
+    }
+
+    /**
+     * Wraps a Runnable instance by setting its thread context {@link org.apache.logging.log4j.core.LoggerContext}
+     * before execution and clearing it after execution.
+     *
+     * @param servletContext the ServletContext to locate a LoggerContext for
+     * @param runnable       the Runnable to wrap execution for
+     * @return a wrapped Runnable
+     * @since 2.0.1
+     */
+    public static Runnable wrapExecutionContext(final ServletContext servletContext, final Runnable runnable) {
+        return () -> {
+            final Log4jWebSupport webSupport = getWebLifeCycle(servletContext);
+            webSupport.setLoggerContext();
+            try {
+                runnable.run();
+            } finally {
+                webSupport.clearLoggerContext();
+            }
+        };
+    }
+
+    public static Map.Entry<String, Object> createExternalEntry(ServletContext servletContext) {
+        return new AbstractMap.SimpleEntry<>(SERVLET_CONTEXT, servletContext);
+    }
+
+    public static void setServletContext(LoggerContext lc, ServletContext servletContext) {
+        if (lc != null) {
+            lc.putObject(SERVLET_CONTEXT, servletContext);
+        }
+    }
+
+    /**
+     * Gets the current {@link ServletContext} if it has already been assigned to a LoggerContext's external context.
+     *
+     * @return the current ServletContext attached to a LoggerContext or {@code null} if none could be found
+     * @since 2.1
+     */
+    public static ServletContext getServletContext() {
+        org.apache.logging.log4j.spi.LoggerContext lc = ContextAnchor.THREAD_CONTEXT.get();
+        if (lc == null) {
+            lc = LogManager.getContext(false);
+        }
+
+        Object obj = lc != null ? lc.getObject(SERVLET_CONTEXT) : null;
+        if (obj instanceof ServletContext) {
+            return (ServletContext) obj;
+        }
+        return null;
+    }
+}
diff --git a/log4j-jakarta-web/src/main/java/org/apache/logging/log4j/web/WebLookup.java b/log4j-jakarta-web/src/main/java/org/apache/logging/log4j/web/WebLookup.java
new file mode 100644
index 0000000..774437b
--- /dev/null
+++ b/log4j-jakarta-web/src/main/java/org/apache/logging/log4j/web/WebLookup.java
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.web;
+// Please note that if you move this class, make sure to update the Interpolator class (if still applicable) or remove
+// this comment if no longer relevant
+
+import jakarta.servlet.ServletContext;
+
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.config.plugins.Plugin;
+import org.apache.logging.log4j.core.lookup.AbstractLookup;
+import org.apache.logging.log4j.util.Strings;
+
+@Plugin(name = "web", category = "Lookup")
+public class WebLookup extends AbstractLookup {
+    private static final String ATTR_PREFIX = "attr.";
+    private static final String INIT_PARAM_PREFIX = "initParam.";
+
+    @Override
+    public String lookup(final LogEvent event, final String key) {
+        final ServletContext ctx = WebLoggerContextUtils.getServletContext();
+        if (ctx == null) {
+            return null;
+        }
+
+        if (key.startsWith(ATTR_PREFIX)) {
+            final String attrName = key.substring(ATTR_PREFIX.length());
+            final Object attrValue = ctx.getAttribute(attrName);
+            return attrValue == null ? null : attrValue.toString();
+        }
+
+        if (key.startsWith(INIT_PARAM_PREFIX)) {
+            final String paramName = key.substring(INIT_PARAM_PREFIX.length());
+            return ctx.getInitParameter(paramName);
+        }
+
+        if ("rootDir".equals(key)) {
+            final String root = ctx.getRealPath("/");
+            if (root == null) {
+                final String msg = "Failed to resolve web:rootDir -- " +
+                        "servlet container unable to translate virtual path " +
+                        " to real path (probably not deployed as exploded";
+                throw new IllegalStateException(msg);
+            }
+            return root;
+        }
+
+        if ("contextPathName".equals(key)) {
+            String path = ctx.getContextPath();
+            if (path.trim().contains("/")) {
+                String[] fields = path.split("/");
+                for (String field : fields) {
+                    if (field.length() > 0) {
+                        return field;
+                    }
+                }
+                return null;
+            }
+            return ctx.getContextPath();
+        }
+
+        if ("contextPath".equals(key)) {
+            return ctx.getContextPath();
+        }
+
+        if ("servletContextName".equals(key)) {
+            return ctx.getServletContextName();
+        }
+
+        if ("serverInfo".equals(key)) {
+            return ctx.getServerInfo();
+        }
+
+        if ("effectiveMajorVersion".equals(key)) {
+            return String.valueOf(ctx.getEffectiveMajorVersion());
+        }
+
+        if ("effectiveMinorVersion".equals(key)) {
+            return String.valueOf(ctx.getEffectiveMinorVersion());
+        }
+
+        if ("majorVersion".equals(key)) {
+            return String.valueOf(ctx.getMajorVersion());
+        }
+
+        if ("minorVersion".equals(key)) {
+            return String.valueOf(ctx.getMinorVersion());
+        }
+
+        if (ctx.getAttribute(key) != null) {
+            return ctx.getAttribute(key).toString();
+        }
+
+        if (ctx.getInitParameter(key) != null) {
+            return ctx.getInitParameter(key);
+        }
+
+        ctx.log(getClass().getName() + " unable to resolve key " + Strings.quote(key));
+        return null;
+    }
+}
diff --git a/log4j-jakarta-web/src/main/java/org/apache/logging/log4j/web/appender/ServletAppender.java b/log4j-jakarta-web/src/main/java/org/apache/logging/log4j/web/appender/ServletAppender.java
new file mode 100644
index 0000000..ca0c141
--- /dev/null
+++ b/log4j-jakarta-web/src/main/java/org/apache/logging/log4j/web/appender/ServletAppender.java
@@ -0,0 +1,130 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.web.appender;
+
+import java.io.Serializable;
+
+import jakarta.servlet.ServletContext;
+
+import org.apache.logging.log4j.core.Filter;
+import org.apache.logging.log4j.core.Layout;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.appender.AbstractAppender;
+import org.apache.logging.log4j.core.config.Property;
+import org.apache.logging.log4j.core.config.plugins.Plugin;
+import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute;
+import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
+import org.apache.logging.log4j.core.layout.AbstractStringLayout;
+import org.apache.logging.log4j.core.layout.PatternLayout;
+import org.apache.logging.log4j.web.WebLoggerContextUtils;
+
+/**
+ * Logs using the ServletContext's log method
+ */
+@Plugin(name = "Servlet", category = "Core", elementType = "appender", printObject = true)
+public class ServletAppender extends AbstractAppender {
+
+	public static class Builder<B extends Builder<B>> extends AbstractAppender.Builder<B>
+			implements org.apache.logging.log4j.core.util.Builder<ServletAppender> {
+
+        @PluginBuilderAttribute
+        private boolean logThrowables;
+
+		@Override
+		public ServletAppender build() {
+			final String name = getName();
+			if (name == null) {
+				LOGGER.error("No name provided for ServletAppender");
+			}
+			final ServletContext servletContext = WebLoggerContextUtils.getServletContext();
+			if (servletContext == null) {
+				LOGGER.error("No servlet context is available");
+				return null;
+			}
+			Layout<? extends Serializable> layout = getLayout();
+			if (layout == null) {
+				layout = PatternLayout.createDefaultLayout();
+			} else if (!(layout instanceof AbstractStringLayout)) {
+				LOGGER.error("Layout must be a StringLayout to log to ServletContext");
+				return null;
+			}
+			return new ServletAppender(name, layout, getFilter(), servletContext, isIgnoreExceptions(), logThrowables);
+		}
+
+        /**
+         * Logs with {@link ServletContext#log(String, Throwable)} if true and with {@link ServletContext#log(String)} if false.
+         *
+         * @return whether to log a Throwable with the servlet context.
+         */
+        public boolean isLogThrowables() {
+            return logThrowables;
+        }
+
+        /**
+         * Logs with {@link ServletContext#log(String, Throwable)} if true and with {@link ServletContext#log(String)} if false.
+         */
+        public void setLogThrowables(final boolean logThrowables) {
+            this.logThrowables = logThrowables;
+        }
+
+	}
+
+    @PluginBuilderFactory
+    public static <B extends Builder<B>> B newBuilder() {
+        return new Builder<B>().asBuilder();
+    }
+
+    private final ServletContext servletContext;
+    private final boolean logThrowables;
+
+    private ServletAppender(final String name, final Layout<? extends Serializable> layout, final Filter filter,
+            final ServletContext servletContext, final boolean ignoreExceptions, final boolean logThrowables) {
+        super(name, filter, layout, ignoreExceptions, Property.EMPTY_ARRAY);
+        this.servletContext = servletContext;
+        this.logThrowables = logThrowables;
+    }
+
+    @Override
+    public void append(final LogEvent event) {
+        final String serialized = ((AbstractStringLayout) getLayout()).toSerializable(event);
+        if (logThrowables) {
+            servletContext.log(serialized, event.getThrown());
+        } else {
+            servletContext.log(serialized);
+        }
+    }
+
+    /**
+     * Creates a Servlet Appender.
+     * @param layout The layout to use (required). Must extend {@link AbstractStringLayout}.
+     * @param filter The Filter or null.
+     * @param name The name of the Appender (required).
+     * @param ignoreExceptions If {@code true} (default) exceptions encountered when appending events are logged;
+     *                         otherwise they are propagated to the caller.
+     * @return The ServletAppender.
+     * @deprecated Use {@link #newBuilder()}.
+     */
+    @Deprecated
+    public static ServletAppender createAppender(final Layout<? extends Serializable> layout, final Filter filter,
+            final String name, final boolean ignoreExceptions) {
+        // @formatter:off
+    	return newBuilder().setFilter(filter).setIgnoreExceptions(ignoreExceptions).setLayout(layout).setName(name)
+    			.build();
+    	// @formatter:on
+    }
+
+}
diff --git a/log4j-jakarta-web/src/main/java/org/apache/logging/log4j/web/package-info.java b/log4j-jakarta-web/src/main/java/org/apache/logging/log4j/web/package-info.java
new file mode 100644
index 0000000..3a918a8
--- /dev/null
+++ b/log4j-jakarta-web/src/main/java/org/apache/logging/log4j/web/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+
+/**
+ * The classes in this package are responsible for properly initializing and deinitializing Log4j in a Java EE web
+ * application.
+ *
+ * @see <a href="http://logging.apache.org/log4j/2.x/manual/webapp.html">Using Log4j 2 in Web Applications</a>
+ */
+package org.apache.logging.log4j.web;
diff --git a/log4j-jakarta-web/src/main/resources/META-INF/services/javax.servlet.ServletContainerInitializer b/log4j-jakarta-web/src/main/resources/META-INF/services/javax.servlet.ServletContainerInitializer
new file mode 100644
index 0000000..6850dab
--- /dev/null
+++ b/log4j-jakarta-web/src/main/resources/META-INF/services/javax.servlet.ServletContainerInitializer
@@ -0,0 +1,22 @@
+org.apache.logging.log4j.web.Log4jServletContainerInitializer
+#
+# See https://issues.apache.org/jira/browse/LOG4J2-890
+# See https://issues.jboss.org/browse/WFLY-4458
+#
+
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache license, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the license for the specific language governing permissions and
+# limitations under the license.
+#
diff --git a/log4j-jakarta-web/src/main/resources/META-INF/web-fragment.xml b/log4j-jakarta-web/src/main/resources/META-INF/web-fragment.xml
new file mode 100644
index 0000000..ae5af58
--- /dev/null
+++ b/log4j-jakarta-web/src/main/resources/META-INF/web-fragment.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements.  See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<web-fragment xmlns="http://java.sun.com/xml/ns/javaee"
+              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+              xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
+                                  http://java.sun.com/xml/ns/javaee/web-fragment_3_0.xsd"
+              version="3.0" metadata-complete="true">
+    <!-- The Log4j web fragment must be loaded before all other fragments. The configuration below should make this
+        happen automatically. If you experience problems, try specifying an <absolute-ordering> in your web.xml
+        deployment descriptor. -->
+    <name>log4j</name>
+    <distributable />
+    <ordering>
+        <before>
+            <others />
+        </before>
+    </ordering>
+</web-fragment>
\ No newline at end of file
diff --git a/log4j-jakarta-web/src/site/markdown/index.md b/log4j-jakarta-web/src/site/markdown/index.md
new file mode 100644
index 0000000..8da38f2
--- /dev/null
+++ b/log4j-jakarta-web/src/site/markdown/index.md
@@ -0,0 +1,30 @@
+<!-- vim: set syn=markdown : -->
+<!--
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<!-- TODO: turn this into a velocity template for all the version numbers -->
+
+# Web Servlet Containers
+
+The Web module provides support for automatically enabling Log4j in Servlet containers.
+
+See the user manual page on [Web Applications and JSPs](../manual/webapp.html)
+for details on using Log4j 2 in Web Applications.
+
+## Requirements
+
+The Web module requires Servlet 5.0 at minimum and is dependent on the Log4j 2 API and implementation.
+For more information, see [Runtime Dependencies](../runtime-dependencies.html).
diff --git a/log4j-jakarta-web/src/site/site.xml b/log4j-jakarta-web/src/site/site.xml
new file mode 100644
index 0000000..0f3fd0f
--- /dev/null
+++ b/log4j-jakarta-web/src/site/site.xml
@@ -0,0 +1,52 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements.  See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<project name="Log4j Web Support"
+         xmlns="http://maven.apache.org/DECORATION/1.4.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/DECORATION/1.4.0 http://maven.apache.org/xsd/decoration-1.4.0.xsd">
+  <body>
+    <links>
+      <item name="Apache" href="http://www.apache.org/" />
+      <item name="Logging Services" href="http://logging.apache.org/"/>
+      <item name="Log4j" href="../index.html"/>
+    </links>
+
+    <!-- Component-specific reports -->
+    <menu ref="reports"/>
+
+	<!-- Overall Project Info -->
+    <menu name="Log4j Project Information" img="icon-info-sign">
+      <item name="Dependencies" href="../dependencies.html" />
+      <item name="Dependency Convergence" href="../dependency-convergence.html" />
+      <item name="Dependency Management" href="../dependency-management.html" />
+      <item name="Project Team" href="../team-list.html" />
+      <item name="Mailing Lists" href="../mail-lists.html" />
+      <item name="Issue Tracking" href="../issue-tracking.html" />
+      <item name="Project License" href="../license.html" />
+      <item name="Source Repository" href="../source-repository.html" />
+      <item name="Project Summary" href="../project-summary.html" />
+    </menu>
+
+    <menu name="Log4j Project Reports" img="icon-cog">
+      <item name="Changes Report" href="../changes-report.html" />
+      <item name="JIRA Report" href="../jira-report.html" />
+      <item name="Surefire Report" href="../surefire-report.html" />
+      <item name="RAT Report" href="../rat-report.html" />
+    </menu>
+  </body>
+</project>
diff --git a/log4j-jakarta-web/src/test/java/org/apache/logging/log4j/web/Log4jServletContainerInitializerTest.java b/log4j-jakarta-web/src/test/java/org/apache/logging/log4j/web/Log4jServletContainerInitializerTest.java
new file mode 100644
index 0000000..003360d
--- /dev/null
+++ b/log4j-jakarta-web/src/test/java/org/apache/logging/log4j/web/Log4jServletContainerInitializerTest.java
@@ -0,0 +1,184 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.web;
+
+import java.util.EnumSet;
+import java.util.EventListener;
+import jakarta.servlet.DispatcherType;
+import jakarta.servlet.Filter;
+import jakarta.servlet.FilterRegistration;
+import jakarta.servlet.ServletContext;
+
+import org.apache.logging.log4j.util.Strings;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.BDDMockito.any;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.BDDMockito.never;
+import static org.mockito.BDDMockito.then;
+import static org.mockito.BDDMockito.willThrow;
+import static org.mockito.Mockito.mock;
+
+@ExtendWith(MockitoExtension.class)
+public class Log4jServletContainerInitializerTest {
+    @Mock
+    private ServletContext servletContext;
+    @Mock
+    private Log4jWebLifeCycle initializer;
+    @Captor
+    private ArgumentCaptor<Class<? extends Filter>> filterCaptor;
+    @Captor
+    private ArgumentCaptor<EventListener> listenerCaptor;
+
+    private Log4jServletContainerInitializer containerInitializer;
+
+    @BeforeEach
+    public void setUp() {
+        this.containerInitializer = new Log4jServletContainerInitializer();
+    }
+
+    @Test
+    public void testOnStartupWithServletVersion2_x() throws Exception {
+        given(servletContext.getMajorVersion()).willReturn(2);
+
+        this.containerInitializer.onStartup(null, this.servletContext);
+    }
+
+    @Test
+    public void testOnStartupWithServletVersion3_xEffectiveVersion2_x() throws Exception {
+        given(servletContext.getMajorVersion()).willReturn(3);
+        given(servletContext.getEffectiveMajorVersion()).willReturn(2);
+
+        this.containerInitializer.onStartup(null, this.servletContext);
+    }
+
+    @Test
+    public void testOnStartupWithServletVersion3_xEffectiveVersion3_xDisabledTrue() throws Exception {
+        given(servletContext.getMajorVersion()).willReturn(3);
+        given(servletContext.getEffectiveMajorVersion()).willReturn(3);
+        given(servletContext.getInitParameter(eq(Log4jWebSupport.IS_LOG4J_AUTO_INITIALIZATION_DISABLED))).willReturn(
+            "true");
+
+        this.containerInitializer.onStartup(null, this.servletContext);
+    }
+
+    @Test
+    public void testOnStartupWithServletVersion3_xEffectiveVersion3_xShutdownDisabled() throws Exception {
+        final FilterRegistration.Dynamic registration = mock(FilterRegistration.Dynamic.class);
+        given(servletContext.getMajorVersion()).willReturn(3);
+        given(servletContext.getEffectiveMajorVersion()).willReturn(3);
+        given(servletContext.getInitParameter(eq(Log4jWebSupport.IS_LOG4J_AUTO_SHUTDOWN_DISABLED)))
+                      .willReturn("true");
+        given(servletContext.getInitParameter(eq(Log4jWebSupport.IS_LOG4J_AUTO_INITIALIZATION_DISABLED))).willReturn(
+                null);
+        given(servletContext.addFilter(eq("log4jServletFilter"), filterCaptor.capture())).willReturn(registration);
+        given(servletContext.getAttribute(Log4jWebSupport.SUPPORT_ATTRIBUTE)).willReturn(initializer);
+
+        this.containerInitializer.onStartup(null, this.servletContext);
+
+        then(initializer).should().start();
+        then(initializer).should().setLoggerContext();
+        then(registration).should().setAsyncSupported(eq(true));
+        then(registration).should().addMappingForUrlPatterns(eq(EnumSet.allOf(DispatcherType.class)), eq(false), eq("/*"));
+
+        // initParam IS_LOG4J_AUTO_SHUTDOWN_DISABLED is "true" so addListener shouldn't be called.
+        then(servletContext).should(never()).addListener(any(Log4jServletContextListener.class));
+    }
+
+    @Test
+    public void testOnStartupWithServletVersion3_xEffectiveVersion3_xDisabledTRUE() throws Exception {
+        given(servletContext.getMajorVersion()).willReturn(3);
+        given(servletContext.getEffectiveMajorVersion()).willReturn(3);
+        given(servletContext.getInitParameter(eq(Log4jWebSupport.IS_LOG4J_AUTO_INITIALIZATION_DISABLED))).willReturn(
+            "TRUE");
+
+        this.containerInitializer.onStartup(null, this.servletContext);
+    }
+
+    @Test
+    public void testOnStartupWithServletVersion3_xEffectiveVersion3_x() throws Exception {
+        final FilterRegistration.Dynamic registration = mock(FilterRegistration.Dynamic.class);
+        given(servletContext.getMajorVersion()).willReturn(3);
+        given(servletContext.getEffectiveMajorVersion()).willReturn(3);
+        given(servletContext.getInitParameter(eq(Log4jWebSupport.IS_LOG4J_AUTO_INITIALIZATION_DISABLED))).willReturn(
+            null);
+        given(servletContext.addFilter(eq("log4jServletFilter"), filterCaptor.capture())).willReturn(registration);
+        given(servletContext.getAttribute(Log4jWebSupport.SUPPORT_ATTRIBUTE)).willReturn(initializer);
+
+        containerInitializer.onStartup(null, servletContext);
+
+        then(initializer).should().start();
+        then(initializer).should().setLoggerContext();
+        then(servletContext).should().addListener(listenerCaptor.capture());
+        then(registration).should().setAsyncSupported(eq(true));
+        then(registration).should().addMappingForUrlPatterns(eq(EnumSet.allOf(DispatcherType.class)), eq(false), eq("/*"));
+
+        assertNotNull(listenerCaptor.getValue(), "The listener should not be null.");
+        assertSame(Log4jServletContextListener.class,
+            listenerCaptor.getValue().getClass(),
+            "The listener is not correct.");
+
+        assertNotNull(filterCaptor.getValue(), "The filter should not be null.");
+        assertSame(Log4jServletFilter.class, filterCaptor.getValue(), "The filter is not correct.");
+    }
+
+    @Test
+    public void testOnStartupCanceledDueToPreExistingFilter() throws Exception {
+        given(servletContext.getMajorVersion()).willReturn(3);
+        given(servletContext.getEffectiveMajorVersion()).willReturn(3);
+        given(servletContext.getInitParameter(eq(Log4jWebSupport.IS_LOG4J_AUTO_INITIALIZATION_DISABLED))).willReturn(
+            "false");
+        given(servletContext.addFilter(eq("log4jServletFilter"), filterCaptor.capture())).willReturn(null);
+
+        this.containerInitializer.onStartup(null, this.servletContext);
+
+        assertNotNull(filterCaptor.getValue(), "The filter should not be null.");
+        assertSame(Log4jServletFilter.class, filterCaptor.getValue(), "The filter is not correct.");
+    }
+
+    @Test
+    public void testOnStartupFailedDueToInitializerFailure() throws Exception {
+        final FilterRegistration.Dynamic registration = mock(FilterRegistration.Dynamic.class);
+        final IllegalStateException exception = new IllegalStateException(Strings.EMPTY);
+        given(servletContext.getMajorVersion()).willReturn(3);
+        given(servletContext.getEffectiveMajorVersion()).willReturn(3);
+        given(servletContext.getInitParameter(eq(Log4jWebSupport.IS_LOG4J_AUTO_INITIALIZATION_DISABLED))).willReturn(
+            "balderdash");
+        given(servletContext.addFilter(eq("log4jServletFilter"), filterCaptor.capture())).willReturn(registration);
+        given(servletContext.getAttribute(Log4jWebSupport.SUPPORT_ATTRIBUTE)).willReturn(initializer);
+        willThrow(exception).given(initializer).start();
+
+        try {
+            this.containerInitializer.onStartup(null, this.servletContext);
+            fail("Expected the exception thrown by the initializer; got no exception.");
+        } catch (final IllegalStateException e) {
+            assertSame(exception, e, "The exception is not correct.");
+        }
+
+        then(initializer).should().start();
+        assertNotNull(filterCaptor.getValue(), "The filter should not be null.");
+        assertSame(Log4jServletFilter.class, filterCaptor.getValue(), "The filter is not correct.");
+    }
+}
diff --git a/log4j-jakarta-web/src/test/java/org/apache/logging/log4j/web/Log4jServletContextListenerTest.java b/log4j-jakarta-web/src/test/java/org/apache/logging/log4j/web/Log4jServletContextListenerTest.java
new file mode 100644
index 0000000..bfdae99
--- /dev/null
+++ b/log4j-jakarta-web/src/test/java/org/apache/logging/log4j/web/Log4jServletContextListenerTest.java
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.web;
+
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.ServletContextEvent;
+
+import org.apache.logging.log4j.util.Strings;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.BDDMockito.eq;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.BDDMockito.then;
+import static org.mockito.BDDMockito.willThrow;
+
+@ExtendWith(MockitoExtension.class)
+public class Log4jServletContextListenerTest {
+	/* event and servletContext are marked lenient because they aren't used in the
+	 * testDestroyWithNoInit but are only accessed during initialization
+	 */
+	@Mock(lenient = true)
+	private ServletContextEvent event;
+	@Mock(lenient = true)
+    private ServletContext servletContext;
+    @Mock
+    private Log4jWebLifeCycle initializer;
+
+    private Log4jServletContextListener listener;
+
+    @BeforeEach
+    public void setUp() {
+        this.listener = new Log4jServletContextListener();
+        given(event.getServletContext()).willReturn(servletContext);
+        given(servletContext.getAttribute(Log4jWebSupport.SUPPORT_ATTRIBUTE)).willReturn(initializer);
+    }
+
+    @Test
+    public void testInitAndDestroy() throws Exception {
+        this.listener.contextInitialized(this.event);
+
+        then(initializer).should().start();
+        then(initializer).should().setLoggerContext();
+
+        this.listener.contextDestroyed(this.event);
+
+        then(initializer).should().clearLoggerContext();
+        then(initializer).should().stop();
+    }
+
+    @Test
+    public void testInitFailure() throws Exception {
+        willThrow(new IllegalStateException(Strings.EMPTY)).given(initializer).start();
+
+        try {
+            this.listener.contextInitialized(this.event);
+            fail("Expected a RuntimeException.");
+        } catch (final RuntimeException e) {
+            assertEquals("Failed to initialize Log4j properly.", e.getMessage(), "The message is not correct.");
+        }
+    }
+
+    @Test
+    public void initializingLog4jServletContextListenerShouldFaileWhenAutoShutdownIsTrue() throws Exception {
+        given(servletContext.getInitParameter(eq(Log4jWebSupport.IS_LOG4J_AUTO_SHUTDOWN_DISABLED)))
+                .willReturn("true");
+        ensureInitializingFailsWhenAuthShutdownIsEnabled();
+    }
+
+    @Test
+    public void initializingLog4jServletContextListenerShouldFaileWhenAutoShutdownIsTRUE() throws Exception {
+        given(servletContext.getInitParameter(eq(Log4jWebSupport.IS_LOG4J_AUTO_SHUTDOWN_DISABLED)))
+                .willReturn("TRUE");
+        ensureInitializingFailsWhenAuthShutdownIsEnabled();
+    }
+    
+    private void ensureInitializingFailsWhenAuthShutdownIsEnabled() {
+        try {
+            this.listener.contextInitialized(this.event);
+            fail("Expected a RuntimeException.");
+        } catch (final RuntimeException e) {
+            String expectedMessage = 
+                    "Do not use " + Log4jServletContextListener.class.getSimpleName() + " when " 
+                    + Log4jWebSupport.IS_LOG4J_AUTO_SHUTDOWN_DISABLED + " is true. Please use " 
+                    + Log4jShutdownOnContextDestroyedListener.class.getSimpleName() + " instead of " 
+                    + Log4jServletContextListener.class.getSimpleName() + ".";
+
+            assertEquals(expectedMessage, e.getMessage(), "The message is not correct");
+        }
+    }
+}
diff --git a/log4j-jakarta-web/src/test/java/org/apache/logging/log4j/web/Log4jServletFilterTest.java b/log4j-jakarta-web/src/test/java/org/apache/logging/log4j/web/Log4jServletFilterTest.java
new file mode 100644
index 0000000..0997f85
--- /dev/null
+++ b/log4j-jakarta-web/src/test/java/org/apache/logging/log4j/web/Log4jServletFilterTest.java
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.web;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.FilterConfig;
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.BeforeEach;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.BDDMockito.then;
+import static org.mockito.Mockito.reset;
+
+@ExtendWith(MockitoExtension.class)
+public class Log4jServletFilterTest {
+    @Mock(lenient = true) // because filterConfig is not used in testDestroy
+    private FilterConfig filterConfig;
+    @Mock(lenient = true) // because filterConfig is not used in testDestroy
+    private ServletContext servletContext;
+    @Mock
+    private Log4jWebLifeCycle initializer;
+    @Mock
+    private ServletRequest request;
+    @Mock
+    private ServletResponse response;
+    @Mock
+    private FilterChain chain;
+
+    private Log4jServletFilter filter;
+
+    @BeforeEach
+    public void setUp() {
+        given(filterConfig.getServletContext()).willReturn(servletContext);
+        given(servletContext.getAttribute(Log4jWebSupport.SUPPORT_ATTRIBUTE)).willReturn(initializer);
+        this.filter = new Log4jServletFilter();
+    }
+
+    @Test
+    public void testInitAndDestroy() throws Exception {
+        this.filter.init(this.filterConfig);
+
+        then(initializer).should().clearLoggerContext();
+
+        this.filter.destroy();
+
+        then(initializer).should().setLoggerContext();
+    }
+
+    @Test
+    public void testDestroy() {
+    	assertThrows(IllegalStateException.class, () -> {
+    		this.filter.destroy();
+    	});
+    }
+
+    @Test
+    public void testDoFilterFirstTime() throws Exception {
+        this.filter.init(this.filterConfig);
+
+        then(initializer).should().clearLoggerContext();
+        reset(initializer);
+
+        given(request.getAttribute(Log4jServletFilter.ALREADY_FILTERED_ATTRIBUTE)).willReturn(null);
+
+        this.filter.doFilter(request, response, chain);
+
+        then(request).should().setAttribute(eq(Log4jServletFilter.ALREADY_FILTERED_ATTRIBUTE), eq(true));
+        then(initializer).should().setLoggerContext();
+        then(chain).should().doFilter(same(request), same(response));
+        then(chain).shouldHaveNoMoreInteractions();
+        then(initializer).should().clearLoggerContext();
+    }
+
+    @Test
+    public void testDoFilterSecondTime() throws Exception {
+        this.filter.init(this.filterConfig);
+
+        then(initializer).should().clearLoggerContext();
+
+        given(request.getAttribute(Log4jServletFilter.ALREADY_FILTERED_ATTRIBUTE)).willReturn(true);
+
+        this.filter.doFilter(request, response, chain);
+
+        then(chain).should().doFilter(same(request), same(response));
+        then(chain).shouldHaveNoMoreInteractions();
+    }
+}
diff --git a/log4j-jakarta-web/src/test/java/org/apache/logging/log4j/web/Log4jShutdownOnContextDestroyedListenerTest.java b/log4j-jakarta-web/src/test/java/org/apache/logging/log4j/web/Log4jShutdownOnContextDestroyedListenerTest.java
new file mode 100644
index 0000000..76c5de3
--- /dev/null
+++ b/log4j-jakarta-web/src/test/java/org/apache/logging/log4j/web/Log4jShutdownOnContextDestroyedListenerTest.java
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.web;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.BDDMockito.then;
+import static org.mockito.Mockito.never;
+
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.ServletContextEvent;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+public class Log4jShutdownOnContextDestroyedListenerTest {
+    @Mock(lenient = true)
+    private ServletContextEvent event;
+    @Mock(lenient = true)
+    private ServletContext servletContext;
+    @Mock
+    private Log4jWebLifeCycle initializer;
+
+    private Log4jShutdownOnContextDestroyedListener listener;
+
+    public void setUp(boolean mockInitializer) {
+        this.listener = new Log4jShutdownOnContextDestroyedListener();
+        given(event.getServletContext()).willReturn(servletContext);
+        if (mockInitializer) {        	
+        	given(servletContext.getAttribute(Log4jWebSupport.SUPPORT_ATTRIBUTE))
+        			.willReturn(initializer);
+        }
+    }
+		
+    @Test
+    public void testInitAndDestroy() throws Exception {
+    	setUp(true);
+        this.listener.contextInitialized(this.event);
+
+        then(initializer).should(never()).start();
+        then(initializer).should(never()).setLoggerContext();
+
+        this.listener.contextDestroyed(this.event);
+
+        then(initializer).should().clearLoggerContext();
+        then(initializer).should().stop();
+    }
+
+    @Test
+    public void testDestroy() throws Exception {
+    	setUp(true);
+        this.listener.contextDestroyed(this.event);
+
+        then(initializer).should(never()).clearLoggerContext();
+        then(initializer).should(never()).stop();
+    }
+    
+    @Test
+    public void whenNoInitializerInContextTheContextInitializedShouldThrowAnException() {
+    	setUp(false);
+    	
+    	assertThrows(IllegalStateException.class, () -> {
+    		this.listener.contextInitialized(this.event);
+    	});
+    }
+}
diff --git a/log4j-jakarta-web/src/test/java/org/apache/logging/log4j/web/Log4jWebInitializerImplTest.java b/log4j-jakarta-web/src/test/java/org/apache/logging/log4j/web/Log4jWebInitializerImplTest.java
new file mode 100644
index 0000000..808b98e
--- /dev/null
+++ b/log4j-jakarta-web/src/test/java/org/apache/logging/log4j/web/Log4jWebInitializerImplTest.java
@@ -0,0 +1,403 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.web;
+
+import java.net.URI;
+import java.net.URL;
+import java.util.HashSet;
+import jakarta.servlet.ServletContext;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.config.DefaultConfiguration;
+import org.apache.logging.log4j.core.config.composite.CompositeConfiguration;
+import org.apache.logging.log4j.core.impl.ContextAnchor;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.BDDMockito.then;
+
+@ExtendWith(MockitoExtension.class)
+public class Log4jWebInitializerImplTest {
+	/* Marking servletContext lenient because otherwise testCompositeLocationParameterWithEmptyUriListSetsDefaultConfiguration fails
+	 * when null is passed in as the initial param because Mockito deciced null isn't a String rather than the absence of a string.
+	 */
+	@Mock(lenient = true)
+	private ServletContext servletContext;
+    @Captor
+    private ArgumentCaptor<Log4jWebLifeCycle> initializerCaptor;
+    @Captor
+    private ArgumentCaptor<LoggerContext> loggerContextCaptor;
+
+    private Log4jWebInitializerImpl initializerImpl;
+
+    @BeforeEach
+    public void setUp() {
+        given(servletContext.getAttribute(Log4jWebSupport.SUPPORT_ATTRIBUTE)).willReturn(null);
+
+        final Log4jWebLifeCycle initializer = WebLoggerContextUtils.getWebLifeCycle(this.servletContext);
+
+        then(servletContext).should().setAttribute(eq(Log4jWebSupport.SUPPORT_ATTRIBUTE), initializerCaptor.capture());
+        assertNotNull(initializer, "The initializer should not be null.");
+        assertSame(initializer, initializerCaptor.getValue(), "The capture is not correct.");
+        assertTrue(initializer instanceof Log4jWebInitializerImpl, "The initializer is not correct.");
+
+        this.initializerImpl = (Log4jWebInitializerImpl) initializer;
+    }
+
+    @Test
+    public void testDeinitializeBeforeInitialize() {
+    	assertThrows(IllegalStateException.class, () -> {
+    		this.initializerImpl.stop();
+    	});
+    }
+
+    @Test
+    public void testSetLoggerContextBeforeInitialize() {
+        assertNull(ContextAnchor.THREAD_CONTEXT.get(), "The context should be null.");
+
+        this.initializerImpl.setLoggerContext();
+
+        assertNull(ContextAnchor.THREAD_CONTEXT.get(), "The context should still be null.");
+    }
+
+    @Test
+    public void testClearLoggerContextBeforeInitialize() {
+        assertNull(ContextAnchor.THREAD_CONTEXT.get(), "The context should be null.");
+
+        this.initializerImpl.clearLoggerContext();
+
+        assertNull(ContextAnchor.THREAD_CONTEXT.get(), "The context should still be null.");
+    }
+
+    @Test
+    public void testInitializeWithNoParametersThenSetLoggerContextThenDeinitialize() {
+        given(servletContext.getInitParameter(eq(Log4jWebSupport.LOG4J_CONTEXT_NAME))).willReturn(null);
+        given(servletContext.getInitParameter(eq(Log4jWebSupport.LOG4J_CONFIG_LOCATION))).willReturn(null);
+        given(servletContext.getInitParameter(eq(Log4jWebSupport.IS_LOG4J_CONTEXT_SELECTOR_NAMED))).willReturn(null);
+        given(servletContext.getServletContextName()).willReturn("helloWorld01");
+        given(servletContext.getResourcePaths("/WEB-INF/")).willReturn(null);
+
+        this.initializerImpl.start();
+
+        then(servletContext).should().setAttribute(eq(Log4jWebSupport.CONTEXT_ATTRIBUTE), loggerContextCaptor.capture());
+        assertNotNull(loggerContextCaptor.getValue(), "The context attribute should not be null.");
+        final org.apache.logging.log4j.spi.LoggerContext loggerContext = loggerContextCaptor.getValue();
+
+        assertNull(ContextAnchor.THREAD_CONTEXT.get(), "The context should still be null.");
+
+        this.initializerImpl.setLoggerContext();
+
+        final LoggerContext context = ContextAnchor.THREAD_CONTEXT.get();
+        assertNotNull(context, "The context should not be null.");
+        assertSame(loggerContext, context, "The context is not correct.");
+
+        this.initializerImpl.clearLoggerContext();
+
+        assertNull(ContextAnchor.THREAD_CONTEXT.get(), "The context should be null again.");
+
+        this.initializerImpl.stop();
+
+        then(servletContext).should().removeAttribute(eq(Log4jWebSupport.CONTEXT_ATTRIBUTE));
+
+        assertNull(ContextAnchor.THREAD_CONTEXT.get(), "The context should again still be null.");
+
+        this.initializerImpl.setLoggerContext();
+
+        assertNull(ContextAnchor.THREAD_CONTEXT.get(), "The context should finally still be null.");
+    }
+
+    @Test
+    public void testInitializeWithClassLoaderNoParametersThenSetLoggerContextThenDeinitialize() {
+        given(servletContext.getInitParameter(eq(Log4jWebSupport.LOG4J_CONTEXT_NAME))).willReturn(null);
+        given(servletContext.getInitParameter(eq(Log4jWebSupport.LOG4J_CONFIG_LOCATION))).willReturn(null);
+        given(servletContext.getInitParameter(eq(Log4jWebSupport.IS_LOG4J_CONTEXT_SELECTOR_NAMED))).willReturn("false");
+        given(servletContext.getServletContextName()).willReturn("helloWorld02");
+        given(servletContext.getResourcePaths("/WEB-INF/")).willReturn(null);
+        given(servletContext.getClassLoader()).willReturn(getClass().getClassLoader());
+        assertNull(ContextAnchor.THREAD_CONTEXT.get(), "The context should be null.");
+
+        this.initializerImpl.start();
+
+        then(servletContext).should().setAttribute(eq(Log4jWebSupport.CONTEXT_ATTRIBUTE), loggerContextCaptor.capture());
+        assertNotNull(loggerContextCaptor.getValue(), "The context attribute should not be null.");
+        final org.apache.logging.log4j.spi.LoggerContext loggerContext = loggerContextCaptor.getValue();
+
+        assertNull(ContextAnchor.THREAD_CONTEXT.get(), "The context should still be null.");
+
+        this.initializerImpl.setLoggerContext();
+
+        final LoggerContext context = ContextAnchor.THREAD_CONTEXT.get();
+        assertNotNull(context, "The context should not be null.");
+        assertSame(loggerContext, context, "The context is not correct.");
+
+        this.initializerImpl.clearLoggerContext();
+
+        assertNull(ContextAnchor.THREAD_CONTEXT.get(), "The context should be null again.");
+
+        this.initializerImpl.stop();
+
+        then(servletContext).should().removeAttribute(eq(Log4jWebSupport.CONTEXT_ATTRIBUTE));
+
+        assertNull(ContextAnchor.THREAD_CONTEXT.get(), "The context should again still be null.");
+
+        this.initializerImpl.setLoggerContext();
+
+        assertNull(ContextAnchor.THREAD_CONTEXT.get(), "The context should finally still be null.");
+    }
+
+    @Test
+    public void testInitializeIsIdempotent() {
+        given(servletContext.getInitParameter(eq(Log4jWebSupport.LOG4J_CONTEXT_NAME))).willReturn(null);
+        given(servletContext.getInitParameter(eq(Log4jWebSupport.LOG4J_CONFIG_LOCATION))).willReturn(null);
+        given(servletContext.getInitParameter(eq(Log4jWebSupport.IS_LOG4J_CONTEXT_SELECTOR_NAMED))).willReturn("nothing");
+        given(servletContext.getServletContextName()).willReturn("helloWorld03");
+        given(servletContext.getResourcePaths("/WEB-INF/")).willReturn(null);
+        given(servletContext.getClassLoader()).willReturn(getClass().getClassLoader());
+        assertNull(ContextAnchor.THREAD_CONTEXT.get(), "The context should be null.");
+
+        this.initializerImpl.start();
+
+        then(servletContext).should().setAttribute(eq(Log4jWebSupport.CONTEXT_ATTRIBUTE), loggerContextCaptor.capture());
+        assertNotNull(loggerContextCaptor.getValue(), "The context attribute should not be null.");
+
+        this.initializerImpl.start();
+        this.initializerImpl.start();
+        this.initializerImpl.start();
+        this.initializerImpl.stop();
+
+        then(servletContext).should().removeAttribute(eq(Log4jWebSupport.CONTEXT_ATTRIBUTE));
+    }
+
+    @Test
+    public void testInitializeFailsAfterDeinitialize() {
+        given(servletContext.getInitParameter(eq(Log4jWebSupport.LOG4J_CONTEXT_NAME))).willReturn(null);
+        given(servletContext.getInitParameter(eq(Log4jWebSupport.LOG4J_CONFIG_LOCATION))).willReturn(null);
+        given(servletContext.getInitParameter(eq(Log4jWebSupport.IS_LOG4J_CONTEXT_SELECTOR_NAMED))).willReturn(null);
+        given(servletContext.getServletContextName()).willReturn("helloWorld04");
+        given(servletContext.getResourcePaths("/WEB-INF/")).willReturn(null);
+        given(servletContext.getClassLoader()).willReturn(getClass().getClassLoader());
+        assertNull(ContextAnchor.THREAD_CONTEXT.get(), "The context should be null.");
+
+        this.initializerImpl.start();
+
+        then(servletContext).should().setAttribute(eq(Log4jWebSupport.CONTEXT_ATTRIBUTE), loggerContextCaptor.capture());
+        assertNotNull(loggerContextCaptor.getValue(), "The context attribute should not be null.");
+
+        this.initializerImpl.stop();
+
+        then(servletContext).should().removeAttribute(eq(Log4jWebSupport.CONTEXT_ATTRIBUTE));
+
+    	assertThrows(IllegalStateException.class, () -> {
+    		this.initializerImpl.start();
+    	});
+    }
+
+    @Test
+    public void testDeinitializeIsIdempotent() {
+        given(servletContext.getInitParameter(eq(Log4jWebSupport.LOG4J_CONTEXT_NAME))).willReturn(null);
+        given(servletContext.getInitParameter(eq(Log4jWebSupport.LOG4J_CONFIG_LOCATION))).willReturn(null);
+        given(servletContext.getInitParameter(eq(Log4jWebSupport.IS_LOG4J_CONTEXT_SELECTOR_NAMED))).willReturn(null);
+        given(servletContext.getServletContextName()).willReturn("helloWorld05");
+        given(servletContext.getResourcePaths("/WEB-INF/")).willReturn(null);
+        given(servletContext.getClassLoader()).willReturn(getClass().getClassLoader());
+        assertNull(ContextAnchor.THREAD_CONTEXT.get(), "The context should be null.");
+
+        this.initializerImpl.start();
+
+        then(servletContext).should().setAttribute(eq(Log4jWebSupport.CONTEXT_ATTRIBUTE), loggerContextCaptor.capture());
+        assertNotNull(loggerContextCaptor.getValue(), "The context attribute should not be null.");
+
+        this.initializerImpl.stop();
+        this.initializerImpl.stop();
+        this.initializerImpl.stop();
+        then(servletContext).should().removeAttribute(eq(Log4jWebSupport.CONTEXT_ATTRIBUTE));
+    }
+
+    @Test
+    public void testInitializeUsingJndiSelectorFails() {
+        given(servletContext.getInitParameter(eq(Log4jWebSupport.LOG4J_CONTEXT_NAME))).willReturn(null);
+        given(servletContext.getInitParameter(eq(Log4jWebSupport.LOG4J_CONFIG_LOCATION))).willReturn(null);
+        given(servletContext.getInitParameter(eq(Log4jWebSupport.IS_LOG4J_CONTEXT_SELECTOR_NAMED))).willReturn("true");
+        given(servletContext.getResourcePaths("/WEB-INF/")).willReturn(null);
+        assertNull(ContextAnchor.THREAD_CONTEXT.get(), "The context should be null.");
+
+    	assertThrows(IllegalStateException.class, () -> {
+    		this.initializerImpl.start();
+    	});
+    }
+
+    @Test
+    public void testInitializeUsingJndiSelector() {
+        given(servletContext.getInitParameter(eq(Log4jWebSupport.LOG4J_CONTEXT_NAME))).willReturn("helloWorld06");
+        given(servletContext.getInitParameter(eq(Log4jWebSupport.LOG4J_CONFIG_LOCATION))).willReturn(null);
+        given(servletContext.getInitParameter(eq(Log4jWebSupport.IS_LOG4J_CONTEXT_SELECTOR_NAMED))).willReturn("true");
+        given(servletContext.getResourcePaths("/WEB-INF/")).willReturn(null);
+        assertNull(ContextAnchor.THREAD_CONTEXT.get(), "The context should be null.");
+
+        this.initializerImpl.start();
+
+        then(servletContext).should().setAttribute(eq(Log4jWebSupport.CONTEXT_ATTRIBUTE), loggerContextCaptor.capture());
+        assertNull(loggerContextCaptor.getValue(), "The context attribute should be null.");
+
+        assertNull(ContextAnchor.THREAD_CONTEXT.get(), "The context should still be null.");
+
+        this.initializerImpl.setLoggerContext();
+
+        assertNull(ContextAnchor.THREAD_CONTEXT.get(), "The context should still be null because no named selector.");
+
+        this.initializerImpl.clearLoggerContext();
+
+        assertNull(ContextAnchor.THREAD_CONTEXT.get(), "The context should be null again.");
+
+        this.initializerImpl.stop();
+
+        assertNull(ContextAnchor.THREAD_CONTEXT.get(), "The context should again still be null.");
+
+        this.initializerImpl.setLoggerContext();
+
+        assertNull(ContextAnchor.THREAD_CONTEXT.get(), "The context should finally still be null.");
+    }
+
+    @Test
+    public void testWrapExecutionWithNoParameters() {
+        given(servletContext.getInitParameter(eq(Log4jWebSupport.LOG4J_CONTEXT_NAME))).willReturn(null);
+        given(servletContext.getInitParameter(eq(Log4jWebSupport.LOG4J_CONFIG_LOCATION))).willReturn(null);
+        given(servletContext.getInitParameter(eq(Log4jWebSupport.IS_LOG4J_CONTEXT_SELECTOR_NAMED))).willReturn(null);
+        given(servletContext.getServletContextName()).willReturn("helloWorld07");
+        given(servletContext.getResourcePaths("/WEB-INF/")).willReturn(null);
+        assertNull(ContextAnchor.THREAD_CONTEXT.get(), "The context should be null.");
+
+        this.initializerImpl.start();
+
+        then(servletContext).should().setAttribute(eq(Log4jWebSupport.CONTEXT_ATTRIBUTE), loggerContextCaptor.capture());
+        assertNotNull(loggerContextCaptor.getValue(), "The context attribute should not be null.");
+        final org.apache.logging.log4j.spi.LoggerContext loggerContext = loggerContextCaptor.getValue();
+
+        assertNull(ContextAnchor.THREAD_CONTEXT.get(), "The context should still be null.");
+
+        final Runnable runnable = () -> {
+            final LoggerContext context = ContextAnchor.THREAD_CONTEXT.get();
+            assertNotNull(context, "The context should not be null.");
+            assertSame(loggerContext, context, "The context is not correct.");
+        };
+
+        this.initializerImpl.wrapExecution(runnable);
+
+        assertNull(ContextAnchor.THREAD_CONTEXT.get(), "The context should be null again.");
+
+        this.initializerImpl.stop();
+
+        then(servletContext).should().removeAttribute(eq(Log4jWebSupport.CONTEXT_ATTRIBUTE));
+
+        assertNull(ContextAnchor.THREAD_CONTEXT.get(), "The context should again still be null.");
+
+        this.initializerImpl.setLoggerContext();
+
+        assertNull(ContextAnchor.THREAD_CONTEXT.get(), "The context should finally still be null.");
+    }
+
+    @Test
+    public void testMissingLocationParameterWithNoMatchingResourceSetsNoConfigLocation() {
+        given(servletContext.getResourcePaths("/WEB-INF/")).willReturn(new HashSet<String>());
+
+        this.initializerImpl.start();
+
+        then(servletContext).should().setAttribute(eq(Log4jWebSupport.CONTEXT_ATTRIBUTE), loggerContextCaptor.capture());
+        assertNotNull(loggerContextCaptor.getValue(), "The context attribute should not be null.");
+
+        assertThat(loggerContextCaptor.getValue().getConfigLocation(), is(nullValue()));
+
+        this.initializerImpl.stop();
+    }
+
+    @Test
+    public void testMissingLocationParameterWithOneMatchingResourceUsesResourceConfigLocation() throws Exception {
+        given(servletContext.getResourcePaths("/WEB-INF/")).willReturn(new HashSet<>(singletonList("/WEB-INF/log4j2.xml")));
+        given(servletContext.getResource("/WEB-INF/log4j2.xml")).willReturn(new URL("file:/a/b/c/WEB-INF/log4j2.xml"));
+
+        this.initializerImpl.start();
+
+        then(servletContext).should().setAttribute(eq(Log4jWebSupport.CONTEXT_ATTRIBUTE), loggerContextCaptor.capture());
+        assertNotNull(loggerContextCaptor.getValue(), "The context attribute should not be null.");
+
+        assertThat(loggerContextCaptor.getValue().getConfigLocation(), is(URI.create("file:/a/b/c/WEB-INF/log4j2.xml")));
+
+        this.initializerImpl.stop();
+    }
+
+    @Test
+    public void testMissingLocationParameterWithManyMatchingResourcesUsesFirstMatchingResourceConfigLocation() throws Exception {
+        given(servletContext.getInitParameter(eq(Log4jWebSupport.LOG4J_CONTEXT_NAME))).willReturn("mycontext");
+        given(servletContext.getResourcePaths("/WEB-INF/")).willReturn(
+                new HashSet<>(asList("/WEB-INF/a.xml", "/WEB-INF/log4j2-mycontext.xml", "/WEB-INF/log4j2.xml")));
+        given(servletContext.getResource("/WEB-INF/log4j2-mycontext.xml")).willReturn(
+                new URL("file:/a/b/c/WEB-INF/log4j2-mycontext.xml"));
+
+        this.initializerImpl.start();
+
+        then(servletContext).should().setAttribute(eq(Log4jWebSupport.CONTEXT_ATTRIBUTE), loggerContextCaptor.capture());
+        assertNotNull(loggerContextCaptor.getValue(), "The context attribute should not be null.");
+
+        assertThat(loggerContextCaptor.getValue().getConfigLocation(),
+                is(URI.create("file:/a/b/c/WEB-INF/log4j2-mycontext.xml")));
+
+        this.initializerImpl.stop();
+    }
+
+    @Test
+    public void testCompositeLocationParameterWithEmptyUriListSetsDefaultConfiguration() {
+        given(servletContext.getInitParameter(eq(Log4jWebSupport.LOG4J_CONFIG_LOCATION))).willReturn(",,,");
+
+        this.initializerImpl.start();
+
+        then(servletContext).should().setAttribute(eq(Log4jWebSupport.CONTEXT_ATTRIBUTE), loggerContextCaptor.capture());
+        assertNotNull(loggerContextCaptor.getValue(), "The context attribute should not be null.");
+
+        assertThat(loggerContextCaptor.getValue().getConfiguration(), is(instanceOf(DefaultConfiguration.class)));
+
+        this.initializerImpl.stop();
+    }
+
+    @Test
+    public void testCompositeLocationParameterSetsCompositeConfiguration() {
+        given(servletContext.getInitParameter(eq(Log4jWebSupport.LOG4J_CONTEXT_NAME))).willReturn("mycontext");
+        given(servletContext.getInitParameter(eq(Log4jWebSupport.LOG4J_CONFIG_LOCATION))).willReturn(
+                "log4j2-combined.xml,log4j2-override.xml");
+
+        this.initializerImpl.start();
+
+        then(servletContext).should().setAttribute(eq(Log4jWebSupport.CONTEXT_ATTRIBUTE), loggerContextCaptor.capture());
+        assertNotNull(loggerContextCaptor.getValue(), "The context attribute should not be null.");
+
+        assertThat(loggerContextCaptor.getValue().getConfiguration(), is(instanceOf(CompositeConfiguration.class)));
+
+        this.initializerImpl.stop();
+    }
+}
diff --git a/log4j-jakarta-web/src/test/java/org/apache/logging/log4j/web/PropertyTest.java b/log4j-jakarta-web/src/test/java/org/apache/logging/log4j/web/PropertyTest.java
new file mode 100644
index 0000000..40945c2
--- /dev/null
+++ b/log4j-jakarta-web/src/test/java/org/apache/logging/log4j/web/PropertyTest.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.web;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.core.impl.Log4jContextFactory;
+import org.apache.logging.log4j.util.Constants;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ *
+ */
+public class PropertyTest {
+
+    @Test
+    public void testShutdownHookDisabled() {
+        assertFalse(
+                ((Log4jContextFactory) LogManager.getFactory()).isShutdownHookEnabled(),
+                "Shutdown hook should be disabled by default in web applications");
+    }
+
+    @Test
+    public void testIsWebApp() {
+        assertTrue(Constants.IS_WEB_APP, "When servlet classes are available IS_WEB_APP should default to true");
+    }
+}
diff --git a/log4j-jakarta-web/src/test/java/org/apache/logging/log4j/web/ServletAppenderTest.java b/log4j-jakarta-web/src/test/java/org/apache/logging/log4j/web/ServletAppenderTest.java
new file mode 100644
index 0000000..2b0aa3b
--- /dev/null
+++ b/log4j-jakarta-web/src/test/java/org/apache/logging/log4j/web/ServletAppenderTest.java
@@ -0,0 +1,58 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements. See the NOTICE file distributed with
+* this work for additional information regarding copyright ownership.
+* The ASF licenses this file to You under the Apache license, Version 2.0
+* (the "License"); you may not use this file except in compliance with
+* the License. You may obtain a copy of the License at
+*
+*      http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the license for the specific language governing permissions and
+* limitations under the license.
+*/
+package org.apache.logging.log4j.web;
+
+/**
+ *
+ */
+public class ServletAppenderTest {
+
+// TODO: re-enable when https://github.com/spring-projects/spring-framework/issues/25354 is fixed
+
+//    private static final String CONFIG = "WEB-INF/classes/log4j-servlet.xml";
+//
+//    @Test
+//    public void testAppender() throws Exception {
+//        ContextAnchor.THREAD_CONTEXT.remove();
+//        final ServletContext servletContext = new MockServletContext();
+//        servletContext.setAttribute("TestAttr", "AttrValue");
+//        servletContext.setInitParameter("TestParam", "ParamValue");
+//        servletContext.setAttribute("Name1", "Ben");
+//        servletContext.setInitParameter("Name2", "Jerry");
+//        servletContext.setInitParameter(Log4jWebSupport.LOG4J_CONFIG_LOCATION, CONFIG);
+//        final Log4jWebLifeCycle initializer = WebLoggerContextUtils.getWebLifeCycle(servletContext);
+//        try {
+//            initializer.start();
+//            initializer.setLoggerContext();
+//            final LoggerContext ctx = ContextAnchor.THREAD_CONTEXT.get();
+//            assertNotNull(ctx, "No LoggerContext");
+//            assertNotNull(WebLoggerContextUtils.getServletContext(), "No ServletContext");
+//            final Configuration configuration = ctx.getConfiguration();
+//            assertNotNull(configuration, "No configuration");
+//            final Appender appender = configuration.getAppender("Servlet");
+//            assertNotNull(appender, "No ServletAppender");
+//            final Logger logger = LogManager.getLogger("Test");
+//            logger.info("This is a test");
+//            logger.error("This is a test 2", new IllegalStateException().fillInStackTrace());
+//        } catch (final IllegalStateException e) {
+//            fail("Failed to initialize Log4j properly." + e.getMessage());
+//        } finally {
+//            initializer.stop();
+//            ContextAnchor.THREAD_CONTEXT.remove();
+//        }
+//    }
+}
diff --git a/log4j-jakarta-web/src/test/java/org/apache/logging/log4j/web/TestAsyncServlet.java b/log4j-jakarta-web/src/test/java/org/apache/logging/log4j/web/TestAsyncServlet.java
new file mode 100644
index 0000000..36113f6
--- /dev/null
+++ b/log4j-jakarta-web/src/test/java/org/apache/logging/log4j/web/TestAsyncServlet.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.web;
+
+import java.io.IOException;
+import jakarta.servlet.AsyncContext;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+// TODO: write an integration test that deploys this Servlet 3.0 class to Tomcat and Jetty
+public class TestAsyncServlet extends HttpServlet {
+
+    private static final long serialVersionUID = 1L;
+
+    @Override
+    protected void doGet(final HttpServletRequest req, final 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!");
+        }));
+    }
+
+    @Override
+    protected void doPost(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
+        final AsyncContext asyncContext = req.startAsync();
+        asyncContext.start(() -> {
+            final Log4jWebSupport webSupport =
+                WebLoggerContextUtils.getWebLifeCycle(TestAsyncServlet.this.getServletContext());
+            webSupport.setLoggerContext();
+            // do stuff
+            webSupport.clearLoggerContext();
+        });
+    }
+}
diff --git a/log4j-jakarta-web/src/test/java/org/apache/logging/log4j/web/WebLookupTest.java b/log4j-jakarta-web/src/test/java/org/apache/logging/log4j/web/WebLookupTest.java
new file mode 100644
index 0000000..2c8c15f
--- /dev/null
+++ b/log4j-jakarta-web/src/test/java/org/apache/logging/log4j/web/WebLookupTest.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.web;
+
+public class WebLookupTest {
+
+// TODO: re-enable when https://github.com/spring-projects/spring-framework/issues/25354 is fixed
+
+//    @Test
+//    public void testLookup() throws Exception {
+//        ContextAnchor.THREAD_CONTEXT.remove();
+//        final ServletContext servletContext = new MockServletContext();
+//        ((MockServletContext) servletContext).setContextPath("/WebApp");
+//        servletContext.setAttribute("TestAttr", "AttrValue");
+//        servletContext.setInitParameter("TestParam", "ParamValue");
+//        servletContext.setAttribute("Name1", "Ben");
+//        servletContext.setInitParameter("Name2", "Jerry");
+//        final Log4jWebLifeCycle initializer = WebLoggerContextUtils.getWebLifeCycle(servletContext);
+//        try {
+//            initializer.start();
+//            initializer.setLoggerContext();
+//            final LoggerContext ctx = ContextAnchor.THREAD_CONTEXT.get();
+//            assertNotNull(ctx, "No LoggerContext");
+//            assertNotNull(WebLoggerContextUtils.getServletContext(), "No ServletContext");
+//            final Configuration config = ctx.getConfiguration();
+//            assertNotNull(config, "No Configuration");
+//            final StrSubstitutor substitutor = config.getStrSubstitutor();
+//            assertNotNull(substitutor, "No Interpolator");
+//            String value = substitutor.replace("${web:initParam.TestParam}");
+//            assertNotNull(value, "No value for TestParam");
+//            assertEquals("ParamValue", value, "Incorrect value for TestParam: " + value);
+//            value = substitutor.replace("${web:attr.TestAttr}");
+//            assertNotNull(value, "No value for TestAttr");
+//            assertEquals("AttrValue", value, "Incorrect value for TestAttr: " + value);
+//            value = substitutor.replace("${web:Name1}");
+//            assertNotNull(value, "No value for Name1");
+//            assertEquals("Ben", value, "Incorrect value for Name1: " + value);
+//            value = substitutor.replace("${web:Name2}");
+//            assertNotNull(value, "No value for Name2");
+//            assertEquals("Jerry", value, "Incorrect value for Name2: " + value);
+//            value = substitutor.replace("${web:contextPathName}");
+//            assertNotNull(value, "No value for context name");
+//            assertEquals("WebApp", value, "Incorrect value for context name");
+//        } catch (final IllegalStateException e) {
+//            fail("Failed to initialize Log4j properly." + e.getMessage());
+//        }
+//        initializer.stop();
+//        ContextAnchor.THREAD_CONTEXT.remove();
+//    }
+//
+//    @Test
+//    public void testLookup2() throws Exception {
+//        ContextAnchor.THREAD_CONTEXT.remove();
+//        final ServletContext servletContext = new MockServletContext();
+//        ((MockServletContext) servletContext).setContextPath("/");
+//        servletContext.setAttribute("TestAttr", "AttrValue");
+//        servletContext.setInitParameter("myapp.logdir", "target");
+//        servletContext.setAttribute("Name1", "Ben");
+//        servletContext.setInitParameter("Name2", "Jerry");
+//        servletContext.setInitParameter("log4jConfiguration", "WEB-INF/classes/log4j-webvar.xml");
+//        final Log4jWebLifeCycle initializer = WebLoggerContextUtils.getWebLifeCycle(servletContext);
+//        initializer.start();
+//        initializer.setLoggerContext();
+//        final LoggerContext ctx = ContextAnchor.THREAD_CONTEXT.get();
+//        assertNotNull(ctx, "No LoggerContext");
+//        assertNotNull(WebLoggerContextUtils.getServletContext(), "No ServletContext");
+//        final Configuration config = ctx.getConfiguration();
+//        assertNotNull(config, "No Configuration");
+//        final Map<String, Appender> appenders = config.getAppenders();
+//        for (final Map.Entry<String, Appender> entry : appenders.entrySet()) {
+//            if (entry.getKey().equals("file")) {
+//                final FileAppender fa = (FileAppender) entry.getValue();
+//                assertEquals("target/myapp.log", fa.getFileName());
+//            }
+//        }
+//        final StrSubstitutor substitutor = config.getStrSubstitutor();
+//        String value = substitutor.replace("${web:contextPathName:-default}");
+//        assertEquals("default", value, "Incorrect value for context name");
+//        assertNotNull(value, "No value for context name");
+//        initializer.stop();
+//        ContextAnchor.THREAD_CONTEXT.remove();
+//    }
+
+}
diff --git a/log4j-jakarta-web/src/test/resources/WEB-INF/classes/log4j-servlet.xml b/log4j-jakarta-web/src/test/resources/WEB-INF/classes/log4j-servlet.xml
new file mode 100644
index 0000000..220508b
--- /dev/null
+++ b/log4j-jakarta-web/src/test/resources/WEB-INF/classes/log4j-servlet.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements.  See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<Configuration status="WARN" name="ServletTest">
+
+    <Appenders>
+        <Servlet name="Servlet" logThrowables="true">
+            <PatternLayout pattern="%m%n%ex{none}"/>
+        </Servlet>
+    </Appenders>
+
+    <Loggers>
+        <Root level="debug">
+            <AppenderRef ref="Servlet"/>
+        </Root>
+    </Loggers>
+
+</Configuration>
\ No newline at end of file
diff --git a/log4j-jakarta-web/src/test/resources/WEB-INF/classes/log4j-webvar.xml b/log4j-jakarta-web/src/test/resources/WEB-INF/classes/log4j-webvar.xml
new file mode 100644
index 0000000..de3777a
--- /dev/null
+++ b/log4j-jakarta-web/src/test/resources/WEB-INF/classes/log4j-webvar.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements. See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF licenses this file to You under the Apache License, Version 2.0
+  ~ (the "License"); you may not use this file except in compliance with
+  ~ the License. You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<Configuration status="OFF">
+  <Appenders>
+    <File name="file" fileName="${web:initParam.myapp.logdir}/myapp.log" append="true">
+      <PatternLayout pattern="%d [%t] %-5p %c - %m%n"/>
+    </File>
+  </Appenders>
+  <Loggers>
+    <Root level="warn">
+      <AppenderRef ref="file"/>
+    </Root>
+  </Loggers>
+</Configuration>
\ No newline at end of file
diff --git a/log4j-jakarta-web/src/test/resources/log4j2-combined.xml b/log4j-jakarta-web/src/test/resources/log4j2-combined.xml
new file mode 100644
index 0000000..043d972
--- /dev/null
+++ b/log4j-jakarta-web/src/test/resources/log4j2-combined.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements.  See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<Configuration status="OFF">
+  <Appenders>
+    <Console name="Console" target="SYSTEM_OUT">
+      <PatternLayout pattern="%d [%t] %-5level: %msg%n%throwable" />
+    </Console>
+  </Appenders>
+  <Loggers>
+    <Logger name="org.foo" level="DEBUG" />
+    <Root level="TRACE">
+      <AppenderRef ref="Console" />
+    </Root>
+  </Loggers>
+</Configuration>
\ No newline at end of file
diff --git a/log4j-jakarta-web/src/test/resources/log4j2-override.xml b/log4j-jakarta-web/src/test/resources/log4j2-override.xml
new file mode 100644
index 0000000..bba8f9d
--- /dev/null
+++ b/log4j-jakarta-web/src/test/resources/log4j2-override.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements.  See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<Configuration>
+  <Loggers>
+    <Logger name="org.foo" level="ERROR" />
+  </Loggers>
+</Configuration>
\ No newline at end of file
diff --git a/log4j-smtp/pom.xml b/log4j-smtp/pom.xml
index 18ccb7a..944a11e 100644
--- a/log4j-smtp/pom.xml
+++ b/log4j-smtp/pom.xml
@@ -57,11 +57,18 @@
       <groupId>org.apache.logging.log4j</groupId>
       <artifactId>log4j-api</artifactId>
       <type>test-jar</type>
+      <scope>test</scope>
     </dependency>
     <dependency>
       <groupId>org.apache.logging.log4j</groupId>
       <artifactId>log4j-core</artifactId>
       <type>test-jar</type>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.lmax</groupId>
+      <artifactId>disruptor</artifactId>
+      <scope>test</scope>
     </dependency>
   </dependencies>
 
diff --git a/pom.xml b/pom.xml
index d756773..381f3bf 100644
--- a/pom.xml
+++ b/pom.xml
@@ -452,6 +452,11 @@
         <version>${project.version}</version>
       </dependency>
       <dependency>
+        <groupId>org.apache.logging.log4j</groupId>
+        <artifactId>log4j-jakarta-web</artifactId>
+        <version>${project.version}</version>
+      </dependency>
+      <dependency>
         <groupId>com.sleepycat</groupId>
         <artifactId>je</artifactId>
         <version>5.0.73</version>
@@ -1697,6 +1702,7 @@
     <module>log4j-mongodb4</module>
     <module>log4j-cassandra</module>
     <module>log4j-web</module>
+    <module>log4j-jakarta-web</module>
     <module>log4j-perf</module>
     <module>log4j-iostreams</module>
     <module>log4j-jul</module>
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 58717d3..f1c3db2 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -173,6 +173,9 @@
       <action issue="LOG4J2-3189" dev="ckozak" type="add">
         Improve NameAbbreviator worst-case performance.
       </action>
+      <action issue="LOG4J2-2978" dev="rgoers" type="add" due-to="Michael Seele">
+        Add support for Jakarta EE 9 (Tomcat 10 / Jetty 11)
+      </action>
       <action issue="LOG4J2-3170" dev="vy" type="add" due-to="Gareth Smith">
         Make CRLF/HTML encoding run in O(n) worst-case time, rather than O(n^2).
       </action>
diff --git a/src/site/asciidoc/manual/webapp.adoc b/src/site/asciidoc/manual/webapp.adoc
index e11159f..897459b 100644
--- a/src/site/asciidoc/manual/webapp.adoc
+++ b/src/site/asciidoc/manual/webapp.adoc
@@ -30,6 +30,11 @@ when the web application undeploys. How this works varies depending on
 whether your application is a link:#Servlet-3.0[Servlet 3.0 or newer] or
 link:#Servlet-2.5[Servlet 2.5] web application.
 
+
+Due to the namespace change from `javax` to `jakarta` you need to use
+`log4j-jakarta-web` instead of `log4j-web` for Servlet 5.0 or newer.
+
+
 In either case, you'll need to add the `log4j-web` module to your
 deployment as detailed in the link:../maven-artifacts.html[Maven, Ivy,
 and Gradle Artifacts] manual page.
diff --git a/src/site/site.xml b/src/site/site.xml
index ef83f81..d140490 100644
--- a/src/site/site.xml
+++ b/src/site/site.xml
@@ -318,6 +318,7 @@
       <item name="Log4j Tag Library" href="log4j-taglib/index.html"/>
       <item name="Log4j JMX GUI" href="log4j-jmx-gui/index.html"/>
       <item name="Log4j Web Application Support" href="log4j-web/index.html"/>
+      <item name="Log4j Jakarta Web Application Support" href="log4j-jakarta-web/index.html"/>
       <item name="Log4j Application Server Integration" href="log4j-appserver/index.html"/>
       <item name="Log4j CouchDB appender" href="log4j-couchdb/index.html"/>
       <item name="Log4j MongoDB3 appender" href="log4j-mongodb3/index.html"/>