From a4cdb388d0159d634164ae8540c7a489e8e427e1 Mon Sep 17 00:00:00 2001 From: Bhuvan Somisetty Date: Sat, 27 Jun 2026 00:07:17 +0530 Subject: [PATCH 1/3] Suppress duplicate warning log for same application logger factory class In Spring Boot, LoggingApplicationListener can initialize the logging system multiple times. When the Java agent hooks into the initialization, this triggers multiple registration calls of Slf4jApplicationLoggerBridge, causing a warning log that multiple application logger implementations were provided. Track the class of the registered factory and return early if a subsequent registration is for the same factory class. A warning is still logged if a different factory class is provided. Fixes #14889 Signed-off-by: bhuvan-somisetty --- .../application/ApplicationLoggerFactory.java | 6 +++ .../ApplicationLoggerFactoryTest.java | 51 ++++++++++++++++--- 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/javaagent-internal-logging-application/src/main/java/io/opentelemetry/javaagent/logging/application/ApplicationLoggerFactory.java b/javaagent-internal-logging-application/src/main/java/io/opentelemetry/javaagent/logging/application/ApplicationLoggerFactory.java index 1050ad04311e..dd77a8e17d74 100644 --- a/javaagent-internal-logging-application/src/main/java/io/opentelemetry/javaagent/logging/application/ApplicationLoggerFactory.java +++ b/javaagent-internal-logging-application/src/main/java/io/opentelemetry/javaagent/logging/application/ApplicationLoggerFactory.java @@ -17,6 +17,7 @@ final class ApplicationLoggerFactory extends ApplicationLoggerBridge private final AtomicBoolean installed = new AtomicBoolean(); @Nullable private volatile InternalLogger.Factory actual = null; + @Nullable private volatile Class actualFactoryClass; private final ConcurrentMap inMemoryLoggers = new ConcurrentHashMap<>(); @@ -28,8 +29,12 @@ final class ApplicationLoggerFactory extends ApplicationLoggerBridge @Override protected void install(InternalLogger.Factory applicationLoggerFactory) { + Class incomingClass = applicationLoggerFactory.getClass(); // just use the first bridge that gets discovered and ignore the rest if (!installed.compareAndSet(false, true)) { + if (incomingClass.equals(actualFactoryClass)) { + return; + } applicationLoggerFactory .create(ApplicationLoggerBridge.class.getName()) .log( @@ -39,6 +44,7 @@ protected void install(InternalLogger.Factory applicationLoggerFactory) { null); return; } + actualFactoryClass = incomingClass; // flushing may cause additional classes to be loaded (e.g. slf4j loads logback, which we // instrument), so we're doing this repeatedly to clear the in-memory store and preserve the diff --git a/javaagent-internal-logging-application/src/test/java/io/opentelemetry/javaagent/logging/application/ApplicationLoggerFactoryTest.java b/javaagent-internal-logging-application/src/test/java/io/opentelemetry/javaagent/logging/application/ApplicationLoggerFactoryTest.java index 417631a896fe..49eb61e443cc 100644 --- a/javaagent-internal-logging-application/src/test/java/io/opentelemetry/javaagent/logging/application/ApplicationLoggerFactoryTest.java +++ b/javaagent-internal-logging-application/src/test/java/io/opentelemetry/javaagent/logging/application/ApplicationLoggerFactoryTest.java @@ -12,6 +12,7 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -53,25 +54,49 @@ void shouldNotDuplicateLoggers() { @Test void shouldOnlyInstallTheFirstBridge() { + InternalLogger.Factory factory1 = mock(FirstFactory.class); + InternalLogger.Factory factory2 = mock(SecondFactory.class); + when(logStore.currentSize()).thenReturn(1, 0, 0); - when(applicationLoggerBridge.create(any())).thenReturn(applicationLogger); + when(factory2.create(any())).thenReturn(applicationLogger); - underTest.install(applicationLoggerBridge); + underTest.install(factory1); verify(logStore, times(3)).currentSize(); - verify(logStore).flush(applicationLoggerBridge); - verify(logStore).setApplicationLoggerFactory(applicationLoggerBridge); + verify(logStore).flush(factory1); + verify(logStore).setApplicationLoggerFactory(factory1); verify(logStore).freeMemory(); - underTest.install(applicationLoggerBridge); + underTest.install(factory2); // verify logged warning - verify(applicationLoggerBridge).create(ApplicationLoggerBridge.class.getName()); + verify(factory2).create(ApplicationLoggerBridge.class.getName()); verify(applicationLogger).log(eq(WARN), anyString(), isNull()); verifyNoMoreInteractions(logStore); } + @Test + void shouldIgnoreSubsequentInstallationsOfSameClass() { + InternalLogger.Factory factory1 = mock(FirstFactory.class); + InternalLogger.Factory factory2 = mock(FirstFactory.class); + + when(logStore.currentSize()).thenReturn(1, 0, 0); + + underTest.install(factory1); + + verify(logStore, times(3)).currentSize(); + verify(logStore).flush(factory1); + verify(logStore).setApplicationLoggerFactory(factory1); + verify(logStore).freeMemory(); + + underTest.install(factory2); + + // verify no warning logged and no factory interaction for second install + verify(factory2, never()).create(anyString()); + verifyNoMoreInteractions(logStore); + } + @Test void shouldReplaceLoggerAfterTheBridgeIsInstalled() { InternalLogger beforeInstall = underTest.create("logger"); @@ -90,4 +115,18 @@ void shouldReplaceLoggerAfterTheBridgeIsInstalled() { verify(applicationLogger).log(INFO, "after", null); verify(logStore, never()).write(InMemoryLog.create("logger", INFO, "after", null)); } + + static class FirstFactory implements InternalLogger.Factory { + @Override + public InternalLogger create(String name) { + return null; + } + } + + static class SecondFactory implements InternalLogger.Factory { + @Override + public InternalLogger create(String name) { + return null; + } + } } From f1cba107783e5903d294d7db93fad39328525cfb Mon Sep 17 00:00:00 2001 From: Bhuvan Somisetty Date: Sat, 27 Jun 2026 14:23:48 +0530 Subject: [PATCH 2/3] Fix race condition in ApplicationLoggerFactory same-class suppression Replace AtomicBoolean + separate volatile field with a single AtomicReference> so the winning factory class is stored atomically as part of the CAS gate. Any thread that loses the race now always reads a non-null value from installedFactoryClass.get(), closing the narrow window where a same-class re-install could still emit the spurious warning. --- .../application/ApplicationLoggerFactory.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/javaagent-internal-logging-application/src/main/java/io/opentelemetry/javaagent/logging/application/ApplicationLoggerFactory.java b/javaagent-internal-logging-application/src/main/java/io/opentelemetry/javaagent/logging/application/ApplicationLoggerFactory.java index dd77a8e17d74..e2c64596f87c 100644 --- a/javaagent-internal-logging-application/src/main/java/io/opentelemetry/javaagent/logging/application/ApplicationLoggerFactory.java +++ b/javaagent-internal-logging-application/src/main/java/io/opentelemetry/javaagent/logging/application/ApplicationLoggerFactory.java @@ -9,15 +9,15 @@ import io.opentelemetry.javaagent.bootstrap.logging.ApplicationLoggerBridge; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import javax.annotation.Nullable; final class ApplicationLoggerFactory extends ApplicationLoggerBridge implements InternalLogger.Factory { - private final AtomicBoolean installed = new AtomicBoolean(); + private final AtomicReference> installedFactoryClass = + new AtomicReference<>(); @Nullable private volatile InternalLogger.Factory actual = null; - @Nullable private volatile Class actualFactoryClass; private final ConcurrentMap inMemoryLoggers = new ConcurrentHashMap<>(); @@ -30,9 +30,10 @@ final class ApplicationLoggerFactory extends ApplicationLoggerBridge @Override protected void install(InternalLogger.Factory applicationLoggerFactory) { Class incomingClass = applicationLoggerFactory.getClass(); - // just use the first bridge that gets discovered and ignore the rest - if (!installed.compareAndSet(false, true)) { - if (incomingClass.equals(actualFactoryClass)) { + // just use the first bridge that gets discovered and ignore the rest; + // CAS null → incomingClass so the winning class is visible to any thread that loses the race + if (!installedFactoryClass.compareAndSet(null, incomingClass)) { + if (incomingClass.equals(installedFactoryClass.get())) { return; } applicationLoggerFactory @@ -44,7 +45,6 @@ protected void install(InternalLogger.Factory applicationLoggerFactory) { null); return; } - actualFactoryClass = incomingClass; // flushing may cause additional classes to be loaded (e.g. slf4j loads logback, which we // instrument), so we're doing this repeatedly to clear the in-memory store and preserve the From 4beee51efc75031d764d1b270d474b204e799f8c Mon Sep 17 00:00:00 2001 From: Lauri Tulmin Date: Tue, 30 Jun 2026 16:00:25 +0300 Subject: [PATCH 3/3] Apply suggestion from @laurit --- .../logging/application/ApplicationLoggerFactory.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/javaagent-internal-logging-application/src/main/java/io/opentelemetry/javaagent/logging/application/ApplicationLoggerFactory.java b/javaagent-internal-logging-application/src/main/java/io/opentelemetry/javaagent/logging/application/ApplicationLoggerFactory.java index e2c64596f87c..efb4a989a2d6 100644 --- a/javaagent-internal-logging-application/src/main/java/io/opentelemetry/javaagent/logging/application/ApplicationLoggerFactory.java +++ b/javaagent-internal-logging-application/src/main/java/io/opentelemetry/javaagent/logging/application/ApplicationLoggerFactory.java @@ -30,8 +30,7 @@ final class ApplicationLoggerFactory extends ApplicationLoggerBridge @Override protected void install(InternalLogger.Factory applicationLoggerFactory) { Class incomingClass = applicationLoggerFactory.getClass(); - // just use the first bridge that gets discovered and ignore the rest; - // CAS null → incomingClass so the winning class is visible to any thread that loses the race + // just use the first bridge that gets discovered and ignore the rest if (!installedFactoryClass.compareAndSet(null, incomingClass)) { if (incomingClass.equals(installedFactoryClass.get())) { return;