diff --git a/instrumentation/logback/logback-appender-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/appender/v1_0/OpenTelemetryAppender.java b/instrumentation/logback/logback-appender-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/appender/v1_0/OpenTelemetryAppender.java index 4a21b6c4b5..a57a9fd419 100644 --- a/instrumentation/logback/logback-appender-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/appender/v1_0/OpenTelemetryAppender.java +++ b/instrumentation/logback/logback-appender-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/appender/v1_0/OpenTelemetryAppender.java @@ -204,7 +204,10 @@ public class OpenTelemetryAppender extends UnsynchronizedAppenderBase existingOpenTelemetryAppender = findOpenTelemetryAppender(); + Optional + existingMdcAppender = + findAppender( + io.opentelemetry.instrumentation.logback.mdc.v1_0.OpenTelemetryAppender.class); + if (existingMdcAppender.isPresent()) { + initializeMdcAppenderFromProperties( + applicationEnvironmentPreparedEvent, existingMdcAppender.get()); + } else if (isLogbackMdcAppenderAddable(applicationEnvironmentPreparedEvent)) { + addMdcAppender(applicationEnvironmentPreparedEvent); + } + + Optional existingOpenTelemetryAppender = + findAppender(OpenTelemetryAppender.class); if (existingOpenTelemetryAppender.isPresent()) { reInitializeOpenTelemetryAppender( existingOpenTelemetryAppender, applicationEnvironmentPreparedEvent); @@ -30,13 +42,22 @@ class LogbackAppenderInstaller { private static boolean isLogbackAppenderAddable( ApplicationEnvironmentPreparedEvent applicationEnvironmentPreparedEvent) { + return isAppenderAddable( + applicationEnvironmentPreparedEvent, "otel.instrumentation.logback-appender.enabled"); + } + + private static boolean isLogbackMdcAppenderAddable( + ApplicationEnvironmentPreparedEvent applicationEnvironmentPreparedEvent) { + return isAppenderAddable( + applicationEnvironmentPreparedEvent, "otel.instrumentation.logback-mdc.enabled"); + } + + private static boolean isAppenderAddable( + ApplicationEnvironmentPreparedEvent applicationEnvironmentPreparedEvent, String property) { boolean otelSdkDisabled = evaluateBooleanProperty(applicationEnvironmentPreparedEvent, "otel.sdk.disabled", false); boolean logbackInstrumentationEnabled = - evaluateBooleanProperty( - applicationEnvironmentPreparedEvent, - "otel.instrumentation.logback-appender.enabled", - true); + evaluateBooleanProperty(applicationEnvironmentPreparedEvent, property, true); return !otelSdkDisabled && logbackInstrumentationEnabled; } @@ -133,6 +154,58 @@ class LogbackAppenderInstaller { } } + private static void addMdcAppender( + ApplicationEnvironmentPreparedEvent applicationEnvironmentPreparedEvent) { + ch.qos.logback.classic.Logger logger = + (ch.qos.logback.classic.Logger) + LoggerFactory.getILoggerFactory().getLogger(Logger.ROOT_LOGGER_NAME); + io.opentelemetry.instrumentation.logback.mdc.v1_0.OpenTelemetryAppender openTelemetryAppender = + new io.opentelemetry.instrumentation.logback.mdc.v1_0.OpenTelemetryAppender(); + initializeMdcAppenderFromProperties(applicationEnvironmentPreparedEvent, openTelemetryAppender); + openTelemetryAppender.start(); + logger.addAppender(openTelemetryAppender); + } + + private static void initializeMdcAppenderFromProperties( + ApplicationEnvironmentPreparedEvent applicationEnvironmentPreparedEvent, + io.opentelemetry.instrumentation.logback.mdc.v1_0.OpenTelemetryAppender + openTelemetryAppender) { + + // Implemented in the same way as the + // org.springframework.boot.context.logging.LoggingApplicationListener, config properties not + // available + Boolean addBaggage = + evaluateBooleanProperty( + applicationEnvironmentPreparedEvent, "otel.instrumentation.logback-mdc.add-baggage"); + if (addBaggage != null) { + openTelemetryAppender.setAddBaggage(addBaggage); + } + + String traceIdKey = + applicationEnvironmentPreparedEvent + .getEnvironment() + .getProperty("otel.instrumentation.common.logging.trace-id", String.class); + if (traceIdKey != null) { + openTelemetryAppender.setTraceIdKey(traceIdKey); + } + + String spanIdKey = + applicationEnvironmentPreparedEvent + .getEnvironment() + .getProperty("otel.instrumentation.common.logging.span-id", String.class); + if (spanIdKey != null) { + openTelemetryAppender.setSpanIdKey(spanIdKey); + } + + String traceFlagsKey = + applicationEnvironmentPreparedEvent + .getEnvironment() + .getProperty("otel.instrumentation.common.logging.trace-flags", String.class); + if (traceFlagsKey != null) { + openTelemetryAppender.setTraceFlagsKey(traceFlagsKey); + } + } + private static Boolean evaluateBooleanProperty( ApplicationEnvironmentPreparedEvent applicationEnvironmentPreparedEvent, String property) { return applicationEnvironmentPreparedEvent @@ -149,7 +222,7 @@ class LogbackAppenderInstaller { .getProperty(property, Boolean.class, defaultValue); } - private static Optional findOpenTelemetryAppender() { + private static Optional findAppender(Class appenderClass) { ILoggerFactory loggerFactorySpi = LoggerFactory.getILoggerFactory(); if (!(loggerFactorySpi instanceof LoggerContext)) { return Optional.empty(); @@ -159,8 +232,8 @@ class LogbackAppenderInstaller { Iterator> appenderIterator = logger.iteratorForAppenders(); while (appenderIterator.hasNext()) { Appender appender = appenderIterator.next(); - if (appender instanceof OpenTelemetryAppender) { - OpenTelemetryAppender openTelemetryAppender = (OpenTelemetryAppender) appender; + if (appenderClass.isInstance(appender)) { + T openTelemetryAppender = appenderClass.cast(appender); return Optional.of(openTelemetryAppender); } } diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 19ea58c783..f3e0820a79 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -416,6 +416,36 @@ "type": "java.util.List", "description": "MDC attributes to capture. Use the wildcard character * to capture all attributes." }, + { + "name": "otel.instrumentation.logback-mdc.enabled", + "type": "java.lang.Boolean", + "description": "Enable the Logback MDC instrumentation.", + "defaultValue": true + }, + { + "name": "otel.instrumentation.logback-mdc.add-baggage", + "type": "java.lang.Boolean", + "description": "Enable exposing baggage attributes through MDC.", + "defaultValue": false + }, + { + "name": "otel.instrumentation.common.logging.trace-id", + "type": "java.lang.String", + "description": "Customize MDC key name for the trace id.", + "defaultValue": "trace_id" + }, + { + "name": "otel.instrumentation.common.logging.span-id", + "type": "java.lang.String", + "description": "Customize MDC key name for the span id.", + "defaultValue": "span_id" + }, + { + "name": "otel.instrumentation.common.logging.trace-flags", + "type": "java.lang.String", + "description": "Customize MDC key name for the trace flags.", + "defaultValue": "trace_flags" + }, { "name": "otel.instrumentation.micrometer.enabled", "type": "java.lang.Boolean", 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 ad677c0520..c11631cb7d 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 @@ -7,9 +7,15 @@ package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumen import static org.assertj.core.api.Assertions.assertThat; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; +import ch.qos.logback.core.spi.AppenderAttachable; import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.baggage.Baggage; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Scope; import io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender; import io.opentelemetry.instrumentation.testing.internal.AutoCleanupExtension; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; @@ -21,6 +27,7 @@ import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.MDC; import org.springframework.boot.SpringApplication; @@ -66,6 +73,9 @@ class LogbackAppenderTest { ConfigurableApplicationContext context = app.run(); cleanup.deferCleanup(context); + ListAppender listAppender = getListAppender(); + listAppender.list.clear(); + MDC.put("key1", "val1"); MDC.put("key2", "val2"); try { @@ -91,6 +101,14 @@ class LogbackAppenderTest { .containsEntry(AttributeKey.stringKey("key1"), "val1") .containsEntry(AttributeKey.stringKey("key2"), "val2"); }); + + assertThat(listAppender.list) + .satisfiesExactly( + event -> + assertThat(event) + .satisfies( + e -> assertThat(e.getMessage()).isEqualTo("test log message"), + e -> assertThat(e.getMDCPropertyMap()).containsOnlyKeys("key1", "key2"))); } @Test @@ -110,4 +128,123 @@ class LogbackAppenderTest { assertThat(testing.logRecords()).isEmpty(); } + + @Test + void mdcAppender() { + Map properties = new HashMap<>(); + properties.put("logging.config", "classpath:logback-test.xml"); + properties.put("otel.instrumentation.logback-appender.enabled", "false"); + properties.put("otel.instrumentation.logback-mdc.add-baggage", "true"); + properties.put("otel.instrumentation.common.logging.trace-id", "traceid"); + properties.put("otel.instrumentation.common.logging.span-id", "spanid"); + properties.put("otel.instrumentation.common.logging.trace-flags", "traceflags"); + + SpringApplication app = + new SpringApplication( + TestingOpenTelemetryConfiguration.class, OpenTelemetryAppenderAutoConfiguration.class); + app.setDefaultProperties(properties); + ConfigurableApplicationContext context = app.run(); + cleanup.deferCleanup(context); + + ListAppender listAppender = getListAppender(); + listAppender.list.clear(); + + try (Scope ignore = Baggage.current().toBuilder().put("key", "value").build().makeCurrent()) { + Span span = testing.getOpenTelemetry().getTracer("test").spanBuilder("test").startSpan(); + try (Scope ignore2 = span.makeCurrent()) { + LoggerFactory.getLogger("test").info("test log message"); + } + } + + assertThat(testing.logRecords()).isEmpty(); + assertThat(listAppender.list) + .satisfiesExactly( + event -> + assertThat(event) + .satisfies( + e -> assertThat(e.getMessage()).isEqualTo("test log message"), + e -> + assertThat(e.getMDCPropertyMap()) + .containsOnlyKeys( + "traceid", "spanid", "traceflags", "baggage.key"))); + } + + @Test + void shouldInitializeMdcAppender() { + Map properties = new HashMap<>(); + properties.put("logging.config", "classpath:logback-no-otel-appenders.xml"); + properties.put("otel.instrumentation.logback-appender.enabled", "false"); + + SpringApplication app = + new SpringApplication( + TestingOpenTelemetryConfiguration.class, OpenTelemetryAppenderAutoConfiguration.class); + app.setDefaultProperties(properties); + ConfigurableApplicationContext context = app.run(); + cleanup.deferCleanup(context); + + ListAppender listAppender = getListAppender(); + listAppender.list.clear(); + + Span span = testing.getOpenTelemetry().getTracer("test").spanBuilder("test").startSpan(); + try (Scope ignore = span.makeCurrent()) { + LoggerFactory.getLogger("test").info("test log message"); + } + + assertThat(testing.logRecords()).isEmpty(); + assertThat(listAppender.list) + .satisfiesExactly( + event -> + assertThat(event) + .satisfies( + e -> assertThat(e.getMessage()).isEqualTo("test log message"), + e -> + assertThat(e.getMDCPropertyMap()) + .containsOnlyKeys("trace_id", "span_id", "trace_flags"))); + } + + @Test + void shouldNotInitializeMdcAppenderWhenDisabled() { + Map properties = new HashMap<>(); + properties.put("logging.config", "classpath:logback-no-otel-appenders.xml"); + properties.put("otel.instrumentation.logback-appender.enabled", "false"); + properties.put("otel.instrumentation.logback-mdc.enabled", "false"); + + SpringApplication app = + new SpringApplication( + TestingOpenTelemetryConfiguration.class, OpenTelemetryAppenderAutoConfiguration.class); + app.setDefaultProperties(properties); + ConfigurableApplicationContext context = app.run(); + cleanup.deferCleanup(context); + + ListAppender listAppender = getListAppender(); + listAppender.list.clear(); + + Span span = testing.getOpenTelemetry().getTracer("test").spanBuilder("test").startSpan(); + try (Scope ignore = span.makeCurrent()) { + LoggerFactory.getLogger("test").info("test log message"); + } + + assertThat(testing.logRecords()).isEmpty(); + assertThat(listAppender.list) + .satisfiesExactly( + event -> + assertThat(event) + .satisfies( + e -> assertThat(e.getMessage()).isEqualTo("test log message"), + e -> assertThat(e.getMDCPropertyMap()).isEmpty())); + } + + @SuppressWarnings("unchecked") + private static ListAppender getListAppender() { + Logger logger = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); + ch.qos.logback.classic.Logger logbackLogger = (ch.qos.logback.classic.Logger) logger; + ListAppender listAppender = + (ListAppender) logbackLogger.getAppender("List"); + if (listAppender != null) { + return listAppender; + } + AppenderAttachable mdcAppender = + (AppenderAttachable) logbackLogger.getAppender("OpenTelemetryMdc"); + return (ListAppender) mdcAppender.getAppender("List"); + } } diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/testLogbackAppender/resources/logback-no-otel-appenders.xml b/instrumentation/spring/spring-boot-autoconfigure/src/testLogbackAppender/resources/logback-no-otel-appenders.xml new file mode 100644 index 0000000000..d77f5158c4 --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/testLogbackAppender/resources/logback-no-otel-appenders.xml @@ -0,0 +1,18 @@ + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/testLogbackAppender/resources/logback-test.xml b/instrumentation/spring/spring-boot-autoconfigure/src/testLogbackAppender/resources/logback-test.xml index 9b88dc0e09..f4f1d7e470 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/testLogbackAppender/resources/logback-test.xml +++ b/instrumentation/spring/spring-boot-autoconfigure/src/testLogbackAppender/resources/logback-test.xml @@ -12,10 +12,16 @@ class="io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender"> true + + + + +