Skip to content

Commit 2c66a74

Browse files
authored
fix: Prevent LogBuilder memory leak in Log4j API to Logback bridge (apache#3824)
1 parent a501f98 commit 2c66a74

File tree

5 files changed

+103
-7
lines changed

5 files changed

+103
-7
lines changed

log4j-to-slf4j/pom.xml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,45 @@
153153
</execution>
154154
</executions>
155155
</plugin>
156+
156157
</plugins>
157158
</build>
159+
160+
<profiles>
161+
162+
<!-- Fixes incompatible with Java 8 -->
163+
<profile>
164+
165+
<id>java8-incompat-fixes</id>
166+
167+
<!-- CI uses Java 8 for running tests.
168+
Hence, we assume CI=Java8 and apply our changes elsewhere.
169+
170+
One might think why not activate using `<jdk>[16,)` instead?
171+
This doesn't work, since the match is not against "the JDK running tests", but "the JDK running Maven".
172+
These two JDKs can differ due to Maven Toolchains.
173+
See `java8-tests` profile in `/pom.xml` for details. -->
174+
<activation>
175+
<property>
176+
<name>!env.CI</name>
177+
</property>
178+
</activation>
179+
180+
<!-- Illegal access is disabled by default in Java 16 due to JEP-396.
181+
We are relaxing it for tests. -->
182+
<build>
183+
<plugins>
184+
<plugin>
185+
<groupId>org.apache.maven.plugins</groupId>
186+
<artifactId>maven-surefire-plugin</artifactId>
187+
<configuration>
188+
<argLine>--add-opens java.base/java.lang=ALL-UNNAMED</argLine>
189+
</configuration>
190+
</plugin>
191+
</plugins>
192+
</build>
193+
194+
</profile>
195+
196+
</profiles>
158197
</project>

log4j-to-slf4j/src/main/java/org/apache/logging/slf4j/SLF4JLogger.java

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,17 +42,21 @@ public class SLF4JLogger extends AbstractLogger {
4242

4343
private final org.slf4j.Logger logger;
4444
private final LocationAwareLogger locationAwareLogger;
45+
private final boolean useThreadLocal;
4546

4647
public SLF4JLogger(final String name, final MessageFactory messageFactory, final org.slf4j.Logger logger) {
47-
super(name, messageFactory);
48-
this.logger = logger;
49-
this.locationAwareLogger = logger instanceof LocationAwareLogger ? (LocationAwareLogger) logger : null;
48+
this(name, messageFactory, logger, Constants.ENABLE_THREADLOCALS);
5049
}
5150

5251
public SLF4JLogger(final String name, final org.slf4j.Logger logger) {
53-
super(name);
52+
this(name, null, logger);
53+
}
54+
55+
SLF4JLogger(String name, MessageFactory messageFactory, org.slf4j.Logger logger, boolean useThreadLocal) {
56+
super(name, messageFactory);
5457
this.logger = logger;
5558
this.locationAwareLogger = logger instanceof LocationAwareLogger ? (LocationAwareLogger) logger : null;
59+
this.useThreadLocal = useThreadLocal;
5660
}
5761

5862
private int convertLevel(final Level level) {
@@ -364,8 +368,8 @@ public LogBuilder atFatal() {
364368

365369
@Override
366370
protected LogBuilder getLogBuilder(final Level level) {
367-
final SLF4JLogBuilder builder = logBuilder.get();
368-
return Constants.ENABLE_THREADLOCALS && !builder.isInUse()
371+
SLF4JLogBuilder builder;
372+
return useThreadLocal && !(builder = logBuilder.get()).isInUse()
369373
? builder.reset(this, level)
370374
: new SLF4JLogBuilder(this, level);
371375
}

log4j-to-slf4j/src/test/java/org/apache/logging/slf4j/LoggerTest.java renamed to log4j-to-slf4j/src/test/java/org/apache/logging/slf4j/SLF4JLoggerTest.java

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,13 @@
3232
import ch.qos.logback.classic.LoggerContext;
3333
import ch.qos.logback.classic.spi.ILoggingEvent;
3434
import ch.qos.logback.core.testUtil.StringListAppender;
35+
import java.lang.reflect.Field;
3536
import java.lang.reflect.InvocationTargetException;
37+
import java.lang.reflect.Method;
3638
import java.lang.reflect.Proxy;
3739
import java.util.Date;
3840
import java.util.List;
41+
import org.apache.logging.log4j.LogBuilder;
3942
import org.apache.logging.log4j.LogManager;
4043
import org.apache.logging.log4j.Logger;
4144
import org.apache.logging.log4j.ThreadContext;
@@ -45,13 +48,17 @@
4548
import org.apache.logging.log4j.spi.AbstractLogger;
4649
import org.apache.logging.log4j.spi.MessageFactory2Adapter;
4750
import org.apache.logging.log4j.test.junit.UsingStatusListener;
51+
import org.assertj.core.api.Assertions;
4852
import org.junit.jupiter.api.BeforeEach;
4953
import org.junit.jupiter.api.Test;
54+
import org.junit.jupiter.params.ParameterizedTest;
55+
import org.junit.jupiter.params.provider.ValueSource;
56+
import org.junitpioneer.jupiter.Issue;
5057
import org.slf4j.MDC;
5158

5259
@UsingStatusListener
5360
@LoggerContextSource
54-
class LoggerTest {
61+
class SLF4JLoggerTest {
5562

5663
private static final Object OBJ = new Object();
5764
// Log4j objects
@@ -265,4 +272,38 @@ void mdcCannotContainNullKey() {
265272
// expected
266273
}
267274
}
275+
276+
@ParameterizedTest
277+
@ValueSource(booleans = {true, false})
278+
@Issue("https://github.com/apache/logging-log4j2/issues/3819")
279+
void threadLocalUsage(boolean useThreadLocal) throws ReflectiveOperationException {
280+
// Reset the static ThreadLocal in SLF4JLogger
281+
getLogBuilderThreadLocal().remove();
282+
final org.slf4j.Logger slf4jLogger = context.getLogger(getClass());
283+
Logger logger = new SLF4JLogger(slf4jLogger.getName(), null, slf4jLogger, useThreadLocal);
284+
LogBuilder builder1 = logger.atInfo();
285+
builder1.log("Test message");
286+
LogBuilder builder2 = logger.atInfo();
287+
builder2.log("Another test message");
288+
// Check if the same builder is reused based on the useThreadLocal flag
289+
Assertions.assertThat(isThreadLocalPresent())
290+
.as("ThreadLocal should be present iff useThreadLocal is enabled")
291+
.isEqualTo(useThreadLocal);
292+
Assertions.assertThat(builder2 == builder1)
293+
.as("Builder2 should be the same as Builder1 iff useThreadLocal is enabled")
294+
.isEqualTo(useThreadLocal);
295+
}
296+
297+
private static boolean isThreadLocalPresent() throws ReflectiveOperationException {
298+
Method isPresentMethod = ThreadLocal.class.getDeclaredMethod("isPresent");
299+
isPresentMethod.setAccessible(true);
300+
ThreadLocal<?> threadLocal = getLogBuilderThreadLocal();
301+
return (boolean) isPresentMethod.invoke(threadLocal);
302+
}
303+
304+
private static ThreadLocal<?> getLogBuilderThreadLocal() throws ReflectiveOperationException {
305+
Field logBuilderField = SLF4JLogger.class.getDeclaredField("logBuilder");
306+
logBuilderField.setAccessible(true);
307+
return (ThreadLocal<?>) logBuilderField.get(null);
308+
}
268309
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<entry xmlns="https://logging.apache.org/xml/ns"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="
5+
https://logging.apache.org/xml/ns
6+
https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
7+
type="fixed">
8+
<issue id="3819" link="https://github.com/apache/logging-log4j2/issues/3819"/>
9+
<description format="asciidoc">
10+
Fix potential memory leak involving `LogBuilder` in Log4j API to Logback bridge.
11+
</description>
12+
</entry>

0 commit comments

Comments
 (0)