Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import ch.qos.logback.core.Appender;
import io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender;
import java.util.Iterator;
import java.util.Optional;
import org.slf4j.ILoggerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -53,35 +54,126 @@ private static boolean isAssignableFrom(Class<?> type, Class<?>... supportedType
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ApplicationEnvironmentPreparedEvent // Event for which
// org.springframework.boot.context.logging.LoggingApplicationListener
// initializes logging
&& !isOpenTelemetryAppenderAlreadyConfigured()) {
ch.qos.logback.classic.Logger logger =
(ch.qos.logback.classic.Logger)
LoggerFactory.getILoggerFactory().getLogger(Logger.ROOT_LOGGER_NAME);

OpenTelemetryAppender appender = new OpenTelemetryAppender();
appender.start();
logger.addAppender(appender);
// org.springframework.boot.context.logging.LoggingApplicationListener
// initializes logging
) {
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();
openTelemetryAppender.stop(); // 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.
trask marked this conversation as resolved.
Show resolved Hide resolved
initializeOpenTelemetryAppenderFromProperties(
applicationEnvironmentPreparedEvent, openTelemetryAppender);
openTelemetryAppender.start();
}

private static void addOpenTelemetryAppender(
ApplicationEnvironmentPreparedEvent applicationEnvironmentPreparedEvent) {
ch.qos.logback.classic.Logger logger =
(ch.qos.logback.classic.Logger)
LoggerFactory.getILoggerFactory().getLogger(Logger.ROOT_LOGGER_NAME);
OpenTelemetryAppender openTelemetryAppender = new OpenTelemetryAppender();
initializeOpenTelemetryAppenderFromProperties(
applicationEnvironmentPreparedEvent, openTelemetryAppender);
openTelemetryAppender.start();
logger.addAppender(openTelemetryAppender);
}

private static void initializeOpenTelemetryAppenderFromProperties(
ApplicationEnvironmentPreparedEvent applicationEnvironmentPreparedEvent,
OpenTelemetryAppender openTelemetryAppender) {

Boolean codeAttribute =
zeitlinger marked this conversation as resolved.
Show resolved Hide resolved
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 evaluateBooleanProperty(
ApplicationEnvironmentPreparedEvent applicationEnvironmentPreparedEvent, String property) {
return applicationEnvironmentPreparedEvent
.getEnvironment()
.getProperty(property, Boolean.class);
}

private static boolean isOpenTelemetryAppenderAlreadyConfigured() {
private static Optional<OpenTelemetryAppender> findOpenTelemetryAppender() {
ILoggerFactory loggerFactorySpi = LoggerFactory.getILoggerFactory();
if (!(loggerFactorySpi instanceof LoggerContext)) {
return false;
return Optional.empty();
}
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();
if (appender instanceof OpenTelemetryAppender) {
return true;
OpenTelemetryAppender openTelemetryAppender = (OpenTelemetryAppender) appender;
return Optional.of(openTelemetryAppender);
}
}
}
return false;
return Optional.empty();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will this file conflict with the one we're autogenerating?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. See https://docs.spring.io/spring-boot/docs/current/reference/html/configuration-metadata.html#appendix.configuration-metadata.annotation-processor.adding-additional-metadata: "the annotation processor automatically merges items from META-INF/additional-spring-configuration-metadata.json into the main metadata file."

"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."
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,21 @@
import static org.assertj.core.api.Assertions.assertThat;

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.testing.internal.AutoCleanupExtension;
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension;
import io.opentelemetry.sdk.logs.data.LogRecordData;
import java.util.HashMap;
import java.util.List;
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.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
Expand Down Expand Up @@ -49,6 +54,10 @@ public OpenTelemetry openTelemetry() {
void shouldInitializeAppender() {
Map<String, Object> properties = new HashMap<>();
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 =
new SpringApplication(
Expand All @@ -57,15 +66,30 @@ void shouldInitializeAppender() {
ConfigurableApplicationContext context = app.run();
cleanup.deferCleanup(context);

LoggerFactory.getLogger("test").info("test log message");
MDC.put("key1", "val1");
MDC.put("key2", "val2");
try {
LoggerFactory.getLogger("test").info("test log message");
} finally {
MDC.clear();
}

assertThat(testing.logRecords())
List<LogRecordData> logRecords = testing.logRecords();
assertThat(logRecords)
.satisfiesOnlyOnce(
// OTel appender automatically added or from an XML file, it should not
// be added a second time by LogbackAppenderApplicationListener
logRecord -> {
assertThat(logRecord.getInstrumentationScopeInfo().getName()).isEqualTo("test");
assertThat(logRecord.getBody().asString()).contains("test log message");

Attributes attributes = logRecord.getAttributes();
zeitlinger marked this conversation as resolved.
Show resolved Hide resolved
assertThat(attributes.size())
.isEqualTo(
2); // 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
trask marked this conversation as resolved.
Show resolved Hide resolved
assertThat(attributes.get(AttributeKey.stringKey("key1"))).isEqualTo("val1");
assertThat(attributes.get(AttributeKey.stringKey("key2"))).isEqualTo("val2");
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
</encoder>
</appender>
<appender name="OpenTelemetry"
class="io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender"/>
class="io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender">
<captureCodeAttributes>true</captureCodeAttributes>
zeitlinger marked this conversation as resolved.
Show resolved Hide resolved
</appender>

<root level="INFO">
<appender-ref ref="console"/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
otel.instrumentation.logback-appender.experimental.capture-code-attributes=true
Original file line number Diff line number Diff line change
Expand Up @@ -116,5 +116,9 @@ void shouldSendTelemetry() throws InterruptedException {
.as("Should instrument logs")
.startsWith("Starting ")
.contains(this.getClass().getSimpleName());
String codeNamespace = firstLog.getAttributes().get(SemanticAttributes.CODE_NAMESPACE);
zeitlinger marked this conversation as resolved.
Show resolved Hide resolved
assertThat(codeNamespace)
.as("Should capture code attributes")
.isEqualTo("org.springframework.boot.StartupInfoLogger");
}
}
Loading