Skip to content

Commit 1e154d4

Browse files
vyalan0428appkarwasz
authored
Prefix stack traces with a newline in Pattern Layout (#3045)
- 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}) Co-authored-by: AlanYu <[email protected]> Co-authored-by: Piotr P. Karwasz <[email protected]>
1 parent 7683460 commit 1e154d4

File tree

58 files changed

+985
-866
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+985
-866
lines changed

log4j-core-test/src/test/java/foo/TestFriendlyException.java

+23-4
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,21 @@
2727
* A testing friendly exception featuring
2828
* <ul>
2929
* <li>Distinct localized message</li>
30-
* <li>Non-Log4j package origin<sup>1</sup></li>
30+
* <li>Non-Log4j<sup>1</sup> and fixed<sup>2</sup> (to {@code bar}) package origin</li>
3131
* <li>Sufficient causal chain depth</li>
32-
* <li>Cyclic causal chain</li>
32+
* <li>Circular causal chain</li>
3333
* <li>Suppressed exceptions</li>
3434
* <li>Clutter-free stack trace (i.e., elements from JUnit, JDK, etc.)</li>
35-
* <li>Stack trace elements from named modules<sup>2</sup></li>
35+
* <li>Stack trace elements from named modules<sup>3</sup></li>
3636
* </ul>
3737
* <p>
3838
* <sup>1</sup> Helps with observing stack trace manipulation effects of Log4j.
3939
* </p>
4040
* <p>
41-
* <sup>2</sup> Helps with testing module name serialization.
41+
* <sup>2</sup> Helps to make the origin of {@link #INSTANCE} independent of the first test accessing to it.
42+
* </p>
43+
* <p>
44+
* <sup>3</sup> Helps with testing module name serialization.
4245
* </p>
4346
*/
4447
public final class TestFriendlyException extends RuntimeException {
@@ -48,6 +51,9 @@ public final class TestFriendlyException extends RuntimeException {
4851
assertThat(TestFriendlyException.class.getPackage().getName()).doesNotStartWith("org.apache");
4952
}
5053

54+
public static final StackTraceElement ORG_APACHE_REPLACEMENT_STACK_TRACE_ELEMENT =
55+
new StackTraceElement("bar.OrgApacheReplacement", "someMethod", "OrgApacheReplacement.java", 0);
56+
5157
public static final StackTraceElement NAMED_MODULE_STACK_TRACE_ELEMENT = namedModuleStackTraceElement();
5258

5359
@SuppressWarnings("resource")
@@ -127,16 +133,29 @@ private static Stream<StackTraceElement> mapStackTraceElement(
127133

128134
private static Stream<StackTraceElement> filterStackTraceElement(
129135
final StackTraceElement stackTraceElement, final boolean[] seenExcludedStackTraceElement) {
136+
137+
// Short-circuit if we have already encountered an excluded stack trace element
130138
if (seenExcludedStackTraceElement[0]) {
131139
return Stream.empty();
132140
}
141+
142+
// Check if the class name is excluded
133143
final String className = stackTraceElement.getClassName();
134144
for (final String excludedClassNamePrefix : EXCLUDED_CLASS_NAME_PREFIXES) {
135145
if (className.startsWith(excludedClassNamePrefix)) {
136146
seenExcludedStackTraceElement[0] = true;
137147
return Stream.empty();
138148
}
139149
}
150+
151+
// Replace `org.apache`-originating entries with a constant one.
152+
// Without this, `INSTANCE` might yield different origin depending on the first class accessing to it.
153+
// We remove this ambiguity and fix our origin to a constant instead.
154+
if (className.startsWith("org.apache")) {
155+
return Stream.of(ORG_APACHE_REPLACEMENT_STACK_TRACE_ELEMENT);
156+
}
157+
158+
// Otherwise, it looks good
140159
return Stream.of(stackTraceElement);
141160
}
142161

log4j-core-test/src/test/java/org/apache/logging/log4j/core/EventParameterMemoryLeakTest.java

+80-71
Original file line numberDiff line numberDiff line change
@@ -16,102 +16,111 @@
1616
*/
1717
package org.apache.logging.log4j.core;
1818

19-
import static org.hamcrest.MatcherAssert.assertThat;
20-
import static org.hamcrest.Matchers.containsString;
21-
import static org.hamcrest.Matchers.is;
22-
import static org.junit.jupiter.api.Assertions.assertNull;
23-
import static org.junit.jupiter.api.Assertions.assertTrue;
24-
25-
import java.io.BufferedReader;
26-
import java.io.File;
27-
import java.io.FileReader;
28-
import java.util.concurrent.CountDownLatch;
29-
import java.util.concurrent.TimeUnit;
30-
import org.apache.logging.log4j.LogManager;
19+
import static org.apache.logging.log4j.core.GcHelper.awaitGarbageCollection;
20+
import static org.assertj.core.api.Assertions.assertThat;
21+
22+
import java.util.List;
23+
import org.apache.logging.log4j.Level;
3124
import org.apache.logging.log4j.Logger;
32-
import org.apache.logging.log4j.core.config.ConfigurationFactory;
33-
import org.apache.logging.log4j.core.test.CoreLoggerContexts;
34-
import org.junit.jupiter.api.BeforeAll;
25+
import org.apache.logging.log4j.core.config.Configuration;
26+
import org.apache.logging.log4j.core.config.builder.api.AppenderComponentBuilder;
27+
import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilder;
28+
import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilderFactory;
29+
import org.apache.logging.log4j.core.config.builder.api.LayoutComponentBuilder;
30+
import org.apache.logging.log4j.core.config.builder.api.RootLoggerComponentBuilder;
31+
import org.apache.logging.log4j.core.config.builder.impl.BuiltConfiguration;
32+
import org.apache.logging.log4j.core.test.appender.ListAppender;
3533
import org.junit.jupiter.api.Tag;
3634
import org.junit.jupiter.api.Test;
35+
import org.junit.jupiter.api.TestInfo;
36+
import org.junitpioneer.jupiter.SetSystemProperty;
3737

3838
@Tag("functional")
39-
public class EventParameterMemoryLeakTest {
39+
class EventParameterMemoryLeakTest {
40+
41+
@Test
42+
@SetSystemProperty(key = "log4j2.enableDirectEncoders", value = "true")
43+
@SetSystemProperty(key = "log4j2.enableThreadLocals", value = "true")
44+
void parameters_should_be_garbage_collected(final TestInfo testInfo) throws Throwable {
45+
awaitGarbageCollection(() -> {
46+
final ListAppender[] appenderRef = {null};
47+
final Logger[] loggerRef = {null};
48+
try (final LoggerContext ignored = createLoggerContext(testInfo, appenderRef, loggerRef)) {
49+
50+
// Log messages
51+
final ParameterObject parameter = new ParameterObject("paramValue");
52+
loggerRef[0].info("Message with parameter {}", parameter);
53+
loggerRef[0].info(parameter);
54+
loggerRef[0].info("test", new ObjectThrowable(parameter));
55+
loggerRef[0].info("test {}", "hello", new ObjectThrowable(parameter));
4056

41-
@BeforeAll
42-
public static void beforeClass() {
43-
System.setProperty("log4j2.enableThreadlocals", "true");
44-
System.setProperty("log4j2.enableDirectEncoders", "true");
45-
System.setProperty(ConfigurationFactory.CONFIGURATION_FILE_PROPERTY, "EventParameterMemoryLeakTest.xml");
57+
// Verify the logging
58+
final List<String> messages = appenderRef[0].getMessages();
59+
assertThat(messages).hasSize(4);
60+
assertThat(messages.get(0)).isEqualTo("Message with parameter %s", parameter.value);
61+
assertThat(messages.get(1)).isEqualTo(parameter.value);
62+
assertThat(messages.get(2))
63+
.startsWith(String.format("test%n%s: %s", ObjectThrowable.class.getName(), parameter.value));
64+
assertThat(messages.get(3))
65+
.startsWith(
66+
String.format("test hello%n%s: %s", ObjectThrowable.class.getName(), parameter.value));
67+
68+
// Return the GC subject
69+
return parameter;
70+
}
71+
});
4672
}
4773

48-
@Test
49-
@SuppressWarnings("UnusedAssignment") // parameter set to null to allow garbage collection
50-
public void testParametersAreNotLeaked() throws Exception {
51-
final File file = new File("target", "EventParameterMemoryLeakTest.log");
52-
assertTrue(!file.exists() || file.delete(), "Deleted old file before test");
53-
54-
final Logger log = LogManager.getLogger("com.foo.Bar");
55-
final CountDownLatch latch = new CountDownLatch(1);
56-
Object parameter = new ParameterObject("paramValue", latch);
57-
log.info("Message with parameter {}", parameter);
58-
log.info(parameter);
59-
log.info("test", new ObjectThrowable(parameter));
60-
log.info("test {}", "hello", new ObjectThrowable(parameter));
61-
parameter = null;
62-
CoreLoggerContexts.stopLoggerContext(file);
63-
final BufferedReader reader = new BufferedReader(new FileReader(file));
64-
final String line1 = reader.readLine();
65-
final String line2 = reader.readLine();
66-
final String line3 = reader.readLine();
67-
// line4 is empty line because of the line separator after throwable pattern
68-
final String line4 = reader.readLine();
69-
final String line5 = reader.readLine();
70-
final String line6 = reader.readLine();
71-
final String line7 = reader.readLine();
72-
reader.close();
73-
file.delete();
74-
assertThat(line1, containsString("Message with parameter paramValue"));
75-
assertThat(line2, containsString("paramValue"));
76-
assertThat(line3, containsString("paramValue"));
77-
assertThat(line4, is(""));
78-
assertThat(line5, containsString("paramValue"));
79-
assertThat(line6, is(""));
80-
assertNull(line7, "Expected only six lines");
81-
final GarbageCollectionHelper gcHelper = new GarbageCollectionHelper();
82-
gcHelper.run();
83-
try {
84-
assertTrue(latch.await(30, TimeUnit.SECONDS), "Parameter should have been garbage collected");
85-
} finally {
86-
gcHelper.close();
87-
}
74+
private static LoggerContext createLoggerContext(
75+
final TestInfo testInfo, final ListAppender[] appenderRef, final Logger[] loggerRef) {
76+
final String loggerContextName = String.format("%s-LC", testInfo.getDisplayName());
77+
final LoggerContext loggerContext = new LoggerContext(loggerContextName);
78+
final String appenderName = "LIST";
79+
final Configuration configuration = createConfiguration(appenderName);
80+
loggerContext.start(configuration);
81+
appenderRef[0] = configuration.getAppender(appenderName);
82+
assertThat(appenderRef[0]).isNotNull();
83+
final Class<?> testClass = testInfo.getTestClass().orElse(null);
84+
assertThat(testClass).isNotNull();
85+
loggerRef[0] = loggerContext.getLogger(testClass);
86+
return loggerContext;
87+
}
88+
89+
@SuppressWarnings("SameParameterValue")
90+
private static Configuration createConfiguration(final String appenderName) {
91+
final ConfigurationBuilder<BuiltConfiguration> configBuilder =
92+
ConfigurationBuilderFactory.newConfigurationBuilder();
93+
final LayoutComponentBuilder layoutComponentBuilder =
94+
configBuilder.newLayout("PatternLayout").addAttribute("pattern", "%m");
95+
final AppenderComponentBuilder appenderComponentBuilder =
96+
configBuilder.newAppender(appenderName, "List").add(layoutComponentBuilder);
97+
final RootLoggerComponentBuilder loggerComponentBuilder =
98+
configBuilder.newRootLogger(Level.ALL).add(configBuilder.newAppenderRef(appenderName));
99+
return configBuilder
100+
.add(appenderComponentBuilder)
101+
.add(loggerComponentBuilder)
102+
.build(false);
88103
}
89104

90105
private static final class ParameterObject {
106+
91107
private final String value;
92-
private final CountDownLatch latch;
93108

94-
ParameterObject(final String value, final CountDownLatch latch) {
109+
private ParameterObject(final String value) {
95110
this.value = value;
96-
this.latch = latch;
97111
}
98112

99113
@Override
100114
public String toString() {
101115
return value;
102116
}
103-
104-
@Override
105-
protected void finalize() throws Throwable {
106-
latch.countDown();
107-
super.finalize();
108-
}
109117
}
110118

111119
private static final class ObjectThrowable extends RuntimeException {
120+
112121
private final Object object;
113122

114-
ObjectThrowable(final Object object) {
123+
private ObjectThrowable(final Object object) {
115124
super(String.valueOf(object));
116125
this.object = object;
117126
}

log4j-core-test/src/test/java/org/apache/logging/log4j/core/GarbageCollectionHelper.java

-66
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to you under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.logging.log4j.core;
18+
19+
import java.lang.ref.PhantomReference;
20+
import java.lang.ref.Reference;
21+
import java.lang.ref.ReferenceQueue;
22+
import org.junit.jupiter.api.function.ThrowingSupplier;
23+
24+
public final class GcHelper {
25+
26+
private GcHelper() {}
27+
28+
/**
29+
* Waits for the value to be garbage collected.
30+
*
31+
* @param valueSupplier a value provider
32+
*/
33+
@SuppressWarnings({"unused", "UnusedAssignment"})
34+
public static void awaitGarbageCollection(final ThrowingSupplier<?> valueSupplier) throws InterruptedException {
35+
36+
// Create the reference queue
37+
final ReferenceQueue<Object> refQueue = new ReferenceQueue<>();
38+
final Reference<?> ref;
39+
try {
40+
final Object value = valueSupplier.get();
41+
ref = new PhantomReference<>(value, refQueue);
42+
} catch (final Throwable error) {
43+
throw new RuntimeException("failed obtaining value", error);
44+
}
45+
46+
// Wait for the garbage collection
47+
try (final GcPressureGenerator ignored = GcPressureGenerator.ofStarted()) {
48+
final Reference<?> removedRef = refQueue.remove(30_000L);
49+
if (removedRef == null) {
50+
throw new AssertionError("garbage collector did not reclaim the value");
51+
}
52+
}
53+
}
54+
}

0 commit comments

Comments
 (0)