Repository: open-telemetry/opentelemetry-java-instrumentation (2026-03-18T18:27:07Z)

Issue:
Logback OpenTelemetryAppender Spring Boot Autoconfiguration duplicates
### Describe the bug

When using opentelemetry-spring-boot-starter to autoconfigure the OpenTelemetry Logback instrumentation (both [logback-mdc-1.0](https://github.com/open-telemetry/opentelemetry-java-instrumentation/tree/main/instrumentation/logback/logback-mdc-1.0) and [logback-appender-1.0](https://github.com/open-telemetry/opentelemetry-java-instrumentation/tree/main/instrumentation/logback/logback-appender-1.0)), a duplicate `io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender` is added if one is already defined in logback xml.  However, it seems like the intention is for the autoconfiguration to [reinitialize it](https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/0d52841108b0ced51dc448eda8afd2a1b56c323a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/logging/LogbackAppenderInstaller.java#L38-L39).

### Steps to reproduce

Run the `./mvnw test` on the [opentelemetry-logging-test.zip](https://github.com/user-attachments/files/25975530/opentelemetry-logging-test.zip) project, and one of the unit tests will fail showing the problem.


Or manually, you can reproduce it in another project like this:

Use opentelemetry-spring-boot-starter in a Spring Boot application.

Enable both [logback-mdc-1.0](https://github.com/open-telemetry/opentelemetry-java-instrumentation/tree/main/instrumentation/logback/logback-mdc-1.0) and [logback-appender-1.0](https://github.com/open-telemetry/opentelemetry-java-instrumentation/tree/main/instrumentation/logback/logback-appender-1.0)

```yaml
otel:
  metrics:
    exporter: none
  traces:
    exporter: none
  logs:
    exporter: console
  instrumentation:
    common:
      default-enabled: false
    logback-appender:
      enabled: true
      experimental:
        capture-key-value-pair-attributes: true
    logback-mdc:
      enabled: true
```

Add an `io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender` to the logback xml.   Note that sometimes adding this appender in the logback xml is explicitly needed to be able to add the appender to multiple loggers (especially if those loggers have additivity=false and don't forward events to the root logger)

```xml
	<appender name="opentelemetry" class="io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender"/>

	<root level="info">
		<appender-ref ref="console"/>
		<appender-ref ref="opentelemetry"/>
	</root>
```

Start the application


### Expected behavior

Expect the opentelemetry-spring-boot-starter's autoconfiguration to [initialize the opentelemetry appender with the configuration properties defined in the spring environment ](https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/0d52841108b0ced51dc448eda8afd2a1b56c323a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/logging/LogbackAppenderInstaller.java#L37-L39) (in this case, capture-key-value-pair-attributes: true) 

### Actual behavior

The opentelemetry-spring-boot-starter's autoconfiguration:

1. [Adds a new `io.opentelemetry.instrumentation.logback.mdc.v1_0.OpenTelemetryAppender` to the root logger](https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/0d52841108b0ced51dc448eda8afd2a1b56c323a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/logging/LogbackAppenderInstaller.java#L189)
2. [Moves the existing `opentelemetry` appender "beneath" the `io.opentelemetry.instrumentation.logback.mdc.v1_0.OpenTelemetryAppender`](https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/0d52841108b0ced51dc448eda8afd2a1b56c323a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/logging/LogbackAppenderInstaller.java#L190-L197)
3. [Does not find the existing `io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender`](https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/0d52841108b0ced51dc448eda8afd2a1b56c323a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/logging/LogbackAppenderInstaller.java#L35-L37), since it is now beneath the `io.opentelemetry.instrumentation.logback.mdc.v1_0.OpenTelemetryAppender`
4. [Adds a new `io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender` to the root logger](https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/0d52841108b0ced51dc448eda8afd2a1b56c323a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/logging/LogbackAppenderInstaller.java#L41), an initializes it with the properties from the Spring Boot environment

So now, the configuration looks something like this:

```
root logger
    --> io.opentelemetry.instrumentation.logback.mdc.v1_0.OpenTelemetryAppender  (added by autoconfig)
        --> io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender  (existing from logback xml.  NOT configured with Spring environment properties)
    --> io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender  (added by autoconfig, configured with Spring environment properties)
```

Note that there are now two `io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender` instances.   One of them from logback xml.  One of them added by autoconfig.  Only the one added by autoconfig is configured with properties from the Spring environment.

Given [this code](https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/0d52841108b0ced51dc448eda8afd2a1b56c323a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/logging/LogbackAppenderInstaller.java#L37-L39), I would have expected the autoconfiguration to find and reinitialize the appender configured in the logback xml with the Spring environment properties.


### Javaagent or library instrumentation version

2.25.0-alpha

### Environment

**JDK**:  Temurin 25.0.2
**OS**:  Ubuntu 24.04


### Additional context

Another observation:  Later... the opentelemetry-spring-boot-starter autoconfiguration [finds both  `io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender` instances, and injects the `OpenTelemetry` object into them](https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/265b5ea49b8d422b21dc4d3959bed26479c6200f/instrumentation/logback/logback-appender-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/appender/v1_0/OpenTelemetryAppender.java#L73-L81).

So, when injecting `OpenTelemetry` it _does_ find and initialize the existing `io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender`.
But when searching for the existing `io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender` to initialize with Spring environment properties, it _does not_ find it  (if logback-mdc-1.0 instrumentation is also enabled).

If logback-mdc-1.0 instrumentation is disabled, then opentelemetry-spring-boot-starter autoconfiguration will find and reinitialize the `io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender` from the logback.xml.


-----
Regarding _how_ to fix this.  I think the simplest fix is to make [`LogbackAppenderInstaller.findAppender`](https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/265b5ea49b8d422b21dc4d3959bed26479c6200f/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/logging/LogbackAppenderInstaller.java#L263-L280) search through subappenders, similar to [`OpenTelemetryAppender.install`](https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/265b5ea49b8d422b21dc4d3959bed26479c6200f/instrumentation/logback/logback-appender-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/appender/v1_0/OpenTelemetryAppender.java#L76-L80)

This would allow the `LogbackAppenderInstaller` to find the `io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender` that it moved beneath the `io.opentelemetry.instrumentation.logback.mdc.v1_0.OpenTelemetryAppender`.

### Tip

<sub>[React](https://github.blog/news-insights/product-news/add-reactions-to-pull-requests-issues-and-comments/) with 👍 to help prioritize this issue. Please use comments to provide useful context, avoiding `+1` or `me too`, to help us triage it. Learn more [here](https://opentelemetry.io/community/end-user/issue-participation/).</sub>


Discussion:

Patch:
diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/logging/LogbackAppenderInstaller.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/logging/LogbackAppenderInstaller.java
index 1cda1dcf5ae2..13865fea04d6 100644
--- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/logging/LogbackAppenderInstaller.java
+++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/logging/LogbackAppenderInstaller.java
@@ -8,6 +8,7 @@
 import ch.qos.logback.classic.LoggerContext;
 import ch.qos.logback.classic.spi.ILoggingEvent;
 import ch.qos.logback.core.Appender;
+import ch.qos.logback.core.spi.AppenderAttachable;
 import io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender;
 import io.opentelemetry.instrumentation.spring.autoconfigure.internal.EarlyConfig;
 import java.util.Iterator;
@@ -270,9 +271,27 @@ private static <T> Optional<T> findAppender(Class<T> appenderClass) {
       Iterator<Appender<ILoggingEvent>> appenderIterator = logger.iteratorForAppenders();
       while (appenderIterator.hasNext()) {
         Appender<ILoggingEvent> appender = appenderIterator.next();
-        if (appenderClass.isInstance(appender)) {
-          T openTelemetryAppender = appenderClass.cast(appender);
-          return Optional.of(openTelemetryAppender);
+        Optional<T> result = findAppender(appenderClass, appender);
+        if (result.isPresent()) {
+          return result;
+        }
+      }
+    }
+    return Optional.empty();
+  }
+
+  private static <T> Optional<T> findAppender(Class<T> appenderClass, Appender<?> appender) {
+    if (appenderClass.isInstance(appender)) {
+      T openTelemetryAppender = appenderClass.cast(appender);
+      return Optional.of(openTelemetryAppender);
+    } else if (appender instanceof AppenderAttachable) {
+      for (Iterator<? extends Appender<?>> iterator =
+              ((AppenderAttachable<?>) appender).iteratorForAppenders();
+          iterator.hasNext(); ) {
+        Appender<?> childAppender = iterator.next();
+        Optional<T> result = findAppender(appenderClass, childAppender);
+        if (result.isPresent()) {
+          return result;
         }
       }
     }


Test patch:
diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/testLogbackAppender/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/logging/LogbackAppenderTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/testLogbackAppender/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/logging/LogbackAppenderTest.java
index 62c29c1d4912..22b429f84c1a 100644
--- a/instrumentation/spring/spring-boot-autoconfigure/src/testLogbackAppender/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/logging/LogbackAppenderTest.java
+++ b/instrumentation/spring/spring-boot-autoconfigure/src/testLogbackAppender/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/logging/LogbackAppenderTest.java
@@ -8,6 +8,7 @@
 import static io.opentelemetry.api.common.AttributeKey.stringKey;
 import static org.assertj.core.api.Assertions.assertThat;
 
+import ch.qos.logback.classic.LoggerContext;
 import ch.qos.logback.classic.spi.ILoggingEvent;
 import ch.qos.logback.core.Appender;
 import ch.qos.logback.core.read.ListAppender;
@@ -29,6 +30,9 @@
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.slf4j.ILoggerFactory;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.slf4j.MDC;
@@ -39,6 +43,11 @@
 
 class LogbackAppenderTest {
 
+  private static final Class<?> openTelemetryAppenderClass =
+      io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender.class;
+  private static final Class<?> openTelemetryMdcAppenderClass =
+      io.opentelemetry.instrumentation.logback.mdc.v1_0.OpenTelemetryAppender.class;
+
   @RegisterExtension
   static final InstrumentationExtension testing = LibraryInstrumentationExtension.create();
 
@@ -59,10 +68,11 @@ public OpenTelemetry openTelemetry() {
     }
   }
 
-  @Test
-  void shouldInitializeAppender() {
+  @ParameterizedTest
+  @ValueSource(strings = {"logback-test.xml", "logback-test-no-mdc.xml"})
+  void shouldInitializeAppender(String configurationFile) {
     Map<String, Object> properties = new HashMap<>();
-    properties.put("logging.config", "classpath:logback-test.xml");
+    properties.put("logging.config", "classpath:" + configurationFile);
     properties.put(
         "otel.instrumentation.logback-appender.experimental.capture-mdc-attributes", "*");
     properties.put(
@@ -76,6 +86,9 @@ void shouldInitializeAppender() {
     ConfigurableApplicationContext context = app.run();
     cleanup.deferCleanup(context);
 
+    assertThat(countAppenders(openTelemetryAppenderClass)).isEqualTo(1);
+    assertThat(countAppenders(openTelemetryMdcAppenderClass)).isEqualTo(1);
+
     ListAppender<ILoggingEvent> listAppender = getListAppender();
     listAppender.list.clear();
 
@@ -128,6 +141,9 @@ void shouldNotInitializeAppenderWhenDisabled() {
     ConfigurableApplicationContext context = app.run();
     cleanup.deferCleanup(context);
 
+    assertThat(countAppenders(openTelemetryAppenderClass)).isEqualTo(1);
+    assertThat(countAppenders(openTelemetryMdcAppenderClass)).isEqualTo(1);
+
     LoggerFactory.getLogger("test").info("test log message");
 
     assertThat(testing.logRecords()).isEmpty();
@@ -186,6 +202,9 @@ void shouldInitializeMdcAppender() {
     ConfigurableApplicationContext context = app.run();
     cleanup.deferCleanup(context);
 
+    assertThat(countAppenders(openTelemetryAppenderClass)).isEqualTo(0);
+    assertThat(countAppenders(openTelemetryMdcAppenderClass)).isEqualTo(1);
+
     ListAppender<ILoggingEvent> listAppender = getListAppender();
     listAppender.list.clear();
 
@@ -221,6 +240,9 @@ void shouldNotInitializeMdcAppenderWhenDisabled() {
     ConfigurableApplicationContext context = app.run();
     cleanup.deferCleanup(context);
 
+    assertThat(countAppenders(openTelemetryAppenderClass)).isEqualTo(0);
+    assertThat(countAppenders(openTelemetryMdcAppenderClass)).isEqualTo(0);
+
     ListAppender<ILoggingEvent> listAppender = getListAppender();
     listAppender.list.clear();
 
@@ -265,4 +287,36 @@ private static ListAppender<ILoggingEvent> getListAppender() {
     }
     return (ListAppender<ILoggingEvent>) mdcAppender.getAppender("List");
   }
+
+  private static int countAppenders(Appender<?> appender, Class<?> appenderClass) {
+    int count = 0;
+    if (appenderClass.isInstance(appender)) {
+      count++;
+    } else if (appender instanceof AppenderAttachable) {
+      for (Iterator<? extends Appender<?>> iterator =
+              ((AppenderAttachable<?>) appender).iteratorForAppenders();
+          iterator.hasNext(); ) {
+        Appender<?> childAppender = iterator.next();
+        count += countAppenders(childAppender, appenderClass);
+      }
+    }
+    return count;
+  }
+
+  private static int countAppenders(Class<?> appenderClass) {
+    ILoggerFactory loggerFactorySpi = LoggerFactory.getILoggerFactory();
+    if (!(loggerFactorySpi instanceof LoggerContext)) {
+      return 0;
+    }
+    int count = 0;
+    LoggerContext loggerContext = (LoggerContext) loggerFactorySpi;
+    for (ch.qos.logback.classic.Logger logger : loggerContext.getLoggerList()) {
+      Iterator<Appender<ILoggingEvent>> appenderIterator = logger.iteratorForAppenders();
+      while (appenderIterator.hasNext()) {
+        Appender<ILoggingEvent> appender = appenderIterator.next();
+        count += countAppenders(appender, appenderClass);
+      }
+    }
+    return count;
+  }
 }
diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/testLogbackAppender/resources/logback-test-no-mdc.xml b/instrumentation/spring/spring-boot-autoconfigure/src/testLogbackAppender/resources/logback-test-no-mdc.xml
new file mode 100644
index 000000000000..f35e25130436
--- /dev/null
+++ b/instrumentation/spring/spring-boot-autoconfigure/src/testLogbackAppender/resources/logback-test-no-mdc.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+
+  <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
+    <encoder>
+      <pattern>
+        %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+      </pattern>
+    </encoder>
+  </appender>
+  <appender name="OpenTelemetry"
+            class="io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender">
+    <captureCodeAttributes>true</captureCodeAttributes>
+  </appender>
+  <appender name="List" class="ch.qos.logback.core.read.ListAppender" />
+
+  <root level="INFO">
+    <appender-ref ref="console"/>
+    <appender-ref ref="OpenTelemetry"/>
+    <appender-ref ref="List"/>
+  </root>
+
+</configuration>