You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by vy...@apache.org on 2024/02/08 12:29:31 UTC

(logging-log4j2) branch 2.x updated: Make `StatusLogger` self-contained and testable (#2249)

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

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


The following commit(s) were added to refs/heads/2.x by this push:
     new ce1a3a432e Make `StatusLogger` self-contained and testable (#2249)
ce1a3a432e is described below

commit ce1a3a432e7888af60e1b44b31d1d84d0dacb45c
Author: Volkan Yazıcı <vo...@yazi.ci>
AuthorDate: Thu Feb 8 13:29:25 2024 +0100

    Make `StatusLogger` self-contained and testable (#2249)
---
 log4j-api-test/pom.xml                             |   1 -
 ...Extension.java => StatusListenerExtension.java} |  49 +-
 .../test/junit/StatusLoggerMockExtension.java      |  75 +++
 .../log4j/test/junit/UsingStatusListener.java      |   2 +-
 ...tusListener.java => UsingStatusLoggerMock.java} |  12 +-
 .../apache/logging/log4j/AbstractLoggerTest.java   |   3 +
 .../log4j/status/StatusConsoleListenerTest.java    |  29 +-
 .../log4j/status/StatusLoggerLevelTest.java        |  85 +++
 .../apache/logging/log4j/spi/AbstractLogger.java   |  33 +-
 .../logging/log4j/status/SimpleLoggerFactory.java  |  54 --
 .../log4j/status/StatusConsoleListener.java        |  73 +--
 .../apache/logging/log4j/status/StatusData.java    | 135 +++--
 .../apache/logging/log4j/status/StatusLogger.java  | 628 ++++++++++++++-------
 .../apache/logging/log4j/status/package-info.java  |   2 +-
 .../org/apache/logging/log4j/util/Base64Util.java  |   6 +-
 .../log4j/util/EnvironmentPropertySource.java      |  10 +-
 .../org/apache/logging/log4j/util/LoaderUtil.java  |  12 +-
 .../apache/logging/log4j/util/LowLevelLogUtil.java |  93 ---
 .../logging/log4j/util/OsgiServiceLocator.java     |   7 +-
 .../apache/logging/log4j/util/PropertiesUtil.java  |  25 +-
 .../log4j/util/PropertyFilePropertySource.java     |   8 +-
 .../apache/logging/log4j/util/StackLocator.java    |  16 +-
 .../core/appender/OutputStreamManagerTest.java     |  33 +-
 .../rolling/action/AbstractActionTest.java         |  13 +-
 .../core/appender/rolling/action/IfAllTest.java    |  14 +-
 .../core/appender/rolling/action/IfAnyTest.java    |   4 +-
 .../rolling/action/IfLastModifiedTest.java         |   4 +-
 .../core/appender/rolling/action/IfNotTest.java    |   4 +-
 .../log4j/core/async/QueueFullAbstractTest.java    |  90 ---
 ...rTest.java => QueueFullAsyncAppender1Test.java} |   2 +-
 ...Test2.java => QueueFullAsyncAppender2Test.java} |   2 +-
 ...gerTest.java => QueueFullAsyncLogger1Test.java} |   2 +-
 ...erTest2.java => QueueFullAsyncLogger2Test.java} |   2 +-
 ...erTest3.java => QueueFullAsyncLogger3Test.java} |   2 +-
 ...t.java => QueueFullAsyncLoggerConfig1Test.java} |   2 +-
 ...2.java => QueueFullAsyncLoggerConfig2Test.java} |   2 +-
 ...llAsyncLoggerConfigLoggingFromToStringTest.java |  53 --
 ...lAsyncLoggerConfigLoggingFromToStringTest2.java |  37 --
 ...ueueFullAsyncLoggerLoggingFromToStringTest.java |  50 --
 ...eueFullAsyncLoggerLoggingFromToStringTest2.java |  44 --
 .../log4j/core/config/LoggersPluginTest.java       |  14 +-
 .../core/config/xml/XmlConfigurationPropsTest.java |  51 +-
 .../core/impl/NestedLoggingFromToStringTest.java   | 154 -----
 .../src/test/resources/log4j-sync-to-list.xml      |  34 --
 ...derTest.xml => QueueFullAsyncAppender1Test.xml} |   0
 ...est.xml => QueueFullAsyncLoggerConfig1Test.xml} |   0
 .../core/async/QueueFullAsyncLoggerConfigTest.xml  |  27 -
 .../core/config/status/StatusConfiguration.java    | 163 +++---
 .../log4j/core/config/status/package-info.java     |   2 +-
 .../logging/log4j/core/jmx/StatusLoggerAdmin.java  |  88 ++-
 .../logging/log4j/core/jmx/package-info.java       |   2 +-
 51 files changed, 1034 insertions(+), 1219 deletions(-)

diff --git a/log4j-api-test/pom.xml b/log4j-api-test/pom.xml
index c243fad232..0ef2578dd8 100644
--- a/log4j-api-test/pom.xml
+++ b/log4j-api-test/pom.xml
@@ -138,7 +138,6 @@
     <dependency>
       <groupId>org.mockito</groupId>
       <artifactId>mockito-core</artifactId>
-      <scope>test</scope>
     </dependency>
     <dependency>
       <groupId>org.mockito</groupId>
diff --git a/log4j-api-test/src/main/java/org/apache/logging/log4j/test/junit/StatusLoggerExtension.java b/log4j-api-test/src/main/java/org/apache/logging/log4j/test/junit/StatusListenerExtension.java
similarity index 80%
rename from log4j-api-test/src/main/java/org/apache/logging/log4j/test/junit/StatusLoggerExtension.java
rename to log4j-api-test/src/main/java/org/apache/logging/log4j/test/junit/StatusListenerExtension.java
index 7f4c9c0702..7ca5776c58 100644
--- a/log4j-api-test/src/main/java/org/apache/logging/log4j/test/junit/StatusLoggerExtension.java
+++ b/log4j-api-test/src/main/java/org/apache/logging/log4j/test/junit/StatusListenerExtension.java
@@ -16,21 +16,16 @@
  */
 package org.apache.logging.log4j.test.junit;
 
-import java.io.IOException;
-import java.time.Instant;
-import java.time.ZoneId;
-import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.stream.Stream;
 import org.apache.logging.log4j.Level;
-import org.apache.logging.log4j.Logger;
-import org.apache.logging.log4j.message.ParameterizedNoReferenceMessageFactory;
-import org.apache.logging.log4j.simple.SimpleLogger;
+import org.apache.logging.log4j.message.ParameterizedMessage;
+import org.apache.logging.log4j.status.StatusConsoleListener;
 import org.apache.logging.log4j.status.StatusData;
+import org.apache.logging.log4j.status.StatusListener;
 import org.apache.logging.log4j.status.StatusLogger;
 import org.apache.logging.log4j.test.ListStatusListener;
-import org.apache.logging.log4j.util.PropertiesUtil;
 import org.junit.jupiter.api.extension.BeforeAllCallback;
 import org.junit.jupiter.api.extension.BeforeEachCallback;
 import org.junit.jupiter.api.extension.ExtensionContext;
@@ -43,12 +38,12 @@ import org.junit.platform.commons.support.HierarchyTraversalMode;
 import org.junit.platform.commons.support.ModifierSupport;
 import org.junit.platform.commons.support.ReflectionSupport;
 
-class StatusLoggerExtension extends TypeBasedParameterResolver<ListStatusListener>
+class StatusListenerExtension extends TypeBasedParameterResolver<ListStatusListener>
         implements BeforeAllCallback, BeforeEachCallback, TestExecutionExceptionHandler {
 
     private static final Object KEY = ListStatusListener.class;
 
-    public StatusLoggerExtension() {
+    public StatusListenerExtension() {
         super(ListStatusListener.class);
     }
 
@@ -130,33 +125,19 @@ class StatusLoggerExtension extends TypeBasedParameterResolver<ListStatusListene
         }
 
         @Override
-        public void close() throws Throwable {
+        public void close() {
             statusLogger.removeListener(statusListener);
         }
 
         public void handleException(final ExtensionContext context, final Throwable throwable) {
-            final Logger logger = new SimpleLogger(
-                    "StatusLoggerExtension",
-                    Level.ALL,
-                    false,
-                    false,
-                    false,
-                    false,
+            final StatusListener listener = new StatusConsoleListener(Level.ALL, System.err);
+            listener.log(new StatusData(
                     null,
-                    ParameterizedNoReferenceMessageFactory.INSTANCE,
-                    PropertiesUtil.getProperties(),
-                    System.err);
-            logger.error("Test {} failed.\nDumping status data:", context.getDisplayName());
-            final DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_TIME.withZone(ZoneId.systemDefault());
-            statusListener.getStatusData().forEach(data -> {
-                logger.atLevel(data.getLevel())
-                        .withThrowable(data.getThrowable())
-                        .withLocation(data.getStackTraceElement())
-                        .log(
-                                "{} {}",
-                                formatter.format(Instant.ofEpochMilli(data.getTimestamp())),
-                                data.getMessage().getFormattedMessage());
-            });
+                    Level.ERROR,
+                    new ParameterizedMessage("Test `{}` has failed, dumping status data...", context.getDisplayName()),
+                    throwable,
+                    null));
+            statusListener.getStatusData().forEach(listener::log);
         }
     }
 
@@ -186,14 +167,14 @@ class StatusLoggerExtension extends TypeBasedParameterResolver<ListStatusListene
         }
 
         @Override
-        public void close() throws IOException {
+        public void close() {
             // NOP
         }
 
         @Override
         public Stream<StatusData> getStatusData() {
             synchronized (statusData) {
-                final List<StatusData> clone = (List<StatusData>) statusData.clone();
+                final List<StatusData> clone = new ArrayList<>(statusData);
                 return parent != null ? Stream.concat(parent.getStatusData(), clone.stream()) : clone.stream();
             }
         }
diff --git a/log4j-api-test/src/main/java/org/apache/logging/log4j/test/junit/StatusLoggerMockExtension.java b/log4j-api-test/src/main/java/org/apache/logging/log4j/test/junit/StatusLoggerMockExtension.java
new file mode 100644
index 0000000000..2725dc7314
--- /dev/null
+++ b/log4j-api-test/src/main/java/org/apache/logging/log4j/test/junit/StatusLoggerMockExtension.java
@@ -0,0 +1,75 @@
+/*
+ * 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.test.junit;
+
+import static org.apache.logging.log4j.test.junit.ExtensionContextAnchor.getAttribute;
+import static org.apache.logging.log4j.test.junit.ExtensionContextAnchor.setAttribute;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.when;
+
+import org.apache.logging.log4j.status.StatusConsoleListener;
+import org.apache.logging.log4j.status.StatusLogger;
+import org.junit.jupiter.api.extension.AfterAllCallback;
+import org.junit.jupiter.api.extension.BeforeAllCallback;
+import org.junit.jupiter.api.extension.BeforeEachCallback;
+import org.junit.jupiter.api.extension.ExtensionContext;
+
+/**
+ * Replaces {@link StatusLogger} static instance with a mocked one.
+ * <p>
+ * <b>Warning!</b>
+ * Many classes store the result of {@link StatusLogger#getLogger()} in {@code static} field.
+ * Hence, the mock replacement must be performed before anybody tries to access it.
+ * Similarly, we cannot replace the mock in between tests, since it is already stored in {@code static} fields.
+ * That is why we only reset the mocked instance before each test.
+ * </p>
+ *
+ * @see UsingStatusLoggerMock
+ */
+class StatusLoggerMockExtension implements BeforeAllCallback, BeforeEachCallback, AfterAllCallback {
+
+    private static final String KEY_PREFIX = StatusLoggerMockExtension.class.getSimpleName() + '.';
+
+    private static final String INITIAL_STATUS_LOGGER_KEY = KEY_PREFIX + "initialStatusLogger";
+
+    @Override
+    public void beforeAll(final ExtensionContext context) throws Exception {
+        setAttribute(INITIAL_STATUS_LOGGER_KEY, StatusLogger.getLogger(), context);
+        final StatusLogger statusLogger = mock(StatusLogger.class);
+        stubFallbackListener(statusLogger);
+        StatusLogger.setLogger(statusLogger);
+    }
+
+    @Override
+    public void beforeEach(final ExtensionContext context) throws Exception {
+        final StatusLogger statusLogger = StatusLogger.getLogger();
+        reset(statusLogger); // Stubs get reset too!
+        stubFallbackListener(statusLogger);
+    }
+
+    private static void stubFallbackListener(final StatusLogger statusLogger) {
+        final StatusConsoleListener fallbackListener = mock(StatusConsoleListener.class);
+        when(statusLogger.getFallbackListener()).thenReturn(fallbackListener);
+    }
+
+    @Override
+    public void afterAll(final ExtensionContext context) {
+        final StatusLogger statusLogger = getAttribute(INITIAL_STATUS_LOGGER_KEY, StatusLogger.class, context);
+        StatusLogger.setLogger(statusLogger);
+    }
+}
diff --git a/log4j-api-test/src/main/java/org/apache/logging/log4j/test/junit/UsingStatusListener.java b/log4j-api-test/src/main/java/org/apache/logging/log4j/test/junit/UsingStatusListener.java
index e54deb6b42..2ca8124b03 100644
--- a/log4j-api-test/src/main/java/org/apache/logging/log4j/test/junit/UsingStatusListener.java
+++ b/log4j-api-test/src/main/java/org/apache/logging/log4j/test/junit/UsingStatusListener.java
@@ -37,5 +37,5 @@ import org.junit.jupiter.api.extension.ExtendWith;
 @Documented
 @ExtendWith(ExtensionContextAnchor.class)
 @ExtendWith(TestPropertyResolver.class)
-@ExtendWith(StatusLoggerExtension.class)
+@ExtendWith(StatusListenerExtension.class)
 public @interface UsingStatusListener {}
diff --git a/log4j-api-test/src/main/java/org/apache/logging/log4j/test/junit/UsingStatusListener.java b/log4j-api-test/src/main/java/org/apache/logging/log4j/test/junit/UsingStatusLoggerMock.java
similarity index 74%
copy from log4j-api-test/src/main/java/org/apache/logging/log4j/test/junit/UsingStatusListener.java
copy to log4j-api-test/src/main/java/org/apache/logging/log4j/test/junit/UsingStatusLoggerMock.java
index e54deb6b42..105eef9e2d 100644
--- a/log4j-api-test/src/main/java/org/apache/logging/log4j/test/junit/UsingStatusListener.java
+++ b/log4j-api-test/src/main/java/org/apache/logging/log4j/test/junit/UsingStatusLoggerMock.java
@@ -23,19 +23,13 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
 import java.lang.annotation.Target;
-import org.apache.logging.log4j.status.StatusListener;
-import org.apache.logging.log4j.test.ListStatusListener;
 import org.junit.jupiter.api.extension.ExtendWith;
 
 /**
- * Configures and injects a {@link StatusListener} of type
- * {@link ListStatusListener}, that will collect status messages for the test
- * context.
+ * Shortcut to {@link StatusLoggerMockExtension}.
  */
 @Retention(RUNTIME)
 @Target({TYPE, METHOD})
 @Documented
-@ExtendWith(ExtensionContextAnchor.class)
-@ExtendWith(TestPropertyResolver.class)
-@ExtendWith(StatusLoggerExtension.class)
-public @interface UsingStatusListener {}
+@ExtendWith({ExtensionContextAnchor.class, StatusLoggerMockExtension.class})
+public @interface UsingStatusLoggerMock {}
diff --git a/log4j-api-test/src/test/java/org/apache/logging/log4j/AbstractLoggerTest.java b/log4j-api-test/src/test/java/org/apache/logging/log4j/AbstractLoggerTest.java
index 194fb33013..e2ec325ce7 100644
--- a/log4j-api-test/src/test/java/org/apache/logging/log4j/AbstractLoggerTest.java
+++ b/log4j-api-test/src/test/java/org/apache/logging/log4j/AbstractLoggerTest.java
@@ -42,9 +42,12 @@ import org.apache.logging.log4j.util.Supplier;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.parallel.ResourceAccessMode;
 import org.junit.jupiter.api.parallel.ResourceLock;
+import org.junitpioneer.jupiter.SetSystemProperty;
 
 @StatusLoggerLevel("WARN")
 @ResourceLock(value = Resources.MARKER_MANAGER, mode = ResourceAccessMode.READ)
+@SetSystemProperty(key = "log4j2.status.entries", value = "200")
+@SetSystemProperty(key = "log4j2.StatusLogger.level", value = "WARN")
 public class AbstractLoggerTest {
 
     private static final StringBuilder CHAR_SEQ = new StringBuilder("CharSeq");
diff --git a/log4j-api-test/src/test/java/org/apache/logging/log4j/status/StatusConsoleListenerTest.java b/log4j-api-test/src/test/java/org/apache/logging/log4j/status/StatusConsoleListenerTest.java
index 48d9a7346f..0b93ae1fbc 100644
--- a/log4j-api-test/src/test/java/org/apache/logging/log4j/status/StatusConsoleListenerTest.java
+++ b/log4j-api-test/src/test/java/org/apache/logging/log4j/status/StatusConsoleListenerTest.java
@@ -19,11 +19,9 @@ package org.apache.logging.log4j.status;
 import java.io.ByteArrayOutputStream;
 import java.io.PrintStream;
 import org.apache.logging.log4j.Level;
-import org.apache.logging.log4j.LogBuilder;
 import org.apache.logging.log4j.message.Message;
 import org.apache.logging.log4j.message.MessageFactory;
 import org.apache.logging.log4j.message.ParameterizedNoReferenceMessageFactory;
-import org.apache.logging.log4j.simple.SimpleLogger;
 import org.assertj.core.api.Assertions;
 import org.junit.jupiter.api.Test;
 import org.mockito.Mockito;
@@ -33,38 +31,19 @@ public class StatusConsoleListenerTest {
     public static final MessageFactory MESSAGE_FACTORY = ParameterizedNoReferenceMessageFactory.INSTANCE;
 
     @Test
-    void SimpleLogger_should_be_used() {
-
-        // Create a mock `SimpleLoggerFactory`.
-        final SimpleLogger logger = Mockito.mock(SimpleLogger.class);
-        final LogBuilder logBuilder = Mockito.mock(LogBuilder.class);
-        Mockito.when(logger.atLevel(Mockito.any())).thenReturn(logBuilder);
-        Mockito.when(logBuilder.withThrowable(Mockito.any())).thenReturn(logBuilder);
-        Mockito.when(logBuilder.withLocation(Mockito.any())).thenReturn(logBuilder);
-        final SimpleLoggerFactory loggerFactory = Mockito.mock(SimpleLoggerFactory.class);
-        Mockito.when(loggerFactory.createSimpleLogger(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()))
-                .thenReturn(logger);
+    void StatusData_getFormattedStatus_should_be_used() {
 
         // Create the listener.
         final PrintStream stream = Mockito.mock(PrintStream.class);
-        final Level level = Mockito.mock(Level.class);
-        final StatusConsoleListener listener = new StatusConsoleListener(level, stream, loggerFactory);
+        final StatusConsoleListener listener = new StatusConsoleListener(Level.ALL, stream);
 
         // Log a message.
-        final StackTraceElement caller = Mockito.mock(StackTraceElement.class);
         final Message message = Mockito.mock(Message.class);
-        final Throwable throwable = Mockito.mock(Throwable.class);
-        final StatusData statusData = new StatusData(caller, level, message, throwable, null);
+        final StatusData statusData = Mockito.spy(new StatusData(null, Level.TRACE, message, null, null));
         listener.log(statusData);
 
         // Verify the call.
-        Mockito.verify(loggerFactory)
-                .createSimpleLogger(
-                        Mockito.eq("StatusConsoleListener"), Mockito.same(level), Mockito.any(), Mockito.same(stream));
-        Mockito.verify(logger).atLevel(Mockito.same(level));
-        Mockito.verify(logBuilder).withThrowable(Mockito.same(throwable));
-        Mockito.verify(logBuilder).withLocation(Mockito.same(caller));
-        Mockito.verify(logBuilder).log(Mockito.same(message));
+        Mockito.verify(statusData).getFormattedStatus();
     }
 
     @Test
diff --git a/log4j-api-test/src/test/java/org/apache/logging/log4j/status/StatusLoggerLevelTest.java b/log4j-api-test/src/test/java/org/apache/logging/log4j/status/StatusLoggerLevelTest.java
new file mode 100644
index 0000000000..5dedd835ed
--- /dev/null
+++ b/log4j-api-test/src/test/java/org/apache/logging/log4j/status/StatusLoggerLevelTest.java
@@ -0,0 +1,85 @@
+/*
+ * 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.status;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import org.apache.logging.log4j.Level;
+import org.junit.jupiter.api.Test;
+
+class StatusLoggerLevelTest {
+
+    @Test
+    void effective_level_should_be_the_least_specific_one() {
+
+        // Verify the initial level
+        final StatusLogger logger = StatusLogger.getLogger();
+        final Level fallbackListenerLevel = Level.ERROR;
+        assertThat(logger.getLevel()).isEqualTo(fallbackListenerLevel);
+
+        // Register a less specific listener
+        final StatusListener listener1 = mock(StatusListener.class);
+        Level listener1Level = Level.WARN;
+        when(listener1.getStatusLevel()).thenReturn(listener1Level);
+        logger.registerListener(listener1);
+        assertThat(listener1Level).isNotEqualTo(fallbackListenerLevel); // Verify that the level is distinct
+        assertThat(logger.getLevel()).isEqualTo(listener1Level); // Verify that the logger level is changed
+
+        // Register a less specific listener
+        final StatusListener listener2 = mock(StatusListener.class);
+        final Level listener2Level = Level.INFO;
+        when(listener2.getStatusLevel()).thenReturn(listener2Level);
+        logger.registerListener(listener2);
+        assertThat(listener2Level)
+                .isNotEqualTo(fallbackListenerLevel)
+                .isNotEqualTo(listener1Level); // Verify that the level is distinct
+        assertThat(logger.getLevel()).isEqualTo(listener2Level); // Verify that the logger level is changed
+
+        // Register a more specific listener
+        final StatusListener listener3 = mock(StatusListener.class);
+        final Level listener3Level = Level.ERROR;
+        when(listener3.getStatusLevel()).thenReturn(listener3Level);
+        logger.registerListener(listener3);
+        assertThat(listener3Level)
+                .isNotEqualTo(listener1Level)
+                .isNotEqualTo(listener2Level); // Verify that the level is distinct
+        assertThat(logger.getLevel()).isEqualTo(listener2Level); // Verify that the logger level is not changed
+
+        // Update a registered listener level
+        listener1Level = Level.DEBUG;
+        when(listener1.getStatusLevel()).thenReturn(listener1Level);
+        assertThat(listener1Level) // Verify that the level is distinct
+                .isNotEqualTo(fallbackListenerLevel)
+                .isNotEqualTo(listener2Level)
+                .isNotEqualTo(listener3Level);
+        assertThat(logger.getLevel()).isEqualTo(listener1Level); // Verify that the logger level is changed
+
+        // Remove the least specific listener
+        logger.removeListener(listener2);
+        assertThat(logger.getLevel()).isEqualTo(listener1Level); // Verify that the level is changed
+
+        // Remove the most specific listener
+        logger.removeListener(listener3);
+        assertThat(logger.getLevel()).isEqualTo(listener1Level); // Verify that the level is not changed
+
+        // Remove the last listener
+        logger.removeListener(listener1);
+        assertThat(logger.getLevel()).isEqualTo(fallbackListenerLevel); // Verify that the level is changed
+    }
+}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/AbstractLogger.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/AbstractLogger.java
index 8620a5b171..d9543f17b5 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/AbstractLogger.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/AbstractLogger.java
@@ -38,7 +38,6 @@ import org.apache.logging.log4j.util.LambdaUtil;
 import org.apache.logging.log4j.util.LoaderUtil;
 import org.apache.logging.log4j.util.MessageSupplier;
 import org.apache.logging.log4j.util.PerformanceSensitive;
-import org.apache.logging.log4j.util.PropertiesUtil;
 import org.apache.logging.log4j.util.StackLocatorUtil;
 import org.apache.logging.log4j.util.Supplier;
 
@@ -86,14 +85,14 @@ public abstract class AbstractLogger implements ExtendedLogger, LocationAwareLog
     /**
      * The default MessageFactory class.
      */
-    public static final Class<? extends MessageFactory> DEFAULT_MESSAGE_FACTORY_CLASS = createClassForProperty(
-            "log4j2.messageFactory", ReusableMessageFactory.class, ParameterizedMessageFactory.class);
+    public static final Class<? extends MessageFactory> DEFAULT_MESSAGE_FACTORY_CLASS =
+            ParameterizedMessageFactory.class;
 
     /**
      * The default FlowMessageFactory class.
      */
     public static final Class<? extends FlowMessageFactory> DEFAULT_FLOW_MESSAGE_FACTORY_CLASS =
-            createFlowClassForProperty("log4j2.flowMessageFactory", DefaultFlowMessageFactory.class);
+            DefaultFlowMessageFactory.class;
 
     private static final long serialVersionUID = 2L;
 
@@ -198,32 +197,6 @@ public abstract class AbstractLogger implements ExtendedLogger, LocationAwareLog
         return messageFactory.newMessage(CATCHING);
     }
 
-    private static Class<? extends MessageFactory> createClassForProperty(
-            final String property,
-            final Class<ReusableMessageFactory> reusableParameterizedMessageFactoryClass,
-            final Class<ParameterizedMessageFactory> parameterizedMessageFactoryClass) {
-        try {
-            final String fallback = Constants.ENABLE_THREADLOCALS
-                    ? reusableParameterizedMessageFactoryClass.getName()
-                    : parameterizedMessageFactoryClass.getName();
-            final String clsName = PropertiesUtil.getProperties().getStringProperty(property, fallback);
-            return LoaderUtil.loadClass(clsName).asSubclass(MessageFactory.class);
-        } catch (final Throwable throwable) {
-            return parameterizedMessageFactoryClass;
-        }
-    }
-
-    private static Class<? extends FlowMessageFactory> createFlowClassForProperty(
-            final String property, final Class<DefaultFlowMessageFactory> defaultFlowMessageFactoryClass) {
-        try {
-            final String clsName = PropertiesUtil.getProperties()
-                    .getStringProperty(property, defaultFlowMessageFactoryClass.getName());
-            return LoaderUtil.loadClass(clsName).asSubclass(FlowMessageFactory.class);
-        } catch (final Throwable throwable) {
-            return defaultFlowMessageFactoryClass;
-        }
-    }
-
     private static MessageFactory2 createDefaultMessageFactory() {
         try {
             final MessageFactory result = LoaderUtil.newInstanceOf(DEFAULT_MESSAGE_FACTORY_CLASS);
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/status/SimpleLoggerFactory.java b/log4j-api/src/main/java/org/apache/logging/log4j/status/SimpleLoggerFactory.java
deleted file mode 100644
index 85e714b8f4..0000000000
--- a/log4j-api/src/main/java/org/apache/logging/log4j/status/SimpleLoggerFactory.java
+++ /dev/null
@@ -1,54 +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.status;
-
-import java.io.PrintStream;
-import org.apache.logging.log4j.Level;
-import org.apache.logging.log4j.message.MessageFactory;
-import org.apache.logging.log4j.simple.SimpleLogger;
-import org.apache.logging.log4j.util.Strings;
-
-/**
- * {@link org.apache.logging.log4j.simple.SimpleLogger} factory to be used by {@link StatusLogger} and {@link StatusConsoleListener}.
- */
-final class SimpleLoggerFactory {
-
-    private static final SimpleLoggerFactory INSTANCE = new SimpleLoggerFactory();
-
-    private SimpleLoggerFactory() {}
-
-    static SimpleLoggerFactory getInstance() {
-        return INSTANCE;
-    }
-
-    SimpleLogger createSimpleLogger(
-            final String name, final Level level, final MessageFactory messageFactory, final PrintStream stream) {
-        final String dateFormat = StatusLogger.PROPS.getStringProperty(StatusLogger.STATUS_DATE_FORMAT);
-        final boolean dateFormatProvided = Strings.isNotBlank(dateFormat);
-        return new SimpleLogger(
-                name,
-                level,
-                false,
-                true,
-                dateFormatProvided,
-                false,
-                dateFormat,
-                messageFactory,
-                StatusLogger.PROPS,
-                stream);
-    }
-}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/status/StatusConsoleListener.java b/log4j-api/src/main/java/org/apache/logging/log4j/status/StatusConsoleListener.java
index 5961b20cbf..3e30bf1994 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/status/StatusConsoleListener.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/status/StatusConsoleListener.java
@@ -16,24 +16,22 @@
  */
 package org.apache.logging.log4j.status;
 
+import static java.util.Objects.requireNonNull;
+
 import java.io.IOException;
+import java.io.OutputStream;
 import java.io.PrintStream;
-import java.util.Objects;
 import org.apache.logging.log4j.Level;
-import org.apache.logging.log4j.Logger;
-import org.apache.logging.log4j.message.ParameterizedNoReferenceMessageFactory;
 
 /**
- * {@link StatusListener} that writes to the console.
+ * A {@link StatusListener} that writes to the console.
  */
 @SuppressWarnings("UseOfSystemOutOrSystemErr")
 public class StatusConsoleListener implements StatusListener {
 
-    private Level level;
-
-    private final PrintStream stream;
+    private volatile Level level;
 
-    private final Logger logger;
+    private volatile PrintStream stream;
 
     /**
      * Constructs a {@link StatusConsoleListener} instance writing to {@link System#out} using the supplied level.
@@ -56,50 +54,59 @@ public class StatusConsoleListener implements StatusListener {
      * @throws NullPointerException on null {@code level} or {@code stream}
      */
     public StatusConsoleListener(final Level level, final PrintStream stream) {
-        this(level, stream, SimpleLoggerFactory.getInstance());
-    }
-
-    StatusConsoleListener(final Level level, final PrintStream stream, final SimpleLoggerFactory loggerFactory) {
-        this.level = Objects.requireNonNull(level, "level");
-        this.stream = Objects.requireNonNull(stream, "stream");
-        this.logger = Objects.requireNonNull(loggerFactory, "loggerFactory")
-                .createSimpleLogger(
-                        "StatusConsoleListener", level, ParameterizedNoReferenceMessageFactory.INSTANCE, stream);
+        this.level = requireNonNull(level, "level");
+        this.stream = requireNonNull(stream, "stream");
     }
 
     /**
      * Sets the level to a new value.
-     * @param level The new Level.
+     *
+     * @param level the new level
+     * @throws NullPointerException on null {@code level}
      */
     public void setLevel(final Level level) {
-        this.level = level;
+        this.level = requireNonNull(level, "level");
     }
 
     /**
-     * Return the Log Level for which the Listener should receive events.
-     * @return the Log Level.
+     * Sets the output stream to a new value.
+     *
+     * @param stream the new output stream
+     * @throws NullPointerException on null {@code stream}
+     * @since 2.23.0
+     */
+    public void setStream(final PrintStream stream) {
+        this.stream = requireNonNull(stream, "stream");
+    }
+
+    /**
+     * Returns the level for which the listener should receive events.
+     *
+     * @return the log level
      */
     @Override
     public Level getStatusLevel() {
-        return this.level;
+        return level;
     }
 
     /**
      * Writes status messages to the console.
-     * @param data The StatusData.
+     *
+     * @param data a status data
+     * @throws NullPointerException on null {@code data}
      */
     @Override
     public void log(final StatusData data) {
-        logger
-                // Logging using _only_ the following 4 fields set by `StatusLogger#logMessage()`:
-                .atLevel(data.getLevel())
-                .withThrowable(data.getThrowable())
-                .withLocation(data.getStackTraceElement())
-                .log(data.getMessage());
+        requireNonNull(data, "data");
+        if (level.isLessSpecificThan(data.getLevel())) {
+            final String formattedStatus = data.getFormattedStatus();
+            stream.println(formattedStatus);
+        }
     }
 
     /**
      * Adds package name filters to exclude.
+     *
      * @param filters An array of package names to exclude.
      * @deprecated This method is ineffective and only kept for binary backward compatibility.
      */
@@ -108,9 +115,11 @@ public class StatusConsoleListener implements StatusListener {
 
     @Override
     public void close() throws IOException {
-        // only want to close non-system streams
-        if (this.stream != System.out && this.stream != System.err) {
-            this.stream.close();
+        // Get local copy of the `volatile` member
+        final OutputStream localStream = stream;
+        // Close only non-system streams
+        if (localStream != System.out && localStream != System.err) {
+            localStream.close();
         }
     }
 }
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/status/StatusData.java b/log4j-api/src/main/java/org/apache/logging/log4j/status/StatusData.java
index 10883be849..e3b13e8ded 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/status/StatusData.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/status/StatusData.java
@@ -16,14 +16,16 @@
  */
 package org.apache.logging.log4j.status;
 
+import static java.util.Objects.requireNonNull;
 import static org.apache.logging.log4j.util.Chars.SPACE;
 
+import edu.umd.cs.findbugs.annotations.Nullable;
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 import java.io.ByteArrayOutputStream;
 import java.io.PrintStream;
 import java.io.Serializable;
-import java.text.SimpleDateFormat;
-import java.util.Date;
+import java.time.Instant;
+import java.time.format.DateTimeFormatter;
 import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.message.Message;
 
@@ -34,50 +36,84 @@ public class StatusData implements Serializable {
 
     private static final long serialVersionUID = -4341916115118014017L;
 
-    private final long timestamp;
+    private final Instant instant;
+
+    @Nullable
+    private final DateTimeFormatter instantFormatter;
+
+    @Nullable
     private final StackTraceElement caller;
+
     private final Level level;
-    private final Message msg;
-    private String threadName;
+
+    private final Message message;
+
+    private final String threadName;
+
+    @Nullable
     private final Throwable throwable;
 
     /**
-     * Creates the StatusData object.
+     * Constructs the instance using given properties.
      *
-     * @param caller The method that created the event.
-     * @param level The logging level.
-     * @param msg The message String.
-     * @param t The Error or Exception that occurred.
-     * @param threadName The thread name
+     * @param caller the method that created the event
+     * @param level a logging level
+     * @param message a message
+     * @param throwable the error occurred
+     * @param threadName the thread name
      */
     public StatusData(
-            final StackTraceElement caller,
+            @Nullable final StackTraceElement caller,
             final Level level,
-            final Message msg,
-            final Throwable t,
-            final String threadName) {
-        this.timestamp = System.currentTimeMillis();
+            final Message message,
+            @Nullable final Throwable throwable,
+            @Nullable final String threadName) {
+        this(caller, level, message, throwable, threadName, null);
+    }
+
+    StatusData(
+            @Nullable final StackTraceElement caller,
+            final Level level,
+            final Message message,
+            @Nullable final Throwable throwable,
+            @Nullable final String threadName,
+            @Nullable final DateTimeFormatter instantFormatter) {
+        this.instantFormatter = instantFormatter;
+        this.instant = Instant.now();
         this.caller = caller;
-        this.level = level;
-        this.msg = msg;
-        this.throwable = t;
-        this.threadName = threadName;
+        this.level = requireNonNull(level, "level");
+        this.message = requireNonNull(message, "message");
+        this.throwable = throwable;
+        this.threadName =
+                threadName != null ? threadName : Thread.currentThread().getName();
+    }
+
+    /**
+     * Returns the instant of the event.
+     *
+     * @return the event's instant
+     */
+    public Instant getInstant() {
+        return instant;
     }
 
     /**
-     * Returns the event's timestamp.
+     * Returns the instant of the event.
      *
-     * @return The event's timestamp.
+     * @return the event's instant
+     * @deprecated Use {@link #getInstant()} instead.
      */
+    @Deprecated
     public long getTimestamp() {
-        return timestamp;
+        return instant.toEpochMilli();
     }
 
     /**
-     * Returns the StackTraceElement for the method that created the event.
+     * Returns the method that created the event.
      *
-     * @return The StackTraceElement.
+     * @return the method that created the event
      */
+    @Nullable
     public StackTraceElement getStackTraceElement() {
         return caller;
     }
@@ -85,7 +121,7 @@ public class StatusData implements Serializable {
     /**
      * Returns the logging level for the event.
      *
-     * @return The logging level.
+     * @return the event's logging level
      */
     public Level getLevel() {
         return level;
@@ -94,32 +130,35 @@ public class StatusData implements Serializable {
     /**
      * Returns the message associated with the event.
      *
-     * @return The message associated with the event.
+     * @return the message associated with the event
      */
     public Message getMessage() {
-        return msg;
+        return message;
     }
 
+    /**
+     * Returns the name of the thread associated with the event.
+     *
+     * @return the name of the thread associated with the event
+     */
     public String getThreadName() {
-        if (threadName == null) {
-            threadName = Thread.currentThread().getName();
-        }
         return threadName;
     }
 
     /**
-     * Returns the Throwable associated with the event.
+     * Returns the error associated with the event.
      *
-     * @return The Throwable associated with the event.
+     * @return the error associated with the event
      */
+    @Nullable
     public Throwable getThrowable() {
         return throwable;
     }
 
     /**
-     * Formats the StatusData for viewing.
+     * Formats the event in to a log line for viewing.
      *
-     * @return The formatted status data as a String.
+     * @return the formatted event
      */
     @SuppressFBWarnings(
             value = "INFORMATION_EXPOSURE_THROUGH_AN_ERROR_MESSAGE",
@@ -127,38 +166,36 @@ public class StatusData implements Serializable {
     @SuppressWarnings("DefaultCharset")
     public String getFormattedStatus() {
         final StringBuilder sb = new StringBuilder();
-        final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss,SSS");
-        sb.append(format.format(new Date(timestamp)));
+        final String formattedInstant =
+                instantFormatter != null ? instantFormatter.format(instant) : instant.toString();
+        sb.append(formattedInstant);
         sb.append(SPACE);
         sb.append(getThreadName());
         sb.append(SPACE);
         sb.append(level.toString());
         sb.append(SPACE);
-        sb.append(msg.getFormattedMessage());
-        final Object[] params = msg.getParameters();
-        Throwable t;
-        if (throwable == null && params != null && params[params.length - 1] instanceof Throwable) {
-            t = (Throwable) params[params.length - 1];
+        sb.append(message.getFormattedMessage());
+        final Object[] parameters = message.getParameters();
+        Throwable effectiveThrowable;
+        if (throwable == null && parameters != null && parameters[parameters.length - 1] instanceof Throwable) {
+            effectiveThrowable = (Throwable) parameters[parameters.length - 1];
         } else {
-            t = throwable;
+            effectiveThrowable = throwable;
         }
-        if (t != null) {
+        if (effectiveThrowable != null) {
             sb.append(SPACE);
             final ByteArrayOutputStream baos = new ByteArrayOutputStream();
-            t.printStackTrace(new PrintStream(baos));
+            effectiveThrowable.printStackTrace(new PrintStream(baos));
             /*
              * https://errorprone.info/bugpattern/DefaultCharset
              *
              * Since Java 9 we'll be able to provide a charset.
              */
-            sb.append(baos.toString());
+            sb.append(baos);
         }
         return sb.toString();
     }
 
-    /**
-     * Used in tests
-     */
     @Override
     public String toString() {
         return getMessage().getFormattedMessage();
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/status/StatusLogger.java b/log4j-api/src/main/java/org/apache/logging/log4j/status/StatusLogger.java
index e224eb1045..af6d668790 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/status/StatusLogger.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/status/StatusLogger.java
@@ -16,295 +16,558 @@
  */
 package org.apache.logging.log4j.status;
 
-import java.io.Closeable;
+import static java.util.Objects.requireNonNull;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
 import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
-import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
 import java.util.List;
+import java.util.Properties;
 import java.util.Queue;
 import java.util.concurrent.ConcurrentLinkedQueue;
-import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReadWriteLock;
-import java.util.concurrent.locks.ReentrantLock;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
 import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.Marker;
 import org.apache.logging.log4j.message.Message;
 import org.apache.logging.log4j.message.MessageFactory;
 import org.apache.logging.log4j.message.ParameterizedNoReferenceMessageFactory;
-import org.apache.logging.log4j.simple.SimpleLogger;
-import org.apache.logging.log4j.simple.SimpleLoggerContext;
 import org.apache.logging.log4j.spi.AbstractLogger;
 import org.apache.logging.log4j.util.Constants;
-import org.apache.logging.log4j.util.PropertiesUtil;
 
 /**
- * Records events that occur in the logging system. By default, only error messages are logged to {@link System#err}.
- * Normally, the Log4j StatusLogger is configured via the root {@code <Configuration status="LEVEL"/>} node in a Log4j
- * configuration file. However, this can be overridden via a system property named
- * {@value #DEFAULT_STATUS_LISTENER_LEVEL} and will work with any Log4j provider.
- *
- * @see SimpleLogger
- * @see SimpleLoggerContext
+ * Records events that occur in the logging system.
+ * {@link StatusLogger} is expected to be a standalone, self-sufficient component that the logging system can rely on for low-level logging purposes.
+ * <h2>Listeners</h3>
+ * <p>
+ * Each recorded event will first get buffered and then used to notify the registered {@link StatusListener}s.
+ * If none are available, the <em>fallback listener</em> of type {@link StatusConsoleListener} will be used.
+ * </p>
+ * <p>
+ * You can programmatically register listeners using {@link #registerListener(StatusListener)} method.
+ * </p>
+ * <h3>Configuration</h3>
+ * <p>
+ * The {@code StatusLogger} can be configured in following ways:
+ * </p>
+ * <ol>
+ * <li>Passing system properties to the Java process (e.g., {@code -Dlog4j2.StatusLogger.level=INFO})</li>
+ * <li>Providing properties in a {@value StatusLogger#PROPERTIES_FILE_NAME} file in the classpath</li>
+ * <li>Using Log4j configuration (i.e., {@code <Configuration status="WARN" dest="out">} in a {@code log4j2.xml} in the classpath)</li>
+ * </ol>
+ * <p>
+ * It is crucial to understand that there is a time between the first {@code StatusLogger} access and a configuration file (e.g., {@code log4j2.xml}) read.
+ * Consider the following example:
+ * </p>
+ * <ol>
+ * <li>The default level (of fallback listener) is {@code ERROR}</li>
+ * <li>You have {@code <Configuration status="WARN">} in your {@code log4j2.xml}</li>
+ * <li>Until your {@code log4j2.xml} configuration is read, the effective level will be {@code ERROR}</li>
+ * <li>Once your {@code log4j2.xml} configuration is read, the effective level will be {@code WARN} as you configured</li>
+ * </ol>
+ * <p>
+ * Hence, unless you use either system properties or {@value StatusLogger#PROPERTIES_FILE_NAME} file in the classpath, there is a time window that only the defaults will be effective.
+ * </p>
+ * <p>
+ * {@code StatusLogger} is designed as a singleton class accessed statically.
+ * If you are running an application containing multiple Log4j configurations (e.g., in a servlet environment with multiple containers) and you happen to have differing {@code StatusLogger} configurations (e.g, one {@code log4j2.xml} containing {@code <Configuration status="ERROR">} while the other {@code <Configuration status="INFO">}), the last loaded configuration will be effective one.
+ * </p>
+ * <h2>Configuration properties</h3>
+ * <p>
+ * The list of available properties for configuring the {@code StatusLogger} is shared below.
+ * </p>
+ * <table>
+ * <caption>available properties for configuring the <code>StatusLogger</code></caption>
+ * <tr>
+ *     <th>Name</th>
+ *     <th>Default</th>
+ *     <th>Description</th>
+ * </tr>
+ * <tr>
+ *     <td><code>{@value MAX_STATUS_ENTRIES}</code></td>
+ *     <td>0</td>
+ *     <td>
+ *         The maximum number of events buffered.
+ *         Once the limit is reached, older entries will be removed as new entries are added.
+ *     </td>
+ * </tr>
+ * <tr>
+ *     <td><code>{@value DEFAULT_STATUS_LISTENER_LEVEL}</code></td>
+ *     <td>{@code ERROR}</td>
+ *     <td>
+ *         The {@link Level} name to use as the fallback listener level.<br/>
+ *         The fallback listener is used when the listener registry is empty.
+ *         The fallback listener will accept entries filtered by the level provided in this configuration.
+ *     </td>
+ * </tr>
+ * <tr>
+ *     <td><code>{@value STATUS_DATE_FORMAT}</code></td>
+ *     <td>{@code null}</td>
+ *     <td>A {@link java.time.format.DateTimeFormatter} pattern to format the created {@link StatusData}.</td>
+ * </tr>
+ * <tr>
+ *     <td><code>{@value #DEBUG_PROPERTY_NAME}</code></td>
+ *     <td>false</td>
+ *     <td>The debug mode toggle.</td>
+ * </tr>
+ * </table>
+ * <h2>Debug mode</h3>
+ * <p>
+ * When the {@value Constants#LOG4J2_DEBUG} system property is present, any level-related filtering will be skipped and all events will be notified to listeners.
+ * If no listeners are available, the <em>fallback listener</em> of type {@link StatusConsoleListener} will be used.
+ * </p>
  */
-public final class StatusLogger extends AbstractLogger {
+public class StatusLogger extends AbstractLogger {
+
+    private static final long serialVersionUID = 2L;
+
+    /**
+     * The name of the system property that enables debug mode in its presence.
+     * <p>
+     * This is a local clone of {@link Constants#LOG4J2_DEBUG}.
+     * The cloning is necessary to avoid cyclic initialization.
+     * </p>
+     */
+    private static final String DEBUG_PROPERTY_NAME = "log4j2.debug";
 
     /**
-     * System property that can be configured with the number of entries in the queue. Once the limit is reached older
-     * entries will be removed as new entries are added.
+     * The name of the system property that can be configured with the maximum number of events buffered.
+     * <p>
+     * Once the limit is reached, older entries will be removed as new entries are added.
+     * </p>
      */
     public static final String MAX_STATUS_ENTRIES = "log4j2.status.entries";
 
     /**
-     * System property that can be configured with the {@link Level} name to use as the default level for
-     * {@link StatusListener}s.
+     * The name of the system property that can be configured with the {@link Level} name to use as the fallback listener level.
+     * <p>
+     * The fallback listener is used when the listener registry is empty.
+     * The fallback listener will accept entries filtered by the level provided in this configuration.
+     * </p>
+     *
+     * @since 2.8
      */
     public static final String DEFAULT_STATUS_LISTENER_LEVEL = "log4j2.StatusLogger.level";
 
     /**
-     * System property that can be configured with a date-time format string to use as the format for timestamps
-     * in the status logger output. See {@link java.text.SimpleDateFormat} for supported formats.
+     * The name of the system property that can be configured with a {@link java.time.format.DateTimeFormatter} pattern that will be used while formatting the created {@link StatusData}.
+     *
      * @since 2.11.0
      */
     public static final String STATUS_DATE_FORMAT = "log4j2.StatusLogger.DateFormat";
 
-    private static final long serialVersionUID = 2L;
+    /**
+     * The name of the file to be searched in the classpath to read properties from.
+     *
+     * @since 2.23.0
+     */
+    public static final String PROPERTIES_FILE_NAME = "log4j2.StatusLogger.properties";
+
+    /**
+     * Holder for user-provided {@link StatusLogger} configurations.
+     *
+     * @since 2.23.0
+     */
+    public static final class Config {
+
+        private static final Config INSTANCE = new Config();
+
+        private final boolean debugEnabled;
+
+        private final int bufferCapacity;
+
+        private final Level fallbackListenerLevel;
+
+        @Nullable
+        private final DateTimeFormatter instantFormatter;
+
+        /**
+         * Constructs an instance using the given properties.
+         * <b>Users should not create new instances, but use {@link #getInstance()} instead</b>!
+         *
+         * @param debugEnabled the value of the {@value DEBUG_PROPERTY_NAME} property
+         * @param bufferCapacity the value of the {@value MAX_STATUS_ENTRIES} property
+         * @param fallbackListenerLevel the value of the {@value DEFAULT_STATUS_LISTENER_LEVEL} property
+         * @param instantFormatter the value of the {@value STATUS_DATE_FORMAT} property
+         */
+        public Config(
+                boolean debugEnabled,
+                int bufferCapacity,
+                Level fallbackListenerLevel,
+                @Nullable DateTimeFormatter instantFormatter) {
+            this.debugEnabled = debugEnabled;
+            if (bufferCapacity < 0) {
+                throw new IllegalArgumentException(
+                        "was expecting a positive `bufferCapacity`, found: " + bufferCapacity);
+            }
+            this.bufferCapacity = bufferCapacity;
+            this.fallbackListenerLevel = requireNonNull(fallbackListenerLevel, "fallbackListenerLevel");
+            this.instantFormatter = requireNonNull(instantFormatter, "instantFormatter");
+        }
+
+        /**
+         * Constructs an instance using either system properties or a property file (i.e., {@value Config#PROPERTIES_FILE_NAME}) in the classpath, if available.
+         */
+        private Config() {
+            final Properties fileProvidedProperties = readPropertiesFile();
+            this.debugEnabled = readDebugEnabled(fileProvidedProperties);
+            this.bufferCapacity = readBufferCapacity(fileProvidedProperties);
+            this.fallbackListenerLevel = readFallbackListenerLevel(fileProvidedProperties);
+            this.instantFormatter = readInstantFormatter(fileProvidedProperties);
+        }
+
+        /**
+         * Gets the static instance.
+         *
+         * @return a singleton instance
+         */
+        public static Config getInstance() {
+            return INSTANCE;
+        }
+
+        private static boolean readDebugEnabled(final Properties fileProvidedProperties) {
+            final String debug = readProperty(fileProvidedProperties, DEBUG_PROPERTY_NAME);
+            return debug != null;
+        }
 
-    private static final String NOT_AVAIL = "?";
+        private static int readBufferCapacity(final Properties fileProvidedProperties) {
+            final String capacityString = readProperty(fileProvidedProperties, MAX_STATUS_ENTRIES);
+            return capacityString != null ? Integer.parseInt(capacityString) : 0;
+        }
+
+        private static Level readFallbackListenerLevel(final Properties fileProvidedProperties) {
+            final String level = readProperty(fileProvidedProperties, DEFAULT_STATUS_LISTENER_LEVEL);
+            return level != null ? Level.valueOf(level) : Level.ERROR;
+        }
+
+        private static DateTimeFormatter readInstantFormatter(final Properties fileProvidedProperties) {
+            final String format = readProperty(fileProvidedProperties, STATUS_DATE_FORMAT);
+            return format != null ? DateTimeFormatter.ofPattern(format) : null;
+        }
 
-    static final PropertiesUtil PROPS = new PropertiesUtil("log4j2.StatusLogger.properties");
+        private static String readProperty(final Properties fileProvidedProperties, final String propertyName) {
+            final String systemProvidedValue = System.getProperty(propertyName);
+            return systemProvidedValue != null
+                    ? systemProvidedValue
+                    : (String) fileProvidedProperties.get(propertyName);
+        }
 
-    private static final int MAX_ENTRIES = PROPS.getIntegerProperty(MAX_STATUS_ENTRIES, 200);
+        // We need to roll out our own `.properties` reader.
+        // We could have used `PropertiesUtil`, `PropertyFilePropertySource`, etc.
+        // Consequently, they would delegate to `LoaderUtil`, etc.
+        // All these mechanisms expect a working `StatusLogger`.
+        // Hence, in order to be self-sufficient, we cannot rely on them.
+        private static Properties readPropertiesFile() {
+            final Properties properties = new Properties();
+            final URL url = StatusLogger.class.getResource(PROPERTIES_FILE_NAME);
+            if (url == null) {
+                return properties;
+            }
+            try (final InputStream stream = url.openStream()) {
+                properties.load(stream);
+            } catch (final IOException error) {
+                // There is no logging system at this stage.
+                // There is nothing we can do but simply dumping the failure.
+                error.printStackTrace(System.err);
+            }
+            return properties;
+        }
+    }
 
-    private static final String DEFAULT_STATUS_LEVEL = PROPS.getStringProperty(DEFAULT_STATUS_LISTENER_LEVEL);
+    /**
+     * Wrapper for the default instance for lazy initialization.
+     * <p>
+     * The initialization will be performed when the JVM initializes the class.
+     * Since {@code InstanceHolder} has no other fields or methods, class initialization occurs when the {@code INSTANCE} field is first referenced.
+     * </p>
+     *
+     * @see <a href="https://www.infoworld.com/article/2074979/double-checked-locking--clever--but-broken.html?page=2">Double-checked locking: Clever, but broken</a>
+     */
+    private static final class InstanceHolder {
 
-    static final boolean DEBUG_ENABLED =
-            PropertiesUtil.getProperties().getBooleanProperty(Constants.LOG4J2_DEBUG, false, true);
+        private static volatile StatusLogger INSTANCE = new StatusLogger();
+    }
 
-    // LOG4J2-1176: normal parameterized message remembers param object, causing memory leaks.
-    private static final StatusLogger STATUS_LOGGER = new StatusLogger(
-            StatusLogger.class.getName(),
-            ParameterizedNoReferenceMessageFactory.INSTANCE,
-            SimpleLoggerFactory.getInstance());
+    private final Config config;
 
-    private final SimpleLogger logger;
+    private final StatusConsoleListener fallbackListener;
 
-    private final Collection<StatusListener> listeners = new CopyOnWriteArrayList<>();
+    private final List<StatusListener> listeners;
 
-    @SuppressWarnings("NonSerializableFieldInSerializableClass")
-    // ReentrantReadWriteLock is Serializable
-    private final ReadWriteLock listenersLock = new ReentrantReadWriteLock();
+    private final transient ReadWriteLock listenerLock = new ReentrantReadWriteLock();
 
-    private final Queue<StatusData> messages = new BoundedQueue<>(MAX_ENTRIES);
+    private final transient Lock listenerReadLock = listenerLock.readLock();
 
-    @SuppressWarnings("NonSerializableFieldInSerializableClass")
-    // ReentrantLock is Serializable
-    private final Lock msgLock = new ReentrantLock();
+    private final transient Lock listenerWriteLock = listenerLock.writeLock();
 
-    private int listenersLevel;
+    private final Queue<StatusData> buffer = new ConcurrentLinkedQueue<>();
 
     /**
-     * Constructs the singleton instance for the STATUS_LOGGER constant.
-     * <p>
-     * This is now the logger level is set:
-     * </p>
-     * <ol>
-     * <li>If the property {@value Constants#LOG4J2_DEBUG} is {@code "true"}, then use {@link Level#TRACE}, otherwise,</li>
-     * <li>Use {@link Level#ERROR}</li>
-     * </ol>
-     * <p>
-     * This is now the listener level is set:
-     * </p>
-     * <ol>
-     * <li>If the property {@value #DEFAULT_STATUS_LISTENER_LEVEL} is set, then use <em>it</em>, otherwise,</li>
-     * <li>Use {@link Level#WARN}</li>
-     * </ol>
-     * <p>
-     * See:
-     * <ol>
-     * <li>LOG4J2-1813 Provide shorter and more intuitive way to switch on Log4j internal debug logging. If system property
-     * "log4j2.debug" is defined, print all status logging.</li>
-     * <li>LOG4J2-3340 StatusLogger's log Level cannot be changed as advertised.</li>
-     * </ol>
-     * </p>
+     * Constructs the default instance.
+     */
+    private StatusLogger() {
+        this(
+                StatusLogger.class.getSimpleName(),
+                ParameterizedNoReferenceMessageFactory.INSTANCE,
+                Config.getInstance(),
+                new StatusConsoleListener(Config.getInstance().fallbackListenerLevel));
+    }
+
+    /**
+     * Constructs an instance using given properties.
+     * <b>Users should not create new instances, but use {@link #getLogger()} instead!</b>
      *
-     * @param name The logger name.
-     * @param messageFactory The message factory.
+     * @param name the logger name
+     * @param messageFactory the message factory
+     * @param config the configuration
+     * @param fallbackListener the fallback listener
+     * @throws NullPointerException on null {@code name}, {@code messageFactory}, {@code config}, or {@code fallbackListener}
+     * @since 2.23.0
      */
-    private StatusLogger(
-            final String name, final MessageFactory messageFactory, final SimpleLoggerFactory loggerFactory) {
-        super(name, messageFactory);
-        final Level loggerLevel = DEBUG_ENABLED ? Level.TRACE : Level.ERROR;
-        this.logger = loggerFactory.createSimpleLogger("StatusLogger", loggerLevel, messageFactory, System.err);
-        this.listenersLevel = Level.toLevel(DEFAULT_STATUS_LEVEL, Level.WARN).intLevel();
+    public StatusLogger(
+            final String name,
+            final MessageFactory messageFactory,
+            final Config config,
+            final StatusConsoleListener fallbackListener) {
+        super(requireNonNull(name, "name"), requireNonNull(messageFactory, "messageFactory"));
+        this.config = requireNonNull(config, "config");
+        this.fallbackListener = requireNonNull(fallbackListener, "fallbackListener");
+        this.listeners = new ArrayList<>();
     }
 
     /**
-     * Retrieve the StatusLogger.
+     * Gets the static instance.
      *
-     * @return The StatusLogger.
+     * @return the singleton instance
      */
     public static StatusLogger getLogger() {
-        return STATUS_LOGGER;
+        return InstanceHolder.INSTANCE;
+    }
+
+    /**
+     * Sets the static (i.e., singleton) instance returned by {@link #getLogger()}.
+     * This method is intended for testing purposes and can have unforeseen consequences if used in production code.
+     *
+     * @param logger a logger instance
+     * @throws NullPointerException on null {@code logger}
+     * @since 2.23.0
+     */
+    public static void setLogger(final StatusLogger logger) {
+        InstanceHolder.INSTANCE = requireNonNull(logger, "logger");
     }
 
+    /**
+     * Returns the fallback listener.
+     *
+     * @return the fallback listener
+     */
+    public StatusConsoleListener getFallbackListener() {
+        return fallbackListener;
+    }
+
+    /**
+     * Sets the level of the fallback listener.
+     *
+     * @param level a level
+     * @deprecated Instead use the {@link StatusConsoleListener#setLevel(Level) setLevel(Level)} method on the fallback listener returned by {@link #getFallbackListener()}.
+     */
+    @Deprecated
     public void setLevel(final Level level) {
-        logger.setLevel(level);
+        requireNonNull(level, "level");
+        fallbackListener.setLevel(level);
     }
 
     /**
      * Registers a new listener.
      *
-     * @param listener The StatusListener to register.
+     * @param listener a listener to register
      */
     public void registerListener(final StatusListener listener) {
-        listenersLock.writeLock().lock();
+        requireNonNull(listener, "listener");
+        listenerWriteLock.lock();
         try {
             listeners.add(listener);
-            final Level lvl = listener.getStatusLevel();
-            if (listenersLevel < lvl.intLevel()) {
-                listenersLevel = lvl.intLevel();
-            }
         } finally {
-            listenersLock.writeLock().unlock();
+            listenerWriteLock.unlock();
         }
     }
 
     /**
-     * Removes a StatusListener.
+     * Removes the given listener.
      *
-     * @param listener The StatusListener to remove.
+     * @param listener a listener to remove
      */
     public void removeListener(final StatusListener listener) {
-        closeSilently(listener);
-        listenersLock.writeLock().lock();
+        requireNonNull(listener, "listener");
+        listenerWriteLock.lock();
         try {
             listeners.remove(listener);
-            int lowest = Level.toLevel(DEFAULT_STATUS_LEVEL, Level.WARN).intLevel();
-            for (final StatusListener statusListener : listeners) {
-                final int level = statusListener.getStatusLevel().intLevel();
-                if (lowest < level) {
-                    lowest = level;
-                }
-            }
-            listenersLevel = lowest;
+            closeListenerSafely(listener);
         } finally {
-            listenersLock.writeLock().unlock();
+            listenerWriteLock.unlock();
         }
     }
 
-    public void updateListenerLevel(final Level status) {
-        if (status.intLevel() > listenersLevel) {
-            listenersLevel = status.intLevel();
-        }
+    /**
+     * Sets the level of the fallback listener.
+     *
+     * @param level a level
+     * @deprecated Instead use the {@link StatusConsoleListener#setLevel(Level) setLevel(Level)} method on the fallback listener returned by {@link #getFallbackListener()}.
+     */
+    @Deprecated
+    public void updateListenerLevel(final Level level) {
+        requireNonNull(level, "level");
+        fallbackListener.setLevel(level);
     }
 
     /**
-     * Returns a thread safe Iterable for the StatusListener.
+     * Returns the listener collection.
      *
-     * @return An Iterable for the list of StatusListeners.
+     * @return a thread-safe read-only collection of listeners
      */
     public Iterable<StatusListener> getListeners() {
-        return listeners;
+        listenerReadLock.lock();
+        try {
+            return Collections.unmodifiableCollection(listeners);
+        } finally {
+            listenerReadLock.unlock();
+        }
     }
 
     /**
-     * Clears the list of status events and listeners.
+     * Clears the event buffer and removes the <em>registered</em> (not the fallback one!) listeners.
      */
     public void reset() {
-        listenersLock.writeLock().lock();
+        listenerWriteLock.lock();
         try {
-            for (final StatusListener listener : listeners) {
-                closeSilently(listener);
+            final Iterator<StatusListener> listenerIterator = listeners.iterator();
+            while (listenerIterator.hasNext()) {
+                final StatusListener listener = listenerIterator.next();
+                closeListenerSafely(listener);
+                listenerIterator.remove();
             }
         } finally {
-            listeners.clear();
-            listenersLock.writeLock().unlock();
-            // note this should certainly come after the unlock to avoid unnecessary nested locking
-            clear();
+            listenerWriteLock.unlock();
         }
+        buffer.clear();
     }
 
-    private static void closeSilently(final Closeable resource) {
+    private static void closeListenerSafely(final StatusListener listener) {
         try {
-            resource.close();
-        } catch (final IOException ignored) {
-            // ignored
+            listener.close();
+        } catch (final IOException error) {
+            final String message = String.format("failed closing listener: %s", listener);
+            new RuntimeException(message, error).printStackTrace(System.err);
         }
     }
 
     /**
-     * Returns a List of all events as StatusData objects.
+     * Returns buffered events.
      *
-     * @return The list of StatusData objects.
+     * @deprecated Instead of relying on the buffering provided by {@code StatusLogger}, users should register their own listeners to access to logged events.
+     * @return a thread-safe read-only collection of buffered events
      */
+    @Deprecated
     public List<StatusData> getStatusData() {
-        msgLock.lock();
-        try {
-            return new ArrayList<>(messages);
-        } finally {
-            msgLock.unlock();
-        }
+        // Wrapping the buffer clone with an unmodifiable list.
+        // By disallowing modifications, we make it clear to the user that mutations will not get propagated.
+        // `Collections.unmodifiableList(new ArrayList<>(...))` should be replaced with `List.of()` in Java 9+.
+        return Collections.unmodifiableList(new ArrayList<>(buffer));
     }
 
     /**
-     * Clears the list of status events.
+     * Clears the event buffer.
+     *
+     * @deprecated Instead of relying on the buffering provided by {@code StatusLogger}, users should register their own listeners to access to logged events.
      */
+    @Deprecated
     public void clear() {
-        msgLock.lock();
-        try {
-            messages.clear();
-        } finally {
-            msgLock.unlock();
-        }
+        buffer.clear();
     }
 
+    /**
+     * Returns the least specific level among listeners, if registered any; otherwise, the fallback listener level.
+     *
+     * @return the least specific listener level, if registered any; otherwise, the fallback listener level
+     */
     @Override
     public Level getLevel() {
-        return logger.getLevel();
+        Level leastSpecificLevel = fallbackListener.getStatusLevel();
+        // noinspection ForLoopReplaceableByForEach (avoid iterator instantiation)
+        for (int listenerIndex = 0; listenerIndex < listeners.size(); listenerIndex++) {
+            final StatusListener listener = listeners.get(listenerIndex);
+            final Level listenerLevel = listener.getStatusLevel();
+            if (listenerLevel.isLessSpecificThan(leastSpecificLevel)) {
+                leastSpecificLevel = listenerLevel;
+            }
+        }
+        return leastSpecificLevel;
     }
 
-    /**
-     * Adds an event.
-     *
-     * @param marker The Marker
-     * @param fqcn The fully qualified class name of the <b>caller</b>
-     * @param level The logging level
-     * @param msg The message associated with the event.
-     * @param t A Throwable or null.
-     */
     @Override
     public void logMessage(
-            final String fqcn, final Level level, final Marker marker, final Message msg, final Throwable t) {
-        StackTraceElement element = null;
-        if (fqcn != null) {
-            element = getStackTraceElement(fqcn, Thread.currentThread().getStackTrace());
+            final String fqcn,
+            final Level level,
+            final Marker marker,
+            final Message message,
+            final Throwable throwable) {
+        final StatusData statusData = createStatusData(fqcn, level, message, throwable);
+        buffer(statusData);
+        notifyListeners(statusData);
+    }
+
+    private void buffer(final StatusData statusData) {
+        if (config.bufferCapacity == 0) {
+            return;
+        }
+        buffer.add(statusData);
+        while (buffer.size() >= config.bufferCapacity) {
+            buffer.remove();
         }
-        final StatusData data = new StatusData(element, level, msg, t, null);
-        msgLock.lock();
+    }
+
+    private void notifyListeners(final StatusData statusData) {
+        final boolean foundListeners;
+        listenerReadLock.lock();
         try {
-            messages.add(data);
+            foundListeners = !listeners.isEmpty();
+            listeners.forEach(listener -> notifyListener(listener, statusData));
         } finally {
-            msgLock.unlock();
+            listenerReadLock.unlock();
         }
-        // LOG4J2-1813 if system property "log4j2.debug" is defined, all status logging is enabled
-        if (DEBUG_ENABLED || (listeners.size() <= 0)) {
-            logger.logMessage(fqcn, level, marker, msg, t);
-        } else {
-            for (final StatusListener listener : listeners) {
-                if (data.getLevel().isMoreSpecificThan(listener.getStatusLevel())) {
-                    listener.log(data);
-                }
-            }
+        if (!foundListeners) {
+            notifyListener(fallbackListener, statusData);
+        }
+    }
+
+    private void notifyListener(final StatusListener listener, final StatusData statusData) {
+        if (config.debugEnabled || listener.getStatusLevel().isLessSpecificThan(statusData.getLevel())) {
+            listener.log(statusData);
         }
     }
 
-    private StackTraceElement getStackTraceElement(final String fqcn, final StackTraceElement[] stackTrace) {
+    private StatusData createStatusData(
+            @Nullable final String fqcn,
+            final Level level,
+            final Message message,
+            @Nullable final Throwable throwable) {
+        final StackTraceElement caller = getStackTraceElement(fqcn);
+        return new StatusData(caller, level, message, throwable, null, config.instantFormatter);
+    }
+
+    @Nullable
+    private static StackTraceElement getStackTraceElement(@Nullable final String fqcn) {
         if (fqcn == null) {
             return null;
         }
         boolean next = false;
+        final StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
         for (final StackTraceElement element : stackTrace) {
             final String className = element.getClassName();
             if (next && !fqcn.equals(className)) {
@@ -312,7 +575,7 @@ public final class StatusLogger extends AbstractLogger {
             }
             if (fqcn.equals(className)) {
                 next = true;
-            } else if (NOT_AVAIL.equals(className)) {
+            } else if ("?".equals(className)) {
                 break;
             }
         }
@@ -320,7 +583,7 @@ public final class StatusLogger extends AbstractLogger {
     }
 
     @Override
-    public boolean isEnabled(final Level level, final Marker marker, final String message, final Throwable t) {
+    public boolean isEnabled(final Level level, final Marker marker, final String message, final Throwable throwable) {
         return isEnabled(level, marker);
     }
 
@@ -462,53 +725,24 @@ public final class StatusLogger extends AbstractLogger {
     }
 
     @Override
-    public boolean isEnabled(final Level level, final Marker marker, final CharSequence message, final Throwable t) {
+    public boolean isEnabled(
+            final Level level, final Marker marker, final CharSequence message, final Throwable throwable) {
         return isEnabled(level, marker);
     }
 
     @Override
-    public boolean isEnabled(final Level level, final Marker marker, final Object message, final Throwable t) {
+    public boolean isEnabled(final Level level, final Marker marker, final Object message, final Throwable throwable) {
         return isEnabled(level, marker);
     }
 
     @Override
-    public boolean isEnabled(final Level level, final Marker marker, final Message message, final Throwable t) {
+    public boolean isEnabled(final Level level, final Marker marker, final Message message, final Throwable throwable) {
         return isEnabled(level, marker);
     }
 
     @Override
     public boolean isEnabled(final Level level, final Marker marker) {
-        if (DEBUG_ENABLED) {
-            return true;
-        }
-        if (listeners.size() > 0) {
-            return listenersLevel >= level.intLevel();
-        }
-        return logger.isEnabled(level, marker);
-    }
-
-    /**
-     * Queues for status events.
-     *
-     * @param <E> Object type to be stored in the queue.
-     */
-    private class BoundedQueue<E> extends ConcurrentLinkedQueue<E> {
-
-        private static final long serialVersionUID = -3945953719763255337L;
-
-        private final int size;
-
-        BoundedQueue(final int size) {
-            this.size = size;
-        }
-
-        @Override
-        public boolean add(final E object) {
-            super.add(object);
-            while (messages.size() > size) {
-                messages.poll();
-            }
-            return size > 0;
-        }
+        requireNonNull(level, "level");
+        return getLevel().isLessSpecificThan(level);
     }
 }
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/status/package-info.java b/log4j-api/src/main/java/org/apache/logging/log4j/status/package-info.java
index 61af0c2347..407e82cfc0 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/status/package-info.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/status/package-info.java
@@ -19,7 +19,7 @@
  * used by applications reporting on the status of the logging system
  */
 @Export
-@Version("2.20.2")
+@Version("2.23.0")
 package org.apache.logging.log4j.status;
 
 import org.osgi.annotation.bundle.Export;
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/util/Base64Util.java b/log4j-api/src/main/java/org/apache/logging/log4j/util/Base64Util.java
index 34b272856f..f96bffd28d 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/util/Base64Util.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/util/Base64Util.java
@@ -18,7 +18,9 @@ package org.apache.logging.log4j.util;
 
 import java.lang.reflect.Method;
 import java.nio.charset.Charset;
+import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.LoggingException;
+import org.apache.logging.log4j.status.StatusLogger;
 
 /**
  * Base64 encodes Strings. This utility is only necessary because the mechanism to do this changed in Java 8 and
@@ -26,6 +28,8 @@ import org.apache.logging.log4j.LoggingException;
  */
 public final class Base64Util {
 
+    private static final Logger LOGGER = StatusLogger.getLogger();
+
     private static Method encodeMethod = null;
     private static Object encoder = null;
 
@@ -41,7 +45,7 @@ public final class Base64Util {
                 final Class<?> clazz = LoaderUtil.loadClass("javax.xml.bind.DataTypeConverter");
                 encodeMethod = clazz.getMethod("printBase64Binary");
             } catch (Exception ex2) {
-                LowLevelLogUtil.logException("Unable to create a Base64 Encoder", ex2);
+                LOGGER.error("Unable to create a Base64 Encoder", ex2);
             }
         }
     }
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/util/EnvironmentPropertySource.java b/log4j-api/src/main/java/org/apache/logging/log4j/util/EnvironmentPropertySource.java
index e12841cee2..dc73c5056f 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/util/EnvironmentPropertySource.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/util/EnvironmentPropertySource.java
@@ -20,6 +20,8 @@ import aQute.bnd.annotation.Resolution;
 import aQute.bnd.annotation.spi.ServiceProvider;
 import java.util.Collection;
 import java.util.Map;
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.status.StatusLogger;
 
 /**
  * PropertySource implementation that uses environment variables as a source.
@@ -33,7 +35,10 @@ import java.util.Map;
 @ServiceProvider(value = PropertySource.class, resolution = Resolution.OPTIONAL)
 public class EnvironmentPropertySource implements PropertySource {
 
+    private static final Logger LOGGER = StatusLogger.getLogger();
+
     private static final String PREFIX = "LOG4J_";
+
     private static final int DEFAULT_PRIORITY = 100;
 
     @Override
@@ -41,10 +46,9 @@ public class EnvironmentPropertySource implements PropertySource {
         return DEFAULT_PRIORITY;
     }
 
-    private void logException(final SecurityException e) {
+    private void logException(final SecurityException error) {
         // There is no status logger yet.
-        LowLevelLogUtil.logException(
-                "The system environment variables are not available to Log4j due to security restrictions: " + e, e);
+        LOGGER.error("The system environment variables are not available to Log4j due to security restrictions", error);
     }
 
     @Override
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/util/LoaderUtil.java b/log4j-api/src/main/java/org/apache/logging/log4j/util/LoaderUtil.java
index 51ad5675ea..001ad3a5f5 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/util/LoaderUtil.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/util/LoaderUtil.java
@@ -27,6 +27,8 @@ import java.util.Enumeration;
 import java.util.LinkedHashSet;
 import java.util.Objects;
 import java.util.function.Supplier;
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.status.StatusLogger;
 
 /**
  * <em>Consider this class private.</em> Utility class for ClassLoaders.
@@ -39,6 +41,8 @@ import java.util.function.Supplier;
 @InternalApi
 public final class LoaderUtil {
 
+    private static final Logger LOGGER = StatusLogger.getLogger();
+
     /**
      * System property to set to ignore the thread context ClassLoader.
      *
@@ -178,8 +182,8 @@ public final class LoaderUtil {
             return true;
         } catch (final ClassNotFoundException | LinkageError e) {
             return false;
-        } catch (final Throwable e) {
-            LowLevelLogUtil.logException("Unknown error checking for existence of class: " + className, e);
+        } catch (final Throwable error) {
+            LOGGER.error("Unknown error while checking existence of class `{}`", className, error);
             return false;
         }
     }
@@ -474,8 +478,8 @@ public final class LoaderUtil {
                     while (resourceEnum.hasMoreElements()) {
                         resources.add(new UrlResource(cl, resourceEnum.nextElement()));
                     }
-                } catch (final IOException e) {
-                    LowLevelLogUtil.logException(e);
+                } catch (final IOException error) {
+                    LOGGER.error("failed to collect resources of name `{}`", resource, error);
                 }
             }
         }
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/util/LowLevelLogUtil.java b/log4j-api/src/main/java/org/apache/logging/log4j/util/LowLevelLogUtil.java
deleted file mode 100644
index 8f28856687..0000000000
--- a/log4j-api/src/main/java/org/apache/logging/log4j/util/LowLevelLogUtil.java
+++ /dev/null
@@ -1,93 +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.util;
-
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-import java.io.OutputStream;
-import java.io.PrintWriter;
-import java.io.Writer;
-import java.util.Objects;
-
-/**
- * PrintWriter-based logging utility for classes too low level to use {@link org.apache.logging.log4j.status.StatusLogger}.
- * Such classes cannot use StatusLogger as StatusLogger or {@link org.apache.logging.log4j.simple.SimpleLogger} depends
- * on them for initialization. Other framework classes should stick to using StatusLogger.
- *
- * @since 2.6
- */
-final class LowLevelLogUtil {
-
-    /*
-     * https://errorprone.info/bugpattern/DefaultCharset
-     *
-     * We intentionally use the system encoding.
-     */
-    @SuppressWarnings("DefaultCharset")
-    private static PrintWriter writer = new PrintWriter(System.err, true);
-
-    /**
-     * Logs the given message.
-     *
-     * @param message the message to log
-     * @since 2.9.2
-     */
-    public static void log(final String message) {
-        if (message != null) {
-            writer.println(message);
-        }
-    }
-
-    @SuppressFBWarnings(
-            value = "INFORMATION_EXPOSURE_THROUGH_AN_ERROR_MESSAGE",
-            justification = "Log4j prints stacktraces only to logs, which should be private.")
-    public static void logException(final Throwable exception) {
-        if (exception != null) {
-            exception.printStackTrace(writer);
-        }
-    }
-
-    public static void logException(final String message, final Throwable exception) {
-        log(message);
-        logException(exception);
-    }
-
-    /**
-     * Sets the underlying OutputStream where exceptions are printed to.
-     *
-     * @param out the OutputStream to log to
-     */
-    @SuppressWarnings("DefaultCharset")
-    public static void setOutputStream(final OutputStream out) {
-        /*
-         * https://errorprone.info/bugpattern/DefaultCharset
-         *
-         * We intentionally use the system encoding.
-         */
-        LowLevelLogUtil.writer = new PrintWriter(Objects.requireNonNull(out), true);
-    }
-
-    /**
-     * Sets the underlying Writer where exceptions are printed to.
-     *
-     * @param writer the Writer to log to
-     */
-    public static void setWriter(final Writer writer) {
-        LowLevelLogUtil.writer = new PrintWriter(Objects.requireNonNull(writer), true);
-    }
-
-    private LowLevelLogUtil() {}
-}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/util/OsgiServiceLocator.java b/log4j-api/src/main/java/org/apache/logging/log4j/util/OsgiServiceLocator.java
index 95f5588e82..d061ccbd6d 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/util/OsgiServiceLocator.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/util/OsgiServiceLocator.java
@@ -19,6 +19,7 @@ package org.apache.logging.log4j.util;
 import java.lang.invoke.MethodHandles.Lookup;
 import java.util.Objects;
 import java.util.stream.Stream;
+import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.status.StatusLogger;
 import org.osgi.framework.Bundle;
 import org.osgi.framework.BundleContext;
@@ -26,6 +27,8 @@ import org.osgi.framework.FrameworkUtil;
 
 public class OsgiServiceLocator {
 
+    private static final Logger LOGGER = StatusLogger.getLogger();
+
     private static final boolean OSGI_AVAILABLE = checkOsgiAvailable();
 
     private static boolean checkOsgiAvailable() {
@@ -38,8 +41,8 @@ public class OsgiServiceLocator {
             return clazz.getMethod("getBundle", Class.class).invoke(null, OsgiServiceLocator.class) != null;
         } catch (final ClassNotFoundException | NoSuchMethodException | LinkageError e) {
             return false;
-        } catch (final Throwable e) {
-            LowLevelLogUtil.logException("Unknown error checking OSGI environment.", e);
+        } catch (final Throwable error) {
+            LOGGER.error("Unknown error checking OSGI environment.", error);
             return false;
         }
     }
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/util/PropertiesUtil.java b/log4j-api/src/main/java/org/apache/logging/log4j/util/PropertiesUtil.java
index 3084a6d816..daf9163203 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/util/PropertiesUtil.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/util/PropertiesUtil.java
@@ -37,6 +37,8 @@ import java.util.ServiceLoader;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentSkipListSet;
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.status.StatusLogger;
 
 /**
  * <em>Consider this class private.</em>
@@ -54,8 +56,12 @@ import java.util.concurrent.ConcurrentSkipListSet;
 @ServiceConsumer(value = PropertySource.class, resolution = Resolution.OPTIONAL, cardinality = Cardinality.MULTIPLE)
 public final class PropertiesUtil {
 
+    private static final Logger LOGGER = StatusLogger.getLogger();
+
     private static final String LOG4J_PROPERTIES_FILE_NAME = "log4j2.component.properties";
+
     private static final String LOG4J_SYSTEM_PROPERTIES_FILE_NAME = "log4j2.system.properties";
+
     private static final Lazy<PropertiesUtil> COMPONENT_PROPERTIES =
             Lazy.lazy(() -> new PropertiesUtil(LOG4J_PROPERTIES_FILE_NAME, false));
 
@@ -104,13 +110,13 @@ public final class PropertiesUtil {
         if (null != in) {
             try {
                 props.load(in);
-            } catch (final IOException e) {
-                LowLevelLogUtil.logException("Unable to read " + source, e);
+            } catch (final IOException error) {
+                LOGGER.error("Unable to read source `{}`", source, error);
             } finally {
                 try {
                     in.close();
-                } catch (final IOException e) {
-                    LowLevelLogUtil.logException("Unable to close " + source, e);
+                } catch (final IOException error) {
+                    LOGGER.error("Unable to close source `{}`", source, error);
                 }
             }
         }
@@ -238,8 +244,11 @@ public final class PropertiesUtil {
                 return Charset.forName(mapped);
             }
         }
-        LowLevelLogUtil.log("Unable to get Charset '" + charsetName + "' for property '" + name + "', using default "
-                + defaultValue + " and continuing.");
+        LOGGER.error(
+                "Unable to read charset `{}` from property `{}`. Falling back to the default: `{}`",
+                charsetName,
+                name,
+                defaultValue);
         return defaultValue;
     }
 
@@ -424,8 +433,8 @@ public final class PropertiesUtil {
     public static Properties getSystemProperties() {
         try {
             return new Properties(System.getProperties());
-        } catch (final SecurityException ex) {
-            LowLevelLogUtil.logException("Unable to access system properties.", ex);
+        } catch (final SecurityException error) {
+            LOGGER.error("Unable to access system properties.", error);
             // Sandboxed - can't read System Properties
             return new Properties();
         }
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/util/PropertyFilePropertySource.java b/log4j-api/src/main/java/org/apache/logging/log4j/util/PropertyFilePropertySource.java
index 31d28d4f2c..3440af0460 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/util/PropertyFilePropertySource.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/util/PropertyFilePropertySource.java
@@ -21,6 +21,8 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.net.URL;
 import java.util.Properties;
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.status.StatusLogger;
 
 /**
  * PropertySource backed by a properties file. Follows the same conventions as {@link PropertiesPropertySource}.
@@ -29,6 +31,8 @@ import java.util.Properties;
  */
 public class PropertyFilePropertySource extends PropertiesPropertySource {
 
+    private static final Logger LOGGER = StatusLogger.getLogger();
+
     public PropertyFilePropertySource(final String fileName) {
         this(fileName, true);
     }
@@ -45,8 +49,8 @@ public class PropertyFilePropertySource extends PropertiesPropertySource {
         for (final URL url : LoaderUtil.findResources(fileName, useTccl)) {
             try (final InputStream in = url.openStream()) {
                 props.load(in);
-            } catch (final IOException e) {
-                LowLevelLogUtil.logException("Unable to read " + url, e);
+            } catch (final IOException error) {
+                LOGGER.error("Unable to read URL `{}`", url, error);
             }
         }
         return props;
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/util/StackLocator.java b/log4j-api/src/main/java/org/apache/logging/log4j/util/StackLocator.java
index 7fff12d166..bc20af0daf 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/util/StackLocator.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/util/StackLocator.java
@@ -20,6 +20,8 @@ import java.lang.reflect.Method;
 import java.util.ArrayDeque;
 import java.util.Deque;
 import java.util.function.Predicate;
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.status.StatusLogger;
 
 /**
  * <em>Consider this class private.</em> Provides various methods to determine the caller class.
@@ -51,6 +53,8 @@ import java.util.function.Predicate;
 @InternalApi
 public final class StackLocator {
 
+    private static final Logger LOGGER = StatusLogger.getLogger();
+
     /** TODO Consider removing now that we require Java 8. */
     static final int JDK_7U25_OFFSET;
 
@@ -75,18 +79,18 @@ public final class StackLocator {
             } else {
                 o = getCallerClassMethod.invoke(null, 1);
                 if (o == sunReflectionClass) {
-                    LowLevelLogUtil.log(
-                            "WARNING: Unexpected result from sun.reflect.Reflection.getCallerClass(int), adjusting offset for future calls.");
+                    LOGGER.warn(
+                            "Unexpected result from `sun.reflect.Reflection.getCallerClass(int)`, adjusting offset for future calls.");
                     java7u25CompensationOffset = 1;
                 }
             }
         } catch (final Exception | LinkageError e) {
             if (Constants.JAVA_MAJOR_VERSION > 8) {
-                LowLevelLogUtil.log(
-                        "WARNING: Runtime environment or build system does not support multi-release JARs. This will impact location-based features.");
+                LOGGER.warn(
+                        "Runtime environment or build system does not support multi-release JARs. This will impact location-based features.");
             } else {
-                LowLevelLogUtil.log(
-                        "WARNING: sun.reflect.Reflection.getCallerClass is not supported. This will impact location-based features.");
+                LOGGER.warn(
+                        "`sun.reflect.Reflection.getCallerClass(int)` is not supported. This will impact location-based features.");
             }
             getCallerClassMethod = null;
             java7u25CompensationOffset = -1;
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/OutputStreamManagerTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/OutputStreamManagerTest.java
index 7abb2dd95d..c5e25a15c6 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/OutputStreamManagerTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/OutputStreamManagerTest.java
@@ -16,6 +16,7 @@
  */
 package org.apache.logging.log4j.core.appender;
 
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 
@@ -29,29 +30,35 @@ import org.apache.logging.log4j.core.test.junit.LoggerContextSource;
 import org.apache.logging.log4j.status.StatusData;
 import org.apache.logging.log4j.status.StatusLogger;
 import org.junit.jupiter.api.Test;
+import org.junitpioneer.jupiter.SetSystemProperty;
 
 /**
  * OutputStreamManager Tests.
  */
-public class OutputStreamManagerTest {
+@SetSystemProperty(key = "log4j2.status.entries", value = "10")
+class OutputStreamManagerTest {
 
     @Test
     @LoggerContextSource("multipleIncompatibleAppendersTest.xml")
-    public void narrow(final LoggerContext context) {
+    void narrow(final LoggerContext context) {
         final Logger logger = context.getLogger(OutputStreamManagerTest.class);
         logger.info("test");
-        final List<StatusData> statusData = StatusLogger.getLogger().getStatusData();
-        StatusData data = statusData.get(0);
-        if (data.getMessage().getFormattedMessage().contains("WindowsAnsiOutputStream")) {
-            data = statusData.get(1);
+        StatusLogger statusLogger = StatusLogger.getLogger();
+        final List<StatusData> events = statusLogger.getStatusData();
+        assertThat(events).isNotEmpty();
+        StatusData event = events.get(0);
+        if (event.getMessage().getFormattedMessage().contains("WindowsAnsiOutputStream")) {
+            event = events.get(1);
         }
-        assertEquals(Level.ERROR, data.getLevel());
-        assertEquals(
-                "Could not create plugin of type class org.apache.logging.log4j.core.appender.RollingRandomAccessFileAppender for element RollingRandomAccessFile",
-                data.getMessage().getFormattedMessage());
-        assertEquals(
-                "org.apache.logging.log4j.core.config.ConfigurationException: Configuration has multiple incompatible Appenders pointing to the same resource 'target/multiIncompatibleAppender.log'",
-                data.getThrowable().toString());
+        assertThat(event.getLevel()).isEqualTo(Level.ERROR);
+        assertThat(event.getMessage().getFormattedMessage())
+                .isEqualTo(
+                        "Could not create plugin of type class org.apache.logging.log4j.core.appender.RollingRandomAccessFileAppender for element RollingRandomAccessFile");
+        assertThat(event.getThrowable())
+                .isNotNull()
+                .asString()
+                .isEqualTo(
+                        "org.apache.logging.log4j.core.config.ConfigurationException: Configuration has multiple incompatible Appenders pointing to the same resource 'target/multiIncompatibleAppender.log'");
     }
 
     @Test
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/action/AbstractActionTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/action/AbstractActionTest.java
index 49147e3805..f9f3752185 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/action/AbstractActionTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/action/AbstractActionTest.java
@@ -26,15 +26,16 @@ import java.util.List;
 import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.status.StatusData;
 import org.apache.logging.log4j.status.StatusLogger;
-import org.apache.logging.log4j.test.junit.StatusLoggerLevel;
 import org.junit.jupiter.api.Test;
+import org.junitpioneer.jupiter.SetSystemProperty;
 
-@StatusLoggerLevel("WARN")
-public class AbstractActionTest {
+@SetSystemProperty(key = "log4j2.status.entries", value = "10")
+@SetSystemProperty(key = "log4j2.StatusLogger.level", value = "WARN")
+class AbstractActionTest {
 
     // Test for LOG4J2-2658
     @Test
-    public void testExceptionsAreLoggedToStatusLogger() {
+    void testExceptionsAreLoggedToStatusLogger() {
         final StatusLogger statusLogger = StatusLogger.getLogger();
         statusLogger.clear();
         new TestAction().run();
@@ -53,7 +54,7 @@ public class AbstractActionTest {
     }
 
     @Test
-    public void testRuntimeExceptionsAreLoggedToStatusLogger() {
+    void testRuntimeExceptionsAreLoggedToStatusLogger() {
         final StatusLogger statusLogger = StatusLogger.getLogger();
         statusLogger.clear();
         new AbstractAction() {
@@ -71,7 +72,7 @@ public class AbstractActionTest {
     }
 
     @Test
-    public void testErrorsAreLoggedToStatusLogger() {
+    void testErrorsAreLoggedToStatusLogger() {
         final StatusLogger statusLogger = StatusLogger.getLogger();
         statusLogger.clear();
         new AbstractAction() {
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/action/IfAllTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/action/IfAllTest.java
index f8cde81950..8762a4a491 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/action/IfAllTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/action/IfAllTest.java
@@ -33,14 +33,16 @@ import org.apache.logging.log4j.status.StatusLogger;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.ValueSource;
+import org.junitpioneer.jupiter.SetSystemProperty;
 
 /**
  * Tests the And composite condition.
  */
-public class IfAllTest {
+@SetSystemProperty(key = "log4j2.status.entries", value = "10")
+class IfAllTest {
 
     @Test
-    public void testAccept() {
+    void testAccept() {
         final PathCondition TRUE = new FixedCondition(true);
         final PathCondition FALSE = new FixedCondition(false);
         assertTrue(IfAll.createAndCondition(TRUE, TRUE).accept(null, null, null));
@@ -50,12 +52,12 @@ public class IfAllTest {
     }
 
     @Test
-    public void testEmptyIsFalse() {
+    void testEmptyIsFalse() {
         assertFalse(IfAll.createAndCondition().accept(null, null, null));
     }
 
     @Test
-    public void testBeforeTreeWalk() {
+    void testBeforeTreeWalk() {
         final CountingCondition counter = new CountingCondition(true);
         final IfAll and = IfAll.createAndCondition(counter, counter, counter);
         and.beforeFileTreeWalk();
@@ -63,14 +65,14 @@ public class IfAllTest {
     }
 
     @Test
-    public void testCreateAndConditionCalledProgrammaticallyThrowsNPEWhenComponentsNotSpecified() {
+    void testCreateAndConditionCalledProgrammaticallyThrowsNPEWhenComponentsNotSpecified() {
         PathCondition[] components = null;
         assertThrows(NullPointerException.class, () -> IfAll.createAndCondition(components));
     }
 
     @ParameterizedTest
     @ValueSource(strings = "No components provided for IfAll")
-    public void testCreateAndConditionCalledByPluginBuilderReturnsNullAndLogsMessageWhenComponentsNotSpecified(
+    void testCreateAndConditionCalledByPluginBuilderReturnsNullAndLogsMessageWhenComponentsNotSpecified(
             final String expectedMessage) {
         final PluginEntry nullEntry = null;
         final PluginType<IfAll> type = new PluginType<>(nullEntry, IfAll.class, "Dummy");
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/action/IfAnyTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/action/IfAnyTest.java
index aac3eaa326..7ed9251685 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/action/IfAnyTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/action/IfAnyTest.java
@@ -33,11 +33,13 @@ import org.apache.logging.log4j.status.StatusLogger;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.ValueSource;
+import org.junitpioneer.jupiter.SetSystemProperty;
 
 /**
  * Tests the Or composite condition.
  */
-public class IfAnyTest {
+@SetSystemProperty(key = "log4j2.status.entries", value = "10")
+class IfAnyTest {
 
     @Test
     public void test() {
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/action/IfLastModifiedTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/action/IfLastModifiedTest.java
index b731d25f11..333af1ebd4 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/action/IfLastModifiedTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/action/IfLastModifiedTest.java
@@ -35,11 +35,13 @@ import org.apache.logging.log4j.status.StatusLogger;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.ValueSource;
+import org.junitpioneer.jupiter.SetSystemProperty;
 
 /**
  * Tests the FileAgeFilter class.
  */
-public class IfLastModifiedTest {
+@SetSystemProperty(key = "log4j2.status.entries", value = "10")
+class IfLastModifiedTest {
 
     @Test
     public void testGetDurationReturnsConstructorValue() {
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/action/IfNotTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/action/IfNotTest.java
index 698c6d6dee..2398fb2a50 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/action/IfNotTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/action/IfNotTest.java
@@ -33,11 +33,13 @@ import org.apache.logging.log4j.status.StatusLogger;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.ValueSource;
+import org.junitpioneer.jupiter.SetSystemProperty;
 
 /**
  * Tests the Not composite condition.
  */
-public class IfNotTest {
+@SetSystemProperty(key = "log4j2.status.entries", value = "10")
+class IfNotTest {
 
     @Test
     public void test() {
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAbstractTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAbstractTest.java
index 48d40a263b..57734ed983 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAbstractTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAbstractTest.java
@@ -27,8 +27,6 @@ import java.util.Collections;
 import java.util.List;
 import java.util.Stack;
 import java.util.concurrent.CountDownLatch;
-import java.util.stream.Collectors;
-import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.core.Appender;
 import org.apache.logging.log4j.core.LogEvent;
@@ -39,9 +37,7 @@ import org.apache.logging.log4j.core.config.LoggerConfig;
 import org.apache.logging.log4j.core.jmx.RingBufferAdmin;
 import org.apache.logging.log4j.core.util.Constants;
 import org.apache.logging.log4j.core.util.ReflectionUtil;
-import org.apache.logging.log4j.status.StatusData;
 import org.apache.logging.log4j.status.StatusLogger;
-import org.apache.logging.log4j.test.ListStatusListener;
 import org.apache.logging.log4j.test.junit.UsingStatusListener;
 import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Timeout;
@@ -84,35 +80,6 @@ public abstract class QueueFullAbstractTest {
         }
     }
 
-    protected static class DomainObject {
-
-        private final Logger innerLogger;
-        private final Unlocker unlocker;
-        private final int count;
-
-        public DomainObject(final Logger innerLogger, final Unlocker unlocker, final int loggingCount) {
-            this.innerLogger = innerLogger;
-            this.unlocker = unlocker;
-            this.count = loggingCount;
-        }
-
-        @Override
-        public String toString() {
-            for (int i = 0; i < count; i++) {
-                LOGGER.info(
-                        "DomainObject logging message {}. Ring buffer capacity was {}, countdown latch was {}.",
-                        i,
-                        asyncRemainingCapacity(innerLogger),
-                        unlocker.countDownLatch.getCount());
-                unlocker.countDownLatch.countDown();
-                innerLogger.info("Logging in toString() #{}", i);
-            }
-            return "Who's bad?!";
-        }
-    }
-
-    private ListStatusListener statusListener;
-
     protected void testNormalQueueFullKeepsMessagesInOrder(
             final LoggerContext ctx, final BlockingAppender blockingAppender) throws Exception {
         checkConfig(ctx);
@@ -157,63 +124,6 @@ public abstract class QueueFullAbstractTest {
         assertThat(actual).isEmpty();
     }
 
-    public void testLoggingFromToStringCausesOutOfOrderMessages(
-            final LoggerContext ctx, final BlockingAppender blockingAppender) throws Exception {
-        checkConfig(ctx);
-        // Non-reusable messages will call `toString()` on the main thread and block it.
-        final Logger logger = ctx.getLogger(this.getClass());
-
-        blockingAppender.countDownLatch = new CountDownLatch(1);
-        final Unlocker unlocker = new Unlocker(new CountDownLatch(MESSAGE_COUNT - 1), blockingAppender);
-        unlocker.start();
-        asyncRecursiveTest(logger, unlocker, blockingAppender);
-        unlocker.join();
-    }
-
-    void asyncRecursiveTest(final Logger logger, final Unlocker unlocker, final BlockingAppender blockingAppender) {
-        for (int i = 0; i < 1; i++) {
-            LOGGER.info(
-                    "Test logging message {}. Ring buffer capacity was {}, countdown latch was {}.",
-                    i,
-                    asyncRemainingCapacity(logger),
-                    unlocker.countDownLatch.getCount());
-            unlocker.countDownLatch.countDown();
-            final DomainObject obj = new DomainObject(logger, unlocker, MESSAGE_COUNT - 1);
-            logger.info("Logging naughty object #{}: {}", i, obj);
-        }
-
-        LOGGER.info(
-                "Waiting for message delivery: blockingAppender.logEvents.count={}.",
-                blockingAppender.logEvents.size());
-        while (blockingAppender.logEvents.size() < MESSAGE_COUNT) {
-            Thread.yield();
-        }
-        LOGGER.info(
-                "All {} messages have been delivered: blockingAppender.logEvents.count={}.",
-                MESSAGE_COUNT,
-                blockingAppender.logEvents.size());
-
-        final StatusData mostRecentStatusData = statusListener
-                .findStatusData(Level.WARN)
-                .reduce((ignored, data) -> data)
-                .orElse(null);
-        assertThat(mostRecentStatusData).isNotNull();
-        assertThat(mostRecentStatusData.getLevel()).isEqualTo(Level.WARN);
-        assertThat(mostRecentStatusData.getFormattedStatus())
-                .contains("Log4j2 logged an event out of order to prevent deadlock caused by domain "
-                        + "objects logging from their toString method when the async queue is full");
-
-        final List<String> actual = blockingAppender.logEvents.stream()
-                .map(e -> e.getMessage().getFormattedMessage())
-                .collect(Collectors.toList());
-        final String[] expected = new String[MESSAGE_COUNT];
-        for (int i = 0; i < MESSAGE_COUNT - 1; i++) {
-            expected[i] = "Logging in toString() #" + i;
-        }
-        expected[MESSAGE_COUNT - 1] = "Logging naughty object #0: Who's bad?!";
-        assertThat(actual).hasSize(MESSAGE_COUNT).contains(expected);
-    }
-
     static Stack<String> transform(final List<LogEvent> logEvents) {
         final List<String> filtered = new ArrayList<>(logEvents.size());
         for (final LogEvent event : logEvents) {
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncAppenderTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncAppender1Test.java
similarity index 95%
rename from log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncAppenderTest.java
rename to log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncAppender1Test.java
index 0e73bf8c2b..43329dc1ea 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncAppenderTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncAppender1Test.java
@@ -24,7 +24,7 @@ import org.junit.jupiter.api.Test;
 /**
  * Tests queue full scenarios with AsyncAppender.
  */
-public class QueueFullAsyncAppenderTest extends QueueFullAbstractTest {
+public class QueueFullAsyncAppender1Test extends QueueFullAbstractTest {
 
     @Override
     @Test
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncAppenderTest2.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncAppender2Test.java
similarity index 94%
rename from log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncAppenderTest2.java
rename to log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncAppender2Test.java
index d3886818c8..4eab6286a0 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncAppenderTest2.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncAppender2Test.java
@@ -24,7 +24,7 @@ import org.apache.logging.log4j.test.junit.SetTestProperty;
  * is immutable.
  */
 @SetTestProperty(key = "log4j2.formatMsgAsync", value = "true")
-public class QueueFullAsyncAppenderTest2 extends QueueFullAsyncAppenderTest {
+public class QueueFullAsyncAppender2Test extends QueueFullAsyncAppender1Test {
 
     @Override
     protected void checkConfig(final LoggerContext ctx) {
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncLoggerTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncLogger1Test.java
similarity index 96%
rename from log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncLoggerTest.java
rename to log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncLogger1Test.java
index 1aaabfe48d..fe3f42e043 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncLoggerTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncLogger1Test.java
@@ -33,7 +33,7 @@ import org.junit.jupiter.api.Test;
         value = "org.apache.logging.log4j.core.async.AsyncLoggerContextSelector")
 @SetTestProperty(key = "log4j2.asyncLoggerRingBufferSize", value = "128")
 @Tag(Tags.ASYNC_LOGGERS)
-public class QueueFullAsyncLoggerTest extends QueueFullAbstractTest {
+public class QueueFullAsyncLogger1Test extends QueueFullAbstractTest {
 
     @Override
     @Test
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncLoggerTest2.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncLogger2Test.java
similarity index 95%
rename from log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncLoggerTest2.java
rename to log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncLogger2Test.java
index 6271849278..fbfce3951b 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncLoggerTest2.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncLogger2Test.java
@@ -27,7 +27,7 @@ import org.junit.jupiter.api.Tag;
  */
 @SetTestProperty(key = "log4j2.formatMsgAsync", value = "true")
 @Tag(Tags.ASYNC_LOGGERS)
-public class QueueFullAsyncLoggerTest2 extends QueueFullAsyncLoggerTest {
+public class QueueFullAsyncLogger2Test extends QueueFullAsyncLogger1Test {
 
     @Override
     protected void checkConfig(final LoggerContext ctx) {
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncLoggerTest3.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncLogger3Test.java
similarity index 98%
rename from log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncLoggerTest3.java
rename to log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncLogger3Test.java
index 7a048091d2..3be89c6f9b 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncLoggerTest3.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncLogger3Test.java
@@ -44,7 +44,7 @@ import org.junit.jupiter.api.Timeout;
 @SetTestProperty(key = "log4j2.formatMsgAsync", value = "true")
 @SetTestProperty(key = "log4j2.asyncQueueFullPolicy", value = "Discard")
 @Tag(Tags.ASYNC_LOGGERS)
-public class QueueFullAsyncLoggerTest3 extends QueueFullAbstractTest {
+public class QueueFullAsyncLogger3Test extends QueueFullAbstractTest {
 
     @Override
     protected void checkConfig(final LoggerContext ctx) {
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncLoggerConfigTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncLoggerConfig1Test.java
similarity index 96%
rename from log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncLoggerConfigTest.java
rename to log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncLoggerConfig1Test.java
index 93db0470ed..e797792ddb 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncLoggerConfigTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncLoggerConfig1Test.java
@@ -29,7 +29,7 @@ import org.junit.jupiter.api.Test;
  */
 @SetTestProperty(key = "log4j2.asyncLoggerConfigRingBufferSize", value = "128")
 @Tag(Tags.ASYNC_LOGGERS)
-public class QueueFullAsyncLoggerConfigTest extends QueueFullAbstractTest {
+public class QueueFullAsyncLoggerConfig1Test extends QueueFullAbstractTest {
 
     @Override
     @Test
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncLoggerConfigTest2.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncLoggerConfig2Test.java
similarity index 94%
rename from log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncLoggerConfigTest2.java
rename to log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncLoggerConfig2Test.java
index 4c56595a40..a88d4568bd 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncLoggerConfigTest2.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncLoggerConfig2Test.java
@@ -26,7 +26,7 @@ import org.junit.jupiter.api.Tag;
  */
 @SetTestProperty(key = "log4j2.formatMsgAsync", value = "true")
 @Tag(Tags.ASYNC_LOGGERS)
-public class QueueFullAsyncLoggerConfigTest2 extends QueueFullAsyncLoggerConfigTest {
+public class QueueFullAsyncLoggerConfig2Test extends QueueFullAsyncLoggerConfig1Test {
 
     @Override
     protected void checkConfig(final LoggerContext ctx) throws ReflectiveOperationException {
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncLoggerConfigLoggingFromToStringTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncLoggerConfigLoggingFromToStringTest.java
deleted file mode 100644
index e60e8bea22..0000000000
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncLoggerConfigLoggingFromToStringTest.java
+++ /dev/null
@@ -1,53 +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.async;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-import org.apache.logging.log4j.core.LoggerContext;
-import org.apache.logging.log4j.core.test.junit.LoggerContextSource;
-import org.apache.logging.log4j.core.test.junit.Named;
-import org.apache.logging.log4j.core.test.junit.Tags;
-import org.apache.logging.log4j.test.junit.SetTestProperty;
-import org.apache.logging.log4j.util.Constants;
-import org.junit.jupiter.api.Tag;
-import org.junit.jupiter.api.Test;
-
-/**
- * Tests queue full scenarios with AsyncLoggers in configuration.
- */
-@SetTestProperty(key = "log4j2.enableThreadlocals", value = "true")
-@SetTestProperty(key = "log4j2.isWebapp", value = "false")
-@SetTestProperty(key = "log4j2.asyncLoggerConfigRingBufferSize", value = "128")
-@Tag(Tags.ASYNC_LOGGERS)
-public class QueueFullAsyncLoggerConfigLoggingFromToStringTest extends QueueFullAbstractTest {
-
-    @Override
-    @Test
-    @LoggerContextSource
-    public void testLoggingFromToStringCausesOutOfOrderMessages(
-            final LoggerContext ctx, final @Named(APPENDER_NAME) BlockingAppender blockingAppender) throws Exception {
-        super.testLoggingFromToStringCausesOutOfOrderMessages(ctx, blockingAppender);
-    }
-
-    @Override
-    protected void checkConfig(final LoggerContext ctx) throws ReflectiveOperationException {
-        assertAsyncLoggerConfig(ctx, 128);
-        assertThat(Constants.IS_WEB_APP).isFalse();
-        assertThat(Constants.ENABLE_THREADLOCALS).isTrue();
-    }
-}
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncLoggerConfigLoggingFromToStringTest2.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncLoggerConfigLoggingFromToStringTest2.java
deleted file mode 100644
index 54e5a78a97..0000000000
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncLoggerConfigLoggingFromToStringTest2.java
+++ /dev/null
@@ -1,37 +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.async;
-
-import org.apache.logging.log4j.core.LoggerContext;
-import org.apache.logging.log4j.core.test.junit.Tags;
-import org.apache.logging.log4j.test.junit.SetTestProperty;
-import org.junit.jupiter.api.Tag;
-
-/**
- * Tests queue full scenarios with AsyncLoggers in configuration.
- */
-@SetTestProperty(key = "log4j2.formatMsgAsync", value = "true")
-@Tag(Tags.ASYNC_LOGGERS)
-public class QueueFullAsyncLoggerConfigLoggingFromToStringTest2
-        extends QueueFullAsyncLoggerConfigLoggingFromToStringTest {
-
-    @Override
-    protected void checkConfig(final LoggerContext ctx) throws ReflectiveOperationException {
-        super.checkConfig(ctx);
-        assertFormatMessagesInBackground();
-    }
-}
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncLoggerLoggingFromToStringTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncLoggerLoggingFromToStringTest.java
deleted file mode 100644
index 8e98f61c4e..0000000000
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncLoggerLoggingFromToStringTest.java
+++ /dev/null
@@ -1,50 +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.async;
-
-import org.apache.logging.log4j.core.LoggerContext;
-import org.apache.logging.log4j.core.test.junit.LoggerContextSource;
-import org.apache.logging.log4j.core.test.junit.Named;
-import org.apache.logging.log4j.core.test.junit.Tags;
-import org.apache.logging.log4j.core.util.Constants;
-import org.apache.logging.log4j.test.junit.SetTestProperty;
-import org.junit.jupiter.api.Tag;
-import org.junit.jupiter.api.Test;
-
-/**
- * Tests queue full scenarios with pure AsyncLoggers (all loggers async).
- */
-@SetTestProperty(
-        key = Constants.LOG4J_CONTEXT_SELECTOR,
-        value = "org.apache.logging.log4j.core.async.AsyncLoggerContextSelector")
-@SetTestProperty(key = "log4j2.asyncLoggerRingBufferSize", value = "128")
-@Tag(Tags.ASYNC_LOGGERS)
-public class QueueFullAsyncLoggerLoggingFromToStringTest extends QueueFullAbstractTest {
-
-    @Override
-    @Test
-    @LoggerContextSource
-    public void testLoggingFromToStringCausesOutOfOrderMessages(
-            final LoggerContext ctx, final @Named(APPENDER_NAME) BlockingAppender blockingAppender) throws Exception {
-        super.testLoggingFromToStringCausesOutOfOrderMessages(ctx, blockingAppender);
-    }
-
-    @Override
-    protected void checkConfig(final LoggerContext ctx) {
-        assertAsyncLogger(ctx, 128);
-    }
-}
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncLoggerLoggingFromToStringTest2.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncLoggerLoggingFromToStringTest2.java
deleted file mode 100644
index a75977e2f3..0000000000
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncLoggerLoggingFromToStringTest2.java
+++ /dev/null
@@ -1,44 +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.async;
-
-import org.apache.logging.log4j.core.LoggerContext;
-import org.apache.logging.log4j.core.test.junit.Tags;
-import org.apache.logging.log4j.test.junit.SetTestProperty;
-import org.junit.jupiter.api.Disabled;
-import org.junit.jupiter.api.Tag;
-
-/**
- * Tests queue full scenarios with pure AsyncLoggers (all loggers async).
- */
-@SetTestProperty(key = "log4j2.formatMsgAsync", value = "true")
-@Tag(Tags.ASYNC_LOGGERS)
-public class QueueFullAsyncLoggerLoggingFromToStringTest2 extends QueueFullAsyncLoggerLoggingFromToStringTest {
-
-    @Override
-    @Disabled("Causes deadlocks")
-    public void testLoggingFromToStringCausesOutOfOrderMessages(LoggerContext ctx, BlockingAppender blockingAppender)
-            throws Exception {
-        super.testLoggingFromToStringCausesOutOfOrderMessages(ctx, blockingAppender);
-    }
-
-    @Override
-    protected void checkConfig(final LoggerContext ctx) {
-        super.checkConfig(ctx);
-        assertFormatMessagesInBackground();
-    }
-}
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/LoggersPluginTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/LoggersPluginTest.java
index 325466cf3d..70e824937e 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/LoggersPluginTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/LoggersPluginTest.java
@@ -16,8 +16,7 @@
  */
 package org.apache.logging.log4j.core.config;
 
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.assertj.core.api.Assertions.assertThat;
 
 import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.LogManager;
@@ -26,21 +25,22 @@ import org.apache.logging.log4j.core.test.junit.LoggerContextSource;
 import org.apache.logging.log4j.status.StatusData;
 import org.apache.logging.log4j.status.StatusLogger;
 import org.junit.jupiter.api.Test;
+import org.junitpioneer.jupiter.SetSystemProperty;
 
 /**
  * Tests LoggersPlugin.
  */
-@LoggerContextSource("multipleRootLoggersTest.xml")
+@SetSystemProperty(key = "log4j2.status.entries", value = "10")
 public class LoggersPluginTest {
 
     @Test
-    public void testEmptyAttribute() throws Exception {
+    @LoggerContextSource("multipleRootLoggersTest.xml")
+    public void testEmptyAttribute() {
         final Logger logger = LogManager.getLogger();
         logger.info("Test");
         final StatusData data = StatusLogger.getLogger().getStatusData().get(0);
-        // System.out.println(data.getFormattedStatus());
 
-        assertEquals(Level.ERROR, data.getLevel());
-        assertTrue(data.getMessage().getFormattedMessage().contains("multiple root loggers"));
+        assertThat(data.getLevel()).isEqualTo(Level.ERROR);
+        assertThat(data.getMessage().getFormattedMessage()).contains("multiple root loggers");
     }
 }
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/xml/XmlConfigurationPropsTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/xml/XmlConfigurationPropsTest.java
index 8d467fa6e4..e7a13f12ed 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/xml/XmlConfigurationPropsTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/xml/XmlConfigurationPropsTest.java
@@ -17,66 +17,71 @@
 package org.apache.logging.log4j.core.config.xml;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
 
-import java.util.stream.StreamSupport;
+import edu.umd.cs.findbugs.annotations.Nullable;
 import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.core.config.Configuration;
 import org.apache.logging.log4j.core.test.junit.LoggerContextSource;
 import org.apache.logging.log4j.core.util.Constants;
 import org.apache.logging.log4j.status.StatusConsoleListener;
-import org.apache.logging.log4j.status.StatusListener;
 import org.apache.logging.log4j.status.StatusLogger;
 import org.apache.logging.log4j.test.junit.SetTestProperty;
-import org.apache.logging.log4j.test.junit.UsingTestProperties;
+import org.apache.logging.log4j.test.junit.UsingStatusLoggerMock;
 import org.junit.jupiter.api.Test;
-import org.opentest4j.AssertionFailedError;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
 
-@UsingTestProperties
-public class XmlConfigurationPropsTest {
+@UsingStatusLoggerMock
+@ExtendWith(MockitoExtension.class)
+class XmlConfigurationPropsTest {
 
     private static final String CONFIG_NAME = "XmlConfigurationPropsTest";
+
     private static final String CONFIG1 = "org/apache/logging/log4j/core/config/xml/XmlConfigurationPropsTest1.xml";
-    private static final String CONFIG1_NAME = "XmlConfigurationPropsTest1";
 
-    private static StatusConsoleListener findStatusConsoleListener() {
-        final Iterable<StatusListener> listeners = StatusLogger.getLogger().getListeners();
-        return StreamSupport.stream(listeners.spliterator(), false)
-                .filter(StatusConsoleListener.class::isInstance)
-                .map(StatusConsoleListener.class::cast)
-                .findAny()
-                .orElseThrow(() -> new AssertionFailedError("Missing console status listener."));
-    }
+    private static final String CONFIG1_NAME = "XmlConfigurationPropsTest1";
 
     private void testConfiguration(
             final Configuration config,
             final String expectedConfigName,
-            final Level expectedStatusLevel,
-            final Level expectedRootLevel) {
+            @Nullable final Level expectedStatusLevel,
+            @Nullable final Level expectedRootLevel) {
         assertThat(config)
                 .isInstanceOf(XmlConfiguration.class)
                 .extracting(Configuration::getName)
                 .isEqualTo(expectedConfigName);
+        final StatusConsoleListener fallbackListener = StatusLogger.getLogger().getFallbackListener();
+        if (expectedStatusLevel == null) {
+            verify(fallbackListener, never()).setLevel(any());
+        } else {
+            verify(fallbackListener).setLevel(eq(expectedStatusLevel));
+        }
         assertThat(config.getRootLogger().getExplicitLevel()).isEqualTo(expectedRootLevel);
-        assertThat(findStatusConsoleListener().getStatusLevel()).isEqualTo(expectedStatusLevel);
     }
 
     @Test
+    @SetTestProperty(key = "status.level", value = "using gibberish values to enforce defaults")
+    @SetTestProperty(key = "root.level", value = "using gibberish values to enforce defaults")
     @LoggerContextSource
-    public void testNoProps(final Configuration config) {
-        testConfiguration(config, CONFIG_NAME, Level.ERROR, null);
+    void testNoProps(final Configuration config) {
+        testConfiguration(config, CONFIG_NAME, null, null);
     }
 
     @Test
     @SetTestProperty(key = Constants.LOG4J_DEFAULT_STATUS_LEVEL, value = "WARN")
     @LoggerContextSource(value = CONFIG1)
-    public void testDefaultStatus(final Configuration config) {
+    void testDefaultStatus(final Configuration config) {
         testConfiguration(config, CONFIG1_NAME, Level.WARN, null);
     }
 
     @Test
     @SetTestProperty(key = "status.level", value = "INFO")
     @LoggerContextSource
-    public void testWithConfigProp(final Configuration config) {
+    void testWithConfigProp(final Configuration config) {
         testConfiguration(config, CONFIG_NAME, Level.INFO, null);
     }
 
@@ -84,7 +89,7 @@ public class XmlConfigurationPropsTest {
     @SetTestProperty(key = "status.level", value = "INFO")
     @SetTestProperty(key = "root.level", value = "DEBUG")
     @LoggerContextSource
-    public void testWithProps(final Configuration config) {
+    void testWithProps(final Configuration config) {
         testConfiguration(config, CONFIG_NAME, Level.INFO, Level.DEBUG);
     }
 }
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/NestedLoggingFromToStringTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/NestedLoggingFromToStringTest.java
deleted file mode 100644
index 3eb9dd3496..0000000000
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/NestedLoggingFromToStringTest.java
+++ /dev/null
@@ -1,154 +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.impl;
-
-import static org.junit.Assert.assertEquals;
-
-import java.util.List;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-import org.apache.logging.log4j.core.test.appender.ListAppender;
-import org.apache.logging.log4j.core.test.junit.LoggerContextRule;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.Rule;
-import org.junit.Test;
-
-/**
- * There are two logger.info() calls here.
- *
- * The outer one (in main()) indirectly invokes the inner one in the Thing.toString() method.
- *
- * The inner one comes out cleanly, but leaves ReusableParameterizedMessage.indices altered and this messes up
- * the output of the outer call (which is still on the stack).
- *
- * <pre>
- *     16:35:34.781 INFO  [main] problem.demo.apache.log4j2.Log4j2ProblemDemo - getX: values x=3 y=4 z=5
- *     16:35:34.782 INFO  [main] problem.demo.apache.log4j2.Log4j2ProblemDemo - getX: values x=3 y=4 z=5[Thing x=3 y=4 z=5]
- * </pre>
- * @author lwest
- * @since 2016-09-14 in recursion
- */
-public class NestedLoggingFromToStringTest {
-
-    @BeforeClass
-    public static void beforeClass() {
-        System.setProperty("log4j2.is.webapp", "false");
-    }
-
-    @Rule
-    public LoggerContextRule context = new LoggerContextRule("log4j-sync-to-list.xml");
-
-    private ListAppender listAppender;
-    private Logger logger;
-
-    @Before
-    public void before() {
-        listAppender = context.getListAppender("List");
-        logger = LogManager.getLogger(NestedLoggingFromToStringTest.class);
-    }
-
-    static class ParameterizedLoggingThing {
-        final Logger innerLogger = LogManager.getLogger(ParameterizedLoggingThing.class);
-        private final int x = 3, y = 4, z = 5;
-
-        public int getX() {
-            innerLogger.debug("getX: values x={} y={} z={}", x, y, z);
-            return x;
-        }
-
-        @Override
-        public String toString() {
-            return "[" + this.getClass().getSimpleName() + " x=" + getX() + " y=" + y + " z=" + z + "]";
-        }
-    }
-
-    static class ObjectLoggingThing1 {
-        final Logger innerLogger = LogManager.getLogger(ObjectLoggingThing1.class);
-
-        public int getX() {
-            innerLogger.trace(new ObjectLoggingThing2());
-            return 999;
-        }
-
-        @Override
-        public String toString() {
-            return "[" + this.getClass().getSimpleName() + " y=" + getX() + "]";
-        }
-    }
-
-    static class ObjectLoggingThing2 {
-        final Logger innerLogger = LogManager.getLogger(ObjectLoggingThing2.class);
-
-        public int getX() {
-            innerLogger.trace(new ParameterizedLoggingThing());
-            return 123;
-        }
-
-        @Override
-        public String toString() {
-            return "[" + this.getClass().getSimpleName() + " x=" + getX() + "]";
-        }
-    }
-
-    @Test
-    public void testNestedLoggingInLastArgument() {
-        final ParameterizedLoggingThing it = new ParameterizedLoggingThing();
-        logger.info("main: argCount={} it={}", "2", it);
-        final List<String> list = listAppender.getMessages();
-
-        final String expect1 =
-                "DEBUG org.apache.logging.log4j.core.impl.NestedLoggingFromToStringTest.ParameterizedLoggingThing getX: values x=3 y=4 z=5";
-        final String expect2 =
-                "INFO org.apache.logging.log4j.core.impl.NestedLoggingFromToStringTest main: argCount=2 it=[ParameterizedLoggingThing x=3 y=4 z=5]";
-        assertEquals(expect1, list.get(0));
-        assertEquals(expect2, list.get(1));
-    }
-
-    @Test
-    public void testNestedLoggingInFirstArgument() {
-        final ParameterizedLoggingThing it = new ParameterizedLoggingThing();
-        logger.info("next: it={} some{} other{}", it, "AA", "BB");
-        final List<String> list = listAppender.getMessages();
-
-        final String expect1 =
-                "DEBUG org.apache.logging.log4j.core.impl.NestedLoggingFromToStringTest.ParameterizedLoggingThing getX: values x=3 y=4 z=5";
-        final String expect2 =
-                "INFO org.apache.logging.log4j.core.impl.NestedLoggingFromToStringTest next: it=[ParameterizedLoggingThing x=3 y=4 z=5] someAA otherBB";
-        assertEquals(expect1, list.get(0));
-        assertEquals(expect2, list.get(1));
-    }
-
-    @Test
-    public void testDoublyNestedLogging() {
-        logger.info(new ObjectLoggingThing1());
-        final List<String> list = listAppender.getMessages();
-
-        final String expect1 =
-                "DEBUG org.apache.logging.log4j.core.impl.NestedLoggingFromToStringTest.ParameterizedLoggingThing getX: values x=3 y=4 z=5";
-        final String expect2 =
-                "TRACE org.apache.logging.log4j.core.impl.NestedLoggingFromToStringTest.ObjectLoggingThing2 [ParameterizedLoggingThing x=3 y=4 z=5]";
-        final String expect3 =
-                "TRACE org.apache.logging.log4j.core.impl.NestedLoggingFromToStringTest.ObjectLoggingThing1 [ObjectLoggingThing2 x=123]";
-        final String expect4 =
-                "INFO org.apache.logging.log4j.core.impl.NestedLoggingFromToStringTest [ObjectLoggingThing1 y=999]";
-        assertEquals(expect1, list.get(0));
-        assertEquals(expect2, list.get(1));
-        assertEquals(expect3, list.get(2));
-        assertEquals(expect4, list.get(3));
-    }
-}
diff --git a/log4j-core-test/src/test/resources/log4j-sync-to-list.xml b/log4j-core-test/src/test/resources/log4j-sync-to-list.xml
deleted file mode 100644
index e0e95c2fd0..0000000000
--- a/log4j-core-test/src/test/resources/log4j-sync-to-list.xml
+++ /dev/null
@@ -1,34 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-  ~ Licensed to the Apache Software Foundation (ASF) under one or more
-  ~ contributor license agreements.  See the NOTICE file distributed with
-  ~ this work for additional information regarding copyright ownership.
-  ~ The ASF licenses this file to you under the Apache License, Version 2.0
-  ~ (the "License"); you may not use this file except in compliance with
-  ~ the License.  You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<Configuration status="OFF" name="NestedLoggingInToString">
-
-  <Appenders>
-    <Console name="STDOUT">
-      <PatternLayout pattern="%c %m%n"/>
-    </Console>
-    <List name="List">
-      <PatternLayout pattern="%level %logger %m"/>
-    </List>
-  </Appenders>
-
-  <Loggers>
-    <Root level="trace">
-      <AppenderRef ref="List"/>
-    </Root>
-  </Loggers>
-</Configuration>
diff --git a/log4j-core-test/src/test/resources/org/apache/logging/log4j/core/async/QueueFullAsyncAppenderTest.xml b/log4j-core-test/src/test/resources/org/apache/logging/log4j/core/async/QueueFullAsyncAppender1Test.xml
similarity index 100%
rename from log4j-core-test/src/test/resources/org/apache/logging/log4j/core/async/QueueFullAsyncAppenderTest.xml
rename to log4j-core-test/src/test/resources/org/apache/logging/log4j/core/async/QueueFullAsyncAppender1Test.xml
diff --git a/log4j-core-test/src/test/resources/org/apache/logging/log4j/core/async/QueueFullAsyncLoggerConfigLoggingFromToStringTest.xml b/log4j-core-test/src/test/resources/org/apache/logging/log4j/core/async/QueueFullAsyncLoggerConfig1Test.xml
similarity index 100%
rename from log4j-core-test/src/test/resources/org/apache/logging/log4j/core/async/QueueFullAsyncLoggerConfigLoggingFromToStringTest.xml
rename to log4j-core-test/src/test/resources/org/apache/logging/log4j/core/async/QueueFullAsyncLoggerConfig1Test.xml
diff --git a/log4j-core-test/src/test/resources/org/apache/logging/log4j/core/async/QueueFullAsyncLoggerConfigTest.xml b/log4j-core-test/src/test/resources/org/apache/logging/log4j/core/async/QueueFullAsyncLoggerConfigTest.xml
deleted file mode 100644
index 5322b67a1e..0000000000
--- a/log4j-core-test/src/test/resources/org/apache/logging/log4j/core/async/QueueFullAsyncLoggerConfigTest.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-  ~ Licensed to the Apache Software Foundation (ASF) under one or more
-  ~ contributor license agreements.  See the NOTICE file distributed with
-  ~ this work for additional information regarding copyright ownership.
-  ~ The ASF licenses this file to you under the Apache License, Version 2.0
-  ~ (the "License"); you may not use this file except in compliance with
-  ~ the License.  You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<Configuration status="OFF" name="QueueFullAsyncLoggerConfigTest">
-  <Appenders>
-    <Blocking name="Blocking" />
-  </Appenders>
-  <Loggers>
-    <AsyncRoot level="debug">
-      <AppenderRef ref="Blocking" />
-    </AsyncRoot>
-  </Loggers>
-</Configuration>
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/status/StatusConfiguration.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/status/StatusConfiguration.java
index 616bdfd088..1a1c22835a 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/status/StatusConfiguration.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/status/StatusConfiguration.java
@@ -16,38 +16,37 @@
  */
 package org.apache.logging.log4j.core.config.status;
 
+import edu.umd.cs.findbugs.annotations.Nullable;
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.PrintStream;
 import java.net.URI;
 import java.net.URISyntaxException;
-import java.util.Collection;
-import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.core.util.FileUtils;
 import org.apache.logging.log4j.core.util.NetUtils;
 import org.apache.logging.log4j.status.StatusConsoleListener;
-import org.apache.logging.log4j.status.StatusListener;
 import org.apache.logging.log4j.status.StatusLogger;
 
 /**
- * Configuration for setting up {@link StatusConsoleListener} instances.
+ * Configuration for setting up the {@link StatusLogger} fallback listener.
  */
 public class StatusConfiguration {
 
-    @SuppressWarnings("UseOfSystemOutOrSystemErr")
-    private static final PrintStream DEFAULT_STREAM = System.out;
+    private static final StatusLogger LOGGER = StatusLogger.getLogger();
 
-    private static final Level DEFAULT_STATUS = Level.ERROR;
-
-    private final Collection<String> errorMessages = new LinkedBlockingQueue<>();
-    private final StatusLogger logger = StatusLogger.getLogger();
+    private final Lock lock = new ReentrantLock();
 
     private volatile boolean initialized;
 
-    private PrintStream destination = DEFAULT_STREAM;
-    private Level status = DEFAULT_STATUS;
+    @Nullable
+    private PrintStream output;
+
+    @Nullable
+    private Level level;
 
     /**
      * Specifies how verbose the StatusLogger should be.
@@ -72,82 +71,86 @@ public class StatusConfiguration {
     }
 
     /**
-     * Logs an error message to the StatusLogger. If the StatusLogger hasn't been set up yet, queues the message to be
-     * logged after initialization.
+     * Logs an error message to the {@link StatusLogger}.
      *
-     * @param message error message to log.
+     * @param message error message to log
+     * @deprecated Use {@link StatusLogger#getLogger()} and then {@link StatusLogger#error(String)} instead.
      */
+    @Deprecated
     public void error(final String message) {
-        if (!this.initialized) {
-            this.errorMessages.add(message);
-        } else {
-            this.logger.error(message);
-        }
+        LOGGER.error(message);
     }
 
     /**
-     * Specifies the destination for StatusLogger events. This can be {@code out} (default) for using
-     * {@link System#out standard out}, {@code err} for using {@link System#err standard error}, or a file URI to
-     * which log events will be written. If the provided URI is invalid, then the default destination of standard
-     * out will be used.
+     * Sets the output of the {@link StatusLogger} fallback listener.
+     * <p>
+     * Accepted values are as follows:
+     * </p>
+     * <ul>
+     * <li>{@code out} (i.e., {@link System#out})</li>
+     * <li>{@code err} (i.e., {@link System#err})</li>
+     * <li>a URI (e.g., {@code file:///path/to/log4j-status-logs.txt})</li>
+     * </ul>
+     * <p>
+     * Invalid values will be ignored.
+     * </p>
      *
-     * @param destination where status log messages should be output.
+     * @param destination destination where {@link StatusLogger} messages should be output
      * @return {@code this}
      */
-    public StatusConfiguration withDestination(final String destination) {
+    public StatusConfiguration withDestination(@Nullable final String destination) {
         try {
-            this.destination = parseStreamName(destination);
-        } catch (final URISyntaxException e) {
-            this.error("Could not parse URI [" + destination + "]. Falling back to default of stdout.");
-            this.destination = DEFAULT_STREAM;
-        } catch (final FileNotFoundException e) {
-            this.error("File could not be found at [" + destination + "]. Falling back to default of stdout.");
-            this.destination = DEFAULT_STREAM;
+            this.output = parseStreamName(destination);
+        } catch (final URISyntaxException error) {
+            LOGGER.error("Could not parse provided URI: {}", destination, error);
+        } catch (final FileNotFoundException error) {
+            LOGGER.error("File could not be found: {}", destination, error);
         }
         return this;
     }
 
-    private PrintStream parseStreamName(final String name) throws URISyntaxException, FileNotFoundException {
-        if (name == null || name.equalsIgnoreCase("out")) {
-            return DEFAULT_STREAM;
-        }
-        if (name.equalsIgnoreCase("err")) {
-            return System.err;
-        }
-        final URI destUri = NetUtils.toURI(name);
-        final File output = FileUtils.fileFromUri(destUri);
-        if (output == null) {
-            // don't want any NPEs, no sir
-            return DEFAULT_STREAM;
+    @Nullable
+    private static PrintStream parseStreamName(@Nullable final String name)
+            throws URISyntaxException, FileNotFoundException {
+        if (name != null) {
+            if (name.equalsIgnoreCase("out")) {
+                return System.out;
+            } else if (name.equalsIgnoreCase("err")) {
+                return System.err;
+            } else {
+                final URI destUri = NetUtils.toURI(name);
+                final File output = FileUtils.fileFromUri(destUri);
+                if (output != null) {
+                    final FileOutputStream fos = new FileOutputStream(output);
+                    return new PrintStream(fos, true);
+                }
+            }
         }
-        final FileOutputStream fos = new FileOutputStream(output);
-        return new PrintStream(fos, true);
+        return null;
     }
 
     /**
-     * Specifies the logging level by name to use for filtering StatusLogger messages.
+     * Sets the level of the {@link StatusLogger} fallback listener.
      *
-     * @param status name of logger level to filter below.
+     * @param level a level name
      * @return {@code this}
-     * @see Level
      */
-    public StatusConfiguration withStatus(final String status) {
-        this.status = Level.toLevel(status, null);
-        if (this.status == null) {
-            this.error("Invalid status level specified: " + status + ". Defaulting to ERROR.");
-            this.status = Level.ERROR;
+    public StatusConfiguration withStatus(@Nullable final String level) {
+        this.level = Level.toLevel(level, null);
+        if (this.level == null) {
+            LOGGER.error("Invalid status level: {}", level);
         }
         return this;
     }
 
     /**
-     * Specifies the logging level to use for filtering StatusLogger messages.
+     * Sets the level of the {@link StatusLogger} fallback listener.
      *
-     * @param status logger level to filter below.
+     * @param level a level
      * @return {@code this}
      */
-    public StatusConfiguration withStatus(final Level status) {
-        this.status = status;
+    public StatusConfiguration withStatus(@Nullable final Level level) {
+        this.level = level;
         return this;
     }
 
@@ -180,38 +183,20 @@ public class StatusConfiguration {
      * Configures and initializes the StatusLogger using the configured options in this instance.
      */
     public void initialize() {
-        if (!this.initialized) {
-            if (this.status == Level.OFF) {
-                this.initialized = true;
-            } else {
-                final boolean configured = configureExistingStatusConsoleListener();
-                if (!configured) {
-                    final StatusConsoleListener listener = new StatusConsoleListener(this.status, this.destination);
-                    this.logger.registerListener(listener);
+        lock.lock();
+        try {
+            if (!this.initialized) {
+                final StatusConsoleListener fallbackListener = LOGGER.getFallbackListener();
+                if (output != null) {
+                    fallbackListener.setStream(output);
                 }
-                migrateSavedLogMessages();
-            }
-        }
-    }
-
-    private boolean configureExistingStatusConsoleListener() {
-        boolean configured = false;
-        for (final StatusListener statusListener : this.logger.getListeners()) {
-            if (statusListener instanceof StatusConsoleListener) {
-                final StatusConsoleListener listener = (StatusConsoleListener) statusListener;
-                listener.setLevel(this.status);
-                this.logger.updateListenerLevel(this.status);
-                configured = true;
+                if (level != null) {
+                    fallbackListener.setLevel(level);
+                }
+                initialized = true;
             }
+        } finally {
+            lock.unlock();
         }
-        return configured;
-    }
-
-    private void migrateSavedLogMessages() {
-        for (final String message : this.errorMessages) {
-            this.logger.error(message);
-        }
-        this.initialized = true;
-        this.errorMessages.clear();
     }
 }
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/status/package-info.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/status/package-info.java
index e334546cb2..1e20784cf9 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/status/package-info.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/status/package-info.java
@@ -19,7 +19,7 @@
  * Configuration classes for the {@link org.apache.logging.log4j.status.StatusLogger} API.
  */
 @Export
-@Version("2.20.1")
+@Version("2.20.2")
 package org.apache.logging.log4j.core.config.status;
 
 import org.osgi.annotation.bundle.Export;
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/jmx/StatusLoggerAdmin.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/jmx/StatusLoggerAdmin.java
index 03611c6c10..5ffae9c403 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/jmx/StatusLoggerAdmin.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/jmx/StatusLoggerAdmin.java
@@ -20,9 +20,15 @@ import java.io.IOException;
 import java.util.List;
 import java.util.concurrent.Executor;
 import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 import javax.management.MBeanNotificationInfo;
+import javax.management.MBeanRegistration;
+import javax.management.MBeanServer;
 import javax.management.Notification;
 import javax.management.NotificationBroadcasterSupport;
+import javax.management.NotificationFilter;
+import javax.management.NotificationListener;
 import javax.management.ObjectName;
 import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.status.StatusData;
@@ -33,13 +39,17 @@ import org.apache.logging.log4j.status.StatusLogger;
  * Implementation of the {@code StatusLoggerAdminMBean} interface.
  */
 public class StatusLoggerAdmin extends NotificationBroadcasterSupport
-        implements StatusListener, StatusLoggerAdminMBean {
+        implements StatusListener, StatusLoggerAdminMBean, MBeanRegistration {
 
     private final AtomicLong sequenceNo = new AtomicLong();
     private final ObjectName objectName;
     private final String contextName;
     private Level level = Level.WARN;
 
+    private boolean statusListenerRegistered = false;
+
+    private final Lock statusListenerRegistrationGuard = new ReentrantLock();
+
     /**
      * Constructs a new {@code StatusLoggerAdmin} with the {@code Executor} to
      * be used for sending {@code Notification}s asynchronously to listeners.
@@ -62,27 +72,6 @@ public class StatusLoggerAdmin extends NotificationBroadcasterSupport
         } catch (final Exception e) {
             throw new IllegalStateException(e);
         }
-        removeListeners(contextName);
-        StatusLogger.getLogger().registerListener(this);
-    }
-
-    /**
-     * Add listener to StatusLogger for this context, or replace it if it already exists.
-     *
-     * @param ctxName
-     */
-    private void removeListeners(final String ctxName) {
-        final StatusLogger logger = StatusLogger.getLogger();
-        final Iterable<StatusListener> listeners = logger.getListeners();
-        // Remove any StatusLoggerAdmin listeners already registered for this context
-        for (final StatusListener statusListener : listeners) {
-            if (statusListener instanceof StatusLoggerAdmin) {
-                final StatusLoggerAdmin adminListener = (StatusLoggerAdmin) statusListener;
-                if (ctxName != null && ctxName.equals(adminListener.contextName)) {
-                    logger.removeListener(adminListener);
-                }
-            }
-        }
     }
 
     private static MBeanNotificationInfo createNotificationInfo() {
@@ -92,6 +81,61 @@ public class StatusLoggerAdmin extends NotificationBroadcasterSupport
         return new MBeanNotificationInfo(notifTypes, name, description);
     }
 
+    /// BEGIN: Conditional `StatusListener` registration ///////////////////////////////////////////////////////////////
+
+    // `StatusLogger` contains a _fallback listener_ that defaults to a `StatusConsoleListener`.
+    // It is used to propagate logs when no listeners are available.
+    // If JMX registers itself always, unconditionally, this would render the fallback (console) listener ineffective.
+    // That is, no status logs would be written to console when `StatusLoggerAdmin` is in the classpath.
+    // To avoid this undesired behaviour, we register JMX status listener conditionally:
+    // only when there is a party interested in these notifications.
+    // `addNotificationListener()` is a good place to figure out such an interest.
+    // Though `removeNotificationListener()` is not a good place to determine the lack of interest[1].
+    // Hence, we remove the JMX status listener on JMX bean deregistration.
+    //
+    // [1] https://github.com/apache/logging-log4j2/pull/2249#discussion_r1469336334
+
+    @Override
+    public void addNotificationListener(
+            final NotificationListener listener, final NotificationFilter filter, final Object handback) {
+        super.addNotificationListener(listener, filter, handback);
+        statusListenerRegistrationGuard.lock();
+        try {
+            if (!statusListenerRegistered) {
+                StatusLogger.getLogger().registerListener(this);
+                statusListenerRegistered = true;
+            }
+        } finally {
+            statusListenerRegistrationGuard.unlock();
+        }
+    }
+
+    @Override
+    public ObjectName preRegister(final MBeanServer server, final ObjectName name) {
+        return name;
+    }
+
+    @Override
+    public void postRegister(final Boolean registrationDone) {}
+
+    @Override
+    public void preDeregister() {}
+
+    @Override
+    public void postDeregister() {
+        statusListenerRegistrationGuard.lock();
+        try {
+            if (statusListenerRegistered) {
+                StatusLogger.getLogger().removeListener(this);
+                statusListenerRegistered = false;
+            }
+        } finally {
+            statusListenerRegistrationGuard.unlock();
+        }
+    }
+
+    /// END: Conditional `StatusListener` registration /////////////////////////////////////////////////////////////////
+
     @Override
     public String[] getStatusDataHistory() {
         final List<StatusData> data = getStatusData();
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/jmx/package-info.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/jmx/package-info.java
index 07c55a1e0b..480cc2c6b2 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/jmx/package-info.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/jmx/package-info.java
@@ -18,7 +18,7 @@
  * Log4j 2 JMX support.
  */
 @Export
-@Version("2.20.2")
+@Version("2.23.0")
 package org.apache.logging.log4j.core.jmx;
 
 import org.osgi.annotation.bundle.Export;