Allow the Spring starter to configure the OTel Logback appender from system properties (#10355)

This commit is contained in:
Jean Bisutti 2024-02-02 18:27:58 +01:00 committed by GitHub
parent 5f6232fca8
commit 80e5bac4a4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 188 additions and 17 deletions

View File

@ -10,6 +10,7 @@ import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.Appender; import ch.qos.logback.core.Appender;
import io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender; import io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender;
import java.util.Iterator; import java.util.Iterator;
import java.util.Optional;
import org.slf4j.ILoggerFactory; import org.slf4j.ILoggerFactory;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -55,21 +56,115 @@ public class LogbackAppenderApplicationListener implements GenericApplicationLis
if (event instanceof ApplicationEnvironmentPreparedEvent // Event for which if (event instanceof ApplicationEnvironmentPreparedEvent // Event for which
// org.springframework.boot.context.logging.LoggingApplicationListener // org.springframework.boot.context.logging.LoggingApplicationListener
// initializes logging // initializes logging
&& !isOpenTelemetryAppenderAlreadyConfigured()) { ) {
Optional<OpenTelemetryAppender> existingOpenTelemetryAppender = findOpenTelemetryAppender();
ApplicationEnvironmentPreparedEvent applicationEnvironmentPreparedEvent =
(ApplicationEnvironmentPreparedEvent) event;
if (existingOpenTelemetryAppender.isPresent()) {
reInitializeOpenTelemetryAppender(
existingOpenTelemetryAppender, applicationEnvironmentPreparedEvent);
} else {
addOpenTelemetryAppender(applicationEnvironmentPreparedEvent);
}
}
}
private static void reInitializeOpenTelemetryAppender(
Optional<OpenTelemetryAppender> existingOpenTelemetryAppender,
ApplicationEnvironmentPreparedEvent applicationEnvironmentPreparedEvent) {
OpenTelemetryAppender openTelemetryAppender = existingOpenTelemetryAppender.get();
// The OpenTelemetry appender is stopped and restarted from the
// org.springframework.boot.context.logging.LoggingApplicationListener.initialize
// method.
// The OpenTelemetryAppender initializes the LoggingEventMapper in the start() method. So, here
// we stop the OpenTelemetry appender before its re-initialization and its restart.
openTelemetryAppender.stop();
initializeOpenTelemetryAppenderFromProperties(
applicationEnvironmentPreparedEvent, openTelemetryAppender);
openTelemetryAppender.start();
}
private static void addOpenTelemetryAppender(
ApplicationEnvironmentPreparedEvent applicationEnvironmentPreparedEvent) {
ch.qos.logback.classic.Logger logger = ch.qos.logback.classic.Logger logger =
(ch.qos.logback.classic.Logger) (ch.qos.logback.classic.Logger)
LoggerFactory.getILoggerFactory().getLogger(Logger.ROOT_LOGGER_NAME); LoggerFactory.getILoggerFactory().getLogger(Logger.ROOT_LOGGER_NAME);
OpenTelemetryAppender openTelemetryAppender = new OpenTelemetryAppender();
initializeOpenTelemetryAppenderFromProperties(
applicationEnvironmentPreparedEvent, openTelemetryAppender);
openTelemetryAppender.start();
logger.addAppender(openTelemetryAppender);
}
OpenTelemetryAppender appender = new OpenTelemetryAppender(); private static void initializeOpenTelemetryAppenderFromProperties(
appender.start(); ApplicationEnvironmentPreparedEvent applicationEnvironmentPreparedEvent,
logger.addAppender(appender); OpenTelemetryAppender openTelemetryAppender) {
// Implemented in the same way as the
// org.springframework.boot.context.logging.LoggingApplicationListener, config properties not
// available
Boolean codeAttribute =
evaluateBooleanProperty(
applicationEnvironmentPreparedEvent,
"otel.instrumentation.logback-appender.experimental.capture-code-attributes");
if (codeAttribute != null) {
openTelemetryAppender.setCaptureCodeAttributes(codeAttribute.booleanValue());
}
Boolean markerAttribute =
evaluateBooleanProperty(
applicationEnvironmentPreparedEvent,
"otel.instrumentation.logback-appender.experimental.capture-marker-attribute");
if (markerAttribute != null) {
openTelemetryAppender.setCaptureMarkerAttribute(markerAttribute.booleanValue());
}
Boolean keyValuePairAttributes =
evaluateBooleanProperty(
applicationEnvironmentPreparedEvent,
"otel.instrumentation.logback-appender.experimental.capture-key-value-pair-attributes");
if (keyValuePairAttributes != null) {
openTelemetryAppender.setCaptureKeyValuePairAttributes(keyValuePairAttributes.booleanValue());
}
Boolean logAttributes =
evaluateBooleanProperty(
applicationEnvironmentPreparedEvent,
"otel.instrumentation.logback-appender.experimental-log-attributes");
if (logAttributes != null) {
openTelemetryAppender.setCaptureExperimentalAttributes(logAttributes.booleanValue());
}
Boolean loggerContextAttributes =
evaluateBooleanProperty(
applicationEnvironmentPreparedEvent,
"otel.instrumentation.logback-appender.experimental.capture-logger-context-attributes");
if (loggerContextAttributes != null) {
openTelemetryAppender.setCaptureLoggerContext(loggerContextAttributes.booleanValue());
}
String mdcAttributeProperty =
applicationEnvironmentPreparedEvent
.getEnvironment()
.getProperty(
"otel.instrumentation.logback-appender.experimental.capture-mdc-attributes",
String.class);
if (mdcAttributeProperty != null) {
openTelemetryAppender.setCaptureMdcAttributes(mdcAttributeProperty);
} }
} }
private static boolean isOpenTelemetryAppenderAlreadyConfigured() { private static Boolean evaluateBooleanProperty(
ApplicationEnvironmentPreparedEvent applicationEnvironmentPreparedEvent, String property) {
return applicationEnvironmentPreparedEvent
.getEnvironment()
.getProperty(property, Boolean.class);
}
private static Optional<OpenTelemetryAppender> findOpenTelemetryAppender() {
ILoggerFactory loggerFactorySpi = LoggerFactory.getILoggerFactory(); ILoggerFactory loggerFactorySpi = LoggerFactory.getILoggerFactory();
if (!(loggerFactorySpi instanceof LoggerContext)) { if (!(loggerFactorySpi instanceof LoggerContext)) {
return false; return Optional.empty();
} }
LoggerContext loggerContext = (LoggerContext) loggerFactorySpi; LoggerContext loggerContext = (LoggerContext) loggerFactorySpi;
for (ch.qos.logback.classic.Logger logger : loggerContext.getLoggerList()) { for (ch.qos.logback.classic.Logger logger : loggerContext.getLoggerList()) {
@ -77,11 +172,12 @@ public class LogbackAppenderApplicationListener implements GenericApplicationLis
while (appenderIterator.hasNext()) { while (appenderIterator.hasNext()) {
Appender<ILoggingEvent> appender = appenderIterator.next(); Appender<ILoggingEvent> appender = appenderIterator.next();
if (appender instanceof OpenTelemetryAppender) { if (appender instanceof OpenTelemetryAppender) {
return true; OpenTelemetryAppender openTelemetryAppender = (OpenTelemetryAppender) appender;
return Optional.of(openTelemetryAppender);
} }
} }
} }
return false; return Optional.empty();
} }
@Override @Override

View File

@ -0,0 +1,44 @@
{
"groups": [
{
"name": "otel"
}
],
"properties": [
{
"name": "otel.instrumentation.logback-appender.experimental.capture-code-attributes",
"type": "java.lang.Boolean",
"description": "Enable the capture of source code attributes. Note that capturing source code attributes at logging sites might add a performance overhead.",
"defaultValue": false
},
{
"name": "otel.instrumentation.logback-appender.experimental.capture-marker-attribute",
"type": "java.lang.Boolean",
"description": "Enable the capture of Logback markers as attributes.",
"defaultValue": false
},
{
"name": "otel.instrumentation.logback-appender.experimental.capture-key-value-pair-attributes",
"type": "java.lang.Boolean",
"description": "Enable the capture of Logback key value pairs as attributes.",
"defaultValue": false
},
{
"name": "otel.instrumentation.logback-appender.experimental-log-attributes",
"type": "java.lang.Boolean",
"description": "Enable the capture of experimental log attributes thread.name and thread.id.",
"defaultValue": false
},
{
"name": "otel.instrumentation.logback-appender.experimental.capture-logger-context-attributes",
"type": "java.lang.Boolean",
"description": "Enable the capture of Logback logger context properties as attributes.",
"defaultValue": false
},
{
"name": "otel.instrumentation.logback-appender.experimental.capture-mdc-attributes",
"type": "java.lang.String",
"description": "Comma separated list of MDC attributes to capture. Use the wildcard character * to capture all attributes."
}
]
}

View File

@ -8,16 +8,21 @@ package io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.lo
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender; import io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender;
import io.opentelemetry.instrumentation.testing.internal.AutoCleanupExtension; import io.opentelemetry.instrumentation.testing.internal.AutoCleanupExtension;
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension;
import io.opentelemetry.sdk.logs.data.LogRecordData;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.extension.RegisterExtension;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@ -49,6 +54,10 @@ class LogbackAppenderTest {
void shouldInitializeAppender() { void shouldInitializeAppender() {
Map<String, Object> properties = new HashMap<>(); Map<String, Object> properties = new HashMap<>();
properties.put("logging.config", "classpath:logback-test.xml"); properties.put("logging.config", "classpath:logback-test.xml");
properties.put(
"otel.instrumentation.logback-appender.experimental.capture-mdc-attributes", "*");
properties.put(
"otel.instrumentation.logback-appender.experimental.capture-code-attributes", false);
SpringApplication app = SpringApplication app =
new SpringApplication( new SpringApplication(
@ -57,15 +66,30 @@ class LogbackAppenderTest {
ConfigurableApplicationContext context = app.run(); ConfigurableApplicationContext context = app.run();
cleanup.deferCleanup(context); cleanup.deferCleanup(context);
MDC.put("key1", "val1");
MDC.put("key2", "val2");
try {
LoggerFactory.getLogger("test").info("test log message"); LoggerFactory.getLogger("test").info("test log message");
} finally {
MDC.clear();
}
assertThat(testing.logRecords()) List<LogRecordData> logRecords = testing.logRecords();
assertThat(logRecords)
.satisfiesOnlyOnce( .satisfiesOnlyOnce(
// OTel appender automatically added or from an XML file, it should not // OTel appender automatically added or from an XML file, it should not
// be added a second time by LogbackAppenderApplicationListener // be added a second time by LogbackAppenderApplicationListener
logRecord -> { logRecord -> {
assertThat(logRecord.getInstrumentationScopeInfo().getName()).isEqualTo("test"); assertThat(logRecord.getInstrumentationScopeInfo().getName()).isEqualTo("test");
assertThat(logRecord.getBody().asString()).contains("test log message"); assertThat(logRecord.getBody().asString()).contains("test log message");
Attributes attributes = logRecord.getAttributes();
// key1 and key2, the code attributes should not be present because they are enabled
// in the logback.xml file but are disabled with a property
assertThat(attributes.size()).isEqualTo(2);
assertThat(attributes.asMap())
.containsEntry(AttributeKey.stringKey("key1"), "val1")
.containsEntry(AttributeKey.stringKey("key2"), "val2");
}); });
} }

View File

@ -9,7 +9,9 @@
</encoder> </encoder>
</appender> </appender>
<appender name="OpenTelemetry" <appender name="OpenTelemetry"
class="io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender"/> class="io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender">
<captureCodeAttributes>true</captureCodeAttributes>
</appender>
<root level="INFO"> <root level="INFO">
<appender-ref ref="console"/> <appender-ref ref="console"/>

View File

@ -0,0 +1 @@
otel.instrumentation.logback-appender.experimental.capture-code-attributes=true

View File

@ -121,5 +121,9 @@ class OtelSpringStarterSmokeTest {
.as("Should instrument logs") .as("Should instrument logs")
.startsWith("Starting ") .startsWith("Starting ")
.contains(this.getClass().getSimpleName()); .contains(this.getClass().getSimpleName());
assertThat(firstLog.getAttributes().asMap())
.as("Should capture code attributes")
.containsEntry(
SemanticAttributes.CODE_NAMESPACE, "org.springframework.boot.StartupInfoLogger");
} }
} }