diff --git a/log4j-api-test/pom.xml b/log4j-api-test/pom.xml index c243fad2326..0ef2578dd8f 100644 --- a/log4j-api-test/pom.xml +++ b/log4j-api-test/pom.xml @@ -138,7 +138,6 @@ org.mockito mockito-core - test org.mockito 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 7f4c9c0702c..7ca5776c583 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.ModifierSupport; import org.junit.platform.commons.support.ReflectionSupport; -class StatusLoggerExtension extends TypeBasedParameterResolver +class StatusListenerExtension extends TypeBasedParameterResolver implements BeforeAllCallback, BeforeEachCallback, TestExecutionExceptionHandler { private static final Object KEY = ListStatusListener.class; - public StatusLoggerExtension() { + public StatusListenerExtension() { super(ListStatusListener.class); } @@ -130,33 +125,19 @@ public ListStatusListener getStatusListener() { } @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 @@ public Level getStatusLevel() { } @Override - public void close() throws IOException { + public void close() { // NOP } @Override public Stream getStatusData() { synchronized (statusData) { - final List clone = (List) statusData.clone(); + final List 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 00000000000..2725dc7314a --- /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. + *

+ * Warning! + * 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. + *

+ * + * @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 e54deb6b429..2ca8124b03a 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 @@ @Documented @ExtendWith(ExtensionContextAnchor.class) @ExtendWith(TestPropertyResolver.class) -@ExtendWith(StatusLoggerExtension.class) +@ExtendWith(StatusListenerExtension.class) public @interface UsingStatusListener {} diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncLoggerConfigLoggingFromToStringTest2.java b/log4j-api-test/src/main/java/org/apache/logging/log4j/test/junit/UsingStatusLoggerMock.java similarity index 52% rename from log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncLoggerConfigLoggingFromToStringTest2.java rename to log4j-api-test/src/main/java/org/apache/logging/log4j/test/junit/UsingStatusLoggerMock.java index 54e5a78a97f..105eef9e2d2 100644 --- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncLoggerConfigLoggingFromToStringTest2.java +++ b/log4j-api-test/src/main/java/org/apache/logging/log4j/test/junit/UsingStatusLoggerMock.java @@ -14,24 +14,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.logging.log4j.core.async; +package org.apache.logging.log4j.test.junit; -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; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import org.junit.jupiter.api.extension.ExtendWith; /** - * Tests queue full scenarios with AsyncLoggers in configuration. + * Shortcut to {@link StatusLoggerMockExtension}. */ -@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(); - } -} +@Retention(RUNTIME) +@Target({TYPE, METHOD}) +@Documented +@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 194fb330130..e2ec325ce74 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.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 48d9a7346fc..0b93ae1fbcd 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 @@ 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 00000000000..5dedd835edf --- /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 8620a5b1714..d9543f17b52 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.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 DEFAULT_MESSAGE_FACTORY_CLASS = createClassForProperty( - "log4j2.messageFactory", ReusableMessageFactory.class, ParameterizedMessageFactory.class); + public static final Class DEFAULT_MESSAGE_FACTORY_CLASS = + ParameterizedMessageFactory.class; /** * The default FlowMessageFactory class. */ public static final Class DEFAULT_FLOW_MESSAGE_FACTORY_CLASS = - createFlowClassForProperty("log4j2.flowMessageFactory", DefaultFlowMessageFactory.class); + DefaultFlowMessageFactory.class; private static final long serialVersionUID = 2L; @@ -198,32 +197,6 @@ protected Message catchingMsg(final Throwable throwable) { return messageFactory.newMessage(CATCHING); } - private static Class createClassForProperty( - final String property, - final Class reusableParameterizedMessageFactoryClass, - final Class 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 createFlowClassForProperty( - final String property, final Class 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 85e714b8f49..00000000000 --- 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 5961b20cbfd..3e30bf19942 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 StatusConsoleListener(final Level level) { * @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 void setFilters(final String... filters) {} @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 10883be8497..e3b13e8dedc 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 StackTraceElement getStackTraceElement() { /** * 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 Level getLevel() { /** * 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 Throwable getThrowable() { @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 e224eb1045e..af6d6687900 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 } 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. + *

Listeners

+ *

+ * Each recorded event will first get buffered and then used to notify the registered {@link StatusListener}s. + * If none are available, the fallback listener of type {@link StatusConsoleListener} will be used. + *

+ *

+ * You can programmatically register listeners using {@link #registerListener(StatusListener)} method. + *

+ *

Configuration

+ *

+ * The {@code StatusLogger} can be configured in following ways: + *

+ *
    + *
  1. Passing system properties to the Java process (e.g., {@code -Dlog4j2.StatusLogger.level=INFO})
  2. + *
  3. Providing properties in a {@value StatusLogger#PROPERTIES_FILE_NAME} file in the classpath
  4. + *
  5. Using Log4j configuration (i.e., {@code } in a {@code log4j2.xml} in the classpath)
  6. + *
+ *

+ * 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: + *

+ *
    + *
  1. The default level (of fallback listener) is {@code ERROR}
  2. + *
  3. You have {@code } in your {@code log4j2.xml}
  4. + *
  5. Until your {@code log4j2.xml} configuration is read, the effective level will be {@code ERROR}
  6. + *
  7. Once your {@code log4j2.xml} configuration is read, the effective level will be {@code WARN} as you configured
  8. + *
+ *

+ * 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. + *

+ *

+ * {@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 } while the other {@code }), the last loaded configuration will be effective one. + *

+ *

Configuration properties

+ *

+ * The list of available properties for configuring the {@code StatusLogger} is shared below. + *

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
available properties for configuring the StatusLogger
NameDefaultDescription
{@value MAX_STATUS_ENTRIES}0 + * The maximum number of events buffered. + * Once the limit is reached, older entries will be removed as new entries are added. + *
{@value DEFAULT_STATUS_LISTENER_LEVEL}{@code ERROR} + * The {@link Level} name to use as the fallback listener level.
+ * 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. + *
{@value STATUS_DATE_FORMAT}{@code null}A {@link java.time.format.DateTimeFormatter} pattern to format the created {@link StatusData}.
{@value #DEBUG_PROPERTY_NAME}falseThe debug mode toggle.
+ *

Debug mode

+ *

+ * 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 fallback listener of type {@link StatusConsoleListener} will be used. + *

*/ -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. + *

+ * This is a local clone of {@link Constants#LOG4J2_DEBUG}. + * The cloning is necessary to avoid cyclic initialization. + *

+ */ + 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. + *

+ * Once the limit is reached, older entries will be removed as new entries are added. + *

*/ 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. + *

+ * 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. + *

+ * + * @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. + * Users should not create new instances, but use {@link #getInstance()} instead! + * + * @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. + *

+ * 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. + *

+ * + * @see Double-checked locking: Clever, but broken + */ + 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 listeners = new CopyOnWriteArrayList<>(); + private final List listeners; - @SuppressWarnings("NonSerializableFieldInSerializableClass") - // ReentrantReadWriteLock is Serializable - private final ReadWriteLock listenersLock = new ReentrantReadWriteLock(); + private final transient ReadWriteLock listenerLock = new ReentrantReadWriteLock(); - private final Queue 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 buffer = new ConcurrentLinkedQueue<>(); /** - * Constructs the singleton instance for the STATUS_LOGGER constant. - *

- * This is now the logger level is set: - *

- *
    - *
  1. If the property {@value Constants#LOG4J2_DEBUG} is {@code "true"}, then use {@link Level#TRACE}, otherwise,
  2. - *
  3. Use {@link Level#ERROR}
  4. - *
- *

- * This is now the listener level is set: - *

- *
    - *
  1. If the property {@value #DEFAULT_STATUS_LISTENER_LEVEL} is set, then use it, otherwise,
  2. - *
  3. Use {@link Level#WARN}
  4. - *
- *

- * See: - *

    - *
  1. 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.
  2. - *
  3. LOG4J2-3340 StatusLogger's log Level cannot be changed as advertised.
  4. - *
- *

+ * 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. + * Users should not create new instances, but use {@link #getLogger()} instead! * - * @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 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 registered (not the fallback one!) listeners. */ public void reset() { - listenersLock.writeLock().lock(); + listenerWriteLock.lock(); try { - for (final StatusListener listener : listeners) { - closeSilently(listener); + final Iterator 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 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 caller - * @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 @@ private StackTraceElement getStackTraceElement(final String fqcn, final StackTra } if (fqcn.equals(className)) { next = true; - } else if (NOT_AVAIL.equals(className)) { + } else if ("?".equals(className)) { break; } } @@ -320,7 +583,7 @@ private StackTraceElement getStackTraceElement(final String fqcn, final StackTra } @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 boolean isEnabled( } @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 Object type to be stored in the queue. - */ - private class BoundedQueue extends ConcurrentLinkedQueue { - - 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 61af0c23479..407e82cfc0a 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 34b272856fb..f96bffd28d4 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 @@ 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 @@ */ 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 e12841cee27..dc73c5056f6 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.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 @@ @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 int getPriority() { 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 51ad5675eac..001ad3a5f5b 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.LinkedHashSet; import java.util.Objects; import java.util.function.Supplier; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.status.StatusLogger; /** * Consider this class private. Utility class for ClassLoaders. @@ -39,6 +41,8 @@ @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 static boolean isClassAvailable(final String className) { 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 @@ static Collection findUrlResources(final String resource, final boo 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 8f288566879..00000000000 --- 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 95f5588e829..d061ccbd6db 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 @@ 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 @@ 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 @@ private static boolean checkOsgiAvailable() { 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 3084a6d816d..daf9163203a 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.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentSkipListSet; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.status.StatusLogger; /** * Consider this class private. @@ -54,8 +56,12 @@ @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 COMPONENT_PROPERTIES = Lazy.lazy(() -> new PropertiesUtil(LOG4J_PROPERTIES_FILE_NAME, false)); @@ -104,13 +110,13 @@ static Properties loadClose(final InputStream in, final Object source) { 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 Charset getCharsetProperty(final String name, final Charset defaultValue) 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 String getStringProperty(final String name, final String defaultValue) { 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 31d28d4f2c0..3440af04602 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.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 @@ */ public class PropertyFilePropertySource extends PropertiesPropertySource { + private static final Logger LOGGER = StatusLogger.getLogger(); + public PropertyFilePropertySource(final String fileName) { this(fileName, true); } @@ -45,8 +49,8 @@ private static Properties loadPropertiesFile(final String fileName, final boolea 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 7fff12d1667..bc20af0daf3 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.util.ArrayDeque; import java.util.Deque; import java.util.function.Predicate; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.status.StatusLogger; /** * Consider this class private. Provides various methods to determine the caller class. @@ -51,6 +53,8 @@ @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 7abb2dd95d0..c5e25a15c66 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.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 = StatusLogger.getLogger().getStatusData(); - StatusData data = statusData.get(0); - if (data.getMessage().getFormattedMessage().contains("WindowsAnsiOutputStream")) { - data = statusData.get(1); + StatusLogger statusLogger = StatusLogger.getLogger(); + final List 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 49147e3805e..f9f37521857 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 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 void testExceptionsAreLoggedToStatusLogger() { } @Test - public void testRuntimeExceptionsAreLoggedToStatusLogger() { + void testRuntimeExceptionsAreLoggedToStatusLogger() { final StatusLogger statusLogger = StatusLogger.getLogger(); statusLogger.clear(); new AbstractAction() { @@ -71,7 +72,7 @@ public boolean execute() { } @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 f8cde81950e..8762a4a4912 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.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 void testAccept() { } @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 void testBeforeTreeWalk() { } @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 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 aac3eaa326e..7ed92516851 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.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 b731d25f118..333af1ebd46 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.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 698c6d6dee5..2398fb2a501 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.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 48d40a263b9..57734ed983e 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.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.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 void run() { } } - 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 @@ protected static void asyncTest( 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 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 transform(final List logEvents) { final List 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 0e73bf8c2b1..43329dc1eae 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 @@ /** * 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 d3886818c8f..4eab6286a09 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 @@ * 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 1aaabfe48dd..fe3f42e043f 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 @@ 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 6271849278f..fbfce3951bb 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 @@ */ @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 7a048091d26..3be89c6f9bd 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 @@ @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 93db0470ed4..e797792ddb1 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 @@ */ @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 4c56595a405..a88d4568bd0 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 @@ */ @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 e60e8bea227..00000000000 --- 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/QueueFullAsyncLoggerLoggingFromToStringTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/QueueFullAsyncLoggerLoggingFromToStringTest.java deleted file mode 100644 index 8e98f61c4ee..00000000000 --- 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 a75977e2f31..00000000000 --- 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 325466cf3d6..70e824937e5 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.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 8d467fa6e4f..e7a13f12edc 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 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 void testWithConfigProp(final Configuration config) { @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 3eb9dd34960..00000000000 --- 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). - * - *
- *     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]
- * 
- * @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 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 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 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 e0e95c2fd0e..00000000000 --- a/log4j-core-test/src/test/resources/log4j-sync-to-list.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - 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 5322b67a1e6..00000000000 --- a/log4j-core-test/src/test/resources/org/apache/logging/log4j/core/async/QueueFullAsyncLoggerConfigTest.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - 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 616bdfd0881..1a1c22835a7 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 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 static Verbosity toVerbosity(final String value) { } /** - * 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. + *

+ * Accepted values are as follows: + *

+ *
    + *
  • {@code out} (i.e., {@link System#out})
  • + *
  • {@code err} (i.e., {@link System#err})
  • + *
  • a URI (e.g., {@code file:///path/to/log4j-status-logs.txt})
  • + *
+ *

+ * Invalid values will be ignored. + *

* - * @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 StatusConfiguration withVerboseClasses(final String... verboseClasses) { * 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 e334546cb24..1e20784cf97 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 03611c6c104..5ffae9c4038 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.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 @@ * 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 StatusLoggerAdmin(final String contextName, final Executor executor) { } 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 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 @@ private static MBeanNotificationInfo createNotificationInfo() { 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 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 07c55a1e0b0..480cc2c6b27 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;