This is an automated email from the ASF dual-hosted git repository.
vy pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git
The following commit(s) were added to refs/heads/main by this push:
new 0784be4ff2 Prefix stack traces with a newline in Pattern Layout (#3073)
0784be4ff2 is described below
commit 0784be4ff2cddb8267bcb15e5736a9533b3ad2f0
Author: Volkan Yazıcı <[email protected]>
AuthorDate: Thu Oct 10 11:02:04 2024 +0200
Prefix stack traces with a newline in Pattern Layout (#3073)
- Exception converters are reworked to ensure a newline
prefix (which used to be a whitespace)
- Fix property extraction for root exceptions
(e.g., %rEx{short.className})
- Ports #3045
Co-authored-by: AlanYu <[email protected]>
Co-authored-by: Piotr P. Karwasz <[email protected]>
---
.../AsyncLoggerTestArgumentFreedOnErrorTest.java | 64 +++---
.../async/logger/QueueFullAsyncLogger3Test.java | 82 ++------
.../log4j/core/GarbageCollectionHelper.java | 68 ------
.../logging/log4j/core/test/internal/GcHelper.java | 54 +++++
.../core/test/internal/GcPressureGenerator.java | 81 ++++++++
.../src/test/java/foo/TestFriendlyException.java | 27 ++-
.../log4j/core/EventParameterMemoryLeakTest.java | 138 ++++++++-----
...ReusableParameterizedMessageMemoryLeakTest.java | 34 ++-
.../rolling/RollingAppenderDirectWriteTest.java | 5 +-
.../NestedLoggingFromThrowableMessageTest.java | 7 +-
.../logging/log4j/core/internal/GcHelperTest.java | 29 +++
.../PatternLayoutDefaultExceptionHandlerTest.java | 75 +++++++
.../ExtendedThrowablePatternConverterTest.java | 9 +-
.../pattern/RootThrowablePatternConverterTest.java | 27 ++-
.../pattern/ThrowablePatternConverterTest.java | 71 ++++---
.../logging/log4j/core/pattern/ThrowableTest.java | 229 ---------------------
.../logging/log4j/core/util/ThrowablesTest.java | 19 +-
.../resources/EventParameterMemoryLeakTest.xml | 35 ----
.../pattern/ExtendedThrowablePatternConverter.java | 8 +-
.../pattern/RootThrowablePatternConverter.java | 13 +-
...ThrowableExtendedStackTraceRendererFactory.java | 31 +++
.../ThrowableInvertedPropertyRendererFactory.java | 40 ++++
.../ThrowableInvertedStackTraceRenderer.java | 2 +-
...ThrowableInvertedStackTraceRendererFactory.java | 31 +++
.../core/pattern/ThrowablePatternConverter.java | 55 ++---
.../core/pattern/ThrowablePropertyRenderer.java | 98 ---------
.../pattern/ThrowablePropertyRendererFactory.java | 120 +++++++++++
.../core/pattern/ThrowableStackTraceRenderer.java | 10 +
.../ThrowableStackTraceRendererFactory.java | 33 +++
.../apache/logging/log4j/core/util/Throwables.java | 37 ++--
.../json/resolver/StackTraceStringResolver.java | 6 +-
.../modules/ROOT/pages/manual/pattern-layout.adoc | 150 ++++++++------
32 files changed, 899 insertions(+), 789 deletions(-)
diff --git
a/log4j-async-logger/src/test/java/org/apache/logging/log4j/async/logger/AsyncLoggerTestArgumentFreedOnErrorTest.java
b/log4j-async-logger/src/test/java/org/apache/logging/log4j/async/logger/AsyncLoggerTestArgumentFreedOnErrorTest.java
index 68c98d42ba..b3ae9d5999 100644
---
a/log4j-async-logger/src/test/java/org/apache/logging/log4j/async/logger/AsyncLoggerTestArgumentFreedOnErrorTest.java
+++
b/log4j-async-logger/src/test/java/org/apache/logging/log4j/async/logger/AsyncLoggerTestArgumentFreedOnErrorTest.java
@@ -16,63 +16,55 @@
*/
package org.apache.logging.log4j.async.logger;
-import static org.junit.jupiter.api.Assertions.assertTrue;
+import static
org.apache.logging.log4j.core.test.internal.GcHelper.awaitGarbageCollection;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.core.GarbageCollectionHelper;
+import org.apache.logging.log4j.core.Logger;
+import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.test.TestConstants;
-import org.apache.logging.log4j.core.test.junit.ContextSelectorType;
import org.apache.logging.log4j.message.Message;
+import org.apache.logging.log4j.plugins.di.DI;
import org.apache.logging.log4j.test.junit.SetTestProperty;
+import org.apache.logging.log4j.test.junit.UsingStatusListener;
import org.apache.logging.log4j.util.StringBuilderFormattable;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
@Tag("async")
-@SetTestProperty(key = TestConstants.GC_ENABLE_DIRECT_ENCODERS, value = "true")
-@SetTestProperty(key = TestConstants.ASYNC_FORMAT_MESSAGES_IN_BACKGROUND,
value = "true")
-@ContextSelectorType(AsyncLoggerContextSelector.class)
-public class AsyncLoggerTestArgumentFreedOnErrorTest {
+class AsyncLoggerArgumentFreedOnErrorTest {
- // LOG4J2-2725: events are cleared even after failure
+ /**
+ * Tests events are cleared even after failure.
+ *
+ * @see <a
href="https://issues.apache.org/jira/browse/LOG4J2-2725">LOG4J2-2725</a>
+ */
@Test
- public void testMessageIsGarbageCollected() throws Exception {
- final AsyncLogger log = (AsyncLogger)
LogManager.getLogger("com.foo.Bar");
- final CountDownLatch garbageCollectionLatch = new CountDownLatch(1);
- log.fatal(new ThrowingMessage(garbageCollectionLatch));
- try (final GarbageCollectionHelper gcHelper = new
GarbageCollectionHelper()) {
- gcHelper.run();
- assertTrue(
- garbageCollectionLatch.await(30, TimeUnit.SECONDS),
"Parameter should have been garbage collected");
- }
+ @UsingStatusListener // Suppresses `StatusLogger` output, unless there is
a failure
+ @SetTestProperty(key = TestConstants.GC_ENABLE_DIRECT_ENCODERS, value =
"true")
+ @SetTestProperty(key = TestConstants.ASYNC_FORMAT_MESSAGES_IN_BACKGROUND,
value = "true")
+ void parameters_throwing_exception_should_be_garbage_collected(final
TestInfo testInfo) throws Exception {
+ awaitGarbageCollection(() -> {
+ final String loggerContextName = String.format("%s-LC",
testInfo.getDisplayName());
+ try (final LoggerContext loggerContext =
+ new AsyncLoggerContext(loggerContextName, null, null,
DI.createInitializedFactory())) {
+ loggerContext.start();
+ final Logger logger = loggerContext.getRootLogger();
+ final ThrowingMessage parameter = new ThrowingMessage();
+ logger.fatal(parameter);
+ return parameter;
+ }
+ });
}
private static class ThrowingMessage implements Message,
StringBuilderFormattable {
- private final CountDownLatch latch;
-
- ThrowingMessage(final CountDownLatch latch) {
- this.latch = latch;
- }
-
- @Override
- protected void finalize() throws Throwable {
- latch.countDown();
- super.finalize();
- }
+ private ThrowingMessage() {}
@Override
public String getFormattedMessage() {
throw new Error("Expected");
}
- @Override
- public String getFormat() {
- return "";
- }
-
@Override
public Object[] getParameters() {
return new Object[0];
diff --git
a/log4j-async-logger/src/test/java/org/apache/logging/log4j/async/logger/QueueFullAsyncLogger3Test.java
b/log4j-async-logger/src/test/java/org/apache/logging/log4j/async/logger/QueueFullAsyncLogger3Test.java
index 3641aa29b5..c0a23c4900 100644
---
a/log4j-async-logger/src/test/java/org/apache/logging/log4j/async/logger/QueueFullAsyncLogger3Test.java
+++
b/log4j-async-logger/src/test/java/org/apache/logging/log4j/async/logger/QueueFullAsyncLogger3Test.java
@@ -16,29 +16,32 @@
*/
package org.apache.logging.log4j.async.logger;
-import static java.util.concurrent.TimeUnit.SECONDS;
+import static
org.apache.logging.log4j.async.logger.QueueFullAsyncAbstractTest.assertAsyncLogger;
import static
org.apache.logging.log4j.core.test.TestConstants.ASYNC_FORMAT_MESSAGES_IN_BACKGROUND;
import static
org.apache.logging.log4j.core.test.TestConstants.ASYNC_LOGGER_RING_BUFFER_SIZE;
import static
org.apache.logging.log4j.core.test.TestConstants.ASYNC_QUEUE_FULL_POLICY_CLASS_NAME;
+import static
org.apache.logging.log4j.core.test.internal.GcHelper.awaitGarbageCollection;
import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.Assert.assertTrue;
+import java.util.List;
import java.util.concurrent.CountDownLatch;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
import org.apache.logging.log4j.Logger;
-import org.apache.logging.log4j.core.GarbageCollectionHelper;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.async.AsyncQueueFullPolicyFactory;
import org.apache.logging.log4j.core.async.DiscardingAsyncQueueFullPolicy;
import org.apache.logging.log4j.core.impl.CoreProperties;
import org.apache.logging.log4j.core.test.async.BlockingAppender;
+import org.apache.logging.log4j.core.test.async.QueueFullAbstractTest;
import org.apache.logging.log4j.core.test.junit.ContextSelectorType;
import org.apache.logging.log4j.core.test.junit.LoggerContextSource;
import org.apache.logging.log4j.core.test.junit.Named;
import org.apache.logging.log4j.message.Message;
+import org.apache.logging.log4j.message.SimpleMessage;
import org.apache.logging.log4j.status.StatusLogger;
import org.apache.logging.log4j.test.junit.SetTestProperty;
import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.Timeout;
/**
* Tests queue full scenarios with pure AsyncLoggers (all loggers async).
@@ -47,7 +50,7 @@ import org.junit.jupiter.api.Timeout;
@SetTestProperty(key = ASYNC_LOGGER_RING_BUFFER_SIZE, value = "128")
@SetTestProperty(key = ASYNC_FORMAT_MESSAGES_IN_BACKGROUND, value = "true")
@SetTestProperty(key = ASYNC_QUEUE_FULL_POLICY_CLASS_NAME, value = "Discard")
-public class QueueFullAsyncLogger3Test extends QueueFullAsyncAbstractTest {
+class QueueFullAsyncLogger3Test extends QueueFullAbstractTest {
@Override
protected void checkConfig(final LoggerContext ctx) {
@@ -60,64 +63,21 @@ public class QueueFullAsyncLogger3Test extends
QueueFullAsyncAbstractTest {
}
@Test
- @Timeout(value = 15, unit = SECONDS)
@LoggerContextSource
- public void discardedMessagesShouldBeGarbageCollected(
+ void discarded_messages_should_be_garbage_collected(
final LoggerContext ctx, final @Named(APPENDER_NAME)
BlockingAppender blockingAppender)
throws InterruptedException {
- checkConfig(ctx);
- final Logger logger = ctx.getLogger(getClass());
-
- blockingAppender.logEvents = null;
- blockingAppender.countDownLatch = new CountDownLatch(1);
- final int count = 200;
- final CountDownLatch garbageCollectionLatch = new
CountDownLatch(count);
- for (int i = 0; i < count; i++) {
- logger.info(new
CountdownOnGarbageCollectMessage(garbageCollectionLatch));
- }
- blockingAppender.countDownLatch.countDown();
-
- final GarbageCollectionHelper gcHelper = new GarbageCollectionHelper();
- gcHelper.run();
- try {
- assertTrue("Parameter should have been garbage collected",
garbageCollectionLatch.await(30, SECONDS));
- } finally {
- gcHelper.close();
- }
- }
-
- private static final class CountdownOnGarbageCollectMessage implements
Message {
-
- private final CountDownLatch latch;
-
- CountdownOnGarbageCollectMessage(final CountDownLatch latch) {
- this.latch = latch;
- }
-
- @Override
- public String getFormattedMessage() {
- return "formatted";
- }
-
- @Override
- public String getFormat() {
- return null;
- }
-
- @Override
- public Object[] getParameters() {
- return org.apache.logging.log4j.util.Constants.EMPTY_OBJECT_ARRAY;
- }
-
- @Override
- public Throwable getThrowable() {
- return null;
- }
-
- @Override
- protected void finalize() throws Throwable {
- latch.countDown();
- super.finalize();
- }
+ awaitGarbageCollection(() -> {
+ checkConfig(ctx);
+ final Logger logger = ctx.getLogger(getClass());
+ blockingAppender.logEvents = null;
+ blockingAppender.countDownLatch = new CountDownLatch(1);
+ final List<Message> messages = IntStream.range(0, 200)
+ .mapToObj(messageIndex -> new SimpleMessage("message " +
messageIndex))
+ .collect(Collectors.toList());
+ messages.forEach(logger::info);
+ blockingAppender.countDownLatch.countDown();
+ return messages;
+ });
}
}
diff --git
a/log4j-core-test/src/main/java/org/apache/logging/log4j/core/GarbageCollectionHelper.java
b/log4j-core-test/src/main/java/org/apache/logging/log4j/core/GarbageCollectionHelper.java
deleted file mode 100644
index 1feff3edd8..0000000000
---
a/log4j-core-test/src/main/java/org/apache/logging/log4j/core/GarbageCollectionHelper.java
+++ /dev/null
@@ -1,68 +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;
-
-import static org.junit.Assert.assertTrue;
-
-import java.io.Closeable;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-public final class GarbageCollectionHelper implements Closeable, Runnable {
- private static final OutputStream sink = OutputStream.nullOutputStream();
- private final AtomicBoolean running = new AtomicBoolean();
- private final CountDownLatch latch = new CountDownLatch(1);
- private final Thread gcThread = new Thread(new Runnable() {
- @Override
- public void run() {
- try {
- while (running.get()) {
- // Allocate data to help suggest a GC
- try {
- // 1mb of heap
- sink.write(new byte[1024 * 1024]);
- } catch (IOException ignored) {
- }
- // May no-op depending on the jvm configuration
- System.gc();
- }
- } finally {
- latch.countDown();
- }
- }
- });
-
- @Override
- public void run() {
- if (running.compareAndSet(false, true)) {
- gcThread.start();
- }
- }
-
- @Override
- public void close() {
- running.set(false);
- try {
- assertTrue("GarbageCollectionHelper did not shut down cleanly",
latch.await(10, TimeUnit.SECONDS));
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
- }
-}
diff --git
a/log4j-core-test/src/main/java/org/apache/logging/log4j/core/test/internal/GcHelper.java
b/log4j-core-test/src/main/java/org/apache/logging/log4j/core/test/internal/GcHelper.java
new file mode 100644
index 0000000000..7c32eaff26
--- /dev/null
+++
b/log4j-core-test/src/main/java/org/apache/logging/log4j/core/test/internal/GcHelper.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.core.test.internal;
+
+import java.lang.ref.PhantomReference;
+import java.lang.ref.Reference;
+import java.lang.ref.ReferenceQueue;
+import org.junit.jupiter.api.function.ThrowingSupplier;
+
+public final class GcHelper {
+
+ private GcHelper() {}
+
+ /**
+ * Waits for the value to be garbage collected.
+ *
+ * @param valueSupplier a value provider
+ */
+ @SuppressWarnings({"unused", "UnusedAssignment"})
+ public static void awaitGarbageCollection(final ThrowingSupplier<?>
valueSupplier) throws InterruptedException {
+
+ // Create the reference queue
+ final ReferenceQueue<Object> refQueue = new ReferenceQueue<>();
+ final Reference<?> ref;
+ try {
+ final Object value = valueSupplier.get();
+ ref = new PhantomReference<>(value, refQueue);
+ } catch (final Throwable error) {
+ throw new RuntimeException("failed obtaining value", error);
+ }
+
+ // Wait for the garbage collection
+ try (final GcPressureGenerator ignored =
GcPressureGenerator.ofStarted()) {
+ final Reference<?> removedRef = refQueue.remove(30_000L);
+ if (removedRef == null) {
+ throw new AssertionError("garbage collector did not reclaim
the value");
+ }
+ }
+ }
+}
diff --git
a/log4j-core-test/src/main/java/org/apache/logging/log4j/core/test/internal/GcPressureGenerator.java
b/log4j-core-test/src/main/java/org/apache/logging/log4j/core/test/internal/GcPressureGenerator.java
new file mode 100644
index 0000000000..b5be23b6b5
--- /dev/null
+++
b/log4j-core-test/src/main/java/org/apache/logging/log4j/core/test/internal/GcPressureGenerator.java
@@ -0,0 +1,81 @@
+/*
+ * 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.test.internal;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Creates GC pressure by continuously allocating and {@link System#gc()
triggering GC}.
+ */
+final class GcPressureGenerator implements AutoCloseable {
+
+ private final AtomicInteger sink = new AtomicInteger(0);
+
+ private final AtomicBoolean running = new AtomicBoolean(true);
+
+ private final CountDownLatch stopLatch = new CountDownLatch(1);
+
+ private GcPressureGenerator() {
+ startGeneratorThread();
+ }
+
+ private void startGeneratorThread() {
+ final String threadName = GcPressureGenerator.class.getSimpleName();
+ final Thread thread = new Thread(this::generateGarbage, threadName);
+ thread.setDaemon(true); // Avoid blocking JVM exit
+ thread.start();
+ }
+
+ private void generateGarbage() {
+ try {
+ while (running.get()) {
+ final Object object = new byte[1024 * 1024];
+ int positiveValue = Math.abs(object.hashCode());
+ sink.set(positiveValue);
+ System.gc();
+ System.runFinalization();
+ }
+ } finally {
+ stopLatch.countDown();
+ }
+ }
+
+ static GcPressureGenerator ofStarted() {
+ return new GcPressureGenerator();
+ }
+
+ @Override
+ public void close() {
+ final boolean signalled = running.compareAndSet(true, false);
+ if (signalled) {
+ try {
+ final boolean stopped = stopLatch.await(10, TimeUnit.SECONDS);
+ assertThat(stopped).isTrue();
+ assertThat(sink.get()).isPositive();
+ } catch (final InterruptedException error) {
+ // Restore the `interrupted` flag
+ Thread.currentThread().interrupt();
+ throw new RuntimeException(error);
+ }
+ }
+ }
+}
diff --git a/log4j-core-test/src/test/java/foo/TestFriendlyException.java
b/log4j-core-test/src/test/java/foo/TestFriendlyException.java
index 4ce0c7d5a0..7c791dc20d 100644
--- a/log4j-core-test/src/test/java/foo/TestFriendlyException.java
+++ b/log4j-core-test/src/test/java/foo/TestFriendlyException.java
@@ -27,18 +27,21 @@ import org.apache.logging.log4j.util.Constants;
* A testing friendly exception featuring
* <ul>
* <li>Distinct localized message</li>
- * <li>Non-Log4j package origin<sup>1</sup></li>
+ * <li>Non-Log4j<sup>1</sup> and fixed<sup>2</sup> (to {@code bar}) package
origin</li>
* <li>Sufficient causal chain depth</li>
- * <li>Cyclic causal chain</li>
+ * <li>Circular causal chain</li>
* <li>Suppressed exceptions</li>
* <li>Clutter-free stack trace (i.e., elements from JUnit, JDK, etc.)</li>
- * <li>Stack trace elements from named modules<sup>2</sup></li>
+ * <li>Stack trace elements from named modules<sup>3</sup></li>
* </ul>
* <p>
* <sup>1</sup> Helps with observing stack trace manipulation effects of Log4j.
* </p>
* <p>
- * <sup>2</sup> Helps with testing module name serialization.
+ * <sup>2</sup> Helps to make the origin of {@link #INSTANCE} independent of
the first test accessing to it.
+ * </p>
+ * <p>
+ * <sup>3</sup> Helps with testing module name serialization.
* </p>
*/
public final class TestFriendlyException extends RuntimeException {
@@ -48,6 +51,9 @@ public final class TestFriendlyException extends
RuntimeException {
assertThat(TestFriendlyException.class.getPackage().getName()).doesNotStartWith("org.apache");
}
+ public static final StackTraceElement
ORG_APACHE_REPLACEMENT_STACK_TRACE_ELEMENT =
+ new StackTraceElement("bar.OrgApacheReplacement", "someMethod",
"OrgApacheReplacement.java", 0);
+
public static final StackTraceElement NAMED_MODULE_STACK_TRACE_ELEMENT =
namedModuleStackTraceElement();
@SuppressWarnings("resource")
@@ -127,9 +133,13 @@ public final class TestFriendlyException extends
RuntimeException {
private static Stream<StackTraceElement> filterStackTraceElement(
final StackTraceElement stackTraceElement, final boolean[]
seenExcludedStackTraceElement) {
+
+ // Short-circuit if we have already encountered an excluded stack
trace element
if (seenExcludedStackTraceElement[0]) {
return Stream.empty();
}
+
+ // Check if the class name is excluded
final String className = stackTraceElement.getClassName();
for (final String excludedClassNamePrefix :
EXCLUDED_CLASS_NAME_PREFIXES) {
if (className.startsWith(excludedClassNamePrefix)) {
@@ -137,6 +147,15 @@ public final class TestFriendlyException extends
RuntimeException {
return Stream.empty();
}
}
+
+ // Replace `org.apache`-originating entries with a constant one.
+ // Without this, `INSTANCE` might yield different origin depending on
the first class accessing to it.
+ // We remove this ambiguity and fix our origin to a constant instead.
+ if (className.startsWith("org.apache")) {
+ return Stream.of(ORG_APACHE_REPLACEMENT_STACK_TRACE_ELEMENT);
+ }
+
+ // Otherwise, it looks good
return Stream.of(stackTraceElement);
}
diff --git
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/EventParameterMemoryLeakTest.java
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/EventParameterMemoryLeakTest.java
index a036484f4c..009058e772 100644
---
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/EventParameterMemoryLeakTest.java
+++
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/EventParameterMemoryLeakTest.java
@@ -16,76 +16,99 @@
*/
package org.apache.logging.log4j.core;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.containsString;
-import static org.hamcrest.Matchers.is;
-import static org.junit.jupiter.api.Assertions.assertNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileReader;
-import java.lang.ref.Cleaner;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import org.apache.logging.log4j.LogManager;
+import static
org.apache.logging.log4j.core.test.internal.GcHelper.awaitGarbageCollection;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.List;
+import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.Logger;
-import org.apache.logging.log4j.core.test.CoreLoggerContexts;
+import org.apache.logging.log4j.core.config.Configuration;
+import
org.apache.logging.log4j.core.config.builder.api.AppenderComponentBuilder;
+import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilder;
+import
org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilderFactory;
+import org.apache.logging.log4j.core.config.builder.api.LayoutComponentBuilder;
+import
org.apache.logging.log4j.core.config.builder.api.RootLoggerComponentBuilder;
+import org.apache.logging.log4j.core.config.builder.impl.BuiltConfiguration;
import org.apache.logging.log4j.core.test.TestConstants;
+import org.apache.logging.log4j.core.test.appender.ListAppender;
+import org.apache.logging.log4j.plugins.di.DI;
import org.apache.logging.log4j.test.junit.SetTestProperty;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
@Tag("functional")
-@SetTestProperty(key = TestConstants.GC_ENABLE_DIRECT_ENCODERS, value = "true")
-@SetTestProperty(key = TestConstants.CONFIGURATION_FILE, value =
"EventParameterMemoryLeakTest.xml")
-public class EventParameterMemoryLeakTest {
+class EventParameterMemoryLeakTest {
@Test
- @SuppressWarnings("UnusedAssignment") // parameter set to null to allow
garbage collection
- public void testParametersAreNotLeaked() throws Exception {
- final File file = new File("target",
"EventParameterMemoryLeakTest.log");
- assertTrue(!file.exists() || file.delete(), "Deleted old file before
test");
-
- final Logger log = LogManager.getLogger("com.foo.Bar");
- final CountDownLatch latch = new CountDownLatch(1);
- final Cleaner cleaner = Cleaner.create();
- Object parameter = new ParameterObject("paramValue");
- cleaner.register(parameter, latch::countDown);
- log.info("Message with parameter {}", parameter);
- log.info(parameter);
- log.info("test", new ObjectThrowable(parameter));
- log.info("test {}", "hello", new ObjectThrowable(parameter));
- parameter = null;
- CoreLoggerContexts.stopLoggerContext(file);
- final BufferedReader reader = new BufferedReader(new FileReader(file));
- final String line1 = reader.readLine();
- final String line2 = reader.readLine();
- final String line3 = reader.readLine();
- // line4 is empty line because of the line separator after throwable
pattern
- final String line4 = reader.readLine();
- final String line5 = reader.readLine();
- final String line6 = reader.readLine();
- final String line7 = reader.readLine();
- reader.close();
- file.delete();
- assertThat(line1, containsString("Message with parameter paramValue"));
- assertThat(line2, containsString("paramValue"));
- assertThat(line3, containsString("paramValue"));
- assertThat(line4, is(""));
- assertThat(line5, containsString("paramValue"));
- assertThat(line6, is(""));
- assertNull(line7, "Expected only six lines");
- try (final GarbageCollectionHelper gcHelper = new
GarbageCollectionHelper()) {
- gcHelper.run();
- assertTrue(latch.await(30, TimeUnit.SECONDS), "Parameter should
have been garbage collected");
- }
+ @SetTestProperty(key = TestConstants.GC_ENABLE_DIRECT_ENCODERS, value =
"true")
+ void parameters_should_be_garbage_collected(final TestInfo testInfo)
throws Throwable {
+ awaitGarbageCollection(() -> {
+ final ListAppender[] appenderRef = {null};
+ final Logger[] loggerRef = {null};
+ try (final LoggerContext ignored = createLoggerContext(testInfo,
appenderRef, loggerRef)) {
+
+ // Log messages
+ final ParameterObject parameter = new
ParameterObject("paramValue");
+ loggerRef[0].info("Message with parameter {}", parameter);
+ loggerRef[0].info(parameter);
+ loggerRef[0].info("test", new ObjectThrowable(parameter));
+ loggerRef[0].info("test {}", "hello", new
ObjectThrowable(parameter));
+
+ // Verify the logging
+ final List<String> messages = appenderRef[0].getMessages();
+ assertThat(messages).hasSize(4);
+ assertThat(messages.get(0)).isEqualTo("Message with parameter
%s", parameter.value);
+ assertThat(messages.get(1)).isEqualTo(parameter.value);
+ assertThat(messages.get(2))
+ .startsWith(String.format("test%n%s: %s",
ObjectThrowable.class.getName(), parameter.value));
+ assertThat(messages.get(3))
+ .startsWith(
+ String.format("test hello%n%s: %s",
ObjectThrowable.class.getName(), parameter.value));
+
+ // Return the GC subject
+ return parameter;
+ }
+ });
+ }
+
+ private static LoggerContext createLoggerContext(
+ final TestInfo testInfo, final ListAppender[] appenderRef, final
Logger[] loggerRef) {
+ final String loggerContextName = String.format("%s-LC",
testInfo.getDisplayName());
+ final LoggerContext loggerContext =
+ new LoggerContext(loggerContextName, null, (String) null,
DI.createInitializedFactory());
+ final String appenderName = "LIST";
+ final Configuration configuration = createConfiguration(appenderName);
+ loggerContext.start(configuration);
+ appenderRef[0] = configuration.getAppender(appenderName);
+ assertThat(appenderRef[0]).isNotNull();
+ final Class<?> testClass = testInfo.getTestClass().orElse(null);
+ assertThat(testClass).isNotNull();
+ loggerRef[0] = loggerContext.getLogger(testClass);
+ return loggerContext;
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ private static Configuration createConfiguration(final String
appenderName) {
+ final ConfigurationBuilder<BuiltConfiguration> configBuilder =
+ ConfigurationBuilderFactory.newConfigurationBuilder();
+ final LayoutComponentBuilder layoutComponentBuilder =
+
configBuilder.newLayout("PatternLayout").addAttribute("pattern", "%m");
+ final AppenderComponentBuilder appenderComponentBuilder =
+ configBuilder.newAppender(appenderName,
"List").add(layoutComponentBuilder);
+ final RootLoggerComponentBuilder loggerComponentBuilder =
+
configBuilder.newRootLogger(Level.ALL).add(configBuilder.newAppenderRef(appenderName));
+ return configBuilder
+ .add(appenderComponentBuilder)
+ .add(loggerComponentBuilder)
+ .build(false);
}
private static final class ParameterObject {
+
private final String value;
- ParameterObject(final String value) {
+ private ParameterObject(final String value) {
this.value = value;
}
@@ -96,9 +119,10 @@ public class EventParameterMemoryLeakTest {
}
private static final class ObjectThrowable extends RuntimeException {
+
private final Object object;
- ObjectThrowable(final Object object) {
+ private ObjectThrowable(final Object object) {
super(String.valueOf(object));
this.object = object;
}
diff --git
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/ReusableParameterizedMessageMemoryLeakTest.java
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/ReusableParameterizedMessageMemoryLeakTest.java
index 00f8ba2da3..1a425b5eac 100644
---
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/ReusableParameterizedMessageMemoryLeakTest.java
+++
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/ReusableParameterizedMessageMemoryLeakTest.java
@@ -16,10 +16,8 @@
*/
package org.apache.logging.log4j.core;
-import static org.junit.jupiter.api.Assertions.assertTrue;
+import static
org.apache.logging.log4j.core.test.internal.GcHelper.awaitGarbageCollection;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
import org.apache.logging.log4j.message.ReusableMessage;
import org.apache.logging.log4j.message.ReusableMessageFactory;
import org.junit.jupiter.api.Test;
@@ -27,36 +25,28 @@ import org.junit.jupiter.api.Test;
public class ReusableParameterizedMessageMemoryLeakTest {
@Test
- public void testParametersAreNotLeaked() throws Exception {
- final CountDownLatch latch = new CountDownLatch(1);
- final ReusableMessage message = (ReusableMessage)
- ReusableMessageFactory.INSTANCE.newMessage("foo {}", new
ParameterObject("paramValue", latch));
- // Large enough for the parameters, but smaller than the default
reusable array size.
- message.swapParameters(new Object[5]);
- try (final GarbageCollectionHelper gcHelper = new
GarbageCollectionHelper()) {
- gcHelper.run();
- assertTrue(latch.await(30, TimeUnit.SECONDS), "Parameter should
have been garbage collected");
- }
+ public void parameters_should_be_garbage_collected() throws Exception {
+ awaitGarbageCollection(() -> {
+ final ParameterObject parameter = new
ParameterObject("paramValue");
+ final ReusableMessage message =
+ (ReusableMessage)
ReusableMessageFactory.INSTANCE.newMessage("foo {}", parameter);
+ // Large enough for the parameters, but smaller than the default
reusable array size
+ message.swapParameters(new Object[5]);
+ return parameter;
+ });
}
private static final class ParameterObject {
+
private final String value;
- private final CountDownLatch latch;
- ParameterObject(final String value, final CountDownLatch latch) {
+ private ParameterObject(final String value) {
this.value = value;
- this.latch = latch;
}
@Override
public String toString() {
return value;
}
-
- @Override
- protected void finalize() throws Throwable {
- latch.countDown();
- super.finalize();
- }
}
}
diff --git
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/RollingAppenderDirectWriteTest.java
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/RollingAppenderDirectWriteTest.java
index 01a2291e2f..4a34ca917c 100644
---
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/RollingAppenderDirectWriteTest.java
+++
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/RollingAppenderDirectWriteTest.java
@@ -62,8 +62,11 @@ public class RollingAppenderDirectWriteTest {
final InputStream uncompressed =
fileName.endsWith(".gz") ? new GZIPInputStream(is) : is;
final BufferedReader reader = new BufferedReader(new
InputStreamReader(uncompressed, UTF_8))) {
String line;
+ int lineIndex = 0;
while ((line = reader.readLine()) != null) {
- assertThat(line).matches(LINE_PATTERN);
+ assertThat(line)
+ .as("line %d of file `%s`", ++lineIndex, file)
+ .matches(LINE_PATTERN);
++found;
}
}
diff --git
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/NestedLoggingFromThrowableMessageTest.java
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/NestedLoggingFromThrowableMessageTest.java
index 8f1c6c6d9c..108b08b19e 100644
---
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/NestedLoggingFromThrowableMessageTest.java
+++
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/NestedLoggingFromThrowableMessageTest.java
@@ -21,9 +21,9 @@ import static org.junit.Assert.assertTrue;
import java.io.BufferedReader;
import java.io.File;
-import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
+import java.nio.file.Files;
import java.util.HashSet;
import java.util.Set;
import org.apache.logging.log4j.Logger;
@@ -78,14 +78,15 @@ public class NestedLoggingFromThrowableMessageTest {
final Set<String> lines2 = readUniqueLines(file2);
assertEquals("Expected the same data from both appenders", lines1,
lines2);
- assertEquals(3, lines1.size());
+ assertEquals(2, lines1.size());
assertTrue(lines1.contains("INFO NestedLoggingFromThrowableMessageTest
Logging in getMessage "));
assertTrue(lines1.contains("ERROR
NestedLoggingFromThrowableMessageTest Test message"));
}
private static Set<String> readUniqueLines(final File input) throws
IOException {
final Set<String> lines = new HashSet<>();
- try (final BufferedReader reader = new BufferedReader(new
InputStreamReader(new FileInputStream(input)))) {
+ try (final BufferedReader reader =
+ new BufferedReader(new
InputStreamReader(Files.newInputStream(input.toPath())))) {
String line;
while ((line = reader.readLine()) != null) {
assertTrue("Read duplicate line: " + line, lines.add(line));
diff --git
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/internal/GcHelperTest.java
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/internal/GcHelperTest.java
new file mode 100644
index 0000000000..ae994466fe
--- /dev/null
+++
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/internal/GcHelperTest.java
@@ -0,0 +1,29 @@
+/*
+ * 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.internal;
+
+import static
org.apache.logging.log4j.core.test.internal.GcHelper.awaitGarbageCollection;
+
+import org.junit.jupiter.api.Test;
+
+class GcHelperTest {
+
+ @Test
+ void await_should_work() throws InterruptedException {
+ awaitGarbageCollection(Object::new);
+ }
+}
diff --git
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/layout/PatternLayoutDefaultExceptionHandlerTest.java
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/layout/PatternLayoutDefaultExceptionHandlerTest.java
new file mode 100644
index 0000000000..6a5fcbac12
--- /dev/null
+++
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/layout/PatternLayoutDefaultExceptionHandlerTest.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.core.layout;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.apache.logging.log4j.core.Layout;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.config.Configuration;
+import org.apache.logging.log4j.core.config.NullConfiguration;
+import org.apache.logging.log4j.core.impl.Log4jLogEvent;
+import org.assertj.core.api.AbstractStringAssert;
+import org.junit.jupiter.api.Test;
+
+class PatternLayoutDefaultExceptionHandlerTest {
+
+ private static final Configuration CONFIG = new NullConfiguration();
+
+ private static final Exception EXCEPTION = new RuntimeException("foo");
+
+ @Test
+ void default_exception_handler_should_be_provided() {
+ final String threadName = Thread.currentThread().getName();
+ final String exceptionClassName =
EXCEPTION.getClass().getCanonicalName();
+ final String exceptionMessage = EXCEPTION.getMessage();
+ final String firstLine = String.format("%s%n%s: %s", threadName,
exceptionClassName, exceptionMessage);
+ assertThatPatternEncodes("%t", true).startsWith(firstLine);
+ }
+
+ @Test
+ void default_exception_handler_should_be_provided_after_newline() {
+ final String threadName = Thread.currentThread().getName();
+ final String exceptionClassName =
EXCEPTION.getClass().getCanonicalName();
+ final String exceptionMessage = EXCEPTION.getMessage();
+ final String firstLine = String.format("%s%n%s: %s", threadName,
exceptionClassName, exceptionMessage);
+ assertThatPatternEncodes("%t%n", true).startsWith(firstLine);
+ }
+
+ @Test
+ void
default_exception_handler_should_not_be_provided_if_user_provides_one() {
+ final String className = EXCEPTION.getStackTrace()[0].getClassName();
+ assertThatPatternEncodes("%ex{short.className}",
true).isEqualTo(className);
+ }
+
+ @Test
+ void
default_exception_handler_should_not_be_provided_if_alwaysWriteExceptions_disabled()
{
+ final String threadName = Thread.currentThread().getName();
+ assertThatPatternEncodes("%t", false).isEqualTo(threadName);
+ }
+
+ private static AbstractStringAssert<?> assertThatPatternEncodes(
+ final String pattern, final boolean alwaysWriteExceptions) {
+ final Layout layout = PatternLayout.newBuilder()
+ .setConfiguration(CONFIG)
+ .setPattern(pattern)
+ .setAlwaysWriteExceptions(alwaysWriteExceptions)
+ .build();
+ final LogEvent event =
Log4jLogEvent.newBuilder().setThrown(EXCEPTION).build();
+ return assertThat(layout.toSerializable(event)).as("pattern=`%s`",
pattern);
+ }
+}
diff --git
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/ExtendedThrowablePatternConverterTest.java
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/ExtendedThrowablePatternConverterTest.java
index b1418c62e4..ea71b62544 100644
---
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/ExtendedThrowablePatternConverterTest.java
+++
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/ExtendedThrowablePatternConverterTest.java
@@ -17,6 +17,7 @@
package org.apache.logging.log4j.core.pattern;
import static java.util.Arrays.asList;
+import static
org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest.THROWING_METHOD;
import foo.TestFriendlyException;
import java.util.List;
@@ -36,7 +37,7 @@ class ExtendedThrowablePatternConverterTest {
class PropertyTest extends AbstractPropertyTest {
PropertyTest() {
- super("%xEx");
+ super("%xEx", THROWING_METHOD);
}
}
@@ -45,7 +46,7 @@ class ExtendedThrowablePatternConverterTest {
" at " + TestFriendlyException.NAMED_MODULE_STACK_TRACE_ELEMENT +
" ~[?:?]",
" at
foo.TestFriendlyException.create(TestFriendlyException.java:0)
~[test-classes/:?]",
" at
foo.TestFriendlyException.<clinit>(TestFriendlyException.java:0)
~[test-classes/:?]",
- " at
org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest.<clinit>(ThrowablePatternConverterTest.java:0)
[test-classes/:?]",
+ " at " +
TestFriendlyException.ORG_APACHE_REPLACEMENT_STACK_TRACE_ELEMENT + " ~[?:0]",
" Suppressed: foo.TestFriendlyException: r_s [localized]",
" at
foo.TestFriendlyException.create(TestFriendlyException.java:0)
~[test-classes/:?]",
" at
foo.TestFriendlyException.create(TestFriendlyException.java:0)
~[test-classes/:?]",
@@ -109,7 +110,7 @@ class ExtendedThrowablePatternConverterTest {
"foo.TestFriendlyException: r [localized]",
" at " +
TestFriendlyException.NAMED_MODULE_STACK_TRACE_ELEMENT + " ~[?:?]",
" ... suppressed 2 lines",
- " at
org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest.<clinit>(ThrowablePatternConverterTest.java:0)
[test-classes/:?]",
+ " at " +
TestFriendlyException.ORG_APACHE_REPLACEMENT_STACK_TRACE_ELEMENT + " ~[?:0]",
" Suppressed: foo.TestFriendlyException: r_s
[localized]",
" ... suppressed 2 lines",
" ... 2 more",
@@ -136,7 +137,7 @@ class ExtendedThrowablePatternConverterTest {
@MethodSource("org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest#depthTestCases")
void depth_and_package_limited_output_should_match_2(final
DepthTestCase depthTestCase) {
final String pattern = String.format(
- "%s{%d}{filters(org.apache)}%s",
+ "%s{%d}{filters(bar)}%s",
patternPrefix, depthTestCase.maxLineCount,
depthTestCase.separatorTestCase.patternAddendum);
assertStackTraceLines(
depthTestCase,
diff --git
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/RootThrowablePatternConverterTest.java
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/RootThrowablePatternConverterTest.java
index b02618aff4..b7cee50b3a 100644
---
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/RootThrowablePatternConverterTest.java
+++
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/RootThrowablePatternConverterTest.java
@@ -17,13 +17,19 @@
package org.apache.logging.log4j.core.pattern;
import static java.util.Arrays.asList;
+import static
org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest.EXCEPTION;
+import static
org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest.LEVEL;
+import static
org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest.convert;
+import static org.assertj.core.api.Assertions.assertThat;
import foo.TestFriendlyException;
import java.util.List;
import
org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest.AbstractPropertyTest;
import
org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest.AbstractStackTraceTest;
import
org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest.DepthTestCase;
+import org.apache.logging.log4j.core.util.Throwables;
import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
@@ -32,11 +38,14 @@ import org.junit.jupiter.params.provider.MethodSource;
*/
class RootThrowablePatternConverterTest {
+ private static final StackTraceElement THROWING_METHOD =
+ Throwables.getRootCause(EXCEPTION).getStackTrace()[0];
+
@Nested
class PropertyTest extends AbstractPropertyTest {
PropertyTest() {
- super("%rEx");
+ super("%rEx", THROWING_METHOD);
}
}
@@ -59,7 +68,7 @@ class RootThrowablePatternConverterTest {
" at " + TestFriendlyException.NAMED_MODULE_STACK_TRACE_ELEMENT,
" at
foo.TestFriendlyException.create(TestFriendlyException.java:0)",
" at
foo.TestFriendlyException.<clinit>(TestFriendlyException.java:0)",
- " at
org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest.<clinit>(ThrowablePatternConverterTest.java:0)",
+ " at " +
TestFriendlyException.ORG_APACHE_REPLACEMENT_STACK_TRACE_ELEMENT,
" Suppressed: foo.TestFriendlyException: r_s_c [localized]",
" at
foo.TestFriendlyException.create(TestFriendlyException.java:0)",
" at
foo.TestFriendlyException.create(TestFriendlyException.java:0)",
@@ -80,6 +89,16 @@ class RootThrowablePatternConverterTest {
super("%rEx");
}
+ @Test
+ @Override
+ void output_should_be_newline_prefixed() {
+ final String pattern = "%p" + patternPrefix;
+ final String stackTrace = convert(pattern);
+ final String expectedStart = String.format(
+ "%s%n[CIRCULAR REFERENCE: %s", LEVEL,
EXCEPTION.getClass().getCanonicalName());
+ assertThat(stackTrace).as("pattern=`%s`",
pattern).startsWith(expectedStart);
+ }
+
@ParameterizedTest
@MethodSource("org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest#fullStackTracePatterns")
void full_output_should_match(final String pattern) {
@@ -120,7 +139,7 @@ class RootThrowablePatternConverterTest {
"Wrapped by: foo.TestFriendlyException: r
[localized]",
" at " +
TestFriendlyException.NAMED_MODULE_STACK_TRACE_ELEMENT,
" ... suppressed 2 lines",
- " at
org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest.<clinit>(ThrowablePatternConverterTest.java:0)",
+ " at " +
TestFriendlyException.ORG_APACHE_REPLACEMENT_STACK_TRACE_ELEMENT,
" Suppressed: foo.TestFriendlyException: r_s_c
[localized]",
" ... suppressed 2 lines",
" ... 3 more",
@@ -136,7 +155,7 @@ class RootThrowablePatternConverterTest {
@MethodSource("org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest#depthTestCases")
void depth_and_package_limited_output_should_match_2(final
DepthTestCase depthTestCase) {
final String pattern = String.format(
- "%s{%d}{filters(org.apache)}%s",
+ "%s{%d}{filters(bar)}%s",
patternPrefix, depthTestCase.maxLineCount,
depthTestCase.separatorTestCase.patternAddendum);
assertStackTraceLines(
depthTestCase,
diff --git
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/ThrowablePatternConverterTest.java
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/ThrowablePatternConverterTest.java
index c886def43c..e178526fd9 100644
---
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/ThrowablePatternConverterTest.java
+++
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/ThrowablePatternConverterTest.java
@@ -17,6 +17,7 @@
package org.apache.logging.log4j.core.pattern;
import static java.util.Arrays.asList;
+import static org.apache.logging.log4j.util.Strings.LINE_SEPARATOR;
import static org.assertj.core.api.Assertions.assertThat;
import foo.TestFriendlyException;
@@ -33,6 +34,7 @@ import org.apache.logging.log4j.core.impl.Log4jLogEvent;
import org.apache.logging.log4j.core.layout.PatternLayout;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
@@ -41,15 +43,13 @@ import org.junit.jupiter.params.provider.MethodSource;
*/
public class ThrowablePatternConverterTest {
- private static final String NEWLINE = System.lineSeparator();
+ static final Throwable EXCEPTION = TestFriendlyException.INSTANCE;
- private static final Throwable EXCEPTION = TestFriendlyException.INSTANCE;
-
- private static final StackTraceElement THROWING_METHOD =
EXCEPTION.getStackTrace()[0];
+ static final StackTraceElement THROWING_METHOD =
EXCEPTION.getStackTrace()[0];
private static final PatternParser PATTERN_PARSER =
PatternLayout.createPatternParser(null);
- private static final Level LEVEL = Level.FATAL;
+ static final Level LEVEL = Level.FATAL;
static final class SeparatorTestCase {
@@ -75,10 +75,10 @@ public class ThrowablePatternConverterTest {
new SeparatorTestCase("{separator()}", ""),
new SeparatorTestCase("{separator(#)}", "#"),
// Only suffixes
- new SeparatorTestCase("{suffix()}", NEWLINE),
- new SeparatorTestCase("{suffix(~)}", " ~" + NEWLINE),
- new SeparatorTestCase("{suffix(%level)}", " " + level +
NEWLINE),
- new SeparatorTestCase("{suffix(%rEx)}", NEWLINE),
+ new SeparatorTestCase("{suffix()}", LINE_SEPARATOR),
+ new SeparatorTestCase("{suffix(~)}", " ~" + LINE_SEPARATOR),
+ new SeparatorTestCase("{suffix(%level)}", " " + level +
LINE_SEPARATOR),
+ new SeparatorTestCase("{suffix(%rEx)}", LINE_SEPARATOR),
// Both separators and suffixes
new SeparatorTestCase("{separator()}{suffix()}", ""),
new SeparatorTestCase("{separator()}{suffix(~)}", " ~"),
@@ -94,7 +94,7 @@ public class ThrowablePatternConverterTest {
class PropertyTest extends AbstractPropertyTest {
PropertyTest() {
- super("%ex");
+ super("%ex", THROWING_METHOD);
}
}
@@ -102,8 +102,11 @@ public class ThrowablePatternConverterTest {
private final String patternPrefix;
- AbstractPropertyTest(final String patternPrefix) {
+ private final StackTraceElement throwingMethod;
+
+ AbstractPropertyTest(final String patternPrefix, final
StackTraceElement throwingMethod) {
this.patternPrefix = patternPrefix;
+ this.throwingMethod = throwingMethod;
}
@ParameterizedTest
@@ -121,37 +124,34 @@ public class ThrowablePatternConverterTest {
@ParameterizedTest
@MethodSource("org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest#separatorTestCases")
void className_should_be_rendered(final SeparatorTestCase
separatorTestCase) {
- assertConversion(separatorTestCase, "{short.className}",
THROWING_METHOD.getClassName());
+ assertConversion(separatorTestCase, "{short.className}",
throwingMethod.getClassName());
}
@ParameterizedTest
@MethodSource("org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest#separatorTestCases")
void methodName_should_be_rendered(final SeparatorTestCase
separatorTestCase) {
- assertConversion(separatorTestCase, "{short.methodName}",
THROWING_METHOD.getMethodName());
+ assertConversion(separatorTestCase, "{short.methodName}",
throwingMethod.getMethodName());
}
@ParameterizedTest
@MethodSource("org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest#separatorTestCases")
void lineNumber_should_be_rendered(final SeparatorTestCase
separatorTestCase) {
- assertConversion(separatorTestCase, "{short.lineNumber}",
THROWING_METHOD.getLineNumber() + "");
+ assertConversion(separatorTestCase, "{short.lineNumber}",
throwingMethod.getLineNumber() + "");
}
@ParameterizedTest
@MethodSource("org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest#separatorTestCases")
void fileName_should_be_rendered(final SeparatorTestCase
separatorTestCase) {
- assertConversion(separatorTestCase, "{short.fileName}",
THROWING_METHOD.getFileName());
+ assertConversion(separatorTestCase, "{short.fileName}",
throwingMethod.getFileName());
}
private void assertConversion(
final SeparatorTestCase separatorTestCase, final String
pattern, final Object expectedOutput) {
final String effectivePattern = patternPrefix + pattern +
separatorTestCase.patternAddendum;
final String output = convert(effectivePattern);
- final String effectiveExpectedOutput = expectedOutput +
separatorTestCase.conversionEnding;
assertThat(output)
- .as(
- "pattern=`%s`, separatorTestCase=%s,
expectedOutput=`%s`",
- pattern, separatorTestCase, expectedOutput)
- .isEqualTo(effectiveExpectedOutput);
+ .as("pattern=`%s`, separatorTestCase=%s", pattern,
separatorTestCase)
+ .isEqualTo(expectedOutput);
}
}
@@ -182,7 +182,7 @@ public class ThrowablePatternConverterTest {
}
static Stream<String> fullStackTracePatterns() {
- return Stream.of("", "{}", "{full}", "{" + Integer.MAX_VALUE + "}",
"{separator(" + NEWLINE + ")}");
+ return Stream.of("", "{}", "{full}", "{" + Integer.MAX_VALUE + "}",
"{separator(" + LINE_SEPARATOR + ")}");
}
@Nested
@@ -230,8 +230,8 @@ public class ThrowablePatternConverterTest {
int lineCount = 0;
int startIndex = 0;
int newlineIndex;
- while (lineCount < maxLineCount && (newlineIndex =
text.indexOf(NEWLINE, startIndex)) != -1) {
- final int endIndex = newlineIndex + NEWLINE.length();
+ while (lineCount < maxLineCount && (newlineIndex =
text.indexOf(LINE_SEPARATOR, startIndex)) != -1) {
+ final int endIndex = newlineIndex + LINE_SEPARATOR.length();
final String line = text.substring(startIndex, endIndex);
buffer.append(line);
lineCount++;
@@ -266,7 +266,7 @@ public class ThrowablePatternConverterTest {
" at " +
TestFriendlyException.NAMED_MODULE_STACK_TRACE_ELEMENT,
" at
foo.TestFriendlyException.create(TestFriendlyException.java:0)",
" at
foo.TestFriendlyException.<clinit>(TestFriendlyException.java:0)",
- " at
org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest.<clinit>(ThrowablePatternConverterTest.java:0)",
+ " at " +
TestFriendlyException.ORG_APACHE_REPLACEMENT_STACK_TRACE_ELEMENT,
" Suppressed: foo.TestFriendlyException: r_s
[localized]",
" at
foo.TestFriendlyException.create(TestFriendlyException.java:0)",
" at
foo.TestFriendlyException.create(TestFriendlyException.java:0)",
@@ -308,7 +308,7 @@ public class ThrowablePatternConverterTest {
"foo.TestFriendlyException: r [localized]",
" at " +
TestFriendlyException.NAMED_MODULE_STACK_TRACE_ELEMENT,
" ... suppressed 2 lines",
- " at
org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest.<clinit>(ThrowablePatternConverterTest.java:0)",
+ " at " +
TestFriendlyException.ORG_APACHE_REPLACEMENT_STACK_TRACE_ELEMENT,
" Suppressed: foo.TestFriendlyException: r_s
[localized]",
" ... suppressed 2 lines",
" ... 2 more",
@@ -335,7 +335,7 @@ public class ThrowablePatternConverterTest {
@MethodSource("org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest#depthTestCases")
void depth_and_package_limited_output_should_match_2(final
DepthTestCase depthTestCase) {
final String pattern = String.format(
- "%s{%d}{filters(org.apache)}%s",
+ "%s{%d}{filters(bar)}%s",
patternPrefix, depthTestCase.maxLineCount,
depthTestCase.separatorTestCase.patternAddendum);
assertStackTraceLines(
depthTestCase,
@@ -383,6 +383,15 @@ public class ThrowablePatternConverterTest {
this.patternPrefix = patternPrefix;
}
+ @Test
+ void output_should_be_newline_prefixed() {
+ final String pattern = "%p" + patternPrefix;
+ final String stackTrace = convert(pattern);
+ final String expectedStart =
+ String.format("%s%n%s", LEVEL,
EXCEPTION.getClass().getCanonicalName());
+ assertThat(stackTrace).as("pattern=`%s`",
pattern).startsWith(expectedStart);
+ }
+
@ParameterizedTest
@MethodSource("org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest#separatorTestCases")
void none_output_should_be_empty(final SeparatorTestCase
separatorTestCase) {
@@ -400,7 +409,7 @@ public class ThrowablePatternConverterTest {
final String conversionEnding;
if (depthTestCase == null) {
maxLineCount = Integer.MAX_VALUE;
- conversionEnding = NEWLINE;
+ conversionEnding = LINE_SEPARATOR;
} else {
maxLineCount = depthTestCase.maxLineCount;
conversionEnding =
depthTestCase.separatorTestCase.conversionEnding;
@@ -426,14 +435,14 @@ public class ThrowablePatternConverterTest {
}
}
- private static String convert(final String pattern) {
+ static String convert(final String pattern) {
final List<PatternFormatter> patternFormatters =
PATTERN_PARSER.parse(pattern, false, true, true);
- assertThat(patternFormatters).hasSize(1);
- final PatternFormatter patternFormatter = patternFormatters.get(0);
final LogEvent logEvent =
Log4jLogEvent.newBuilder().setThrown(EXCEPTION).setLevel(LEVEL).build();
final StringBuilder buffer = new StringBuilder();
- patternFormatter.format(logEvent, buffer);
+ for (final PatternFormatter patternFormatter : patternFormatters) {
+ patternFormatter.format(logEvent, buffer);
+ }
return buffer.toString();
}
}
diff --git
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/ThrowableTest.java
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/ThrowableTest.java
deleted file mode 100644
index 3b25dce8da..0000000000
---
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/ThrowableTest.java
+++ /dev/null
@@ -1,229 +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.pattern;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-import java.io.PrintWriter;
-import java.util.Collections;
-import java.util.stream.Stream;
-import org.apache.logging.log4j.Level;
-import org.apache.logging.log4j.Logger;
-import org.apache.logging.log4j.core.LoggerContext;
-import org.apache.logging.log4j.core.LoggerTest;
-import org.apache.logging.log4j.core.config.Configuration;
-import org.apache.logging.log4j.core.config.Configurator;
-import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilder;
-import
org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilderFactory;
-import org.apache.logging.log4j.core.config.builder.impl.BuiltConfiguration;
-import org.apache.logging.log4j.core.test.appender.ListAppender;
-import org.apache.logging.log4j.core.util.StringBuilderWriter;
-import org.junit.Test;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.Arguments;
-import org.junit.jupiter.params.provider.MethodSource;
-
-/**
- * Unit tests for {@code throwable}, {@code rThrowable} and {@code xThrowable}
pattern.
- */
-public class ThrowableTest {
- static Stream<Arguments> testConverter_dataSource() {
- final String filters =
"org.junit,org.apache.maven,sun.reflect,java.lang.reflect";
- final Integer depth = 5;
- return Stream.of(
- // Throwable
- Arguments.of("%ex", filters, null, null, null),
- Arguments.of("%ex", null, depth, null, null),
- Arguments.of("%ex", null, null, "I am suffix", "#"),
- // RootThrowable
- Arguments.of("%rEx", filters, null, null, null),
- Arguments.of("%rEx", null, depth, null, null),
- Arguments.of("%rEx", null, null, "I am suffix", "#"),
- // ExtendedThrowable
- Arguments.of("%xEx", filters, null, null, null),
- Arguments.of("%xEx", null, depth, null, null),
- Arguments.of("%xEx", null, null, "I am suffix", "#"));
- }
-
- @ParameterizedTest
- @MethodSource("testConverter_dataSource")
- void testConverter(String exceptionPattern, String filters, Integer depth,
String suffix, String lineSeparator) {
- final String pattern = buildPattern(exceptionPattern, filters, depth,
suffix, lineSeparator);
- final ConfigurationBuilder<BuiltConfiguration> configBuilder =
- ConfigurationBuilderFactory.newConfigurationBuilder();
-
- final String appenderName = "LIST";
- final Configuration config = configBuilder
- .add(configBuilder
- .newAppender(appenderName, "List")
-
.add(configBuilder.newLayout("PatternLayout").addAttribute("pattern", pattern)))
-
.add(configBuilder.newRootLogger(Level.ALL).add(configBuilder.newAppenderRef(appenderName)))
- .build(false);
-
- try (final LoggerContext loggerContext =
Configurator.initialize(config)) {
- // Restart logger context after first test run
- if (loggerContext.isStopped()) {
- loggerContext.start();
- loggerContext.reconfigure(config);
- }
- final Throwable r = createException("r", 1, 3);
-
- final Logger logger = loggerContext.getLogger(LoggerTest.class);
- final ListAppender appender =
loggerContext.getConfiguration().getAppender(appenderName);
- logger.error("Exception", r);
-
- assertThat(appender.getMessages()).hasSize(1);
- final String message = appender.getMessages().get(0);
- assertThat(message).isNotNull();
- verifyFilters(message, filters);
- verifyDepth(message, depth);
- verifySuffix(message, suffix, lineSeparator);
- }
- }
-
- static Stream<Arguments> renderers_dataSource() {
- return Stream.of(
- Arguments.of(new
ThrowableStackTraceRenderer<>(Collections.emptyList(), Integer.MAX_VALUE)),
- Arguments.of(new
ThrowableInvertedStackTraceRenderer(Collections.emptyList(),
Integer.MAX_VALUE)),
- Arguments.of(new
ThrowableExtendedStackTraceRenderer(Collections.emptyList(),
Integer.MAX_VALUE)));
- }
-
- @ParameterizedTest
- @MethodSource("renderers_dataSource")
- void testCircularSuppressedExceptions(final ThrowableStackTraceRenderer<?>
renderer) {
- final Exception e1 = new Exception();
- final Exception e2 = new Exception();
- e2.addSuppressed(e1);
- e1.addSuppressed(e2);
-
- render(renderer, e1);
- }
-
- @ParameterizedTest
- @MethodSource("renderers_dataSource")
- void testCircularSuppressedNestedException(final
ThrowableStackTraceRenderer<?> renderer) {
- final Exception e1 = new Exception();
- final Exception e2 = new Exception(e1);
- e2.addSuppressed(e1);
- e1.addSuppressed(e2);
-
- render(renderer, e1);
- }
-
- @ParameterizedTest
- @MethodSource("renderers_dataSource")
- void testCircularCauseExceptions(final ThrowableStackTraceRenderer<?>
renderer) {
- final Exception e1 = new Exception();
- final Exception e2 = new Exception(e1);
- e1.initCause(e2);
- render(renderer, e1);
- }
-
- /**
- * Default setting ThrowableRenderer render output should equal to
throwable.printStackTrace().
- */
- @Test
- public void testThrowableRenderer() {
- final Throwable throwable = createException("r", 1, 3);
- final ThrowableStackTraceRenderer<?> renderer =
- new ThrowableStackTraceRenderer<>(Collections.emptyList(),
Integer.MAX_VALUE);
- String actual = render(renderer, throwable);
-
assertThat(actual).isEqualTo(getStandardThrowableStackTrace(throwable));
- }
-
- private static String render(final ThrowableStackTraceRenderer<?>
renderer, final Throwable throwable) {
- final StringBuilder stringBuilder = new StringBuilder();
- renderer.renderThrowable(stringBuilder, throwable,
System.lineSeparator());
- return stringBuilder.toString();
- }
-
- private static String getStandardThrowableStackTrace(final Throwable
throwable) {
- final StringBuilder buffer = new StringBuilder();
- final PrintWriter printWriter = new PrintWriter(new
StringBuilderWriter(buffer));
- throwable.printStackTrace(printWriter);
- return buffer.toString();
- }
-
- private static String buildPattern(
- final String exceptionPattern,
- final String filters,
- final Integer depth,
- final String suffix,
- final String lineSeparator) {
- final StringBuilder buffer = new StringBuilder("%m");
- buffer.append(exceptionPattern);
- if (filters != null) {
- buffer.append("{filters(");
- buffer.append(filters);
- buffer.append(")}");
- }
-
- if (depth != null) {
- buffer.append("{");
- buffer.append(depth);
- buffer.append("}");
- }
-
- if (suffix != null) {
- buffer.append("{suffix(");
- buffer.append(suffix);
- buffer.append(")}");
- }
-
- if (lineSeparator != null) {
- buffer.append("{separator(");
- buffer.append(lineSeparator);
- buffer.append(")}");
- }
- return buffer.toString();
- }
-
- private static void verifyFilters(final String message, final String
filters) {
- if (filters != null) {
- assertThat(message).contains("suppressed");
- final String[] filterArray = filters.split(",");
- for (final String filter : filterArray) {
- assertThat(message).doesNotContain(filter);
- }
- } else {
- assertThat(message).doesNotContain("suppressed");
- }
- }
-
- private static void verifyDepth(final String message, final Integer depth)
{
- if (depth != null) {
- assertThat(message).hasLineCount(depth);
- }
- }
-
- private static void verifySuffix(final String message, final String
suffix, final String lineSeparator) {
- if (suffix != null && lineSeparator != null) {
- for (String line : message.split(lineSeparator)) {
- assertThat(line).endsWith(suffix);
- }
- }
- }
-
- private static Throwable createException(final String name, int depth, int
maxDepth) {
- Exception r = new Exception(name);
- if (depth < maxDepth) {
- r.initCause(createException(name + "_c", depth + 1, maxDepth));
- r.addSuppressed(createException(name + "_s", depth + 1, maxDepth));
- }
- return r;
- }
-}
diff --git
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/ThrowablesTest.java
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/ThrowablesTest.java
index 4d3cdfec3e..912d62a1d9 100644
---
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/ThrowablesTest.java
+++
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/ThrowablesTest.java
@@ -21,51 +21,50 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.Test;
-public class ThrowablesTest {
+class ThrowablesTest {
@Test
- public void testGetRootCauseNone() {
+ void testGetRootCauseNone() {
final NullPointerException throwable = new NullPointerException();
assertEquals(throwable, Throwables.getRootCause(throwable));
}
@Test
- public void testGetRootCauseDepth1() {
+ void testGetRootCauseDepth1() {
final Throwable cause = new NullPointerException();
final Throwable error = new UnsupportedOperationException(cause);
assertEquals(cause, Throwables.getRootCause(error));
}
@Test
- public void testGetRootCauseDepth2() {
+ void testGetRootCauseDepth2() {
final Throwable rootCause = new NullPointerException();
final Throwable cause = new UnsupportedOperationException(rootCause);
final Throwable error = new IllegalArgumentException(cause);
assertEquals(rootCause, Throwables.getRootCause(error));
}
- @SuppressWarnings("ThrowableNotThrown")
@Test
- public void testGetRootCauseLoop() {
+ void testGetRootCauseLoop() {
final Throwable cause1 = new RuntimeException();
final Throwable cause2 = new RuntimeException(cause1);
final Throwable cause3 = new RuntimeException(cause2);
cause1.initCause(cause3);
- assertThrows(IllegalArgumentException.class, () ->
Throwables.getRootCause(cause3));
+ assertEquals(cause1, Throwables.getRootCause(cause3));
}
@Test
- public void testRethrowRuntimeException() {
+ void testRethrowRuntimeException() {
assertThrows(NullPointerException.class, () -> Throwables.rethrow(new
NullPointerException()));
}
@Test
- public void testRethrowError() {
+ void testRethrowError() {
assertThrows(UnknownError.class, () -> Throwables.rethrow(new
UnknownError()));
}
@Test
- public void testRethrowCheckedException() {
+ void testRethrowCheckedException() {
assertThrows(NoSuchMethodException.class, () -> Throwables.rethrow(new
NoSuchMethodException()));
}
}
diff --git
a/log4j-core-test/src/test/resources/EventParameterMemoryLeakTest.xml
b/log4j-core-test/src/test/resources/EventParameterMemoryLeakTest.xml
deleted file mode 100644
index 54554cd97d..0000000000
--- a/log4j-core-test/src/test/resources/EventParameterMemoryLeakTest.xml
+++ /dev/null
@@ -1,35 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
- ~ Licensed to the Apache Software Foundation (ASF) under one or more
- ~ contributor license agreements. See the NOTICE file distributed with
- ~ this work for additional information regarding copyright ownership.
- ~ The ASF licenses this file to you under the Apache License, Version 2.0
- ~ (the "License"); you may not use this file except in compliance with
- ~ the License. You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- -->
-<Configuration status="off">
- <Appenders>
- <File name="File"
- fileName="target/EventParameterMemoryLeakTest.log"
- bufferedIO="false"
- immediateFlush="true"
- append="false">
- <PatternLayout>
- <Pattern>%d %p %c{1.} [%t] %m %throwable{short.message}%n</Pattern>
- </PatternLayout>
- </File>
- </Appenders>
- <Loggers>
- <Root level="trace">
- <AppenderRef ref="File"/>
- </Root>
- </Loggers>
-</Configuration>
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ExtendedThrowablePatternConverter.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ExtendedThrowablePatternConverter.java
index edeed93282..954cf25037 100644
---
a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ExtendedThrowablePatternConverter.java
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ExtendedThrowablePatternConverter.java
@@ -33,7 +33,13 @@ import org.jspecify.annotations.Nullable;
public final class ExtendedThrowablePatternConverter extends
ThrowablePatternConverter {
private ExtendedThrowablePatternConverter(@Nullable final Configuration
config, @Nullable final String[] options) {
- super("ExtendedThrowable", "throwable", options, config,
ExtendedThrowablePatternConverter::createRenderer);
+ super(
+ "ExtendedThrowable",
+ "throwable",
+ options,
+ config,
+ ThrowablePropertyRendererFactory.INSTANCE,
+ ThrowableExtendedStackTraceRendererFactory.INSTANCE);
}
private static ThrowableExtendedStackTraceRenderer createRenderer(final
ThrowableFormatOptions options) {
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/RootThrowablePatternConverter.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/RootThrowablePatternConverter.java
index 8b032a893d..aae942d7a4 100644
---
a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/RootThrowablePatternConverter.java
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/RootThrowablePatternConverter.java
@@ -17,7 +17,6 @@
package org.apache.logging.log4j.core.pattern;
import org.apache.logging.log4j.core.config.Configuration;
-import org.apache.logging.log4j.core.impl.ThrowableFormatOptions;
import org.apache.logging.log4j.plugins.Namespace;
import org.apache.logging.log4j.plugins.Plugin;
import org.jspecify.annotations.NullMarked;
@@ -33,11 +32,13 @@ import org.jspecify.annotations.Nullable;
public final class RootThrowablePatternConverter extends
ThrowablePatternConverter {
private RootThrowablePatternConverter(@Nullable final Configuration
config, @Nullable final String[] options) {
- super("RootThrowable", "throwable", options, config,
RootThrowablePatternConverter::createRenderer);
- }
-
- private static ThrowableInvertedStackTraceRenderer createRenderer(final
ThrowableFormatOptions options) {
- return new
ThrowableInvertedStackTraceRenderer(options.getIgnorePackages(),
options.getLines());
+ super(
+ "RootThrowable",
+ "throwable",
+ options,
+ config,
+ ThrowableInvertedPropertyRendererFactory.INSTANCE,
+ ThrowableInvertedStackTraceRendererFactory.INSTANCE);
}
/**
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowableExtendedStackTraceRendererFactory.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowableExtendedStackTraceRendererFactory.java
new file mode 100644
index 0000000000..d95399c21b
--- /dev/null
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowableExtendedStackTraceRendererFactory.java
@@ -0,0 +1,31 @@
+/*
+ * 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.pattern;
+
+import org.apache.logging.log4j.core.impl.ThrowableFormatOptions;
+
+final class ThrowableExtendedStackTraceRendererFactory extends
ThrowableStackTraceRendererFactory {
+
+ static final ThrowableExtendedStackTraceRendererFactory INSTANCE = new
ThrowableExtendedStackTraceRendererFactory();
+
+ private ThrowableExtendedStackTraceRendererFactory() {}
+
+ @Override
+ ThrowableExtendedStackTraceRenderer
createStackTraceRenderer(ThrowableFormatOptions options) {
+ return new
ThrowableExtendedStackTraceRenderer(options.getIgnorePackages(),
options.getLines());
+ }
+}
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowableInvertedPropertyRendererFactory.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowableInvertedPropertyRendererFactory.java
new file mode 100644
index 0000000000..4d0e8eaa43
--- /dev/null
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowableInvertedPropertyRendererFactory.java
@@ -0,0 +1,40 @@
+/*
+ * 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.pattern;
+
+import org.apache.logging.log4j.core.util.Throwables;
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * {@link ThrowablePropertyRendererFactory} implementation where the causal
chain will be processed in reverse order.
+ */
+@NullMarked
+final class ThrowableInvertedPropertyRendererFactory extends
ThrowablePropertyRendererFactory {
+
+ static final ThrowableInvertedPropertyRendererFactory INSTANCE = new
ThrowableInvertedPropertyRendererFactory();
+
+ private ThrowableInvertedPropertyRendererFactory() {
+ super(ThrowableInvertedPropertyRendererFactory::extractThrowingMethod);
+ }
+
+ @Nullable
+ private static StackTraceElement extractThrowingMethod(final Throwable
throwable) {
+ final Throwable rootThrowable = Throwables.getRootCause(throwable);
+ return rootThrowable.getStackTrace()[0];
+ }
+}
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowableInvertedStackTraceRenderer.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowableInvertedStackTraceRenderer.java
index 786016f549..6ad7173462 100644
---
a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowableInvertedStackTraceRenderer.java
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowableInvertedStackTraceRenderer.java
@@ -21,7 +21,7 @@ import java.util.Set;
import org.jspecify.annotations.Nullable;
/**
- * {@link ThrowableStackTraceRenderer} variant where the stack trace causal
chain is rendered in reverse order.
+ * {@link ThrowableStackTraceRenderer} variant where the stack trace causal
chain is processed in reverse order.
*/
final class ThrowableInvertedStackTraceRenderer
extends
ThrowableStackTraceRenderer<ThrowableStackTraceRenderer.Context> {
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowableInvertedStackTraceRendererFactory.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowableInvertedStackTraceRendererFactory.java
new file mode 100644
index 0000000000..d8c61eda9b
--- /dev/null
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowableInvertedStackTraceRendererFactory.java
@@ -0,0 +1,31 @@
+/*
+ * 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.pattern;
+
+import org.apache.logging.log4j.core.impl.ThrowableFormatOptions;
+
+final class ThrowableInvertedStackTraceRendererFactory extends
ThrowableStackTraceRendererFactory {
+
+ static final ThrowableInvertedStackTraceRendererFactory INSTANCE = new
ThrowableInvertedStackTraceRendererFactory();
+
+ private ThrowableInvertedStackTraceRendererFactory() {}
+
+ @Override
+ ThrowableInvertedStackTraceRenderer
createStackTraceRenderer(ThrowableFormatOptions options) {
+ return new
ThrowableInvertedStackTraceRenderer(options.getIgnorePackages(),
options.getLines());
+ }
+}
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowablePatternConverter.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowablePatternConverter.java
index d22926b781..aa6009ed2f 100644
---
a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowablePatternConverter.java
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowablePatternConverter.java
@@ -55,15 +55,15 @@ public class ThrowablePatternConverter extends
LogEventPatternConverter {
private final ThrowableRenderer renderer;
/**
- * @deprecated Use {@link #ThrowablePatternConverter(String, String,
String[], Configuration, Function)} instead.
+ * @deprecated Use {@link #ThrowablePatternConverter(String, String,
String[], Configuration, ThrowablePropertyRendererFactory,
ThrowableStackTraceRendererFactory)} instead.
*/
@Deprecated
protected ThrowablePatternConverter(final String name, final String style,
@Nullable final String[] options) {
- this(name, style, options, null, null);
+ this(name, style, options, null, null, null);
}
/**
- * @deprecated Use {@link #ThrowablePatternConverter(String, String,
String[], Configuration, Function)} instead.
+ * @deprecated Use {@link #ThrowablePatternConverter(String, String,
String[], Configuration, ThrowablePropertyRendererFactory,
ThrowableStackTraceRendererFactory)} instead.
*/
@Deprecated
protected ThrowablePatternConverter(
@@ -71,7 +71,7 @@ public class ThrowablePatternConverter extends
LogEventPatternConverter {
final String style,
@Nullable final String[] options,
@Nullable final Configuration config) {
- this(name, style, options, config, null);
+ this(name, style, options, config, null, null);
}
/**
@@ -89,8 +89,8 @@ public class ThrowablePatternConverter extends
LogEventPatternConverter {
final String style,
@Nullable final String[] options,
@Nullable final Configuration config,
- @Nullable
- final Function<ThrowableFormatOptions,
ThrowableStackTraceRenderer<?>> stackTraceRendererFactory) {
+ @Nullable final ThrowablePropertyRendererFactory
propertyRendererFactory,
+ @Nullable final ThrowableStackTraceRendererFactory
stackTraceRendererFactory) {
// Process `name`, `style`, and `options`
super(name, style);
@@ -103,16 +103,8 @@ public class ThrowablePatternConverter extends
LogEventPatternConverter {
this.formatters = Collections.unmodifiableList(suffixFormatters);
// Create the effective renderer
- final ThrowablePropertyRenderer propertyRenderer =
ThrowablePropertyRenderer.fromOptions(options);
- if (propertyRenderer != null) {
- this.renderer = propertyRenderer;
- } else {
- final Function<ThrowableFormatOptions,
ThrowableStackTraceRenderer<?>> effectiveRendererFactory =
- stackTraceRendererFactory != null
- ? stackTraceRendererFactory
- : ThrowablePatternConverter::createRenderer;
- this.renderer = effectiveRendererFactory.apply(this.options);
- }
+ this.renderer =
+ createEffectiveRenderer(options, this.options,
propertyRendererFactory, stackTraceRendererFactory);
}
/**
@@ -124,7 +116,7 @@ public class ThrowablePatternConverter extends
LogEventPatternConverter {
*/
public static ThrowablePatternConverter newInstance(
@Nullable final Configuration config, @Nullable final String[]
options) {
- return new ThrowablePatternConverter("Throwable", "throwable",
options, config, null);
+ return new ThrowablePatternConverter("Throwable", "throwable",
options, config, null, null);
}
/**
@@ -137,18 +129,10 @@ public class ThrowablePatternConverter extends
LogEventPatternConverter {
final Throwable throwable = event.getThrown();
if (throwable != null) {
final String lineSeparator =
effectiveLineSeparatorProvider.apply(event);
- ensureWhitespaceSuffix(buffer);
renderer.renderThrowable(buffer, throwable, lineSeparator);
}
}
- private static void ensureWhitespaceSuffix(final StringBuilder buffer) {
- final int bufferLength = buffer.length();
- if (bufferLength > 0 &&
!Character.isWhitespace(buffer.charAt(bufferLength - 1))) {
- buffer.append(' ');
- }
- }
-
/**
* Indicates this converter handles {@link Throwable}s.
*
@@ -234,8 +218,25 @@ public class ThrowablePatternConverter extends
LogEventPatternConverter {
}
}
- private static ThrowableStackTraceRenderer<?> createRenderer(final
ThrowableFormatOptions options) {
- return new ThrowableStackTraceRenderer<>(options.getIgnorePackages(),
options.getLines());
+ private static ThrowableRenderer createEffectiveRenderer(
+ final String[] rawOptions,
+ final ThrowableFormatOptions options,
+ @Nullable final ThrowablePropertyRendererFactory
propertyRendererFactory,
+ @Nullable final ThrowableStackTraceRendererFactory
stackTraceRendererFactory) {
+
+ // Try to create a property renderer first
+ final ThrowablePropertyRendererFactory
effectivePropertyRendererFactory =
+ propertyRendererFactory != null ? propertyRendererFactory :
ThrowablePropertyRendererFactory.INSTANCE;
+ final ThrowableRenderer propertyRenderer =
effectivePropertyRendererFactory.createPropertyRenderer(rawOptions);
+ if (propertyRenderer != null) {
+ return propertyRenderer;
+ }
+
+ // Create a stack trace renderer
+ final ThrowableStackTraceRendererFactory
effectiveStackTraceRendererFactory = stackTraceRendererFactory != null
+ ? stackTraceRendererFactory
+ : ThrowableStackTraceRendererFactory.INSTANCE;
+ return
effectiveStackTraceRendererFactory.createStackTraceRenderer(options);
}
/**
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowablePropertyRenderer.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowablePropertyRenderer.java
deleted file mode 100644
index 62fce22e67..0000000000
---
a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowablePropertyRenderer.java
+++ /dev/null
@@ -1,98 +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.pattern;
-
-import org.apache.logging.log4j.core.impl.ThrowableFormatOptions;
-import org.jspecify.annotations.NullMarked;
-import org.jspecify.annotations.Nullable;
-
-@NullMarked
-enum ThrowablePropertyRenderer implements ThrowableRenderer {
- MESSAGE(ThrowableFormatOptions.MESSAGE, (buffer, throwable, lineSeparator)
-> {
- final String message = throwable.getMessage();
- buffer.append(message);
- buffer.append(lineSeparator);
- }),
- LOCALIZED_MESSAGE(ThrowableFormatOptions.LOCALIZED_MESSAGE, (buffer,
throwable, lineSeparator) -> {
- final String localizedMessage = throwable.getLocalizedMessage();
- buffer.append(localizedMessage);
- buffer.append(lineSeparator);
- }),
- CLASS_NAME(ThrowableFormatOptions.CLASS_NAME, ((buffer, throwable,
lineSeparator) -> {
- @Nullable final StackTraceElement[] stackTraceElements =
throwable.getStackTrace();
- if (stackTraceElements != null && stackTraceElements.length > 0) {
- final StackTraceElement throwingMethod = stackTraceElements[0];
- final String className = throwingMethod.getClassName();
- buffer.append(className);
- buffer.append(lineSeparator);
- }
- })),
- METHOD_NAME(ThrowableFormatOptions.METHOD_NAME, ((buffer, throwable,
lineSeparator) -> {
- @Nullable final StackTraceElement[] stackTraceElements =
throwable.getStackTrace();
- if (stackTraceElements != null && stackTraceElements.length > 0) {
- final StackTraceElement throwingMethod = stackTraceElements[0];
- final String methodName = throwingMethod.getMethodName();
- buffer.append(methodName);
- buffer.append(lineSeparator);
- }
- })),
- LINE_NUMBER(ThrowableFormatOptions.LINE_NUMBER, ((buffer, throwable,
lineSeparator) -> {
- @Nullable final StackTraceElement[] stackTraceElements =
throwable.getStackTrace();
- if (stackTraceElements != null && stackTraceElements.length > 0) {
- final StackTraceElement throwingMethod = stackTraceElements[0];
- final int lineNumber = throwingMethod.getLineNumber();
- buffer.append(lineNumber);
- buffer.append(lineSeparator);
- }
- })),
- FILE_NAME(ThrowableFormatOptions.FILE_NAME, ((buffer, throwable,
lineSeparator) -> {
- @Nullable final StackTraceElement[] stackTraceElements =
throwable.getStackTrace();
- if (stackTraceElements != null && stackTraceElements.length > 0) {
- final StackTraceElement throwingMethod = stackTraceElements[0];
- final String fileName = throwingMethod.getFileName();
- buffer.append(fileName);
- buffer.append(lineSeparator);
- }
- }));
-
- private final String name;
-
- private final ThrowableRenderer delegate;
-
- ThrowablePropertyRenderer(final String name, final ThrowableRenderer
delegate) {
- this.name = name;
- this.delegate = delegate;
- }
-
- @Override
- public void renderThrowable(final StringBuilder buffer, final Throwable
throwable, final String lineSeparator) {
- delegate.renderThrowable(buffer, throwable, lineSeparator);
- }
-
- @Nullable
- static ThrowablePropertyRenderer fromOptions(@Nullable final String[]
options) {
- if (options != null && options.length > 0) {
- final String name = options[0];
- for (final ThrowablePropertyRenderer renderer : values()) {
- if (renderer.name.equalsIgnoreCase(name)) {
- return renderer;
- }
- }
- }
- return null;
- }
-}
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowablePropertyRendererFactory.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowablePropertyRendererFactory.java
new file mode 100644
index 0000000000..c0fc761117
--- /dev/null
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowablePropertyRendererFactory.java
@@ -0,0 +1,120 @@
+/*
+ * 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.pattern;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+import org.apache.logging.log4j.core.impl.ThrowableFormatOptions;
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * A factory of {@link ThrowableRenderer} implementations for extracting
certain properties from a {@link Throwable}.
+ */
+@NullMarked
+class ThrowablePropertyRendererFactory {
+
+ private static final ThrowableRenderer MESSAGE_RENDERER = (buffer,
throwable, lineSeparator) -> {
+ final String message = throwable.getMessage();
+ buffer.append(message);
+ };
+
+ private static final ThrowableRenderer LOCALIZED_MESSAGE_RENDERER =
(buffer, throwable, lineSeparator) -> {
+ final String localizedMessage = throwable.getLocalizedMessage();
+ buffer.append(localizedMessage);
+ };
+
+ private static final Function<Throwable, @Nullable StackTraceElement>
THROWING_METHOD_EXTRACTOR = throwable -> {
+ @Nullable final StackTraceElement[] stackTraceElements =
throwable.getStackTrace();
+ return (stackTraceElements != null && stackTraceElements.length > 0) ?
stackTraceElements[0] : null;
+ };
+
+ static final ThrowablePropertyRendererFactory INSTANCE =
+ new ThrowablePropertyRendererFactory(THROWING_METHOD_EXTRACTOR);
+
+ private final Map<String, ThrowableRenderer> rendererByPropertyName;
+
+ ThrowablePropertyRendererFactory(final Function<Throwable, @Nullable
StackTraceElement> throwingMethodExtractor) {
+ this.rendererByPropertyName =
createRendererByPropertyName(throwingMethodExtractor);
+ }
+
+ private static Map<String, ThrowableRenderer> createRendererByPropertyName(
+ final Function<Throwable, @Nullable StackTraceElement>
throwingMethodExtractor) {
+ final Map<String, ThrowableRenderer> map = new HashMap<>();
+ map.put(ThrowableFormatOptions.MESSAGE, MESSAGE_RENDERER);
+ map.put(ThrowableFormatOptions.LOCALIZED_MESSAGE,
LOCALIZED_MESSAGE_RENDERER);
+ map.put(ThrowableFormatOptions.CLASS_NAME,
createClassNameRenderer(throwingMethodExtractor));
+ map.put(ThrowableFormatOptions.METHOD_NAME,
createMethodNameRenderer(throwingMethodExtractor));
+ map.put(ThrowableFormatOptions.LINE_NUMBER,
createLineNumberRenderer(throwingMethodExtractor));
+ map.put(ThrowableFormatOptions.FILE_NAME,
createFileNameRenderer(throwingMethodExtractor));
+ return map;
+ }
+
+ private static ThrowableRenderer createClassNameRenderer(
+ final Function<Throwable, @Nullable StackTraceElement>
throwingMethodExtractor) {
+ return (buffer, throwable, lineSeparator) -> {
+ @Nullable final StackTraceElement throwingMethod =
throwingMethodExtractor.apply(throwable);
+ if (throwingMethod != null) {
+ final String className = throwingMethod.getClassName();
+ buffer.append(className);
+ }
+ };
+ }
+
+ private static ThrowableRenderer createMethodNameRenderer(
+ final Function<Throwable, @Nullable StackTraceElement>
throwingMethodExtractor) {
+ return (buffer, throwable, lineSeparator) -> {
+ @Nullable final StackTraceElement throwingMethod =
throwingMethodExtractor.apply(throwable);
+ if (throwingMethod != null) {
+ final String methodName = throwingMethod.getMethodName();
+ buffer.append(methodName);
+ }
+ };
+ }
+
+ private static ThrowableRenderer createLineNumberRenderer(
+ final Function<Throwable, @Nullable StackTraceElement>
throwingMethodExtractor) {
+ return (buffer, throwable, lineSeparator) -> {
+ @Nullable final StackTraceElement throwingMethod =
throwingMethodExtractor.apply(throwable);
+ if (throwingMethod != null) {
+ final int lineNumber = throwingMethod.getLineNumber();
+ buffer.append(lineNumber);
+ }
+ };
+ }
+
+ private static ThrowableRenderer createFileNameRenderer(
+ final Function<Throwable, @Nullable StackTraceElement>
throwingMethodExtractor) {
+ return (buffer, throwable, lineSeparator) -> {
+ @Nullable final StackTraceElement throwingMethod =
throwingMethodExtractor.apply(throwable);
+ if (throwingMethod != null) {
+ final String fileName = throwingMethod.getFileName();
+ buffer.append(fileName);
+ }
+ };
+ }
+
+ @Nullable
+ final ThrowableRenderer createPropertyRenderer(@Nullable final String[]
options) {
+ if (options != null && options.length > 0) {
+ final String propertyName = options[0];
+ return rendererByPropertyName.get(propertyName);
+ }
+ return null;
+ }
+}
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowableStackTraceRenderer.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowableStackTraceRenderer.java
index b16e9b9836..a6211147a8 100644
---
a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowableStackTraceRenderer.java
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowableStackTraceRenderer.java
@@ -16,6 +16,8 @@
*/
package org.apache.logging.log4j.core.pattern;
+import static org.apache.logging.log4j.util.Strings.LINE_SEPARATOR;
+
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -53,6 +55,7 @@ class ThrowableStackTraceRenderer<C extends
ThrowableStackTraceRenderer.Context>
if (maxLineCount > 0) {
try {
C context = createContext(throwable);
+ ensureNewlineSuffix(buffer);
renderThrowable(buffer, throwable, context, new HashSet<>(),
lineSeparator);
} catch (final Exception error) {
if (error != MAX_LINE_COUNT_EXCEEDED) {
@@ -62,6 +65,13 @@ class ThrowableStackTraceRenderer<C extends
ThrowableStackTraceRenderer.Context>
}
}
+ private static void ensureNewlineSuffix(final StringBuilder buffer) {
+ final int bufferLength = buffer.length();
+ if (bufferLength > 0 && buffer.charAt(bufferLength - 1) != '\n') {
+ buffer.append(LINE_SEPARATOR);
+ }
+ }
+
@SuppressWarnings("unchecked")
C createContext(final Throwable throwable) {
final Map<Throwable, Context.Metadata> metadataByThrowable =
Context.Metadata.ofThrowable(throwable);
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowableStackTraceRendererFactory.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowableStackTraceRendererFactory.java
new file mode 100644
index 0000000000..4e6fee7fcc
--- /dev/null
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowableStackTraceRendererFactory.java
@@ -0,0 +1,33 @@
+/*
+ * 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.pattern;
+
+import org.apache.logging.log4j.core.impl.ThrowableFormatOptions;
+
+/**
+ * A {@link ThrowableStackTraceRenderer} factory contract.
+ */
+class ThrowableStackTraceRendererFactory {
+
+ static final ThrowableStackTraceRendererFactory INSTANCE = new
ThrowableStackTraceRendererFactory();
+
+ ThrowableStackTraceRendererFactory() {}
+
+ ThrowableStackTraceRenderer<?> createStackTraceRenderer(final
ThrowableFormatOptions options) {
+ return new ThrowableStackTraceRenderer<>(options.getIgnorePackages(),
options.getLines());
+ }
+}
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/Throwables.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/Throwables.java
index 226b0f10cb..7c97d495ba 100644
---
a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/Throwables.java
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/Throwables.java
@@ -16,6 +16,8 @@
*/
package org.apache.logging.log4j.core.util;
+import static java.util.Objects.requireNonNull;
+
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.IOException;
import java.io.InterruptedIOException;
@@ -24,7 +26,9 @@ import java.io.PrintWriter;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
/**
* Helps with Throwable objects.
@@ -34,31 +38,22 @@ public final class Throwables {
private Throwables() {}
/**
- * Returns the deepest cause of the given {@code throwable}.
+ * Extracts the deepest exception in the causal chain of the given {@code
throwable}.
+ * Circular references will be handled and ignored.
*
- * @param throwable the throwable to navigate
- * @return the deepest throwable or the given throwable
+ * @param throwable a throwable to navigate
+ * @return the deepest exception in the causal chain
*/
public static Throwable getRootCause(final Throwable throwable) {
-
- // Keep a second pointer that slowly walks the causal chain. If the
fast
- // pointer ever catches the slower pointer, then there's a loop.
- Throwable slowPointer = throwable;
- boolean advanceSlowPointer = false;
-
- Throwable parent = throwable;
- Throwable cause;
- while ((cause = parent.getCause()) != null) {
- parent = cause;
- if (parent == slowPointer) {
- throw new IllegalArgumentException("loop in causal chain");
- }
- if (advanceSlowPointer) {
- slowPointer = slowPointer.getCause();
- }
- advanceSlowPointer = !advanceSlowPointer; // only advance every
other iteration
+ requireNonNull(throwable, "throwable");
+ final Set<Throwable> visitedThrowables = new HashSet<>();
+ Throwable prevCause = throwable;
+ visitedThrowables.add(prevCause);
+ Throwable nextCause;
+ while ((nextCause = prevCause.getCause()) != null &&
visitedThrowables.add(nextCause)) {
+ prevCause = nextCause;
}
- return parent;
+ return prevCause;
}
/**
diff --git
a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceStringResolver.java
b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceStringResolver.java
index dddf66f727..5a3e727a9c 100644
---
a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceStringResolver.java
+++
b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceStringResolver.java
@@ -16,6 +16,8 @@
*/
package org.apache.logging.log4j.layout.template.json.resolver;
+import static org.apache.logging.log4j.util.Strings.LINE_SEPARATOR;
+
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Supplier;
@@ -126,7 +128,7 @@ final class StackTraceStringResolver implements
StackTraceResolver {
final int truncationPointIndex =
findTruncationPointIndex(srcWriter, startIndex, endIndex, sequencePointer);
if (truncationPointIndex > 0) {
dstWriter.append(srcWriter, startIndex, truncationPointIndex);
- dstWriter.append(System.lineSeparator());
+ dstWriter.append(LINE_SEPARATOR);
dstWriter.append(truncationSuffix);
}
@@ -137,7 +139,7 @@ final class StackTraceStringResolver implements
StackTraceResolver {
// Copy the label to avoid stepping over it again.
if (labeledLineStartIndex > 0) {
- dstWriter.append(System.lineSeparator());
+ dstWriter.append(LINE_SEPARATOR);
startIndex = labeledLineStartIndex;
for (; ; ) {
final char c = srcWriter.charAt(startIndex++);
diff --git a/src/site/antora/modules/ROOT/pages/manual/pattern-layout.adoc
b/src/site/antora/modules/ROOT/pages/manual/pattern-layout.adoc
index 57c29d892f..c754462477 100644
--- a/src/site/antora/modules/ROOT/pages/manual/pattern-layout.adoc
+++ b/src/site/antora/modules/ROOT/pages/manual/pattern-layout.adoc
@@ -142,7 +142,7 @@ xref:manual/lookups.adoc#event-context[event evaluation
context].
[WARNING]
====
-If the provided pattern does not contain an exception converter and
<<plugin-attr-alwaysWriteExceptions>> is not disabled, an implicit
<<converter-exception-extended,`%xEX`>> is appended to the pattern.
+If the provided pattern does not contain an exception converter and
<<plugin-attr-alwaysWriteExceptions>> is not disabled, an implicit
<<converter-exception-extended,`%xEx`>> is appended to the pattern.
====
[#plugin-attr-patternSelector]
@@ -176,7 +176,7 @@ If configured, the `replace` element must specify the
regular expression to matc
|Default value |`true`
|===
-If `true` and the user-provided pattern does not contain an exception
converter, an implicit <<converter-exception-extended,`%xEX`>> pattern is
appended.
+If `true` and the user-provided pattern does not contain an exception
converter, an implicit <<converter-exception-extended,`%xEx`>> pattern is
appended.
This means that if you do not include a way to output exceptions in your
pattern, the default exception formatter will be added to the end of the
pattern.
Setting this to `false` disables this behavior and allows you to exclude
exceptions from your pattern output.
@@ -599,69 +599,76 @@ equals{pattern}{test}{substitution}
equalsIgnoreCase{pattern}{test}{substitution}
----
-For example, `%equals{[%marker]}{[]}\{}` will replace `[]` strings produced by
events without markers with an empty string.
+For example, `%equals{[%marker]}{[]}{}` will replace `[]` strings produced by
events without markers with an empty string.
The pattern can be arbitrarily complex and in particular can contain multiple
conversion keywords.
[#converter-exception]
==== Exception
-Outputs the `Throwable` attached to the log event
+Outputs information extracted from the `Throwable` attached to the log event.
+It features two modes:
-.link:../javadoc/log4j-core/org/apache/logging/log4j/core/pattern/ThrowablePatternConverter.html[`ThrowablePatternConverter`]
specifier grammar
+. <<converter-exception-stack-trace,Rendering the exception stack trace>> (the
default mode)
+. <<converter-exception-property,Extracting an exception property>> (message,
class name, line number, etc.)
+
+[WARNING]
+====
+Exception converter is not garbage-free.
+====
+
+[#converter-exception-stack-trace]
+===== Exception stack trace
+
+In this mode, the exception stack trace will be rendered according to the
configuration provided.
+
+[IMPORTANT]
+====
+All rendered exception stack traces are ensured to be prefixed with a new line
obtained using `System.lineSeparator()`.
+====
+
+link:../javadoc/log4j-core/org/apache/logging/log4j/core/pattern/ThrowablePatternConverter.html[`ThrowablePatternConverter`]
specifier grammar **for rendering stack traces**:
[source,text]
----
ex|exception|throwable
{ "none"
- | "full"
- | depth
| "short"
- | "short.className"
- | "short.fileName"
- | "short.lineNumber"
- | "short.methodName"
- | "short.message"
- | "short.localizedMessage"
+ | depth
+ | "full"
}
{filters(package,package,...)}
+ {separator(text)}
{suffix(pattern)}
- {separator(separator)}
----
-By default this will output the full stack trace as one would normally find
with a call to `Throwable#printStackTrace()`.
-
-You can follow the throwable conversion word with an option in the form
`%throwable\{option}`.
-
-`%throwable\{short}` outputs the first line of the `Throwable`.
-
-`%throwable{short.className}` outputs the name of the class where the
exception occurred.
-
-`%throwable{short.methodName}` outputs the name of the method where the
exception occurred.
+If this mode is employed without any configuration, the output will be
identical to the one obtained from `Throwable#printStackTrace()`.
-`%throwable{short.fileName}` outputs the name of the file containing the class
where the exception occurred.
+`none`:: Suppress the output of the converter
-`%throwable{short.lineNumber}` outputs the line number of the file containing
the class where the exception occurred.
+`short`:: Outputs the first line of the stack trace (analogous to `%ex\{1}`)
-`%throwable{short.message}` outputs the message.
+`depth`:: Outputs the first `depth` lines of the stack trace (`%ex\{0}` is
analogous to `%ex\{none}`)
-`%throwable{short.localizedMessage}` outputs the localized message.
+`full`:: Outputs the complete stack trace (analogous to no configuration)
-`%throwable\{n}` outputs the first `n` lines of the stack trace.
-
-Specifying `%throwable\{none}` or `%throwable\{0}` suppresses output of the
exception.
-
-Use `{filters(packages)}`, where `packages` is a list of package names, to
suppress matching stack frames from stack traces.
+`filters(package,package,...)`:: Suppresses stack trace elements of classes
located in packages whose names start with the package names provided.
+Suppressed stack trace elements will be denoted in the output.
+For instance, `%ex{filters(org.junit)}` can be used to suppress JUnit classes
in the rendered stack trace.
+`separator(text)`::
+`suffix(pattern)`::
++
+--
You can change the used line separator in multiple ways:
-* Use `{separator(separator)}` to set the separator string literal.
+* Use `separator(text)` to set the separator string literal.
It defaults to `System.lineSeparator()`.
-The contents of `separator` will be rendered verbatim without being subject to
any processing.
+The contents of `text` will be rendered verbatim without being subject to any
processing.
-* `{suffix(pattern)}` is identical to `{separator(separator)}` with the
exception that the provided `pattern` will be processed as a
xref:manual/pattern-layout.adoc[] conversion pattern before being rendered.
+* `suffix(pattern)` is identical to `{separator(text)}` with the exception
that the provided `pattern` will be processed as a
xref:manual/pattern-layout.adoc[] conversion pattern before being rendered.
Exception-rendering directives in the `pattern` (`%ex`, `%rEx`, etc.) will be
discarded.
-`{separator(...)}` and `{suffix(pattern)}` get concatenated to produce _the
effective line separator_ as follows:
+`{separator(text)}` and `{suffix(pattern)}` get concatenated to produce _the
effective line separator_ as follows:
[source,java]
----
@@ -673,38 +680,52 @@ String effectiveLineSeparator(String separator, String
suffix, LogEvent event) {
}
----
-[WARNING]
+[TIP]
====
-Exception converter is not garbage-free.
+You are strongly advised to avoid using both `separator(text)` and
`suffix(pattern)` at the same time; simply use one instead.
====
+--
-[#converter-exception-extended]
-==== Exception (Extended)
+[#converter-exception-property]
+===== Exception property
-The same as <<converter-exception,the `exception` converter>>, but also
includes class packaging information
+In this mode, extracted attributes of the `Throwable` are injected _verbatim_.
+That is, no newlines, suffixes, prefixes, etc. will be added.
-.link:../javadoc/log4j-core/org/apache/logging/log4j/core/pattern/ThrowablePatternConverter.html[`ThrowablePatternConverter`]
specifier grammar
+link:../javadoc/log4j-core/org/apache/logging/log4j/core/pattern/ThrowablePatternConverter.html[`ThrowablePatternConverter`]
specifier grammar **for extracting properties**:
[source,text]
----
-xEx|xException|xThrowable
- { "none"
- | "full"
- | depth
- | "short"
- | "short.className"
+ex|exception|throwable
+ { "short.className"
| "short.fileName"
| "short.lineNumber"
| "short.methodName"
| "short.message"
| "short.localizedMessage"
}
- {filters(package,package,...)}
- {suffix(pattern)}
- {separator(separator)}
----
-Different from <<converter-exception,the `%throwable` conversion>>, at the end
of each stack element of the exception, a string containing the name of the JAR
file that contains the class or the directory the class is located in and the
`Implementation-Version` as found in that JAR's manifest will be added.
-If the information is uncertain, then the class packaging data will be
preceded by a `~` (tilde) character.
+`short.className`:: Class name of the first stack trace element in the causal
chain
+`short.fileName`:: File name of the first stack trace element in the causal
chain
+`short.lineNumber`:: Line number of the first stack trace element in the
causal chain
+`short.methodName`:: Method name of the first stack trace element in the
causal chain
+`short.message`:: Exception message
+`short.message`:: Localized exception message
+
+[#converter-exception-extended]
+==== Exception (Extended)
+
+The same as <<converter-exception,the `exception` converter>>, but
additionally includes class packaging information in the rendered stack traces.
+
+.link:../javadoc/log4j-core/org/apache/logging/log4j/core/pattern/ExtendedThrowablePatternConverter.html[`ExtendedThrowablePatternConverter`]
specifier grammar
+[source,text]
+----
+xEx|xException|xThrowable
+ [... same as the exception converter grammar ...]
+----
+
+Each stack trace element is suffixed with a string containing the name of the
JAR file that contains the class (or the directory the class is located in) and
the `Implementation-Version` as found in that JAR's manifest.
+If the information is uncertain, then the class packaging information will be
preceded by a `~` (tilde) character.
[#converter-file]
==== File
@@ -1194,28 +1215,21 @@ For instance, `%replace{%logger %msg}{\.}{/}` will
replace all dots in the logge
[#converter-rootException]
==== Root exception
-The same as <<converter-exception,the `exception` converter>>, but the stack
trace is printed starting with the first exception in the causal chain that was
thrown followed by each subsequent wrapping exception
+Same as <<converter-exception,the `exception` converter>>, but the stack trace
causal chain is processed in reverse order.
.link:../javadoc/log4j-core/org/apache/logging/log4j/core/pattern/RootThrowablePatternConverter.html[`RootThrowablePatternConverter`]
specifier grammar
[source,text]
----
rEx|rException|rThrowable
- { "none"
- | "full"
- | depth
- | "short"
- | "short.className"
- | "short.fileName"
- | "short.lineNumber"
- | "short.methodName"
- | "short.message"
- | "short.localizedMessage"
- }
- {filters(package,package,...)}
- {suffix(pattern)}
- {separator(separator)}
+ [... same as the exception converter grammar ...]
----
+[IMPORTANT]
+====
+Note that the inverted causal chain will not only affect the stack trace, but
also extracted properties.
+That is, for instance, `%rEx{short.className}` and `%ex{short.className}`
might yield different results.
+====
+
[#converter-seq]
==== Sequence number