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

pkarwasz pushed a commit to branch fix/main/port-http-watcher
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git

commit 16ad20d440e3b2cc11d94a2c9fb3187f30324397
Author: Piotr P. Karwasz <[email protected]>
AuthorDate: Tue Sep 10 19:46:11 2024 +0200

    Fix reloading of the configuration from HTTP(S)
    
    The `HttpWatcher` didn't propagate the observed
    last modification time back to the configuration.
    As a result, each new configuration was already
    deprecated when it started and the reconfiguration
    process looped.
    
    Closes #2937
    
    Rewrite Jetty tests using WireMock
    
    Closes #2813
    
    Co-authored-by: Volkan Yazıcı <[email protected]>
---
 log4j-core-test/pom.xml                            |  46 +++-
 .../log4j/core/config/ConfigurationSourceTest.java |  62 ++---
 .../filter/HttpThreadContextMapFilterTest.java     | 113 ---------
 .../filter/MutableThreadContextMapFilterTest.java  | 254 ++++++++++++++++-----
 .../log4j/core/net/UrlConnectionFactoryTest.java   | 232 +++++++++----------
 .../logging/log4j/core/net/WireMockUtil.java       |  84 +++++++
 .../logging/log4j/core/util/HttpWatcherTest.java   | 158 +++++++++++++
 .../logging/log4j/core/util/WatchHttpTest.java     | 159 -------------
 .../logging/log4j/core/util/WatchManagerTest.java  | 179 ++++++++-------
 .../ConfigurationSourceTest.xml}                   |  18 +-
 .../src/test/resources/emptyConfig.json            |   4 -
 .../MutableThreadContextMapFilterTest.xml          |  23 +-
 .../src/test/resources/filterConfig.json           |   6 -
 .../apache/logging/log4j/core/LoggerContext.java   |  11 +-
 .../log4j/core/config/AbstractConfiguration.java   |  28 ++-
 .../log4j/core/config/ConfigurationSource.java     | 133 ++++++-----
 .../logging/log4j/core/config/HttpWatcher.java     |  41 +++-
 .../log4j/core/config/xml/XmlConfiguration.java    |  10 +-
 .../core/filter/MutableThreadContextMapFilter.java |  62 +++--
 .../org/apache/logging/log4j/core/util/Loader.java |   5 +-
 .../org/apache/logging/log4j/core/util/Source.java |  40 ++--
 .../logging/log4j/core/util/WatchManager.java      |  20 +-
 .../core/util/internal/HttpInputStreamUtil.java    |  98 ++++++--
 log4j-parent/pom.xml                               |  34 +--
 log4j-perf-test/pom.xml                            |  33 ++-
 log4j-plugins/pom.xml                              |   3 +
 log4j-slf4j2-impl/pom.xml                          |  22 +-
 log4j-to-slf4j/pom.xml                             |  13 ++
 pom.xml                                            |   4 +
 src/changelog/.3.x.x/2937-http-watcher.xml         |   8 +
 30 files changed, 1075 insertions(+), 828 deletions(-)

diff --git a/log4j-core-test/pom.xml b/log4j-core-test/pom.xml
index 47ca1a22d8..b96526c7f7 100644
--- a/log4j-core-test/pom.xml
+++ b/log4j-core-test/pom.xml
@@ -61,8 +61,34 @@
       java.allocation.instrumenter;substitute="java-allocation-instrumenter",
       spring.test;substitute="spring-test"
     </bnd-extra-module-options>
+
+    <!-- Dependency versions -->
+    <slf4j2.version>2.0.16</slf4j2.version>
+    <wiremock.version>3.9.1</wiremock.version>
   </properties>
 
+  <dependencyManagement>
+    <dependencies>
+
+      <!-- Transitive dependency of `wiremock` -->
+      <dependency>
+        <groupId>org.slf4j</groupId>
+        <artifactId>slf4j-api</artifactId>
+        <version>${slf4j2.version}</version>
+      </dependency>
+
+      <!-- Pinned transitive dependencies with CVEs -->
+
+      <!-- Transitive dependency of `log4j2-custom-layout` -->
+      <dependency>
+        <groupId>com.google.code.gson</groupId>
+        <artifactId>gson</artifactId>
+        <version>2.11.0</version>
+      </dependency>
+
+    </dependencies>
+  </dependencyManagement>
+
   <dependencies>
 
     <!-- Used for OSGi bundle support -->
@@ -235,13 +261,6 @@
       <scope>test</scope>
     </dependency>
 
-    <!-- Log4j 1.2 tests -->
-    <dependency>
-      <groupId>log4j</groupId>
-      <artifactId>log4j</artifactId>
-      <scope>test</scope>
-    </dependency>
-
     <dependency>
       <groupId>com.github.ivandzf</groupId>
       <artifactId>log4j2-custom-layout</artifactId>
@@ -282,11 +301,11 @@
       <scope>test</scope>
     </dependency>
 
-    <!-- SLF4J tests -->
+    <!-- Switch to `slf4j-simple` to enable HTTP logging -->
     <dependency>
       <groupId>org.slf4j</groupId>
-      <artifactId>slf4j-api</artifactId>
-      <scope>test</scope>
+      <artifactId>slf4j-nop</artifactId>
+      <version>${slf4j2.version}</version>
     </dependency>
 
     <dependency>
@@ -297,8 +316,9 @@
 
     <!-- Used for testing HTTP Watcher -->
     <dependency>
-      <groupId>com.github.tomakehurst</groupId>
-      <artifactId>wiremock-jre8</artifactId>
+      <groupId>org.wiremock</groupId>
+      <artifactId>wiremock</artifactId>
+      <version>${wiremock.version}</version>
       <scope>test</scope>
     </dependency>
 
@@ -339,6 +359,8 @@
           <runOrder>random</runOrder>
           <systemPropertyVariables>
             <Web.isWebApp>false</Web.isWebApp>
+            <!-- Enables logging of HTTP requests, if `slf4j-nop` is replaced 
by `slf4j-simple` above-->
+            
<org.slf4j.simpleLogger.log.org.eclipse.jetty.server.HttpChannel>DEBUG</org.slf4j.simpleLogger.log.org.eclipse.jetty.server.HttpChannel>
           </systemPropertyVariables>
           <useModulePath>false</useModulePath>
         </configuration>
diff --git 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/ConfigurationSourceTest.java
 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/ConfigurationSourceTest.java
index f17b3b2a55..5001ad2d44 100644
--- 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/ConfigurationSourceTest.java
+++ 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/ConfigurationSourceTest.java
@@ -16,6 +16,7 @@
  */
 package org.apache.logging.log4j.core.config;
 
+import static java.util.Objects.requireNonNull;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
@@ -32,21 +33,28 @@ import java.lang.management.OperatingSystemMXBean;
 import java.net.URL;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.nio.file.Paths;
 import java.util.jar.Attributes;
 import java.util.jar.JarEntry;
 import java.util.jar.JarOutputStream;
 import java.util.jar.Manifest;
+import org.apache.commons.io.IOUtils;
 import org.apache.logging.log4j.core.net.UrlConnectionFactory;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
 
 public class ConfigurationSourceTest {
-    private static final Path JAR_FILE = Paths.get("target", "test-classes", 
"jarfile.jar");
-    private static final Path CONFIG_FILE = Paths.get("target", 
"test-classes", "log4j2-console.xml");
-    private static final byte[] buffer = new byte[1024];
+    /**
+     * The path inside the jar created by {@link #prepareJarConfigURL} 
containing the configuration.
+     */
+    public static final String PATH_IN_JAR = "/config/console.xml";
+
+    private static final String CONFIG_FILE = 
"/config/ConfigurationSourceTest.xml";
+
+    @TempDir
+    private Path tempDir;
 
     @Test
-    public void testJira_LOG4J2_2770_byteArray() throws Exception {
+    void testJira_LOG4J2_2770_byteArray() throws Exception {
         final ConfigurationSource configurationSource =
                 new ConfigurationSource(new ByteArrayInputStream(new byte[] 
{'a', 'b'}));
         assertNotNull(configurationSource.resetInputStream());
@@ -54,20 +62,19 @@ public class ConfigurationSourceTest {
 
     /**
      * Checks if the usage of 'jar:' URLs does not increase the file descriptor
-     * count and the jar file can be deleted.
-     *
-     * @throws Exception
+     * count, and the jar file can be deleted.
      */
     @Test
-    public void testNoJarFileLeak() throws Exception {
-        final URL jarConfigURL = prepareJarConfigURL();
+    void testNoJarFileLeak() throws Exception {
+        final Path jarFile = prepareJarConfigURL(tempDir);
+        final URL jarConfigURL = new URL("jar:" + jarFile.toUri().toURL() + 
"!" + PATH_IN_JAR);
         final long expected = getOpenFileDescriptorCount();
         
UrlConnectionFactory.createConnection(jarConfigURL).getInputStream().close();
         // This can only fail on UNIX
         assertEquals(expected, getOpenFileDescriptorCount());
         // This can only fail on Windows
         try {
-            Files.delete(JAR_FILE);
+            Files.delete(jarFile);
         } catch (IOException e) {
             fail(e);
         }
@@ -75,7 +82,8 @@ public class ConfigurationSourceTest {
 
     @Test
     public void testLoadConfigurationSourceFromJarFile() throws Exception {
-        final URL jarConfigURL = prepareJarConfigURL();
+        final Path jarFile = prepareJarConfigURL(tempDir);
+        final URL jarConfigURL = new URL("jar:" + jarFile.toUri().toURL() + 
"!" + PATH_IN_JAR);
         final long expectedFdCount = getOpenFileDescriptorCount();
         ConfigurationSource configSource = 
ConfigurationSource.fromUri(jarConfigURL.toURI());
         assertNotNull(configSource);
@@ -90,7 +98,7 @@ public class ConfigurationSourceTest {
         assertEquals(expectedFdCount, getOpenFileDescriptorCount());
         // This can only fail on Windows
         try {
-            Files.delete(JAR_FILE);
+            Files.delete(jarFile);
         } catch (IOException e) {
             fail(e);
         }
@@ -104,22 +112,18 @@ public class ConfigurationSourceTest {
         return 0L;
     }
 
-    public static URL prepareJarConfigURL() throws IOException {
-        if (!Files.exists(JAR_FILE)) {
-            final Manifest manifest = new Manifest();
-            manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, 
"1.0");
-            try (final OutputStream os = Files.newOutputStream(JAR_FILE);
-                    final JarOutputStream jar = new JarOutputStream(os, 
manifest);
-                    final InputStream config = 
Files.newInputStream(CONFIG_FILE)) {
-                final JarEntry jarEntry = new JarEntry("config/console.xml");
-                jar.putNextEntry(jarEntry);
-                int len;
-                while ((len = config.read(buffer)) != -1) {
-                    jar.write(buffer, 0, len);
-                }
-                jar.closeEntry();
-            }
+    public static Path prepareJarConfigURL(Path dir) throws IOException {
+        Path jarFile = dir.resolve("jarFile.jar");
+        final Manifest manifest = new Manifest();
+        manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, 
"1.0");
+        try (final OutputStream os = Files.newOutputStream(jarFile);
+                final JarOutputStream jar = new JarOutputStream(os, manifest);
+                final InputStream config =
+                        
requireNonNull(ConfigurationSourceTest.class.getResourceAsStream(CONFIG_FILE))) 
{
+            final JarEntry jarEntry = new JarEntry("config/console.xml");
+            jar.putNextEntry(jarEntry);
+            IOUtils.copy(config, os);
         }
-        return new URL("jar:" + JAR_FILE.toUri().toURL() + 
"!/config/console.xml");
+        return jarFile;
     }
 }
diff --git 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/filter/HttpThreadContextMapFilterTest.java
 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/filter/HttpThreadContextMapFilterTest.java
deleted file mode 100644
index d35737b0b0..0000000000
--- 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/filter/HttpThreadContextMapFilterTest.java
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * 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.core.filter;
-
-import static com.github.tomakehurst.wiremock.client.WireMock.get;
-import static com.github.tomakehurst.wiremock.client.WireMock.ok;
-import static org.assertj.core.api.Assertions.assertThat;
-
-import com.github.tomakehurst.wiremock.WireMockServer;
-import com.github.tomakehurst.wiremock.common.FileSource;
-import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
-import com.github.tomakehurst.wiremock.extension.Parameters;
-import com.github.tomakehurst.wiremock.extension.ResponseTransformer;
-import com.github.tomakehurst.wiremock.http.Request;
-import com.github.tomakehurst.wiremock.http.Response;
-import java.net.HttpURLConnection;
-import java.net.URL;
-import java.util.concurrent.atomic.AtomicInteger;
-import org.apache.logging.log4j.ThreadContext;
-import org.apache.logging.log4j.core.LoggerContext;
-import org.apache.logging.log4j.core.test.appender.ListAppender;
-import org.apache.logging.log4j.core.test.junit.LoggerContextSource;
-import org.apache.logging.log4j.core.test.junit.Named;
-import org.apache.logging.log4j.spi.ExtendedLogger;
-import org.junit.jupiter.api.Test;
-
-/**
- * Tests {@link ThreadContextMapFilter} using a WireMock stub.
- */
-public class HttpThreadContextMapFilterTest {
-
-    @Test
-    @LoggerContextSource("HttpThreadContextMapFilterTest.xml")
-    public void wireMock_logs_should_be_filtered_on_MDC(
-            final LoggerContext loggerContext, @Named("List") final 
ListAppender appender) throws Exception {
-
-        // Create the logger
-        final ExtendedLogger logger = 
loggerContext.getLogger(HttpThreadContextMapFilterTest.class);
-
-        // Create a response transformer; the only way to dynamically 
construct WireMock responses.
-        // We need a dynamic response generation, since there we will issue 
MDC changes and log statements.
-        final ResponseTransformer wireMockResponseTransformer = new 
ResponseTransformer() {
-
-            private final AtomicInteger invocationCounter = new 
AtomicInteger();
-
-            @Override
-            public Response transform(
-                    final Request request,
-                    final Response response,
-                    final FileSource files,
-                    final Parameters parameters) {
-                final int invocationCount = 
invocationCounter.getAndIncrement();
-                ThreadContext.put("invocationCount", "" + invocationCount);
-                logger.info("transforming request #{}", invocationCount);
-                return response;
-            }
-
-            @Override
-            public String getName() {
-                return "mdc-writer";
-            }
-        };
-
-        // Create the WireMock server extended using the response transformer.
-        final WireMockServer wireMockServer = new WireMockServer(
-                
WireMockConfiguration.wireMockConfig().dynamicPort().extensions(wireMockResponseTransformer));
-        
wireMockServer.stubFor(get("/").willReturn(ok().withTransformers(wireMockResponseTransformer.getName())));
-
-        wireMockServer.start();
-        try {
-
-            // Perform some HTTP requests.
-            // `HttpThreadContextMapFilterTest.xml` only allows when 
`invocationCount={0,2}`.
-            // Hence, there preferably needs to be more than 2 requests.
-            final String wireMockStubUrl = wireMockServer.url("/");
-            httpGet(wireMockStubUrl);
-            httpGet(wireMockStubUrl);
-            httpGet(wireMockStubUrl);
-            httpGet(wireMockStubUrl);
-            httpGet(wireMockStubUrl);
-
-            // Verify that `invocationCount={0,2}` filter in 
`HttpThreadContextMapFilterTest.xml` works
-            assertThat(appender.getMessages()).containsOnly("transforming 
request #0", "transforming request #2");
-
-        } finally {
-            wireMockServer.stop();
-        }
-    }
-
-    private static void httpGet(String url) throws Exception {
-        final HttpURLConnection connection = (HttpURLConnection) new 
URL(url).openConnection();
-        connection.connect();
-        try {
-            
assertThat(connection.getResponseCode()).isEqualTo(HttpURLConnection.HTTP_OK);
-        } finally {
-            connection.disconnect();
-        }
-    }
-}
diff --git 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/filter/MutableThreadContextMapFilterTest.java
 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/filter/MutableThreadContextMapFilterTest.java
index 1d4addf287..0afd6dbae8 100644
--- 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/filter/MutableThreadContextMapFilterTest.java
+++ 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/filter/MutableThreadContextMapFilterTest.java
@@ -16,101 +16,233 @@
  */
 package org.apache.logging.log4j.core.filter;
 
-import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.logging.log4j.core.net.WireMockUtil.createMapping;
+import static 
org.apache.logging.log4j.core.test.TestConstants.AUTH_BASIC_PASSWORD;
+import static 
org.apache.logging.log4j.core.test.TestConstants.AUTH_BASIC_USERNAME;
+import static 
org.apache.logging.log4j.core.test.TestConstants.CONFIGURATION_ALLOWED_PROTOCOLS;
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.junit.jupiter.api.Assertions.fail;
 
-import java.io.File;
-import java.io.IOException;
+import com.github.tomakehurst.wiremock.client.BasicCredentials;
+import com.github.tomakehurst.wiremock.client.WireMock;
+import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
+import com.github.tomakehurst.wiremock.junit5.WireMockTest;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.net.URI;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.StandardCopyOption;
-import java.util.concurrent.CountDownLatch;
+import java.nio.file.attribute.FileTime;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.time.temporal.ChronoUnit;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.ReentrantLock;
 import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.ThreadContext;
-import org.apache.logging.log4j.core.Appender;
 import org.apache.logging.log4j.core.LoggerContext;
-import org.apache.logging.log4j.core.config.Configurator;
+import org.apache.logging.log4j.core.config.Configuration;
+import org.apache.logging.log4j.core.config.ConfigurationSource;
 import org.apache.logging.log4j.core.test.appender.ListAppender;
+import org.apache.logging.log4j.plugins.di.ConfigurableInstanceFactory;
+import org.apache.logging.log4j.plugins.di.DI;
 import org.apache.logging.log4j.test.TestProperties;
-import org.apache.logging.log4j.test.junit.Log4jStaticResources;
+import org.apache.logging.log4j.test.junit.SetTestProperty;
 import org.apache.logging.log4j.test.junit.UsingTestProperties;
 import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.parallel.ResourceLock;
+import org.junit.jupiter.api.io.TempDir;
 
 /**
  * Unit test for simple App.
  */
+@SetTestProperty(key = CONFIGURATION_ALLOWED_PROTOCOLS, value = "http,https")
+@SetTestProperty(key = AUTH_BASIC_PASSWORD, value = "log4j")
+@SetTestProperty(key = AUTH_BASIC_USERNAME, value = "log4j")
 @UsingTestProperties
-@ResourceLock(Log4jStaticResources.THREAD_CONTEXT)
-@ResourceLock(Log4jStaticResources.LOG_MANAGER)
-public class MutableThreadContextMapFilterTest implements 
MutableThreadContextMapFilter.FilterConfigUpdateListener {
+@WireMockTest
+class MutableThreadContextMapFilterTest implements 
MutableThreadContextMapFilter.FilterConfigUpdateListener {
 
-    static final String CONFIG = "MutableThreadContextMapFilterTest.xml";
-    static LoggerContext loggerContext = null;
-    static File targetFile = new File("target/test-classes/testConfig.json");
-    static Path target = targetFile.toPath();
-    CountDownLatch updated = new CountDownLatch(1);
+    private static final BasicCredentials CREDENTIALS = new 
BasicCredentials("log4j", "log4j");
+    private static final String FILE_NAME = "testConfig.json";
+    private static final String URL_PATH = "/" + FILE_NAME;
+    private static final String JSON = "application/json";
+
+    private static final byte[] EMPTY_CONFIG = ("{" //
+                    + "  \"configs\":{}" //
+                    + "}")
+            .getBytes(UTF_8);
+    private static final byte[] FILTER_CONFIG = ("{" //
+                    + "  \"configs\": {" //
+                    + "    \"loginId\": [\"rgoers\", \"adam\"]," //
+                    + "    \"corpAcctNumber\": [\"30510263\"]" //
+                    + "  }" //
+                    + "}")
+            .getBytes(UTF_8);
+
+    private static final String CONFIG = 
"filter/MutableThreadContextMapFilterTest.xml";
+    private static final ConfigurableInstanceFactory instanceFactory = 
DI.createInitializedFactory();
+    private static LoggerContext loggerContext = null;
+    private final ReentrantLock lock = new ReentrantLock();
+    private final Condition filterUpdated = lock.newCondition();
+    private final Condition resultVerified = lock.newCondition();
+    private Exception exception;
 
     @AfterEach
-    public void after() {
-        try {
-            Files.deleteIfExists(target);
-        } catch (IOException ioe) {
-            // Ignore this.
-        }
+    void cleanup() {
+        exception = null;
         ThreadContext.clearMap();
-        loggerContext.stop();
-        loggerContext = null;
+        if (loggerContext != null) {
+            loggerContext.stop();
+            loggerContext = null;
+        }
     }
 
     @Test
-    public void filterTest(final TestProperties properties) throws Exception {
-        properties.setProperty("configLocation", 
"target/test-classes/testConfig.json");
-        ThreadContext.put("loginId", "rgoers");
-        Path source = new 
File("target/test-classes/emptyConfig.json").toPath();
-        Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
-        final long fileTime = targetFile.lastModified() - 1000;
-        assertTrue(targetFile.setLastModified(fileTime));
-        loggerContext = Configurator.initialize(null, CONFIG);
-        assertNotNull(loggerContext);
-        final Appender app = 
loggerContext.getConfiguration().getAppender("List");
-        assertNotNull(app);
-        assertTrue(app instanceof ListAppender);
-        final MutableThreadContextMapFilter filter =
-                (MutableThreadContextMapFilter) 
loggerContext.getConfiguration().getFilter();
+    void file_location_works(TestProperties properties, @TempDir Path dir) 
throws Exception {
+        // Set up the test file.
+        Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS);
+        Instant before = now.minus(1, ChronoUnit.MINUTES);
+        Instant after = now.plus(1, ChronoUnit.MINUTES);
+        Path testConfig = dir.resolve("testConfig.json");
+        properties.setProperty("configLocation", testConfig.toString());
+        try (final InputStream inputStream = new 
ByteArrayInputStream(EMPTY_CONFIG)) {
+            Files.copy(inputStream, testConfig);
+            Files.setLastModifiedTime(testConfig, FileTime.from(before));
+        }
+        // Setup Log4j
+        ConfigurationSource source =
+                ConfigurationSource.fromResource(CONFIG, 
getClass().getClassLoader());
+        loggerContext = new LoggerContext("file_location_works", null, (URI) 
null, instanceFactory);
+        Configuration configuration = loggerContext.getConfiguration(source);
+        configuration.initialize(); // To create the components
+        final ListAppender app = configuration.getAppender("LIST");
+        assertThat(app).isNotNull();
+        final MutableThreadContextMapFilter filter = 
(MutableThreadContextMapFilter) configuration.getFilter();
         assertNotNull(filter);
         filter.registerListener(this);
-        final Logger logger = loggerContext.getLogger("Test");
-        logger.debug("This is a test");
-        Assertions.assertEquals(0, ((ListAppender) app).getEvents().size());
-        source = new File("target/test-classes/filterConfig.json").toPath();
-        String msg = null;
-        boolean copied = false;
-        for (int i = 0; i < 5 && !copied; ++i) {
-            Thread.sleep(100 + (100 * i));
-            try {
-                Files.copy(source, target, 
StandardCopyOption.REPLACE_EXISTING);
-                copied = true;
-            } catch (Exception ex) {
-                msg = ex.getMessage();
+
+        lock.lock();
+        try {
+            // Starts the configuration
+            loggerContext.start(configuration);
+
+            final Logger logger = 
loggerContext.getLogger(MutableThreadContextMapFilterTest.class);
+
+            assertThat(filterUpdated.await(20, TimeUnit.SECONDS))
+                    .as("Initial configuration was loaded")
+                    .isTrue();
+            ThreadContext.put("loginId", "rgoers");
+            logger.debug("This is a test");
+            assertThat(app.getEvents()).isEmpty();
+
+            // Prepare the second test case: updated config
+            try (final InputStream inputStream = new 
ByteArrayInputStream(FILTER_CONFIG)) {
+                Files.copy(inputStream, testConfig, 
StandardCopyOption.REPLACE_EXISTING);
+                Files.setLastModifiedTime(testConfig, FileTime.from(after));
             }
+            resultVerified.signalAll();
+
+            assertThat(filterUpdated.await(20, TimeUnit.SECONDS))
+                    .as("Updated configuration was loaded")
+                    .isTrue();
+            logger.debug("This is a test");
+            assertThat(app.getEvents()).hasSize(1);
+
+            // Prepare the third test case: removed config
+            Files.delete(testConfig);
+            resultVerified.signalAll();
+
+            assertThat(filterUpdated.await(20, TimeUnit.SECONDS))
+                    .as("Configuration removal was detected")
+                    .isTrue();
+            logger.debug("This is a test");
+            assertThat(app.getEvents()).hasSize(1);
+            resultVerified.signalAll();
+        } finally {
+            lock.unlock();
         }
-        assertTrue(copied, "File not copied: " + msg);
-        assertNotEquals(fileTime, targetFile.lastModified());
-        if (!updated.await(5, TimeUnit.SECONDS)) {
-            fail("File update was not detected");
+        assertThat(exception).as("Asynchronous exception").isNull();
+    }
+
+    @Test
+    void http_location_works(TestProperties properties, WireMockRuntimeInfo 
info) throws Exception {
+        WireMock wireMock = info.getWireMock();
+        // Setup WireMock
+        // The HTTP Last-Modified header has a precision of 1 second
+        ZonedDateTime now = LocalDateTime.now().atZone(ZoneOffset.UTC);
+        ZonedDateTime before = now.minusMinutes(1);
+        ZonedDateTime after = now.plusMinutes(1);
+        properties.setProperty("configLocation", info.getHttpBaseUrl() + 
URL_PATH);
+        // Setup Log4j
+        ConfigurationSource source =
+                ConfigurationSource.fromResource(CONFIG, 
getClass().getClassLoader());
+        loggerContext = new LoggerContext("http_location_works", null, (URI) 
null, instanceFactory);
+        Configuration configuration = loggerContext.getConfiguration(source);
+        configuration.initialize(); // To create the components
+        final ListAppender app = configuration.getAppender("LIST");
+        assertThat(app).isNotNull();
+        final MutableThreadContextMapFilter filter = 
(MutableThreadContextMapFilter) configuration.getFilter();
+        assertNotNull(filter);
+        filter.registerListener(this);
+        lock.lock();
+        try {
+            // Prepare the first test case: original empty config
+            wireMock.importStubMappings(createMapping(URL_PATH, CREDENTIALS, 
EMPTY_CONFIG, JSON, before));
+            // Starts the configuration
+            loggerContext.start(configuration);
+
+            final Logger logger = 
loggerContext.getLogger(MutableThreadContextMapFilterTest.class);
+
+            assertThat(filterUpdated.await(2, TimeUnit.SECONDS))
+                    .as("Initial configuration was loaded")
+                    .isTrue();
+            ThreadContext.put("loginId", "rgoers");
+            logger.debug("This is a test");
+            assertThat(app.getEvents()).isEmpty();
+
+            // Prepare the second test case: updated config
+            wireMock.removeMappings();
+            wireMock.importStubMappings(createMapping(URL_PATH, CREDENTIALS, 
FILTER_CONFIG, JSON, after));
+            resultVerified.signalAll();
+
+            assertThat(filterUpdated.await(2, TimeUnit.SECONDS))
+                    .as("Updated configuration was loaded")
+                    .isTrue();
+            logger.debug("This is a test");
+            assertThat(app.getEvents()).hasSize(1);
+
+            // Prepare the third test case: removed config
+            wireMock.removeMappings();
+            resultVerified.signalAll();
+
+            assertThat(filterUpdated.await(2, TimeUnit.SECONDS))
+                    .as("Configuration removal was detected")
+                    .isTrue();
+            logger.debug("This is a test");
+            assertThat(app.getEvents()).hasSize(1);
+            resultVerified.signalAll();
+        } finally {
+            lock.unlock();
         }
-        logger.debug("This is a test");
-        Assertions.assertEquals(1, ((ListAppender) app).getEvents().size());
+        assertThat(exception).as("Asynchronous exception").isNull();
     }
 
     @Override
     public void onEvent() {
-        updated.countDown();
+        lock.lock();
+        try {
+            filterUpdated.signalAll();
+            resultVerified.await();
+        } catch (final InterruptedException e) {
+            exception = e;
+        } finally {
+            lock.unlock();
+        }
     }
 }
diff --git 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/net/UrlConnectionFactoryTest.java
 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/net/UrlConnectionFactoryTest.java
index 3a5d8ba5b1..670f24d22e 100644
--- 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/net/UrlConnectionFactoryTest.java
+++ 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/net/UrlConnectionFactoryTest.java
@@ -16,20 +16,27 @@
  */
 package org.apache.logging.log4j.core.net;
 
-import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
-import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
-import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
-import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED;
-import static javax.servlet.http.HttpServletResponse.SC_OK;
-import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static java.util.Objects.requireNonNull;
+import static org.apache.hc.core5.http.HttpStatus.SC_INTERNAL_SERVER_ERROR;
+import static org.apache.hc.core5.http.HttpStatus.SC_NOT_MODIFIED;
+import static org.apache.hc.core5.http.HttpStatus.SC_OK;
+import static 
org.apache.logging.log4j.core.config.ConfigurationSourceTest.PATH_IN_JAR;
+import static org.apache.logging.log4j.core.net.WireMockUtil.createMapping;
+import static 
org.apache.logging.log4j.core.test.TestConstants.AUTH_BASIC_PASSWORD;
+import static 
org.apache.logging.log4j.core.test.TestConstants.AUTH_BASIC_USERNAME;
+import static 
org.apache.logging.log4j.core.test.TestConstants.CONFIGURATION_ALLOWED_PROTOCOLS;
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.junit.jupiter.api.Assertions.fail;
 
+import com.github.tomakehurst.wiremock.client.BasicCredentials;
+import com.github.tomakehurst.wiremock.client.WireMock;
+import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
+import com.github.tomakehurst.wiremock.junit5.WireMockTest;
 import com.sun.management.UnixOperatingSystemMXBean;
-import java.io.File;
+import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.lang.management.ManagementFactory;
@@ -38,105 +45,108 @@ import java.net.HttpURLConnection;
 import java.net.URI;
 import java.net.URL;
 import java.nio.file.Files;
-import java.util.Base64;
-import java.util.Enumeration;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.ZonedDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.apache.commons.io.IOUtils;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.core.config.ConfigurationSource;
 import org.apache.logging.log4j.core.config.ConfigurationSourceTest;
-import org.apache.logging.log4j.core.test.TestConstants;
+import 
org.apache.logging.log4j.core.impl.CoreProperties.AuthenticationProperties;
+import org.apache.logging.log4j.core.net.ssl.SslConfiguration;
+import org.apache.logging.log4j.core.net.ssl.SslConfigurationFactory;
+import org.apache.logging.log4j.core.util.AuthorizationProvider;
 import org.apache.logging.log4j.kit.env.PropertyEnvironment;
 import org.apache.logging.log4j.test.junit.SetTestProperty;
-import org.eclipse.jetty.http.HttpHeader;
-import org.eclipse.jetty.server.Server;
-import org.eclipse.jetty.server.ServerConnector;
-import org.eclipse.jetty.servlet.DefaultServlet;
-import org.eclipse.jetty.servlet.ServletContextHandler;
-import org.eclipse.jetty.servlet.ServletHolder;
-import org.junit.jupiter.api.AfterAll;
-import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.condition.DisabledOnOs;
 import org.junit.jupiter.api.condition.OS;
-import org.junit.jupiter.api.parallel.Isolated;
+import org.junit.jupiter.api.io.TempDir;
 import org.junitpioneer.jupiter.RetryingTest;
 
 /**
  * Tests the UrlConnectionFactory
  */
-@Isolated
-public class UrlConnectionFactoryTest {
+@WireMockTest
+@SetTestProperty(key = CONFIGURATION_ALLOWED_PROTOCOLS, value = "jar,http")
+class UrlConnectionFactoryTest {
 
     private static final Logger LOGGER = 
LogManager.getLogger(UrlConnectionFactoryTest.class);
-    private static final String BASIC = "Basic ";
-    private static final String expectedCreds = "testuser:password";
-    private static Server server;
-    private static final Base64.Decoder decoder = Base64.getDecoder();
-    private static int port;
-
-    @BeforeAll
-    public static void startServer() throws Exception {
-        try {
-            server = new Server(0);
-            final ServletContextHandler context = new ServletContextHandler();
-            final ServletHolder defaultServ = new ServletHolder("default", 
TestServlet.class);
-            defaultServ.setInitParameter("resourceBase", 
System.getProperty("user.dir"));
-            defaultServ.setInitParameter("dirAllowed", "true");
-            context.addServlet(defaultServ, "/");
-            server.setHandler(context);
-
-            // Start Server
-            server.start();
-            port = ((ServerConnector) 
server.getConnectors()[0]).getLocalPort();
-        } catch (Throwable ex) {
-            ex.printStackTrace();
-            throw ex;
+
+    private static final String URL_PATH = "/log4j2-config.xml";
+    private static final BasicCredentials CREDENTIALS = new 
BasicCredentials("testUser", "password");
+    private static final byte[] CONFIG_FILE_BODY;
+    private static final String CONTENT_TYPE = "application/xml";
+
+    private final PropertyEnvironment environment = 
PropertyEnvironment.getGlobal();
+    private final SslConfiguration sslConfiguration = 
SslConfigurationFactory.getSslConfiguration(environment);
+
+    static {
+        try (InputStream input = requireNonNull(
+                
UrlConnectionFactoryTest.class.getClassLoader().getResourceAsStream("log4j2-config.xml")))
 {
+            ByteArrayOutputStream output = new ByteArrayOutputStream();
+            IOUtils.copy(input, output);
+            CONFIG_FILE_BODY = output.toByteArray();
+        } catch (IOException e) {
+            throw new AssertionError(e);
         }
     }
 
-    @AfterAll
-    public static void stopServer() throws Exception {
-        server.stop();
+    @AfterEach
+    void cleanup(WireMockRuntimeInfo info) {
+        info.getWireMock().removeMappings();
     }
 
     @Test
-    @SetTestProperty(key = TestConstants.AUTH_BASIC_USERNAME, value = "foo")
-    @SetTestProperty(key = TestConstants.AUTH_BASIC_PASSWORD, value = "bar")
-    @SetTestProperty(key = TestConstants.CONFIGURATION_ALLOWED_PROTOCOLS, 
value = "http")
-    public void testBadCrdentials() throws Exception {
-        final URI uri = new URI("http://localhost:"; + port + 
"/log4j2-config.xml");
+    @SetTestProperty(key = AUTH_BASIC_USERNAME, value = "foo")
+    @SetTestProperty(key = AUTH_BASIC_PASSWORD, value = "bar")
+    void testBadCredentials(WireMockRuntimeInfo info) throws Exception {
+        WireMock wireMock = info.getWireMock();
+        // RFC 1123 format rounds to full seconds
+        ZonedDateTime now = 
ZonedDateTime.now().truncatedTo(ChronoUnit.SECONDS);
+        wireMock.importStubMappings(createMapping(URL_PATH, CREDENTIALS, 
CONFIG_FILE_BODY, CONTENT_TYPE, now));
+        final URI uri = new URI(info.getHttpBaseUrl() + URL_PATH);
         final ConfigurationSource source = ConfigurationSource.fromUri(uri);
         assertNull(source, "A ConfigurationSource should not have been 
returned");
     }
 
     @Test
-    @SetTestProperty(key = TestConstants.AUTH_BASIC_USERNAME, value = 
"testuser")
-    @SetTestProperty(key = TestConstants.AUTH_BASIC_PASSWORD, value = 
"password")
-    @SetTestProperty(key = TestConstants.CONFIGURATION_ALLOWED_PROTOCOLS, 
value = "http")
-    public void withAuthentication() throws Exception {
-        final URI uri = new URI("http://localhost:"; + port + 
"/log4j2-config.xml");
+    @SetTestProperty(key = AUTH_BASIC_USERNAME, value = "testUser")
+    @SetTestProperty(key = AUTH_BASIC_PASSWORD, value = "password")
+    public void withAuthentication(WireMockRuntimeInfo info) throws Exception {
+        WireMock wireMock = info.getWireMock();
+        // RFC 1123 format rounds to full seconds
+        ZonedDateTime now = 
ZonedDateTime.now().truncatedTo(ChronoUnit.SECONDS);
+        wireMock.importStubMappings(createMapping(URL_PATH, CREDENTIALS, 
CONFIG_FILE_BODY, CONTENT_TYPE, now));
+        final URI uri = new URI(info.getHttpBaseUrl() + URL_PATH);
         final ConfigurationSource source = ConfigurationSource.fromUri(uri);
         assertNotNull(source, "No ConfigurationSource returned");
         final InputStream is = source.getInputStream();
         assertNotNull(is, "No data returned");
         is.close();
         final long lastModified = source.getLastModified();
+        assertThat(lastModified).isEqualTo(now.toInstant().toEpochMilli());
         int result = verifyNotModified(uri, lastModified);
         assertEquals(SC_NOT_MODIFIED, result, "File was modified");
-        final File file = new File("target/test-classes/log4j2-config.xml");
-        if (!file.setLastModified(System.currentTimeMillis())) {
-            fail("Unable to set last modified time");
-        }
+
+        wireMock.removeMappings();
+        now = now.plusMinutes(5);
+        wireMock.importStubMappings(createMapping(URL_PATH, CREDENTIALS, 
CONFIG_FILE_BODY, CONTENT_TYPE, now));
         result = verifyNotModified(uri, lastModified);
         assertEquals(SC_OK, result, "File was not modified");
     }
 
-    private int verifyNotModified(final URI uri, final long 
lastModifiedMillis) throws Exception {
-        final HttpURLConnection urlConnection = 
UrlConnectionFactory.createConnection(
-                uri.toURL(), lastModifiedMillis, null, null, 
PropertyEnvironment.getGlobal());
+    private int verifyNotModified(URI uri, long lastModifiedMillis) throws 
Exception {
+        AuthorizationProvider authorizationProvider =
+                
AuthorizationProvider.getAuthorizationProvider(environment.getProperty(AuthenticationProperties.class));
+        HttpURLConnection urlConnection = 
UrlConnectionFactory.createConnection(
+                uri.toURL(), lastModifiedMillis, sslConfiguration, 
authorizationProvider, environment);
         urlConnection.connect();
 
         try {
@@ -149,20 +159,39 @@ public class UrlConnectionFactoryTest {
 
     @RetryingTest(maxAttempts = 5, suspendForMs = 1000)
     @DisabledOnOs(value = OS.WINDOWS, disabledReason = "Fails frequently on 
Windows (#2011)")
-    public void testNoJarFileLeak() throws Exception {
-        ConfigurationSourceTest.prepareJarConfigURL();
-        final URL url = new 
File("target/test-classes/jarfile.jar").toURI().toURL();
+    @SetTestProperty(key = AUTH_BASIC_USERNAME, value = "testUser")
+    @SetTestProperty(key = AUTH_BASIC_PASSWORD, value = "password")
+    void testNoJarFileLeak(@TempDir Path dir, WireMockRuntimeInfo info) throws 
Exception {
+        Path jarFile = ConfigurationSourceTest.prepareJarConfigURL(dir);
         // Retrieve using 'file:'
-        URL jarUrl = new URL("jar:" + url.toString() + "!/config/console.xml");
+        URL jarUrl = new URL("jar:" + jarFile.toUri().toURL() + "!" + 
PATH_IN_JAR);
         long expected = getOpenFileDescriptorCount();
         UrlConnectionFactory.createConnection(jarUrl).getInputStream().close();
         assertEquals(expected, getOpenFileDescriptorCount());
+
+        // Prepare mock
+        ByteArrayOutputStream body = new ByteArrayOutputStream();
+        try (InputStream inputStream = Files.newInputStream(jarFile)) {
+            IOUtils.copy(inputStream, body);
+        }
+        WireMock wireMock = info.getWireMock();
+        wireMock.register(WireMock.get("/jarFile.jar")
+                .willReturn(
+                        
aResponse().withStatus(200).withBodyFile("jarFile.jar").withBody(body.toByteArray())));
         // Retrieve using 'http:'
-        jarUrl = new URL("jar:http://localhost:"; + port + 
"/jarfile.jar!/config/console.xml");
-        final File tmpDir = new File(System.getProperty("java.io.tmpdir"));
-        expected = tmpDir.list().length;
+        jarUrl = new URL("jar:" + info.getHttpBaseUrl() + "/jarFile.jar!" + 
PATH_IN_JAR);
+        // URLConnection leaves JAR files in the temporary directory
+        Path tmpDir = Paths.get(System.getProperty("java.io.tmpdir"));
+        List<Path> expectedFiles;
+        try (Stream<Path> stream = Files.list(tmpDir)) {
+            expectedFiles = stream.collect(Collectors.toList());
+        }
         UrlConnectionFactory.createConnection(jarUrl).getInputStream().close();
-        assertEquals(expected, tmpDir.list().length, "File descriptor leak");
+        List<Path> actualFiles;
+        try (Stream<Path> stream = Files.list(tmpDir)) {
+            actualFiles = stream.collect(Collectors.toList());
+        }
+        assertThat(actualFiles).containsExactlyElementsOf(expectedFiles);
     }
 
     private long getOpenFileDescriptorCount() {
@@ -172,53 +201,4 @@ public class UrlConnectionFactoryTest {
         }
         return 0L;
     }
-
-    public static class TestServlet extends DefaultServlet {
-
-        private static final long serialVersionUID = -2885158530511450659L;
-
-        @Override
-        protected void doGet(final HttpServletRequest request, final 
HttpServletResponse response)
-                throws ServletException, IOException {
-            final Enumeration<String> headers = 
request.getHeaders(HttpHeader.AUTHORIZATION.toString());
-            if (headers == null) {
-                response.sendError(SC_UNAUTHORIZED, "No Auth header");
-                return;
-            }
-            while (headers.hasMoreElements()) {
-                final String authData = headers.nextElement();
-                assertTrue(authData.startsWith(BASIC), "Not a Basic auth 
header");
-                final String credentials = new 
String(decoder.decode(authData.substring(BASIC.length())));
-                if (!expectedCreds.equals(credentials)) {
-                    response.sendError(SC_UNAUTHORIZED, "Invalid credentials");
-                    return;
-                }
-            }
-            final String servletPath = request.getServletPath();
-            if (servletPath != null) {
-                File file = new File("target/classes" + servletPath);
-                if (!file.exists()) {
-                    file = new File("target/test-classes" + servletPath);
-                }
-                if (!file.exists()) {
-                    response.sendError(SC_NOT_FOUND);
-                    return;
-                }
-                final long modifiedSince = 
request.getDateHeader(HttpHeader.IF_MODIFIED_SINCE.toString());
-                final long lastModified = (file.lastModified() / 1000) * 1000;
-                LOGGER.debug("LastModified: {}, modifiedSince: {}", 
lastModified, modifiedSince);
-                if (modifiedSince > 0 && lastModified <= modifiedSince) {
-                    response.setStatus(SC_NOT_MODIFIED);
-                    return;
-                }
-                response.setDateHeader(HttpHeader.LAST_MODIFIED.toString(), 
lastModified);
-                response.setContentLengthLong(file.length());
-                Files.copy(file.toPath(), response.getOutputStream());
-                response.getOutputStream().flush();
-                response.setStatus(SC_OK);
-            } else {
-                response.sendError(SC_BAD_REQUEST, "Unsupported request");
-            }
-        }
-    }
 }
diff --git 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/net/WireMockUtil.java
 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/net/WireMockUtil.java
new file mode 100644
index 0000000000..f865574232
--- /dev/null
+++ 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/net/WireMockUtil.java
@@ -0,0 +1,84 @@
+/*
+ * 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.core.net;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.absent;
+import static com.github.tomakehurst.wiremock.client.WireMock.after;
+import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl;
+import static com.github.tomakehurst.wiremock.client.WireMock.before;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalToDateTime;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.notContaining;
+import static com.github.tomakehurst.wiremock.stubbing.StubImport.stubImport;
+import static com.google.common.net.HttpHeaders.AUTHORIZATION;
+import static com.google.common.net.HttpHeaders.CONTENT_TYPE;
+import static com.google.common.net.HttpHeaders.IF_MODIFIED_SINCE;
+import static com.google.common.net.HttpHeaders.LAST_MODIFIED;
+
+import com.github.tomakehurst.wiremock.client.BasicCredentials;
+import com.github.tomakehurst.wiremock.stubbing.StubImport;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+
+public class WireMockUtil {
+
+    private static final DateTimeFormatter formatter = 
DateTimeFormatter.RFC_1123_DATE_TIME.withZone(ZoneOffset.UTC);
+
+    /**
+     * Establishes a set of mapping to serve a file
+     *
+     * @param urlPath The URL path of the served file
+     * @param credentials The credentials to use for authentication
+     * @param body The body of the file
+     * @param contentType The MIME content type of the file
+     * @param lastModified The last modification date of the file
+     * @return A set of mappings
+     */
+    public static StubImport createMapping(
+            String urlPath, BasicCredentials credentials, byte[] body, String 
contentType, ZonedDateTime lastModified) {
+        int idx = urlPath.lastIndexOf('/');
+        String fileName = idx == -1 ? urlPath : urlPath.substring(idx + 1);
+        return stubImport()
+                // Lack of authentication data
+                .stub(get(anyUrl())
+                        .withHeader(AUTHORIZATION, absent())
+                        
.willReturn(aResponse().withStatus(401).withStatusMessage("Not Authenticated")))
+                // Wrong authentication data
+                .stub(get(anyUrl())
+                        .withHeader(AUTHORIZATION, 
notContaining(credentials.asAuthorizationHeaderValue()))
+                        
.willReturn(aResponse().withStatus(403).withStatusMessage("Not Authorized")))
+                // Serves the file
+                .stub(get(urlPath)
+                        .withBasicAuth(credentials.username, 
credentials.password)
+                        .withHeader(IF_MODIFIED_SINCE, 
before(lastModified).or(absent()))
+                        .willReturn(aResponse()
+                                .withStatus(200)
+                                .withBodyFile(fileName)
+                                .withBody(body)
+                                .withHeader(LAST_MODIFIED, 
formatter.format(lastModified))
+                                .withHeader(CONTENT_TYPE, contentType)))
+                // The file was not updated since lastModified
+                .stub(get(urlPath)
+                        .withBasicAuth(credentials.username, 
credentials.password)
+                        .withHeader(IF_MODIFIED_SINCE, 
after(lastModified).or(equalToDateTime(lastModified)))
+                        .willReturn(
+                                
aResponse().withStatus(304).withHeader(LAST_MODIFIED, 
formatter.format(lastModified))))
+                .build();
+    }
+}
diff --git 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/HttpWatcherTest.java
 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/HttpWatcherTest.java
new file mode 100644
index 0000000000..a7b6eb34df
--- /dev/null
+++ 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/HttpWatcherTest.java
@@ -0,0 +1,158 @@
+/*
+ * 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.core.util;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static java.util.Collections.singletonList;
+import static 
org.apache.logging.log4j.core.test.TestConstants.CONFIGURATION_ALLOWED_PROTOCOLS;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import com.github.tomakehurst.wiremock.client.WireMock;
+import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
+import com.github.tomakehurst.wiremock.junit5.WireMockTest;
+import com.github.tomakehurst.wiremock.stubbing.StubMapping;
+import java.net.URL;
+import java.time.Instant;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
+import java.util.List;
+import java.util.Queue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+import org.apache.logging.log4j.core.config.AbstractConfiguration;
+import org.apache.logging.log4j.core.config.Configuration;
+import org.apache.logging.log4j.core.config.ConfigurationSource;
+import org.apache.logging.log4j.core.config.HttpWatcher;
+import org.apache.logging.log4j.core.config.Reconfigurable;
+import org.apache.logging.log4j.kit.env.PropertyEnvironment;
+import org.apache.logging.log4j.plugins.di.ConfigurableInstanceFactory;
+import org.apache.logging.log4j.plugins.di.DI;
+import org.apache.logging.log4j.test.junit.SetTestProperty;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test the WatchManager
+ */
+@SetTestProperty(key = CONFIGURATION_ALLOWED_PROTOCOLS, value = "http,https")
+@WireMockTest
+class HttpWatcherTest {
+
+    private static final DateTimeFormatter formatter = 
DateTimeFormatter.RFC_1123_DATE_TIME.withZone(ZoneOffset.UTC);
+    private static final String XML = "application/xml";
+    private static final ConfigurableInstanceFactory instanceFactory = 
DI.createInitializedFactory();
+
+    @Test
+    void testModified(final WireMockRuntimeInfo info) throws Exception {
+        final WireMock wireMock = info.getWireMock();
+
+        final BlockingQueue<String> queue = new LinkedBlockingQueue<>();
+        List<Consumer<Reconfigurable>> listeners =
+                singletonList(new TestConfigurationListener(queue, 
"log4j-test1.xml"));
+        // HTTP Last-Modified is in seconds
+        Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS);
+        Instant previous = now.minus(5, ChronoUnit.MINUTES);
+        final URL url = new URL(info.getHttpBaseUrl() + "/log4j-test1.xml");
+        final Configuration configuration = createConfiguration(url);
+
+        final StubMapping stubMapping = 
wireMock.register(get("/log4j-test1.xml")
+                .willReturn(aResponse()
+                        .withBodyFile("log4j-test1.xml")
+                        .withStatus(200)
+                        .withHeader("Last-Modified", 
formatter.format(previous))
+                        .withHeader("Content-Type", XML)));
+        Watcher watcher = new HttpWatcher(configuration, null, listeners, 
previous.toEpochMilli());
+        watcher.watching(new Source(url));
+        try {
+            assertThat(watcher.isModified()).as("File was modified").isTrue();
+            assertThat(watcher.getLastModified()).as("File modification 
time").isEqualTo(previous.toEpochMilli());
+            // Check if listeners are correctly called
+            // Note: listeners are called asynchronously
+            watcher.modified();
+            String str = queue.poll(1, TimeUnit.SECONDS);
+            assertThat(str).isEqualTo("log4j-test1.xml");
+            ConfigurationSource configurationSource = 
configuration.getConfigurationSource();
+            // Check that the last modified time of the ConfigurationSource 
was modified as well
+            // See: https://github.com/apache/logging-log4j2/issues/2937
+            assertThat(configurationSource.getLastModified())
+                    .as("Last modification time of current 
ConfigurationSource")
+                    .isEqualTo(0L);
+            configurationSource = configurationSource.resetInputStream();
+            assertThat(configurationSource.getLastModified())
+                    .as("Last modification time of next ConfigurationSource")
+                    .isEqualTo(previous.toEpochMilli());
+        } finally {
+            wireMock.removeStubMapping(stubMapping);
+        }
+    }
+
+    @Test
+    void testNotModified(final WireMockRuntimeInfo info) throws Exception {
+        final WireMock wireMock = info.getWireMock();
+
+        final BlockingQueue<String> queue = new LinkedBlockingQueue<>();
+        List<Consumer<Reconfigurable>> listeners =
+                singletonList(new TestConfigurationListener(queue, 
"log4j-test2.xml"));
+        // HTTP Last-Modified is in seconds
+        Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS);
+        Instant previous = now.minus(5, ChronoUnit.MINUTES);
+        final URL url = new URL(info.getHttpBaseUrl() + "/log4j-test2.xml");
+        final Configuration configuration = createConfiguration(url);
+
+        final StubMapping stubMapping = 
wireMock.register(get("/log4j-test2.xml")
+                .willReturn(aResponse()
+                        .withStatus(304)
+                        .withHeader("Last-Modified", formatter.format(now) + " 
GMT")
+                        .withHeader("Content-Type", XML)));
+        Watcher watcher = new HttpWatcher(configuration, null, listeners, 
previous.toEpochMilli());
+        watcher.watching(new Source(url));
+        try {
+            assertThat(watcher.isModified()).as("File was modified").isFalse();
+            // If the file was not modified, neither should be the last 
modification time
+            
assertThat(watcher.getLastModified()).isEqualTo(previous.toEpochMilli());
+            // Check that the last modified time of the ConfigurationSource 
was not modified either
+            ConfigurationSource configurationSource = 
configuration.getConfigurationSource();
+            assertThat(configurationSource.getLastModified())
+                    .as("Last modification time of current 
ConfigurationSource")
+                    .isEqualTo(0L);
+            configurationSource = configurationSource.resetInputStream();
+            assertThat(configurationSource.getLastModified())
+                    .as("Last modification time of next ConfigurationSource")
+                    .isEqualTo(0L);
+        } finally {
+            wireMock.removeStubMapping(stubMapping);
+        }
+    }
+
+    // Creates a configuration with a predefined configuration source
+    private static Configuration createConfiguration(URL url) {
+        ConfigurationSource configurationSource = new ConfigurationSource(new 
Source(url), new byte[0], 0L);
+        return new AbstractConfiguration(
+                null, configurationSource, 
instanceFactory.getInstance(PropertyEnvironment.class), instanceFactory) {};
+    }
+
+    private record TestConfigurationListener(Queue<String> queue, String name) 
implements Consumer<Reconfigurable> {
+
+        @Override
+        public void accept(Reconfigurable ignored) {
+            queue.add(name);
+        }
+    }
+}
diff --git 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/WatchHttpTest.java
 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/WatchHttpTest.java
deleted file mode 100644
index 7e5057a113..0000000000
--- 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/WatchHttpTest.java
+++ /dev/null
@@ -1,159 +0,0 @@
-/*
- * 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.core.util;
-
-import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
-import static com.github.tomakehurst.wiremock.client.WireMock.get;
-import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.jupiter.api.Assumptions.assumeTrue;
-
-import com.github.tomakehurst.wiremock.client.WireMock;
-import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
-import com.github.tomakehurst.wiremock.junit5.WireMockTest;
-import com.github.tomakehurst.wiremock.stubbing.StubMapping;
-import java.net.URL;
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.List;
-import java.util.Queue;
-import java.util.TimeZone;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.TimeUnit;
-import java.util.function.Consumer;
-import org.apache.logging.log4j.core.config.Configuration;
-import org.apache.logging.log4j.core.config.ConfigurationScheduler;
-import org.apache.logging.log4j.core.config.DefaultConfiguration;
-import org.apache.logging.log4j.core.config.HttpWatcher;
-import org.apache.logging.log4j.core.config.Reconfigurable;
-import org.apache.logging.log4j.core.test.TestConstants;
-import org.apache.logging.log4j.core.util.datetime.FastDateFormat;
-import org.apache.logging.log4j.status.StatusLogger;
-import org.apache.logging.log4j.test.junit.SetTestProperty;
-import org.apache.logging.log4j.util.PropertiesUtil;
-import org.junit.jupiter.api.Test;
-
-/**
- * Test the WatchManager
- */
-@SetTestProperty(key = TestConstants.CONFIGURATION_ALLOWED_PROTOCOLS, value = 
"http,https")
-@WireMockTest
-public class WatchHttpTest {
-
-    private static final String FORCE_RUN_KEY = 
WatchHttpTest.class.getSimpleName() + ".forceRun";
-    private final String file = "log4j-test1.xml";
-    private static final TimeZone UTC = TimeZone.getTimeZone("UTC");
-    private static FastDateFormat formatter = FastDateFormat.getInstance("EEE, 
dd MMM yyyy HH:mm:ss", UTC);
-    private static final String XML = "application/xml";
-
-    private static final boolean IS_WINDOWS = 
PropertiesUtil.getProperties().isOsWindows();
-
-    @Test
-    public void testWatchManager(final WireMockRuntimeInfo info) throws 
Exception {
-        assumeTrue(!IS_WINDOWS || Boolean.getBoolean(FORCE_RUN_KEY));
-        final WireMock wireMock = info.getWireMock();
-
-        final BlockingQueue<String> queue = new LinkedBlockingQueue<>();
-        final List<Consumer<Reconfigurable>> listeners = new ArrayList<>();
-        listeners.add(new TestConfigurationListener(queue, "log4j-test1.xml"));
-        final Calendar now = Calendar.getInstance(UTC);
-        final Calendar previous = now;
-        previous.add(Calendar.MINUTE, -5);
-        final Configuration configuration = new DefaultConfiguration();
-        final URL url = new URL(info.getHttpBaseUrl() + "/log4j-test1.xml");
-        final StubMapping stubMapping = 
wireMock.register(get(urlPathEqualTo("/log4j-test1.xml"))
-                .willReturn(aResponse()
-                        .withBodyFile(file)
-                        .withStatus(200)
-                        .withHeader("Last-Modified", 
formatter.format(previous) + " GMT")
-                        .withHeader("Content-Type", XML)));
-        final ConfigurationScheduler scheduler = new ConfigurationScheduler();
-        scheduler.incrementScheduledItems();
-        final WatchManager watchManager = new WatchManager(scheduler, 
StatusLogger.getLogger());
-        watchManager.setIntervalSeconds(1);
-        scheduler.start();
-        watchManager.start();
-        try {
-            watchManager.watch(
-                    new Source(url.toURI()),
-                    new HttpWatcher(configuration, null, listeners, 
previous.getTimeInMillis()));
-            final String str = queue.poll(2, TimeUnit.SECONDS);
-            assertNotNull("File change not detected", str);
-        } finally {
-            watchManager.stop();
-            scheduler.stop();
-            wireMock.removeStubMapping(stubMapping);
-        }
-    }
-
-    @Test
-    public void testNotModified(final WireMockRuntimeInfo info) throws 
Exception {
-        assumeTrue(!IS_WINDOWS || Boolean.getBoolean(FORCE_RUN_KEY));
-        final WireMock wireMock = info.getWireMock();
-
-        final BlockingQueue<String> queue = new LinkedBlockingQueue<>();
-        final List<Consumer<Reconfigurable>> listeners = new ArrayList<>();
-        listeners.add(new TestConfigurationListener(queue, "log4j-test2.xml"));
-        final TimeZone timeZone = TimeZone.getTimeZone("UTC");
-        final Calendar now = Calendar.getInstance(timeZone);
-        final Calendar previous = now;
-        previous.add(Calendar.MINUTE, -5);
-        final Configuration configuration = new DefaultConfiguration();
-        final URL url = new URL(info.getHttpBaseUrl() + "/log4j-test2.xml");
-        final StubMapping stubMapping = 
wireMock.register(get(urlPathEqualTo("/log4j-test2.xml"))
-                .willReturn(aResponse()
-                        .withBodyFile(file)
-                        .withStatus(304)
-                        .withHeader("Last-Modified", formatter.format(now) + " 
GMT")
-                        .withHeader("Content-Type", XML)));
-        final ConfigurationScheduler scheduler = new ConfigurationScheduler();
-        scheduler.incrementScheduledItems();
-        final WatchManager watchManager = new WatchManager(scheduler, 
StatusLogger.getLogger());
-        watchManager.setIntervalSeconds(1);
-        scheduler.start();
-        watchManager.start();
-        try {
-            watchManager.watch(
-                    new Source(url.toURI()),
-                    new HttpWatcher(configuration, null, listeners, 
previous.getTimeInMillis()));
-            final String str = queue.poll(2, TimeUnit.SECONDS);
-            assertNull("File changed.", str);
-        } finally {
-            watchManager.stop();
-            scheduler.stop();
-            wireMock.removeStubMapping(stubMapping);
-        }
-    }
-
-    private static class TestConfigurationListener implements 
Consumer<Reconfigurable> {
-        private final Queue<String> queue;
-        private final String name;
-
-        public TestConfigurationListener(final Queue<String> queue, final 
String name) {
-            this.queue = queue;
-            this.name = name;
-        }
-
-        @Override
-        public void accept(final Reconfigurable reconfigurable) {
-            // System.out.println("Reconfiguration detected for " + name);
-            queue.add(name);
-        }
-    }
-}
diff --git 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/WatchManagerTest.java
 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/WatchManagerTest.java
index 318252f771..0bfceaf343 100644
--- 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/WatchManagerTest.java
+++ 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/WatchManagerTest.java
@@ -18,6 +18,12 @@ package org.apache.logging.log4j.core.util;
 
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import java.io.File;
 import java.io.FileOutputStream;
@@ -31,7 +37,8 @@ import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.TimeUnit;
 import org.apache.logging.log4j.core.config.ConfigurationScheduler;
 import org.apache.logging.log4j.status.StatusLogger;
-import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.condition.DisabledOnOs;
 import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
@@ -42,107 +49,111 @@ import org.junit.jupiter.api.condition.OS;
  */
 @DisabledOnOs(OS.WINDOWS)
 @EnabledIfSystemProperty(named = "WatchManagerTest.forceRun", matches = "true")
-@Tag("sleepy")
-public class WatchManagerTest {
+class WatchManagerTest {
 
     private final String testFile = "target/testWatchFile";
     private final String originalFile = "target/test-classes/log4j-test1.xml";
     private final String newFile = "target/test-classes/log4j-test1.yaml";
 
-    @Test
-    public void testWatchManager() throws Exception {
-        final ConfigurationScheduler scheduler = new ConfigurationScheduler();
+    private ConfigurationScheduler scheduler;
+    private WatchManager watchManager;
+
+    @BeforeEach
+    void setUp() {
+        scheduler = new ConfigurationScheduler();
         scheduler.incrementScheduledItems();
-        final WatchManager watchManager = new WatchManager(scheduler, 
StatusLogger.getLogger());
+        watchManager = new WatchManager(scheduler, StatusLogger.getLogger());
         watchManager.setIntervalSeconds(1);
         scheduler.start();
         watchManager.start();
-        try {
-            final File sourceFile = new File(originalFile);
-            Path source = Paths.get(sourceFile.toURI());
-            try (final FileOutputStream targetStream = new 
FileOutputStream(testFile)) {
-                Files.copy(source, targetStream);
-            }
-            final File updateFile = new File(newFile);
-            final File targetFile = new File(testFile);
-            final BlockingQueue<File> queue = new LinkedBlockingQueue<>();
-            watchManager.watchFile(targetFile, new TestWatcher(queue));
-            Thread.sleep(1000);
-            source = Paths.get(updateFile.toURI());
-            Files.copy(source, Paths.get(targetFile.toURI()), 
StandardCopyOption.REPLACE_EXISTING);
-            Thread.sleep(1000);
-            final File f = queue.poll(1, TimeUnit.SECONDS);
-            assertNotNull(f, "File change not detected");
-        } finally {
-            watchManager.stop();
-            scheduler.stop();
-        }
+    }
+
+    @AfterEach
+    void tearDown() {
+        watchManager.stop();
+        scheduler.stop();
+        watchManager = null;
+        scheduler = null;
     }
 
     @Test
-    public void testWatchManagerReset() throws Exception {
-        final ConfigurationScheduler scheduler = new ConfigurationScheduler();
-        scheduler.incrementScheduledItems();
-        final WatchManager watchManager = new WatchManager(scheduler, 
StatusLogger.getLogger());
-        watchManager.setIntervalSeconds(1);
-        scheduler.start();
-        watchManager.start();
-        try {
-            final File sourceFile = new File(originalFile);
-            Path source = Paths.get(sourceFile.toURI());
-            try (final FileOutputStream targetStream = new 
FileOutputStream(testFile)) {
-                Files.copy(source, targetStream);
-            }
-            final File updateFile = new File(newFile);
-            final File targetFile = new File(testFile);
-            final BlockingQueue<File> queue = new LinkedBlockingQueue<>();
-            watchManager.watchFile(targetFile, new TestWatcher(queue));
-            watchManager.stop();
-            Thread.sleep(1000);
-            source = Paths.get(updateFile.toURI());
-            Files.copy(source, Paths.get(targetFile.toURI()), 
StandardCopyOption.REPLACE_EXISTING);
-            watchManager.reset();
-            watchManager.start();
-            Thread.sleep(1000);
-            final File f = queue.poll(1, TimeUnit.SECONDS);
-            assertNull(f, "File change detected");
-        } finally {
-            watchManager.stop();
-            scheduler.stop();
+    void testWatchManager() throws Exception {
+        final File sourceFile = new File(originalFile);
+        Path source = Paths.get(sourceFile.toURI());
+        try (final FileOutputStream targetStream = new 
FileOutputStream(testFile)) {
+            Files.copy(source, targetStream);
         }
+        final File updateFile = new File(newFile);
+        final File targetFile = new File(testFile);
+        final BlockingQueue<File> queue = new LinkedBlockingQueue<>();
+        watchManager.watchFile(targetFile, new TestWatcher(queue));
+        Thread.sleep(1000);
+        source = Paths.get(updateFile.toURI());
+        Files.copy(source, Paths.get(targetFile.toURI()), 
StandardCopyOption.REPLACE_EXISTING);
+        Thread.sleep(1000);
+        final File f = queue.poll(1, TimeUnit.SECONDS);
+        assertNotNull(f, "File change not detected");
     }
 
     @Test
-    public void testWatchManagerResetFile() throws Exception {
-        final ConfigurationScheduler scheduler = new ConfigurationScheduler();
-        scheduler.incrementScheduledItems();
-        final WatchManager watchManager = new WatchManager(scheduler, 
StatusLogger.getLogger());
-        watchManager.setIntervalSeconds(1);
-        scheduler.start();
+    void testWatchManagerReset() throws Exception {
+        final File sourceFile = new File(originalFile);
+        Path source = Paths.get(sourceFile.toURI());
+        try (final FileOutputStream targetStream = new 
FileOutputStream(testFile)) {
+            Files.copy(source, targetStream);
+        }
+        final File updateFile = new File(newFile);
+        final File targetFile = new File(testFile);
+        final BlockingQueue<File> queue = new LinkedBlockingQueue<>();
+        watchManager.watchFile(targetFile, new TestWatcher(queue));
+        watchManager.stop();
+        Thread.sleep(1000);
+        source = Paths.get(updateFile.toURI());
+        Files.copy(source, Paths.get(targetFile.toURI()), 
StandardCopyOption.REPLACE_EXISTING);
+        watchManager.reset();
         watchManager.start();
-        try {
-            final File sourceFile = new File(originalFile);
-            Path source = Paths.get(sourceFile.toURI());
-            try (final FileOutputStream targetStream = new 
FileOutputStream(testFile)) {
-                Files.copy(source, targetStream);
-            }
-            final File updateFile = new File(newFile);
-            final File targetFile = new File(testFile);
-            final BlockingQueue<File> queue = new LinkedBlockingQueue<>();
-            watchManager.watchFile(targetFile, new TestWatcher(queue));
-            watchManager.stop();
-            Thread.sleep(1000);
-            source = Paths.get(updateFile.toURI());
-            Files.copy(source, Paths.get(targetFile.toURI()), 
StandardCopyOption.REPLACE_EXISTING);
-            watchManager.reset(targetFile);
-            watchManager.start();
-            Thread.sleep(1000);
-            final File f = queue.poll(1, TimeUnit.SECONDS);
-            assertNull(f, "File change detected");
-        } finally {
-            watchManager.stop();
-            scheduler.stop();
+        Thread.sleep(1000);
+        final File f = queue.poll(1, TimeUnit.SECONDS);
+        assertNull(f, "File change detected");
+    }
+
+    @Test
+    void testWatchManagerResetFile() throws Exception {
+        final File sourceFile = new File(originalFile);
+        Path source = Paths.get(sourceFile.toURI());
+        try (final FileOutputStream targetStream = new 
FileOutputStream(testFile)) {
+            Files.copy(source, targetStream);
         }
+        final File updateFile = new File(newFile);
+        final File targetFile = new File(testFile);
+        final BlockingQueue<File> queue = new LinkedBlockingQueue<>();
+        watchManager.watchFile(targetFile, new TestWatcher(queue));
+        watchManager.stop();
+        Thread.sleep(1000);
+        source = Paths.get(updateFile.toURI());
+        Files.copy(source, Paths.get(targetFile.toURI()), 
StandardCopyOption.REPLACE_EXISTING);
+        watchManager.reset(targetFile);
+        watchManager.start();
+        Thread.sleep(1000);
+        final File f = queue.poll(1, TimeUnit.SECONDS);
+        assertNull(f, "File change detected");
+    }
+
+    /**
+     * Verify the
+     */
+    @Test
+    void testWatchManagerCallsWatcher() {
+        Source source = mock(Source.class);
+        Watcher watcher = mock(Watcher.class);
+        when(watcher.isModified()).thenReturn(false);
+        watchManager.watch(source, watcher);
+        verify(watcher, timeout(2000)).isModified();
+        verify(watcher, never()).modified();
+        when(watcher.isModified()).thenReturn(true);
+        clearInvocations(watcher);
+        verify(watcher, timeout(2000)).isModified();
+        verify(watcher).modified();
     }
 
     private static class TestWatcher implements FileWatcher {
diff --git a/log4j-core-test/src/test/resources/log4j-sync-to-list.xml 
b/log4j-core-test/src/test/resources/config/ConfigurationSourceTest.xml
similarity index 71%
rename from log4j-core-test/src/test/resources/log4j-sync-to-list.xml
rename to log4j-core-test/src/test/resources/config/ConfigurationSourceTest.xml
index e0e95c2fd0..df84260a13 100644
--- a/log4j-core-test/src/test/resources/log4j-sync-to-list.xml
+++ b/log4j-core-test/src/test/resources/config/ConfigurationSourceTest.xml
@@ -15,20 +15,18 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<Configuration status="OFF" name="NestedLoggingInToString">
-
+<Configuration xmlns="https://logging.apache.org/xml/ns"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+               xsi:schemaLocation="
+                   https://logging.apache.org/xml/ns
+                   https://logging.apache.org/xml/ns/log4j-config-3.xsd";>
   <Appenders>
-    <Console name="STDOUT">
-      <PatternLayout pattern="%c %m%n"/>
+    <Console name="Console">
+      <PatternLayout/>
     </Console>
-    <List name="List">
-      <PatternLayout pattern="%level %logger %m"/>
-    </List>
   </Appenders>
-
   <Loggers>
-    <Root level="trace">
-      <AppenderRef ref="List"/>
+    <Root level="TRACE">
+      <AppenderRef ref="Console"/>
     </Root>
   </Loggers>
 </Configuration>
diff --git a/log4j-core-test/src/test/resources/emptyConfig.json 
b/log4j-core-test/src/test/resources/emptyConfig.json
deleted file mode 100644
index 37086f2b1f..0000000000
--- a/log4j-core-test/src/test/resources/emptyConfig.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
-  "configs": {
-  }
-}
\ No newline at end of file
diff --git 
a/log4j-core-test/src/test/resources/MutableThreadContextMapFilterTest.xml 
b/log4j-core-test/src/test/resources/filter/MutableThreadContextMapFilterTest.xml
similarity index 68%
rename from 
log4j-core-test/src/test/resources/MutableThreadContextMapFilterTest.xml
rename to 
log4j-core-test/src/test/resources/filter/MutableThreadContextMapFilterTest.xml
index 39a02650da..34eb059c4e 100644
--- a/log4j-core-test/src/test/resources/MutableThreadContextMapFilterTest.xml
+++ 
b/log4j-core-test/src/test/resources/filter/MutableThreadContextMapFilterTest.xml
@@ -15,22 +15,21 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<Configuration name="ConfigTest" status="ERROR">
-  <MutableThreadContextMapFilter configLocation="${test:configLocation}" 
pollInterval="1" onMatch="ACCEPT"
+<Configuration xmlns="https://logging.apache.org/xml/ns";
+               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+               xsi:schemaLocation="
+                   https://logging.apache.org/xml/ns
+                   https://logging.apache.org/xml/ns/log4j-config-2.xsd";>
+  <MutableThreadContextMapFilter configLocation="${test:configLocation}"
+                                 pollInterval="1"
+                                 onMatch="ACCEPT"
                                  onMismatch="NEUTRAL"/>
   <Appenders>
-    <Console name="STDOUT">
-      <PatternLayout pattern="%m%n"/>
-    </Console>
-    <List name="List">
-    </List>
+    <List name="LIST"/>
   </Appenders>
   <Loggers>
-    <Logger name="Test" level="error">
-      <AppenderRef ref="List"/>
-    </Logger>
-    <Root level="error">
-      <AppenderRef ref="STDOUT"/>
+    <Root level="ERROR">
+      <AppenderRef ref="LIST"/>
     </Root>
   </Loggers>
 </Configuration>
diff --git a/log4j-core-test/src/test/resources/filterConfig.json 
b/log4j-core-test/src/test/resources/filterConfig.json
deleted file mode 100644
index 91c8143ec2..0000000000
--- a/log4j-core-test/src/test/resources/filterConfig.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
-  "configs": {
-    "loginId": ["rgoers", "adam"],
-    "corpAcctNumber": ["30510263"]
-  }
-}
\ No newline at end of file
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/LoggerContext.java 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/LoggerContext.java
index 2eb829db49..4b5d3dd0ed 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/LoggerContext.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/LoggerContext.java
@@ -341,7 +341,7 @@ public class LoggerContext extends AbstractLifeCycle
      * @param config The new Configuration.
      */
     public void start(final Configuration config) {
-        LOGGER.debug("Starting {} with configuration {}...", this, config);
+        LOGGER.info("Starting {}[name={}] with configuration {}...", 
getClass().getSimpleName(), getName(), config);
         if (configLock.tryLock()) {
             try {
                 if (this.isInitialized() || this.isStopped()) {
@@ -355,7 +355,7 @@ public class LoggerContext extends AbstractLifeCycle
             }
         }
         setConfiguration(config);
-        LOGGER.debug("{} started OK with configuration {}.", this, config);
+        LOGGER.info("{}[name={}] started with configuration {}.", 
getClass().getSimpleName(), getName(), config);
     }
 
     private void setUpShutdownHook() {
@@ -815,7 +815,12 @@ public class LoggerContext extends AbstractLifeCycle
     private void reconfigure(final URI configURI) {
         final Object externalContext = externalMap.get(EXTERNAL_CONTEXT_KEY);
         final ClassLoader cl = externalContext instanceof ClassLoader ? 
(ClassLoader) externalContext : null;
-        LOGGER.debug("Reconfiguration started for {} at URI {} with optional 
ClassLoader: {}", this, configURI, cl);
+        LOGGER.debug(
+                "Reconfiguration started for context[name={}] at URI {} ({}) 
with optional ClassLoader: {}",
+                contextName,
+                configURI,
+                this,
+                cl);
         final Configuration instance = getConfiguration(contextName, 
configURI, cl);
         if (instance == null) {
             LOGGER.error(
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/AbstractConfiguration.java
 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/AbstractConfiguration.java
index e6c1f4427c..b62b1f0b80 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/AbstractConfiguration.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/AbstractConfiguration.java
@@ -19,7 +19,9 @@ package org.apache.logging.log4j.core.config;
 import aQute.bnd.annotation.Cardinality;
 import aQute.bnd.annotation.Resolution;
 import aQute.bnd.annotation.spi.ServiceConsumer;
+import java.io.File;
 import java.lang.ref.WeakReference;
+import java.net.URI;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
@@ -322,9 +324,10 @@ public abstract class AbstractConfiguration extends 
AbstractFilterable implement
         if (configSource != null && (configSource.getFile() != null || 
configSource.getURL() != null)) {
             if (monitorIntervalSeconds > 0) {
                 watchManager.setIntervalSeconds(monitorIntervalSeconds);
-                if (configSource.getFile() != null) {
-                    final Source cfgSource = new Source(configSource);
-                    final long lastModified = 
configSource.getFile().lastModified();
+                File file = configSource.getFile();
+                if (file != null) {
+                    final Source cfgSource = new Source(file);
+                    final long lastModified = file.lastModified();
                     final ConfigurationFileWatcher watcher =
                             new ConfigurationFileWatcher(this, reconfigurable, 
listeners, lastModified);
                     watchManager.watch(cfgSource, watcher);
@@ -342,8 +345,10 @@ public abstract class AbstractConfiguration extends 
AbstractFilterable implement
     }
 
     private void monitorSource(final Reconfigurable reconfigurable, final 
ConfigurationSource configSource) {
-        if (configSource.getLastModified() > 0) {
-            final Source cfgSource = new Source(configSource);
+        URI uri = configSource.getURI();
+        if (uri != null && configSource.getLastModified() > 0) {
+            File file = configSource.getFile();
+            final Source cfgSource = file != null ? new Source(file) : new 
Source(uri);
             final Watcher watcher = instanceFactory
                     .getInstance(WatcherFactory.class)
                     .newWatcher(cfgSource, this, reconfigurable, listeners, 
configSource.getLastModified());
@@ -364,9 +369,13 @@ public abstract class AbstractConfiguration extends 
AbstractFilterable implement
         if (getState().equals(State.INITIALIZING)) {
             initialize();
         }
-        LOGGER.debug("Starting configuration {}", this);
+        LOGGER.info("Starting configuration {}...", this);
         this.setStarting();
         if (watchManager.getIntervalSeconds() >= 0) {
+            LOGGER.info(
+                    "Start watching for changes to {} every {} seconds",
+                    getConfigurationSource(),
+                    watchManager.getIntervalSeconds());
             watchManager.start();
         }
         for (final ConfigurationExtension extension : extensions) {
@@ -386,7 +395,7 @@ public abstract class AbstractConfiguration extends 
AbstractFilterable implement
             root.start(); // LOG4J2-336
         }
         super.start();
-        LOGGER.debug("Started configuration {} OK.", this);
+        LOGGER.info("Configuration {} started.", this);
     }
 
     /**
@@ -394,9 +403,9 @@ public abstract class AbstractConfiguration extends 
AbstractFilterable implement
      */
     @Override
     public boolean stop(final long timeout, final TimeUnit timeUnit) {
+        LOGGER.info("Stopping configuration {}...", this);
         this.setStopping();
         super.stop(timeout, timeUnit, false);
-        LOGGER.trace("Stopping {}...", this);
 
         // Stop the components that are closest to the application first:
         // 1. Notify all LoggerConfigs' ReliabilityStrategy that the 
configuration will be stopped.
@@ -485,7 +494,7 @@ public abstract class AbstractConfiguration extends 
AbstractFilterable implement
             advertiser.unadvertise(advertisement);
         }
         setStopped();
-        LOGGER.debug("Stopped {} OK", this);
+        LOGGER.info("Configuration {} stopped.", this);
         return true;
     }
 
@@ -513,6 +522,7 @@ public abstract class AbstractConfiguration extends 
AbstractFilterable implement
         // default does nothing, subclasses do work.
     }
 
+    @SuppressWarnings("deprecation")
     protected Level getDefaultStatus() {
         return instanceFactory.getInstance(Constants.STATUS_LOGGER_LEVEL_KEY);
     }
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/ConfigurationSource.java
 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/ConfigurationSource.java
index 48ae11b9bf..a270e04f7e 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/ConfigurationSource.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/ConfigurationSource.java
@@ -33,6 +33,7 @@ import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.Objects;
 import javax.net.ssl.HttpsURLConnection;
+import org.apache.logging.log4j.Logger;
 import 
org.apache.logging.log4j.core.impl.CoreProperties.AuthenticationProperties;
 import org.apache.logging.log4j.core.net.ssl.LaxHostnameVerifier;
 import org.apache.logging.log4j.core.net.ssl.SslConfiguration;
@@ -42,13 +43,19 @@ import org.apache.logging.log4j.core.util.FileUtils;
 import org.apache.logging.log4j.core.util.Loader;
 import org.apache.logging.log4j.core.util.Source;
 import org.apache.logging.log4j.kit.env.PropertyEnvironment;
+import org.apache.logging.log4j.status.StatusLogger;
 import org.apache.logging.log4j.util.LoaderUtil;
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
 
 /**
  * Represents the source for the logging configuration.
  */
+@NullMarked
 public class ConfigurationSource {
 
+    private static final Logger LOGGER = StatusLogger.getLogger();
+
     /**
      * ConfigurationSource to use with Configurations that do not require a 
"real" configuration source.
      */
@@ -61,11 +68,12 @@ public class ConfigurationSource {
     private static final String HTTPS = "https";
 
     private final InputStream stream;
-    private volatile byte[] data;
-    private volatile Source source;
-    private final long lastModified;
+    private volatile byte @Nullable [] data;
+    private final @Nullable Source source;
+    // The initial modification time when the `ConfigurationSource` is created
+    private final long initialLastModified;
     // Set when the configuration has been updated so reset can use it for the 
next lastModified timestamp.
-    private volatile long modifiedMillis;
+    private volatile long currentLastModified;
 
     /**
      * Constructs a new {@code ConfigurationSource} with the specified input 
stream that originated from the specified
@@ -84,7 +92,7 @@ public class ConfigurationSource {
         } catch (final Exception ex) {
             // There is a problem with the file. It will be handled somewhere 
else.
         }
-        this.lastModified = modified;
+        this.currentLastModified = this.initialLastModified = modified;
     }
 
     /**
@@ -104,7 +112,7 @@ public class ConfigurationSource {
         } catch (Exception ex) {
             // There is a problem with the file. It will be handled somewhere 
else.
         }
-        this.lastModified = modified;
+        this.currentLastModified = this.initialLastModified = modified;
     }
 
     /**
@@ -115,10 +123,7 @@ public class ConfigurationSource {
      * @param url the URL where the input stream originated
      */
     public ConfigurationSource(final InputStream stream, final URL url) {
-        this.stream = Objects.requireNonNull(stream, "stream is null");
-        this.data = null;
-        this.lastModified = 0;
-        this.source = new Source(url);
+        this(stream, url, 0L);
     }
 
     /**
@@ -132,7 +137,7 @@ public class ConfigurationSource {
     public ConfigurationSource(final InputStream stream, final URL url, final 
long lastModified) {
         this.stream = Objects.requireNonNull(stream, "stream is null");
         this.data = null;
-        this.lastModified = lastModified;
+        this.currentLastModified = this.initialLastModified = lastModified;
         this.source = new Source(url);
     }
 
@@ -147,23 +152,30 @@ public class ConfigurationSource {
         this(stream.readAllBytes(), null, 0);
     }
 
-    public ConfigurationSource(final Source source, final byte[] data, final 
long lastModified) throws IOException {
+    public ConfigurationSource(final Source source, final byte[] data, final 
long lastModified) {
         Objects.requireNonNull(source, "source is null");
         this.data = Objects.requireNonNull(data, "data is null");
         this.stream = new ByteArrayInputStream(data);
-        this.lastModified = lastModified;
+        this.currentLastModified = this.initialLastModified = lastModified;
         this.source = source;
     }
 
-    private ConfigurationSource(final byte[] data, final URL url, final long 
lastModified) {
-        this.data = Objects.requireNonNull(data, "data is null");
-        this.stream = new ByteArrayInputStream(data);
-        this.lastModified = lastModified;
-        if (url == null) {
-            this.data = data;
-        } else {
-            this.source = new Source(url);
+    private ConfigurationSource(byte[] data, @Nullable Source source, long 
lastModified) {
+        this(data, source, new ByteArrayInputStream(data), lastModified);
+    }
+
+    /**
+     * @throws NullPointerException if both {@code stream} and {@code data} 
are {@code null}.
+     */
+    private ConfigurationSource(
+            byte @Nullable [] data, @Nullable Source source, InputStream 
stream, long lastModified) {
+        if (data == null && source == null) {
+            throw new NullPointerException("both `data` and `source` are 
null");
         }
+        this.stream = stream;
+        this.data = data;
+        this.source = source;
+        this.currentLastModified = this.initialLastModified = lastModified;
     }
 
     /**
@@ -172,29 +184,17 @@ public class ConfigurationSource {
      *
      * @return the configuration source file, or {@code null}
      */
-    public File getFile() {
+    public @Nullable File getFile() {
         return source == null ? null : source.getFile();
     }
 
-    private boolean isFile() {
-        return source != null && source.getFile() != null;
-    }
-
-    private boolean isURL() {
-        return source != null && source.getURI() != null;
-    }
-
-    private boolean isLocation() {
-        return source != null && source.getLocation() != null;
-    }
-
     /**
      * Returns the configuration source URL, or {@code null} if this 
configuration source is based on a file or has
      * neither a file nor an URL.
      *
      * @return the configuration source URL, or {@code null}
      */
-    public URL getURL() {
+    public @Nullable URL getURL() {
         return source == null ? null : source.getURL();
     }
 
@@ -202,24 +202,30 @@ public class ConfigurationSource {
         this.data = data;
     }
 
-    public void setModifiedMillis(final long modifiedMillis) {
-        this.modifiedMillis = modifiedMillis;
+    /**
+     * Updates the last known modification time of the resource.
+     *
+     * @param currentLastModified The modification time of the resource in 
millis.
+     */
+    public void setModifiedMillis(final long currentLastModified) {
+        this.currentLastModified = currentLastModified;
     }
 
     /**
      * Returns a URI representing the configuration resource or null if it 
cannot be determined.
      * @return The URI.
      */
-    public URI getURI() {
+    public @Nullable URI getURI() {
         return source == null ? null : source.getURI();
     }
 
     /**
-     * Returns the time the resource was last modified or 0 if it is not 
available.
+     * Returns the last modification time known when the {@code 
ConfigurationSource} was created.
+     *
      * @return the last modified time of the resource.
      */
     public long getLastModified() {
-        return lastModified;
+        return initialLastModified;
     }
 
     /**
@@ -228,7 +234,7 @@ public class ConfigurationSource {
      *
      * @return a string describing the configuration source file or URL, or 
{@code null}
      */
-    public String getLocation() {
+    public @Nullable String getLocation() {
         return source == null ? null : source.getLocation();
     }
 
@@ -247,26 +253,31 @@ public class ConfigurationSource {
      * @return a new {@code ConfigurationSource}
      * @throws IOException if a problem occurred while opening the new input 
stream
      */
-    public ConfigurationSource resetInputStream() throws IOException {
+    public @Nullable ConfigurationSource resetInputStream() throws IOException 
{
+        byte[] data = this.data;
         if (source != null && data != null) {
-            return new ConfigurationSource(source, data, this.lastModified);
-        } else if (isFile()) {
-            return new ConfigurationSource(new FileInputStream(getFile()), 
getFile());
-        } else if (isURL() && data != null) {
+            return new ConfigurationSource(source, data, currentLastModified);
+        }
+        File file = getFile();
+        if (file != null) {
+            return new 
ConfigurationSource(Files.newInputStream(file.toPath()), getFile());
+        }
+        URL url = getURL();
+        if (url != null && data != null) {
             // Creates a ConfigurationSource without accessing the URL since 
the data was provided.
-            return new ConfigurationSource(data, getURL(), modifiedMillis == 0 
? lastModified : modifiedMillis);
-        } else if (isURL()) {
-            return fromUri(getURI());
-        } else if (data != null) {
-            return new ConfigurationSource(data, null, lastModified);
+            return new ConfigurationSource(data, new Source(url), 
currentLastModified);
         }
-        return null;
+        URI uri = getURI();
+        if (uri != null) {
+            return fromUri(uri);
+        }
+        return data == null ? null : new ConfigurationSource(data, null, 
currentLastModified);
     }
 
     @Override
     public String toString() {
-        if (isLocation()) {
-            return getLocation();
+        if (source != null) {
+            return source.getLocation();
         }
         if (this == NULL_SOURCE) {
             return "NULL_SOURCE";
@@ -274,6 +285,7 @@ public class ConfigurationSource {
         if (this == COMPOSITE_SOURCE) {
             return "COMPOSITE_SOURCE";
         }
+        byte[] data = this.data;
         final int length = data == null ? -1 : data.length;
         return "stream (" + length + " bytes, unknown location)";
     }
@@ -281,9 +293,9 @@ public class ConfigurationSource {
     /**
      * Loads the configuration from a URI.
      * @param configLocation A URI representing the location of the 
configuration.
-     * @return The ConfigurationSource for the configuration.
+     * @return The ConfigurationSource for the configuration or {@code null}.
      */
-    public static ConfigurationSource fromUri(final URI configLocation) {
+    public static @Nullable ConfigurationSource fromUri(final URI 
configLocation) {
         final File configFile = FileUtils.fileFromUri(configLocation);
         if (configFile != null && configFile.exists() && configFile.canRead()) 
{
             try {
@@ -316,18 +328,15 @@ public class ConfigurationSource {
      * @param loader The default ClassLoader to use.
      * @return The ConfigurationSource for the configuration.
      */
-    public static ConfigurationSource fromResource(final String resource, 
final ClassLoader loader) {
+    public static @Nullable ConfigurationSource fromResource(String resource, 
@Nullable ClassLoader loader) {
         final URL url = Loader.getResource(resource, loader);
-        if (url == null) {
-            return null;
-        }
-        return getConfigurationSource(url);
+        return url == null ? null : getConfigurationSource(url);
     }
 
     @SuppressFBWarnings(
             value = "PATH_TRAVERSAL_IN",
             justification = "The name of the accessed files is based on a 
configuration value.")
-    private static ConfigurationSource getConfigurationSource(final URL url) {
+    private static @Nullable ConfigurationSource getConfigurationSource(final 
URL url) {
         try {
             final URLConnection urlConnection = url.openConnection();
             // A "jar:" URL file remains open after the stream is closed, so 
do not cache it.
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/HttpWatcher.java
 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/HttpWatcher.java
index 07e40ca4a7..3201f7d097 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/HttpWatcher.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/HttpWatcher.java
@@ -16,11 +16,14 @@
  */
 package org.apache.logging.log4j.core.config;
 
+import static java.util.Objects.requireNonNull;
+import static org.apache.logging.log4j.util.Strings.toRootUpperCase;
+
 import java.io.IOException;
-import java.io.InputStream;
 import java.net.MalformedURLException;
 import java.net.URISyntaxException;
 import java.net.URL;
+import java.time.Instant;
 import java.util.List;
 import java.util.function.Consumer;
 import org.apache.logging.log4j.Logger;
@@ -112,37 +115,51 @@ public class HttpWatcher extends AbstractWatcher {
             final LastModifiedSource source = new 
LastModifiedSource(url.toURI(), lastModifiedMillis);
             final HttpInputStreamUtil.Result result =
                     HttpInputStreamUtil.getInputStream(source, properties, 
authorizationProvider, sslConfiguration);
+            // Update lastModifiedMillis
+            // https://github.com/apache/logging-log4j2/issues/2937
+            lastModifiedMillis = source.getLastModified();
+            // The result of the HTTP/HTTPS request is already logged at 
`DEBUG` by `HttpInputStreamUtil`
+            // We only log the important events at `INFO` or more.
             switch (result.getStatus()) {
                 case NOT_MODIFIED: {
-                    LOGGER.debug("Configuration Not Modified");
                     return false;
                 }
                 case SUCCESS: {
                     final ConfigurationSource configSource = 
getConfiguration().getConfigurationSource();
                     try {
-                        final InputStream is = result.getInputStream();
-                        configSource.setData(is.readAllBytes());
-                        final long lastModified = source.getLastModified();
-                        configSource.setModifiedMillis(lastModified);
-                        lastModifiedMillis = lastModified;
-                        LOGGER.debug("Content was modified for {}", 
url.toString());
+                        // In this case `result.getInputStream()` is not null.
+                        configSource.setData(
+                                
requireNonNull(result.getInputStream()).readAllBytes());
+                        
configSource.setModifiedMillis(source.getLastModified());
+                        LOGGER.info(
+                                "{} resource at {} was modified on {}",
+                                () -> toRootUpperCase(url.getProtocol()),
+                                () -> url.toExternalForm(),
+                                () -> 
Instant.ofEpochMilli(source.getLastModified()));
                         return true;
                     } catch (final IOException e) {
-                        LOGGER.error("Error accessing configuration at {}: 
{}", url, e.getMessage());
+                        // Dead code since result.getInputStream() is a 
ByteArrayInputStream
+                        LOGGER.error("Error accessing configuration at {}", 
url.toExternalForm(), e);
                         return false;
                     }
                 }
                 case NOT_FOUND: {
-                    LOGGER.info("Unable to locate configuration at {}", 
url.toString());
+                    LOGGER.warn(
+                            "{} resource at {} was not found",
+                            () -> toRootUpperCase(url.getProtocol()),
+                            () -> url.toExternalForm());
                     return false;
                 }
                 default: {
-                    LOGGER.warn("Unexpected error accessing configuration at 
{}", url.toString());
+                    LOGGER.warn(
+                            "Unexpected error retrieving {} resource at {}",
+                            () -> toRootUpperCase(url.getProtocol()),
+                            () -> url.toExternalForm());
                     return false;
                 }
             }
         } catch (final URISyntaxException ex) {
-            LOGGER.error("Bad configuration URL: {}, {}", url.toString(), 
ex.getMessage());
+            LOGGER.error("Bad configuration file URL {}", 
url.toExternalForm(), ex);
             return false;
         }
     }
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/xml/XmlConfiguration.java
 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/xml/XmlConfiguration.java
index 01cb345f39..eb0a97a0b4 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/xml/XmlConfiguration.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/xml/XmlConfiguration.java
@@ -20,6 +20,7 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
@@ -175,7 +176,7 @@ public class XmlConfiguration extends AbstractConfiguration 
implements Reconfigu
      *
      * @param xIncludeAware enabled XInclude
      * @return a new DocumentBuilder
-     * @throws ParserConfigurationException
+     * @throws ParserConfigurationException if a DocumentBuilder cannot be 
created, which satisfies the configuration requested.
      */
     static DocumentBuilder newDocumentBuilder(final boolean xIncludeAware) 
throws ParserConfigurationException {
         final DocumentBuilderFactory factory = 
DocumentBuilderFactory.newInstance();
@@ -241,7 +242,7 @@ public class XmlConfiguration extends AbstractConfiguration 
implements Reconfigu
             return;
         }
         constructHierarchy(rootNode, rootElement);
-        if (status.size() > 0) {
+        if (!status.isEmpty()) {
             for (final Status s : status) {
                 LOGGER.error("Error processing element {} ({}): {}", s.name, 
s.element, s.errorType);
             }
@@ -295,7 +296,7 @@ public class XmlConfiguration extends AbstractConfiguration 
implements Reconfigu
         }
 
         final String text = buffer.toString().trim();
-        if (text.length() > 0 || (!node.hasChildren() && !node.isRoot())) {
+        if (!text.isEmpty() || (!node.hasChildren() && !node.isRoot())) {
             node.setValue(text);
         }
     }
@@ -337,7 +338,8 @@ public class XmlConfiguration extends AbstractConfiguration 
implements Reconfigu
 
     @Override
     public String toString() {
-        return getClass().getSimpleName() + "[location=" + 
getConfigurationSource() + "]";
+        return getClass().getSimpleName() + "[location=" + 
getConfigurationSource() + ", lastModified="
+                + 
Instant.ofEpochMilli(getConfigurationSource().getLastModified()) + "]";
     }
 
     /**
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/MutableThreadContextMapFilter.java
 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/MutableThreadContextMapFilter.java
index 396033ed21..f694a19b6f 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/MutableThreadContextMapFilter.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/MutableThreadContextMapFilter.java
@@ -66,6 +66,8 @@ import org.apache.logging.log4j.util.PerformanceSensitive;
 @PerformanceSensitive("allocation")
 public class MutableThreadContextMapFilter extends AbstractFilter {
 
+    private static final String HTTP = "http";
+    private static final String HTTPS = "https";
     private static final KeyValuePair[] EMPTY_ARRAY = {};
 
     private volatile Filter filter;
@@ -376,29 +378,42 @@ public class MutableThreadContextMapFilter extends 
AbstractFilter {
             final PropertyEnvironment properties = 
configuration.getEnvironment();
             final SslConfiguration sslConfiguration = 
SslConfigurationFactory.getSslConfiguration(properties);
             final ConfigResult result = getConfig(source, 
authorizationProvider, properties, sslConfiguration);
-            if (result.status == Status.SUCCESS) {
-                filter = ThreadContextMapFilter.newBuilder()
-                        .setPairs(result.pairs)
-                        .setOperator("or")
-                        .setOnMatch(getOnMatch())
-                        .setOnMismatch(getOnMismatch())
-                        
.setContextDataInjector(configuration.getComponent(ContextDataInjector.KEY))
-                        .get();
-                LOGGER.info("Filter configuration was updated: {}", 
filter.toString());
-                for (FilterConfigUpdateListener listener : listeners) {
-                    listener.onEvent();
-                }
-            } else if (result.status == Status.NOT_FOUND) {
-                if (!(filter instanceof NoOpFilter)) {
-                    LOGGER.info("Filter configuration was removed");
+            switch (result.status) {
+                case SUCCESS:
+                    filter = ThreadContextMapFilter.newBuilder()
+                            .setPairs(result.pairs)
+                            .setOperator("or")
+                            .setOnMatch(getOnMatch())
+                            .setOnMismatch(getOnMismatch())
+                            
.setContextDataInjector(configuration.getComponent(ContextDataInjector.KEY))
+                            .get();
+                    LOGGER.info("MutableThreadContextMapFilter configuration 
was updated: {}", filter.toString());
+                    break;
+                case NOT_FOUND:
+                    if (!(filter instanceof NoOpFilter)) {
+                        LOGGER.info("MutableThreadContextMapFilter 
configuration was removed");
+                        filter = new NoOpFilter();
+                    }
+                    break;
+                case EMPTY:
+                    LOGGER.debug("MutableThreadContextMapFilter configuration 
is empty");
                     filter = new NoOpFilter();
+                    break;
+            }
+            switch (result.status) {
+                    // These results cause changes in the filter
+                    // We call the listeners
+                case SUCCESS:
+                case NOT_FOUND:
+                case EMPTY:
                     for (FilterConfigUpdateListener listener : listeners) {
                         listener.onEvent();
                     }
-                }
-            } else if (result.status == Status.EMPTY) {
-                LOGGER.debug("Filter configuration is empty");
-                filter = new NoOpFilter();
+                    break;
+                    // These results do no cause changes in the filter
+                case ERROR:
+                case NOT_MODIFIED:
+                    break;
             }
         }
     }
@@ -407,7 +422,7 @@ public class MutableThreadContextMapFilter extends 
AbstractFilter {
             value = "PATH_TRAVERSAL_IN",
             justification = "The location of the file comes from a 
configuration value.")
     private static LastModifiedSource getSource(final String configLocation) {
-        LastModifiedSource source = null;
+        LastModifiedSource source;
         try {
             final URI uri = new URI(configLocation);
             if (uri.getScheme() != null) {
@@ -430,8 +445,9 @@ public class MutableThreadContextMapFilter extends 
AbstractFilter {
         final File inputFile = source.getFile();
         final ConfigResult configResult = new ConfigResult();
         InputStream inputStream = null;
-        HttpInputStreamUtil.Result result = null;
+        HttpInputStreamUtil.Result result;
         final long lastModified = source.getLastModified();
+        URI uri = source.getURI();
         try {
             if (inputFile != null && inputFile.exists()) {
                 try {
@@ -446,7 +462,7 @@ public class MutableThreadContextMapFilter extends 
AbstractFilter {
                 } catch (Exception ex) {
                     result = new HttpInputStreamUtil.Result(Status.ERROR);
                 }
-            } else if (source.getURI() != null) {
+            } else if (HTTP.equalsIgnoreCase(uri.getScheme()) || 
HTTPS.equalsIgnoreCase(uri.getScheme())) {
                 try {
                     result = HttpInputStreamUtil.getInputStream(source, props, 
authorizationProvider, sslConfiguration);
                     inputStream = result.getInputStream();
@@ -508,7 +524,7 @@ public class MutableThreadContextMapFilter extends 
AbstractFilter {
                             LOGGER.warn("Ignoring the value for {}, which is 
not an array: {}", key, jsonArray);
                         }
                     }
-                    if (pairs.size() > 0) {
+                    if (!pairs.isEmpty()) {
                         configResult.pairs = pairs.toArray(EMPTY_ARRAY);
                         configResult.status = Status.SUCCESS;
                     } else {
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/Loader.java 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/Loader.java
index ed65209a13..84667ce948 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/Loader.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/Loader.java
@@ -24,6 +24,7 @@ import 
org.apache.logging.log4j.core.impl.CoreProperties.LoaderProperties;
 import org.apache.logging.log4j.kit.env.PropertyEnvironment;
 import org.apache.logging.log4j.status.StatusLogger;
 import org.apache.logging.log4j.util.LoaderUtil;
+import org.jspecify.annotations.Nullable;
 
 /**
  * Load resources (or images) from various sources.
@@ -81,9 +82,9 @@ public final class Loader {
      * </ol>
      * @param resource The resource to load.
      * @param defaultLoader The default ClassLoader.
-     * @return A URL to the resource.
+     * @return A URL to the resource or {@code null}.
      */
-    public static URL getResource(final String resource, final ClassLoader 
defaultLoader) {
+    public static @Nullable URL getResource(final String resource, final 
ClassLoader defaultLoader) {
         try {
             ClassLoader classLoader = getThreadContextClassLoader();
             if (classLoader != null) {
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/Source.java 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/Source.java
index 327363d233..b5a5cb6e2b 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/Source.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/Source.java
@@ -16,6 +16,8 @@
  */
 package org.apache.logging.log4j.core.util;
 
+import static java.util.Objects.requireNonNull;
+
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 import java.io.File;
 import java.io.IOException;
@@ -30,10 +32,13 @@ import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.core.config.ConfigurationSource;
 import org.apache.logging.log4j.status.StatusLogger;
 import org.apache.logging.log4j.util.Strings;
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
 
 /**
  * Represents the source for the logging configuration as an immutable object.
  */
+@NullMarked
 public class Source {
     private static final Logger LOGGER = StatusLogger.getLogger();
 
@@ -45,9 +50,9 @@ public class Source {
         }
     }
 
-    private static File toFile(final Path path) {
+    private static @Nullable File toFile(Path path) {
         try {
-            return Objects.requireNonNull(path, "path").toFile();
+            return requireNonNull(path, "path").toFile();
         } catch (final UnsupportedOperationException e) {
             return null;
         }
@@ -57,9 +62,9 @@ public class Source {
     @SuppressFBWarnings(
             value = "PATH_TRAVERSAL_IN",
             justification = "The URI should be specified in a configuration 
file.")
-    private static File toFile(final URI uri) {
+    private static @Nullable File toFile(URI uri) {
         try {
-            final String scheme = Objects.requireNonNull(uri, 
"uri").getScheme();
+            final String scheme = requireNonNull(uri, "uri").getScheme();
             if (Strings.isBlank(scheme) || scheme.equals("file")) {
                 return new File(uri.getPath());
             } else {
@@ -67,20 +72,20 @@ public class Source {
                 return null;
             }
         } catch (final Exception e) {
-            LOGGER.debug("uri is malformed: " + uri.toString());
+            LOGGER.debug("uri is malformed: " + uri);
             return null;
         }
     }
 
     private static URI toURI(final URL url) {
         try {
-            return Objects.requireNonNull(url, "url").toURI();
+            return requireNonNull(url, "url").toURI();
         } catch (final URISyntaxException e) {
             throw new IllegalArgumentException(e);
         }
     }
 
-    private final File file;
+    private final @Nullable File file;
     private final URI uri;
     private final String location;
 
@@ -88,21 +93,23 @@ public class Source {
      * Constructs a Source from a ConfigurationSource.
      *
      * @param source The ConfigurationSource.
+     * @throws NullPointerException if {@code source} is {@code null}.
      */
     public Source(final ConfigurationSource source) {
         this.file = source.getFile();
-        this.uri = source.getURI();
-        this.location = source.getLocation();
+        this.uri = requireNonNull(source.getURI());
+        this.location = requireNonNull(source.getLocation());
     }
 
     /**
      * Constructs a new {@code Source} with the specified file.
      * file.
      *
-     * @param file the file where the input stream originated
+     * @param file the file where the input stream originated.
+     * @throws NullPointerException if {@code file} is {@code null}.
      */
     public Source(final File file) {
-        this.file = Objects.requireNonNull(file, "file");
+        this.file = requireNonNull(file, "file");
         this.location = normalize(file);
         this.uri = file.toURI();
     }
@@ -111,9 +118,10 @@ public class Source {
      * Constructs a new {@code Source} from the specified Path.
      *
      * @param path the Path where the input stream originated
+     * @throws NullPointerException if {@code path} is {@code null}.
      */
     public Source(final Path path) {
-        final Path normPath = Objects.requireNonNull(path, "path").normalize();
+        final Path normPath = requireNonNull(path, "path").normalize();
         this.file = toFile(normPath);
         this.uri = normPath.toUri();
         this.location = normPath.toString();
@@ -123,9 +131,10 @@ public class Source {
      * Constructs a new {@code Source} from the specified URI.
      *
      * @param uri the URI where the input stream originated
+     * @throws NullPointerException if {@code uri} is {@code null}.
      */
     public Source(final URI uri) {
-        final URI normUri = Objects.requireNonNull(uri, "uri").normalize();
+        final URI normUri = requireNonNull(uri, "uri").normalize();
         this.uri = normUri;
         this.location = normUri.toString();
         this.file = toFile(normUri);
@@ -135,6 +144,7 @@ public class Source {
      * Constructs a new {@code Source} from the specified URL.
      *
      * @param url the URL where the input stream originated
+     * @throws NullPointerException if this URL is {@code null}.
      * @throws IllegalArgumentException if this URL is not formatted strictly 
according to RFC2396 and cannot be
      *         converted to a URI.
      */
@@ -162,7 +172,7 @@ public class Source {
      *
      * @return the configuration source file, or {@code null}
      */
-    public File getFile() {
+    public @Nullable File getFile() {
         return file;
     }
 
@@ -185,7 +195,7 @@ public class Source {
             value = "PATH_TRAVERSAL_IN",
             justification = "The `file`, `uri` and `location` fields come from 
Log4j properties.")
     public Path getPath() {
-        return file != null ? file.toPath() : uri != null ? Paths.get(uri) : 
Paths.get(location);
+        return file != null ? file.toPath() : Paths.get(uri);
     }
 
     /**
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/WatchManager.java 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/WatchManager.java
index 5118f1cb9c..a7e242b622 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/WatchManager.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/WatchManager.java
@@ -20,6 +20,7 @@ import aQute.bnd.annotation.Cardinality;
 import aQute.bnd.annotation.Resolution;
 import aQute.bnd.annotation.spi.ServiceConsumer;
 import java.io.File;
+import java.time.Instant;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
@@ -53,8 +54,9 @@ import org.apache.logging.log4j.util.ServiceLoaderUtil;
 public class WatchManager extends AbstractLifeCycle {
 
     private static final class ConfigurationMonitor {
-        private volatile long lastModifiedMillis;
         private final Watcher watcher;
+        // Only used for logging
+        private volatile long lastModifiedMillis;
 
         public ConfigurationMonitor(final long lastModifiedMillis, final 
Watcher watcher) {
             this.watcher = watcher;
@@ -115,15 +117,11 @@ public class WatchManager extends AbstractLifeCycle {
                 final ConfigurationMonitor monitor = entry.getValue();
                 if (monitor.getWatcher().isModified()) {
                     final long lastModified = 
monitor.getWatcher().getLastModified();
-                    if (logger.isInfoEnabled()) {
-                        logger.info(
-                                "Source '{}' was modified on {} ({}), previous 
modification was on {} ({})",
-                                source,
-                                millisToString(lastModified),
-                                lastModified,
-                                millisToString(monitor.lastModifiedMillis),
-                                monitor.lastModifiedMillis);
-                    }
+                    logger.info(
+                            "Configuration source at `{}` was modified on 
`{}`, previous modification was on `{}`",
+                            () -> source,
+                            () -> Instant.ofEpochMilli(lastModified),
+                            () -> 
Instant.ofEpochMilli(monitor.lastModifiedMillis));
                     monitor.lastModifiedMillis = lastModified;
                     monitor.getWatcher().modified();
                 }
@@ -188,7 +186,7 @@ public class WatchManager extends AbstractLifeCycle {
     }
 
     public boolean hasEventListeners() {
-        return eventServiceList.size() > 0;
+        return !eventServiceList.isEmpty();
     }
 
     private String millisToString(final long millis) {
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/HttpInputStreamUtil.java
 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/HttpInputStreamUtil.java
index b596188328..10204e30f3 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/HttpInputStreamUtil.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/HttpInputStreamUtil.java
@@ -16,17 +16,24 @@
  */
 package org.apache.logging.log4j.core.util.internal;
 
+import static org.apache.logging.log4j.util.Strings.toRootUpperCase;
+
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.HttpURLConnection;
+import java.time.Instant;
 import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.core.config.ConfigurationException;
 import org.apache.logging.log4j.core.net.UrlConnectionFactory;
 import org.apache.logging.log4j.core.net.ssl.SslConfiguration;
 import org.apache.logging.log4j.core.util.AuthorizationProvider;
+import org.apache.logging.log4j.core.util.Source;
 import org.apache.logging.log4j.kit.env.PropertyEnvironment;
 import org.apache.logging.log4j.status.StatusLogger;
+import org.apache.logging.log4j.util.Supplier;
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
 
 /**
  * Utility method for reading data from an HTTP InputStream.
@@ -36,9 +43,20 @@ public final class HttpInputStreamUtil {
     private static final Logger LOGGER = StatusLogger.getLogger();
     private static final int NOT_MODIFIED = 304;
     private static final int NOT_AUTHORIZED = 401;
+    private static final int FORBIDDEN = 403;
     private static final int NOT_FOUND = 404;
     private static final int OK = 200;
 
+    /**
+     * Retrieves an HTTP resource if it has been modified.
+     * <p>
+     *     Side effects: if the request is successful, the last modified time 
of the {@code source}
+     *     parameter is modified.
+     * </p>
+     * @param source The location of the HTTP resource
+     * @param authorizationProvider The authentication data for the HTTP 
request
+     * @return A {@link Result} object containing the status code and body of 
the response
+     */
     public static Result getInputStream(
             final LastModifiedSource source,
             final PropertyEnvironment props,
@@ -54,12 +72,16 @@ public final class HttpInputStreamUtil {
                 final int code = connection.getResponseCode();
                 switch (code) {
                     case NOT_MODIFIED: {
-                        LOGGER.debug("Configuration not modified");
+                        LOGGER.debug(
+                                "{} resource {}: not modified since {}",
+                                formatProtocol(source),
+                                () -> source,
+                                () -> Instant.ofEpochMilli(lastModified));
                         result.status = Status.NOT_MODIFIED;
                         return result;
                     }
                     case NOT_FOUND: {
-                        LOGGER.debug("Unable to access {}: Not Found", 
source.toString());
+                        LOGGER.debug("{} resource {}: not found", 
formatProtocol(source), () -> source);
                         result.status = Status.NOT_FOUND;
                         return result;
                     }
@@ -67,60 +89,88 @@ public final class HttpInputStreamUtil {
                         try (final InputStream is = 
connection.getInputStream()) {
                             
source.setLastModified(connection.getLastModified());
                             LOGGER.debug(
-                                    "Content was modified for {}. previous 
lastModified: {}, new lastModified: {}",
-                                    source.toString(),
-                                    lastModified,
-                                    connection.getLastModified());
+                                    "{} resource {}: last modified on {}",
+                                    formatProtocol(source),
+                                    () -> source,
+                                    () -> 
Instant.ofEpochMilli(connection.getLastModified()));
                             result.status = Status.SUCCESS;
-                            result.inputStream = new 
ByteArrayInputStream(is.readAllBytes());
+                            result.bytes = is.readAllBytes();
                             return result;
                         } catch (final IOException e) {
                             try (final InputStream es = 
connection.getErrorStream()) {
-                                LOGGER.info(
-                                        "Error accessing configuration at {}: 
{}",
-                                        source.toString(),
-                                        es.readAllBytes());
+                                if (LOGGER.isDebugEnabled()) {
+                                    LOGGER.debug(
+                                            "Error accessing {} resource at 
{}: {}",
+                                            formatProtocol(source).get(),
+                                            source,
+                                            es.readAllBytes(),
+                                            e);
+                                }
                             } catch (final IOException ioe) {
-                                LOGGER.error(
-                                        "Error accessing configuration at {}: 
{}", source.toString(), e.getMessage());
+                                LOGGER.debug(
+                                        "Error accessing {} resource at {}",
+                                        formatProtocol(source),
+                                        () -> source,
+                                        () -> e);
                             }
-                            throw new ConfigurationException("Unable to access 
" + source.toString(), e);
+                            throw new ConfigurationException("Unable to access 
" + source, e);
                         }
                     }
                     case NOT_AUTHORIZED: {
-                        throw new ConfigurationException("Authorization 
failed");
+                        throw new ConfigurationException("Authentication 
required for " + source);
+                    }
+                    case FORBIDDEN: {
+                        throw new ConfigurationException("Access denied to " + 
source);
                     }
                     default: {
                         if (code < 0) {
-                            LOGGER.info("Invalid response code returned");
+                            LOGGER.debug("{} resource {}: invalid response 
code", formatProtocol(source), source);
                         } else {
-                            LOGGER.info("Unexpected response code returned 
{}", code);
+                            LOGGER.debug(
+                                    "{} resource {}: unexpected response code 
{}",
+                                    formatProtocol(source),
+                                    source,
+                                    code);
                         }
-                        throw new ConfigurationException("Unable to access " + 
source.toString());
+                        throw new ConfigurationException("Unable to access " + 
source);
                     }
                 }
             } finally {
                 connection.disconnect();
             }
         } catch (IOException e) {
-            LOGGER.warn("Error accessing {}: {}", source.toString(), 
e.getMessage());
-            throw new ConfigurationException("Unable to access " + 
source.toString(), e);
+            LOGGER.debug("Error accessing {} resource at {}", 
formatProtocol(source), source, e);
+            throw new ConfigurationException("Unable to access " + source, e);
         }
     }
 
+    private static Supplier<String> formatProtocol(Source source) {
+        return () -> toRootUpperCase(source.getURI().getScheme());
+    }
+
+    @NullMarked
     public static class Result {
 
-        private InputStream inputStream;
+        private byte @Nullable [] bytes = null;
         private Status status;
 
-        public Result() {}
+        public Result() {
+            this(Status.ERROR);
+        }
 
         public Result(final Status status) {
             this.status = status;
         }
 
-        public InputStream getInputStream() {
-            return inputStream;
+        /**
+         * Returns the data if the status is {@link Status#SUCCESS}.
+         * <p>
+         *     In any other case the result is {@code null}.
+         * </p>
+         * @return The contents of the HTTP response or null if empty.
+         */
+        public @Nullable InputStream getInputStream() {
+            return bytes != null ? new ByteArrayInputStream(bytes) : null;
         }
 
         public Status getStatus() {
diff --git a/log4j-parent/pom.xml b/log4j-parent/pom.xml
index 2cce92c1fb..e189f1a2de 100644
--- a/log4j-parent/pom.xml
+++ b/log4j-parent/pom.xml
@@ -117,7 +117,6 @@
     <javax-jms.version>2.0.1</javax-jms.version>
     
<java-allocation-instrumenter.version>3.3.4</java-allocation-instrumenter.version>
     <jctools.version>4.0.5</jctools.version>
-    <jetty.version>9.4.55.v20240627</jetty.version>
     <jmdns.version>3.5.12</jmdns.version>
     <jmh.version>1.37</jmh.version>
     <json-unit.version>3.4.1</json-unit.version>
@@ -140,11 +139,8 @@
     <osgi.resource.version>1.0.1</osgi.resource.version>
     <pax-exam.version>4.13.5</pax-exam.version>
     <plexus-utils.version>3.5.1</plexus-utils.version>
-    <slf4j2.version>2.0.15</slf4j2.version>
     <system-stubs.version>2.1.7</system-stubs.version>
-    <tomcat-juli.version>10.1.30</tomcat-juli.version>
     <velocity.version>1.7</velocity.version>
-    <wiremock.version>2.35.2</wiremock.version>
     <xmlunit.version>2.10.0</xmlunit.version>
 
     <!-- =====================================================
@@ -212,14 +208,6 @@
         <scope>import</scope>
       </dependency>
 
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>jetty-bom</artifactId>
-        <version>${jetty.version}</version>
-        <type>pom</type>
-        <scope>import</scope>
-      </dependency>
-
       <dependency>
         <groupId>org.junit</groupId>
         <artifactId>junit-bom</artifactId>
@@ -575,12 +563,6 @@
         <version>${plexus-utils.version}</version>
       </dependency>
 
-      <dependency>
-        <groupId>org.slf4j</groupId>
-        <artifactId>slf4j-api</artifactId>
-        <version>${slf4j2.version}</version>
-      </dependency>
-
       <dependency>
         <groupId>uk.org.webcompere</groupId>
         <artifactId>system-stubs-core</artifactId>
@@ -594,12 +576,6 @@
         <version>${system-stubs.version}</version>
       </dependency>
 
-      <dependency>
-        <groupId>org.apache.tomcat</groupId>
-        <artifactId>tomcat-juli</artifactId>
-        <version>${tomcat-juli.version}</version>
-      </dependency>
-
       <dependency>
         <groupId>org.graalvm.truffle</groupId>
         <artifactId>truffle-api</artifactId>
@@ -612,13 +588,6 @@
         <version>${velocity.version}</version>
       </dependency>
 
-      <!-- Used for testing `HttpAppender`: -->
-      <dependency>
-        <groupId>com.github.tomakehurst</groupId>
-        <artifactId>wiremock-jre8</artifactId>
-        <version>${wiremock.version}</version>
-      </dependency>
-
       <dependency>
         <groupId>org.xmlunit</groupId>
         <artifactId>xmlunit-core</artifactId>
@@ -750,7 +719,8 @@
                     <!-- JCL replacements -->
                     <exclude>org.slf4j:jcl-over-slf4j</exclude>
                     <exclude>org.springframework:spring-jcl</exclude>
-                    <!-- Log4j 1.x replacements -->
+                    <!-- Log4j 1.x and replacements -->
+                    <exclude>log4j:log4j</exclude>
                     <exclude>org.slf4j:log4j-over-slf4j</exclude>
                     <exclude>ch.qos.reload4j:reload4j</exclude>
                     <!-- Bridges to Log4j 1.x -->
diff --git a/log4j-perf-test/pom.xml b/log4j-perf-test/pom.xml
index aadc3c8933..d6cd2076e1 100644
--- a/log4j-perf-test/pom.xml
+++ b/log4j-perf-test/pom.xml
@@ -37,8 +37,30 @@
     <bnd.baseline.skip>true</bnd.baseline.skip>
     <maven.deploy.skip>true</maven.deploy.skip>
     <module.name>org.apache.logging.log4j.perf</module.name>
+
+    <!-- Dependency versions -->
+    <reload4j.version>1.2.25</reload4j.version>
+    <slf4j2.version>2.0.16</slf4j2.version>
   </properties>
 
+  <dependencyManagement>
+    <dependencies>
+
+      <dependency>
+        <groupId>org.slf4j</groupId>
+        <artifactId>slf4j-api</artifactId>
+        <version>${slf4j2.version}</version>
+      </dependency>
+
+      <dependency>
+        <groupId>ch.qos.reload4j</groupId>
+        <artifactId>reload4j</artifactId>
+        <version>${reload4j.version}</version>
+      </dependency>
+
+    </dependencies>
+  </dependencyManagement>
+
   <dependencies>
     <dependency>
       <groupId>org.openjdk.jmh</groupId>
@@ -108,10 +130,6 @@
       <groupId>org.openjdk.jmh</groupId>
       <artifactId>jmh-core</artifactId>
     </dependency>
-    <dependency>
-      <groupId>log4j</groupId>
-      <artifactId>log4j</artifactId>
-    </dependency>
     <dependency>
       <groupId>ch.qos.logback</groupId>
       <artifactId>logback-classic</artifactId>
@@ -122,6 +140,10 @@
       <artifactId>logback-core</artifactId>
       <scope>compile</scope>
     </dependency>
+    <dependency>
+      <groupId>ch.qos.reload4j</groupId>
+      <artifactId>reload4j</artifactId>
+    </dependency>
     <dependency>
       <groupId>org.slf4j</groupId>
       <artifactId>slf4j-api</artifactId>
@@ -145,7 +167,7 @@
       </plugin>
 
       <!--
-        ~ Unban Logback.
+        ~ Unban Logback and Reload4j
         -->
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
@@ -158,6 +180,7 @@
                 <bannedDependencies>
                   <includes>
                     <include>ch.qos.logback:*</include>
+                    <include>ch.qos.reload4j:reload4j</include>
                   </includes>
                 </bannedDependencies>
               </rules>
diff --git a/log4j-plugins/pom.xml b/log4j-plugins/pom.xml
index ac88ccb680..379bf79656 100644
--- a/log4j-plugins/pom.xml
+++ b/log4j-plugins/pom.xml
@@ -44,14 +44,17 @@
   </properties>
 
   <dependencies>
+
     <dependency>
       <groupId>org.apache.logging.log4j</groupId>
       <artifactId>log4j-api</artifactId>
     </dependency>
+
     <dependency>
       <groupId>org.apache.logging.log4j</groupId>
       <artifactId>log4j-kit</artifactId>
     </dependency>
+
   </dependencies>
 
   <build>
diff --git a/log4j-slf4j2-impl/pom.xml b/log4j-slf4j2-impl/pom.xml
index 7019645401..b48e3f50fe 100644
--- a/log4j-slf4j2-impl/pom.xml
+++ b/log4j-slf4j2-impl/pom.xml
@@ -36,17 +36,19 @@
     (Refer to the `log4j-to-slf4j` artifact for forwarding the Log4j API to 
SLF4J.)</description>
 
   <properties>
-
-    <!--
-      ~ OSGi and JPMS options
-      -->
-    <bnd.baseline.skip>false</bnd.baseline.skip>
-    <bnd-extra-module-options>
-      <!-- The module descriptor is in `META-INF/versions/9`
-           BND can not find it -->
-      org.slf4j;substitute="slf4j-api"
-    </bnd-extra-module-options>
+    <slf4j2.version>2.0.16</slf4j2.version>
   </properties>
+
+  <dependencyManagement>
+    <dependencies>
+      <dependency>
+        <groupId>org.slf4j</groupId>
+        <artifactId>slf4j-api</artifactId>
+        <version>${slf4j2.version}</version>
+      </dependency>
+    </dependencies>
+  </dependencyManagement>
+
   <dependencies>
     <dependency>
       <groupId>org.osgi</groupId>
diff --git a/log4j-to-slf4j/pom.xml b/log4j-to-slf4j/pom.xml
index 413fde7a59..2240022d4c 100644
--- a/log4j-to-slf4j/pom.xml
+++ b/log4j-to-slf4j/pom.xml
@@ -49,7 +49,20 @@
       <!-- Remove `transitive` for optional dependencies -->
       org.jspecify;transitive=false
     </bnd-extra-module-options>
+
+    <slf4j2.version>2.0.16</slf4j2.version>
   </properties>
+
+  <dependencyManagement>
+    <dependencies>
+      <dependency>
+        <groupId>org.slf4j</groupId>
+        <artifactId>slf4j-api</artifactId>
+        <version>${slf4j2.version}</version>
+      </dependency>
+    </dependencies>
+  </dependencyManagement>
+
   <dependencies>
     <dependency>
       <groupId>org.osgi</groupId>
diff --git a/pom.xml b/pom.xml
index dc412611b0..5b4e43691f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -374,6 +374,10 @@
   <dependencyManagement>
     <dependencies>
 
+      <!-- List of managed dependencies (in alphabetical order) that are 
intended for public consumption.
+           Modules that are not used anywhere (e.g., `log4j-core-its`, 
`log4j-osgi-test`) should not be placed here.
+           Modules that are used only internally (e.g., 
`log4j-layout-template-json-test`) should be added to `dependencyManagement > 
dependencies` in `log4j-parent/pom.xml`. -->
+
       <dependency>
         <groupId>org.apache.logging.log4j</groupId>
         <artifactId>log4j-api</artifactId>
diff --git a/src/changelog/.3.x.x/2937-http-watcher.xml 
b/src/changelog/.3.x.x/2937-http-watcher.xml
new file mode 100644
index 0000000000..ec3e1a1426
--- /dev/null
+++ b/src/changelog/.3.x.x/2937-http-watcher.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<entry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+       xmlns="https://logging.apache.org/xml/ns";
+       xsi:schemaLocation="https://logging.apache.org/xml/ns 
https://logging.apache.org/xml/ns/log4j-changelog-0.xsd";
+       type="fixed">
+  <issue id="2937" 
link="https://github.com/apache/logging-log4j2/issues/2937"/>
+  <description format="asciidoc">Fix reloading of the configuration from an 
HTTP(S) source</description>
+</entry>


Reply via email to