Skip to content

Commit 80e5bac

Browse files
authored
Allow the Spring starter to configure the OTel Logback appender from system properties (#10355)
1 parent 5f6232f commit 80e5bac

File tree

6 files changed

+188
-17
lines changed

6 files changed

+188
-17
lines changed

instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/instrumentation/logging/LogbackAppenderApplicationListener.java

+110-14
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import ch.qos.logback.core.Appender;
1111
import io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender;
1212
import java.util.Iterator;
13+
import java.util.Optional;
1314
import org.slf4j.ILoggerFactory;
1415
import org.slf4j.Logger;
1516
import org.slf4j.LoggerFactory;
@@ -53,35 +54,130 @@ private static boolean isAssignableFrom(Class<?> type, Class<?>... supportedType
5354
@Override
5455
public void onApplicationEvent(ApplicationEvent event) {
5556
if (event instanceof ApplicationEnvironmentPreparedEvent // Event for which
56-
// org.springframework.boot.context.logging.LoggingApplicationListener
57-
// initializes logging
58-
&& !isOpenTelemetryAppenderAlreadyConfigured()) {
59-
ch.qos.logback.classic.Logger logger =
60-
(ch.qos.logback.classic.Logger)
61-
LoggerFactory.getILoggerFactory().getLogger(Logger.ROOT_LOGGER_NAME);
62-
63-
OpenTelemetryAppender appender = new OpenTelemetryAppender();
64-
appender.start();
65-
logger.addAppender(appender);
57+
// org.springframework.boot.context.logging.LoggingApplicationListener
58+
// initializes logging
59+
) {
60+
Optional<OpenTelemetryAppender> existingOpenTelemetryAppender = findOpenTelemetryAppender();
61+
ApplicationEnvironmentPreparedEvent applicationEnvironmentPreparedEvent =
62+
(ApplicationEnvironmentPreparedEvent) event;
63+
if (existingOpenTelemetryAppender.isPresent()) {
64+
reInitializeOpenTelemetryAppender(
65+
existingOpenTelemetryAppender, applicationEnvironmentPreparedEvent);
66+
} else {
67+
addOpenTelemetryAppender(applicationEnvironmentPreparedEvent);
68+
}
69+
}
70+
}
71+
72+
private static void reInitializeOpenTelemetryAppender(
73+
Optional<OpenTelemetryAppender> existingOpenTelemetryAppender,
74+
ApplicationEnvironmentPreparedEvent applicationEnvironmentPreparedEvent) {
75+
OpenTelemetryAppender openTelemetryAppender = existingOpenTelemetryAppender.get();
76+
// The OpenTelemetry appender is stopped and restarted from the
77+
// org.springframework.boot.context.logging.LoggingApplicationListener.initialize
78+
// method.
79+
// The OpenTelemetryAppender initializes the LoggingEventMapper in the start() method. So, here
80+
// we stop the OpenTelemetry appender before its re-initialization and its restart.
81+
openTelemetryAppender.stop();
82+
initializeOpenTelemetryAppenderFromProperties(
83+
applicationEnvironmentPreparedEvent, openTelemetryAppender);
84+
openTelemetryAppender.start();
85+
}
86+
87+
private static void addOpenTelemetryAppender(
88+
ApplicationEnvironmentPreparedEvent applicationEnvironmentPreparedEvent) {
89+
ch.qos.logback.classic.Logger logger =
90+
(ch.qos.logback.classic.Logger)
91+
LoggerFactory.getILoggerFactory().getLogger(Logger.ROOT_LOGGER_NAME);
92+
OpenTelemetryAppender openTelemetryAppender = new OpenTelemetryAppender();
93+
initializeOpenTelemetryAppenderFromProperties(
94+
applicationEnvironmentPreparedEvent, openTelemetryAppender);
95+
openTelemetryAppender.start();
96+
logger.addAppender(openTelemetryAppender);
97+
}
98+
99+
private static void initializeOpenTelemetryAppenderFromProperties(
100+
ApplicationEnvironmentPreparedEvent applicationEnvironmentPreparedEvent,
101+
OpenTelemetryAppender openTelemetryAppender) {
102+
103+
// Implemented in the same way as the
104+
// org.springframework.boot.context.logging.LoggingApplicationListener, config properties not
105+
// available
106+
Boolean codeAttribute =
107+
evaluateBooleanProperty(
108+
applicationEnvironmentPreparedEvent,
109+
"otel.instrumentation.logback-appender.experimental.capture-code-attributes");
110+
if (codeAttribute != null) {
111+
openTelemetryAppender.setCaptureCodeAttributes(codeAttribute.booleanValue());
112+
}
113+
114+
Boolean markerAttribute =
115+
evaluateBooleanProperty(
116+
applicationEnvironmentPreparedEvent,
117+
"otel.instrumentation.logback-appender.experimental.capture-marker-attribute");
118+
if (markerAttribute != null) {
119+
openTelemetryAppender.setCaptureMarkerAttribute(markerAttribute.booleanValue());
66120
}
121+
122+
Boolean keyValuePairAttributes =
123+
evaluateBooleanProperty(
124+
applicationEnvironmentPreparedEvent,
125+
"otel.instrumentation.logback-appender.experimental.capture-key-value-pair-attributes");
126+
if (keyValuePairAttributes != null) {
127+
openTelemetryAppender.setCaptureKeyValuePairAttributes(keyValuePairAttributes.booleanValue());
128+
}
129+
130+
Boolean logAttributes =
131+
evaluateBooleanProperty(
132+
applicationEnvironmentPreparedEvent,
133+
"otel.instrumentation.logback-appender.experimental-log-attributes");
134+
if (logAttributes != null) {
135+
openTelemetryAppender.setCaptureExperimentalAttributes(logAttributes.booleanValue());
136+
}
137+
138+
Boolean loggerContextAttributes =
139+
evaluateBooleanProperty(
140+
applicationEnvironmentPreparedEvent,
141+
"otel.instrumentation.logback-appender.experimental.capture-logger-context-attributes");
142+
if (loggerContextAttributes != null) {
143+
openTelemetryAppender.setCaptureLoggerContext(loggerContextAttributes.booleanValue());
144+
}
145+
146+
String mdcAttributeProperty =
147+
applicationEnvironmentPreparedEvent
148+
.getEnvironment()
149+
.getProperty(
150+
"otel.instrumentation.logback-appender.experimental.capture-mdc-attributes",
151+
String.class);
152+
if (mdcAttributeProperty != null) {
153+
openTelemetryAppender.setCaptureMdcAttributes(mdcAttributeProperty);
154+
}
155+
}
156+
157+
private static Boolean evaluateBooleanProperty(
158+
ApplicationEnvironmentPreparedEvent applicationEnvironmentPreparedEvent, String property) {
159+
return applicationEnvironmentPreparedEvent
160+
.getEnvironment()
161+
.getProperty(property, Boolean.class);
67162
}
68163

69-
private static boolean isOpenTelemetryAppenderAlreadyConfigured() {
164+
private static Optional<OpenTelemetryAppender> findOpenTelemetryAppender() {
70165
ILoggerFactory loggerFactorySpi = LoggerFactory.getILoggerFactory();
71166
if (!(loggerFactorySpi instanceof LoggerContext)) {
72-
return false;
167+
return Optional.empty();
73168
}
74169
LoggerContext loggerContext = (LoggerContext) loggerFactorySpi;
75170
for (ch.qos.logback.classic.Logger logger : loggerContext.getLoggerList()) {
76171
Iterator<Appender<ILoggingEvent>> appenderIterator = logger.iteratorForAppenders();
77172
while (appenderIterator.hasNext()) {
78173
Appender<ILoggingEvent> appender = appenderIterator.next();
79174
if (appender instanceof OpenTelemetryAppender) {
80-
return true;
175+
OpenTelemetryAppender openTelemetryAppender = (OpenTelemetryAppender) appender;
176+
return Optional.of(openTelemetryAppender);
81177
}
82178
}
83179
}
84-
return false;
180+
return Optional.empty();
85181
}
86182

87183
@Override
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"groups": [
3+
{
4+
"name": "otel"
5+
}
6+
],
7+
"properties": [
8+
{
9+
"name": "otel.instrumentation.logback-appender.experimental.capture-code-attributes",
10+
"type": "java.lang.Boolean",
11+
"description": "Enable the capture of source code attributes. Note that capturing source code attributes at logging sites might add a performance overhead.",
12+
"defaultValue": false
13+
},
14+
{
15+
"name": "otel.instrumentation.logback-appender.experimental.capture-marker-attribute",
16+
"type": "java.lang.Boolean",
17+
"description": "Enable the capture of Logback markers as attributes.",
18+
"defaultValue": false
19+
},
20+
{
21+
"name": "otel.instrumentation.logback-appender.experimental.capture-key-value-pair-attributes",
22+
"type": "java.lang.Boolean",
23+
"description": "Enable the capture of Logback key value pairs as attributes.",
24+
"defaultValue": false
25+
},
26+
{
27+
"name": "otel.instrumentation.logback-appender.experimental-log-attributes",
28+
"type": "java.lang.Boolean",
29+
"description": "Enable the capture of experimental log attributes thread.name and thread.id.",
30+
"defaultValue": false
31+
},
32+
{
33+
"name": "otel.instrumentation.logback-appender.experimental.capture-logger-context-attributes",
34+
"type": "java.lang.Boolean",
35+
"description": "Enable the capture of Logback logger context properties as attributes.",
36+
"defaultValue": false
37+
},
38+
{
39+
"name": "otel.instrumentation.logback-appender.experimental.capture-mdc-attributes",
40+
"type": "java.lang.String",
41+
"description": "Comma separated list of MDC attributes to capture. Use the wildcard character * to capture all attributes."
42+
}
43+
]
44+
}

instrumentation/spring/spring-boot-autoconfigure/src/testLogbackAppender/java/io/opentelemetry/instrumentation/spring/autoconfigure/instrumentation/logging/LogbackAppenderTest.java

+26-2
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,21 @@
88
import static org.assertj.core.api.Assertions.assertThat;
99

1010
import io.opentelemetry.api.OpenTelemetry;
11+
import io.opentelemetry.api.common.AttributeKey;
12+
import io.opentelemetry.api.common.Attributes;
1113
import io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender;
1214
import io.opentelemetry.instrumentation.testing.internal.AutoCleanupExtension;
1315
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
1416
import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension;
17+
import io.opentelemetry.sdk.logs.data.LogRecordData;
1518
import java.util.HashMap;
19+
import java.util.List;
1620
import java.util.Map;
1721
import org.junit.jupiter.api.BeforeEach;
1822
import org.junit.jupiter.api.Test;
1923
import org.junit.jupiter.api.extension.RegisterExtension;
2024
import org.slf4j.LoggerFactory;
25+
import org.slf4j.MDC;
2126
import org.springframework.boot.SpringApplication;
2227
import org.springframework.context.ConfigurableApplicationContext;
2328
import org.springframework.context.annotation.Bean;
@@ -49,6 +54,10 @@ public OpenTelemetry openTelemetry() {
4954
void shouldInitializeAppender() {
5055
Map<String, Object> properties = new HashMap<>();
5156
properties.put("logging.config", "classpath:logback-test.xml");
57+
properties.put(
58+
"otel.instrumentation.logback-appender.experimental.capture-mdc-attributes", "*");
59+
properties.put(
60+
"otel.instrumentation.logback-appender.experimental.capture-code-attributes", false);
5261

5362
SpringApplication app =
5463
new SpringApplication(
@@ -57,15 +66,30 @@ void shouldInitializeAppender() {
5766
ConfigurableApplicationContext context = app.run();
5867
cleanup.deferCleanup(context);
5968

60-
LoggerFactory.getLogger("test").info("test log message");
69+
MDC.put("key1", "val1");
70+
MDC.put("key2", "val2");
71+
try {
72+
LoggerFactory.getLogger("test").info("test log message");
73+
} finally {
74+
MDC.clear();
75+
}
6176

62-
assertThat(testing.logRecords())
77+
List<LogRecordData> logRecords = testing.logRecords();
78+
assertThat(logRecords)
6379
.satisfiesOnlyOnce(
6480
// OTel appender automatically added or from an XML file, it should not
6581
// be added a second time by LogbackAppenderApplicationListener
6682
logRecord -> {
6783
assertThat(logRecord.getInstrumentationScopeInfo().getName()).isEqualTo("test");
6884
assertThat(logRecord.getBody().asString()).contains("test log message");
85+
86+
Attributes attributes = logRecord.getAttributes();
87+
// key1 and key2, the code attributes should not be present because they are enabled
88+
// in the logback.xml file but are disabled with a property
89+
assertThat(attributes.size()).isEqualTo(2);
90+
assertThat(attributes.asMap())
91+
.containsEntry(AttributeKey.stringKey("key1"), "val1")
92+
.containsEntry(AttributeKey.stringKey("key2"), "val2");
6993
});
7094
}
7195

instrumentation/spring/spring-boot-autoconfigure/src/testLogbackAppender/resources/logback-test.xml

+3-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
</encoder>
1010
</appender>
1111
<appender name="OpenTelemetry"
12-
class="io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender"/>
12+
class="io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender">
13+
<captureCodeAttributes>true</captureCodeAttributes>
14+
</appender>
1315

1416
<root level="INFO">
1517
<appender-ref ref="console"/>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
otel.instrumentation.logback-appender.experimental.capture-code-attributes=true

smoke-tests-otel-starter/src/test/java/io/opentelemetry/smoketest/OtelSpringStarterSmokeTest.java

+4
Original file line numberDiff line numberDiff line change
@@ -121,5 +121,9 @@ void shouldSendTelemetry() throws InterruptedException {
121121
.as("Should instrument logs")
122122
.startsWith("Starting ")
123123
.contains(this.getClass().getSimpleName());
124+
assertThat(firstLog.getAttributes().asMap())
125+
.as("Should capture code attributes")
126+
.containsEntry(
127+
SemanticAttributes.CODE_NAMESPACE, "org.springframework.boot.StartupInfoLogger");
124128
}
125129
}

0 commit comments

Comments
 (0)