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 © {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"/>