InstrumentationConfig part 5: library logging appenders (#6321)

* InstrumentationConfig part 5: library logging appenders

* Logback

* remove log4j hackery

* fix tests

* Remove unused

Co-authored-by: Trask Stalnaker <trask.stalnaker@gmail.com>
This commit is contained in:
Mateusz Rzeszutek 2022-07-18 23:28:26 +02:00 committed by GitHub
parent 96cac8184c
commit 257009f944
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 201 additions and 85 deletions

View File

@ -5,10 +5,14 @@
package io.opentelemetry.javaagent.instrumentation.log4j.appender.v2_17;
import static java.util.Collections.emptyList;
import io.opentelemetry.instrumentation.api.appender.internal.LogBuilder;
import io.opentelemetry.instrumentation.log4j.appender.v2_17.internal.ContextDataAccessor;
import io.opentelemetry.instrumentation.log4j.appender.v2_17.internal.LogEventMapper;
import io.opentelemetry.javaagent.bootstrap.AgentLogEmitterProvider;
import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import javax.annotation.Nullable;
@ -19,8 +23,29 @@ import org.apache.logging.log4j.message.Message;
public final class Log4jHelper {
private static final LogEventMapper<Map<String, String>> mapper =
new LogEventMapper<>(ContextDataAccessorImpl.INSTANCE);
private static final LogEventMapper<Map<String, String>> mapper;
static {
InstrumentationConfig config = InstrumentationConfig.get();
boolean captureExperimentalAttributes =
config.getBoolean("otel.instrumentation.log4j-appender.experimental-log-attributes", false);
boolean captureMapMessageAttributes =
config.getBoolean(
"otel.instrumentation.log4j-appender.experimental.capture-map-message-attributes",
false);
List<String> captureContextDataAttributes =
config.getList(
"otel.instrumentation.log4j-appender.experimental.capture-context-data-attributes",
emptyList());
mapper =
new LogEventMapper<>(
ContextDataAccessorImpl.INSTANCE,
captureExperimentalAttributes,
captureMapMessageAttributes,
captureContextDataAttributes);
}
public static void capture(Logger logger, Level level, Message message, Throwable throwable) {
String instrumentationName = logger.getName();

View File

@ -12,9 +12,3 @@ dependencies {
testImplementation("io.opentelemetry:opentelemetry-sdk-logs")
testImplementation("io.opentelemetry:opentelemetry-sdk-testing")
}
tasks.withType<Test>().configureEach {
// TODO run tests both with and without experimental log attributes
jvmArgs("-Dotel.instrumentation.log4j-appender.experimental.capture-map-message-attributes=true")
jvmArgs("-Dotel.instrumentation.log4j-appender.experimental.capture-context-data-attributes=*")
}

View File

@ -5,6 +5,8 @@
package io.opentelemetry.instrumentation.log4j.appender.v2_17;
import static java.util.Collections.emptyList;
import io.opentelemetry.instrumentation.api.appender.internal.LogBuilder;
import io.opentelemetry.instrumentation.api.appender.internal.LogEmitterProvider;
import io.opentelemetry.instrumentation.api.appender.internal.LogEmitterProviderHolder;
@ -13,9 +15,13 @@ import io.opentelemetry.instrumentation.log4j.appender.v2_17.internal.LogEventMa
import io.opentelemetry.instrumentation.sdk.appender.internal.DelegatingLogEmitterProvider;
import io.opentelemetry.sdk.logs.SdkLogEmitterProvider;
import java.io.Serializable;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.apache.logging.log4j.ThreadContext;
import org.apache.logging.log4j.core.Appender;
import org.apache.logging.log4j.core.Core;
import org.apache.logging.log4j.core.Filter;
@ -24,8 +30,10 @@ import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.appender.AbstractAppender;
import org.apache.logging.log4j.core.config.Property;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute;
import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
import org.apache.logging.log4j.core.time.Instant;
import org.apache.logging.log4j.message.MapMessage;
import org.apache.logging.log4j.util.ReadOnlyStringMap;
@Plugin(
@ -39,8 +47,7 @@ public class OpenTelemetryAppender extends AbstractAppender {
private static final LogEmitterProviderHolder logEmitterProviderHolder =
new LogEmitterProviderHolder();
private static final LogEventMapper<ReadOnlyStringMap> mapper =
new LogEventMapper<>(ContextDataAccessorImpl.INSTANCE);
private final LogEventMapper<ReadOnlyStringMap> mapper;
@PluginBuilderFactory
public static <B extends Builder<B>> B builder() {
@ -50,10 +57,43 @@ public class OpenTelemetryAppender extends AbstractAppender {
static class Builder<B extends Builder<B>> extends AbstractAppender.Builder<B>
implements org.apache.logging.log4j.core.util.Builder<OpenTelemetryAppender> {
@PluginBuilderAttribute private boolean captureExperimentalAttributes;
@PluginBuilderAttribute private boolean captureMapMessageAttributes;
@PluginBuilderAttribute private String captureContextDataAttributes;
/**
* Sets whether experimental attributes should be set to logs. These attributes may be changed
* or removed in the future, so only enable this if you know you do not require attributes
* filled by this instrumentation to be stable across versions.
*/
public B setCaptureExperimentalAttributes(boolean captureExperimentalAttributes) {
this.captureExperimentalAttributes = captureExperimentalAttributes;
return asBuilder();
}
/** Sets whether log4j {@link MapMessage} attributes should be copied to logs. */
public B setCaptureMapMessageAttributes(boolean captureMapMessageAttributes) {
this.captureMapMessageAttributes = captureMapMessageAttributes;
return asBuilder();
}
/** Configures the {@link ThreadContext} attributes that will be copied to logs. */
public B setCaptureContextDataAttributes(String captureContextDataAttributes) {
this.captureContextDataAttributes = captureContextDataAttributes;
return asBuilder();
}
@Override
public OpenTelemetryAppender build() {
return new OpenTelemetryAppender(
getName(), getLayout(), getFilter(), isIgnoreExceptions(), getPropertyArray());
getName(),
getLayout(),
getFilter(),
isIgnoreExceptions(),
getPropertyArray(),
captureExperimentalAttributes,
captureMapMessageAttributes,
captureContextDataAttributes);
}
}
@ -62,8 +102,28 @@ public class OpenTelemetryAppender extends AbstractAppender {
Layout<? extends Serializable> layout,
Filter filter,
boolean ignoreExceptions,
Property[] properties) {
Property[] properties,
boolean captureExperimentalAttributes,
boolean captureMapMessageAttributes,
String captureContextDataAttributes) {
super(name, filter, layout, ignoreExceptions, properties);
this.mapper =
new LogEventMapper<>(
ContextDataAccessorImpl.INSTANCE,
captureExperimentalAttributes,
captureMapMessageAttributes,
splitAndFilterBlanksAndNulls(captureContextDataAttributes));
}
private static List<String> splitAndFilterBlanksAndNulls(String value) {
if (value == null) {
return emptyList();
}
return Arrays.stream(value.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.collect(Collectors.toList());
}
@Override

View File

@ -5,15 +5,12 @@
package io.opentelemetry.instrumentation.log4j.appender.v2_17.internal;
import static java.util.Collections.emptyList;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributesBuilder;
import io.opentelemetry.context.Context;
import io.opentelemetry.instrumentation.api.appender.internal.LogBuilder;
import io.opentelemetry.instrumentation.api.appender.internal.Severity;
import io.opentelemetry.instrumentation.api.config.Config;
import io.opentelemetry.instrumentation.api.internal.cache.Cache;
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
import java.io.PrintWriter;
@ -33,44 +30,26 @@ public final class LogEventMapper<T> {
private static final String SPECIAL_MAP_MESSAGE_ATTRIBUTE = "message";
private static final boolean captureExperimentalAttributes =
Config.get()
.getBoolean("otel.instrumentation.log4j-appender.experimental-log-attributes", false);
private static final Cache<String, AttributeKey<String>> contextDataAttributeKeyCache =
Cache.bounded(100);
private static final Cache<String, AttributeKey<String>> mapMessageAttributeKeyCache =
Cache.bounded(100);
private final boolean captureMapMessageAttributes;
private final List<String> captureContextDataAttributes;
// cached as an optimization
private final boolean captureAllContextDataAttributes;
private final ContextDataAccessor<T> contextDataAccessor;
public LogEventMapper(ContextDataAccessor<T> contextDataAccessor) {
this(
contextDataAccessor,
Config.get()
.getBoolean(
"otel.instrumentation.log4j-appender.experimental.capture-map-message-attributes",
false),
Config.get()
.getList(
"otel.instrumentation.log4j-appender.experimental.capture-context-data-attributes",
emptyList()));
}
private final boolean captureExperimentalAttributes;
private final boolean captureMapMessageAttributes;
private final List<String> captureContextDataAttributes;
private final boolean captureAllContextDataAttributes;
// visible for testing
LogEventMapper(
public LogEventMapper(
ContextDataAccessor<T> contextDataAccessor,
boolean captureExperimentalAttributes,
boolean captureMapMessageAttributes,
List<String> captureContextDataAttributes) {
this.contextDataAccessor = contextDataAccessor;
this.captureExperimentalAttributes = captureExperimentalAttributes;
this.captureMapMessageAttributes = captureMapMessageAttributes;
this.captureContextDataAttributes = captureContextDataAttributes;
this.captureAllContextDataAttributes =

View File

@ -24,7 +24,7 @@ import java.util.function.BiConsumer;
import javax.annotation.Nullable;
import org.apache.logging.log4j.message.StringMapMessage;
import org.apache.logging.log4j.message.StructuredDataMessage;
import org.junit.Test;
import org.junit.jupiter.api.Test;
class LogEventMapperTest {
@ -32,7 +32,7 @@ class LogEventMapperTest {
void testDefault() {
// given
LogEventMapper<Map<String, String>> mapper =
new LogEventMapper<>(ContextDataAccessorImpl.INSTANCE, false, emptyList());
new LogEventMapper<>(ContextDataAccessorImpl.INSTANCE, false, false, emptyList());
Map<String, String> contextData = new HashMap<>();
contextData.put("key1", "value1");
contextData.put("key2", "value2");
@ -49,7 +49,7 @@ class LogEventMapperTest {
void testSome() {
// given
LogEventMapper<Map<String, String>> mapper =
new LogEventMapper<>(ContextDataAccessorImpl.INSTANCE, false, singletonList("key2"));
new LogEventMapper<>(ContextDataAccessorImpl.INSTANCE, false, false, singletonList("key2"));
Map<String, String> contextData = new HashMap<>();
contextData.put("key1", "value1");
contextData.put("key2", "value2");
@ -67,7 +67,7 @@ class LogEventMapperTest {
void testAll() {
// given
LogEventMapper<Map<String, String>> mapper =
new LogEventMapper<>(ContextDataAccessorImpl.INSTANCE, false, singletonList("*"));
new LogEventMapper<>(ContextDataAccessorImpl.INSTANCE, false, false, singletonList("*"));
Map<String, String> contextData = new HashMap<>();
contextData.put("key1", "value1");
contextData.put("key2", "value2");
@ -87,7 +87,7 @@ class LogEventMapperTest {
void testCaptureMapMessageDisabled() {
// given
LogEventMapper<Map<String, String>> mapper =
new LogEventMapper<>(ContextDataAccessorImpl.INSTANCE, false, singletonList("*"));
new LogEventMapper<>(ContextDataAccessorImpl.INSTANCE, false, false, singletonList("*"));
StringMapMessage message = new StringMapMessage();
message.put("key1", "value1");
@ -108,7 +108,7 @@ class LogEventMapperTest {
void testCaptureMapMessageWithSpecialAttribute() {
// given
LogEventMapper<Map<String, String>> mapper =
new LogEventMapper<>(ContextDataAccessorImpl.INSTANCE, true, singletonList("*"));
new LogEventMapper<>(ContextDataAccessorImpl.INSTANCE, false, true, singletonList("*"));
StringMapMessage message = new StringMapMessage();
message.put("key1", "value1");
@ -129,7 +129,7 @@ class LogEventMapperTest {
void testCaptureMapMessageWithoutSpecialAttribute() {
// given
LogEventMapper<Map<String, String>> mapper =
new LogEventMapper<>(ContextDataAccessorImpl.INSTANCE, true, singletonList("*"));
new LogEventMapper<>(ContextDataAccessorImpl.INSTANCE, false, true, singletonList("*"));
StringMapMessage message = new StringMapMessage();
message.put("key1", "value1");
@ -153,7 +153,7 @@ class LogEventMapperTest {
void testCaptureStructuredDataMessage() {
// given
LogEventMapper<Map<String, String>> mapper =
new LogEventMapper<>(ContextDataAccessorImpl.INSTANCE, true, singletonList("*"));
new LogEventMapper<>(ContextDataAccessorImpl.INSTANCE, false, true, singletonList("*"));
StructuredDataMessage message = new StructuredDataMessage("an id", "a message", "a type");
message.put("key1", "value1");

View File

@ -6,13 +6,12 @@
<PatternLayout
pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} traceId: %X{trace_id} spanId: %X{span_id} flags: %X{trace_flags} - %msg%n"/>
</Console>
<ListAppender name="ListAppender"/>
<OpenTelemetry name="OpenTelemetryAppender"/>
<!-- TODO run tests both with and without experimental log attributes -->
<OpenTelemetry name="OpenTelemetryAppender" captureMapMessageAttributes="true" captureContextDataAttributes="*"/>
</Appenders>
<Loggers>
<Logger name="TestLogger" level="All">
<AppenderRef ref="OpenTelemetryAppender" level="All"/>
<AppenderRef ref="ListAppender" level="All"/>
<AppenderRef ref="Console" level="All"/>
</Logger>
<Root>

View File

@ -5,6 +5,7 @@
package io.opentelemetry.javaagent.instrumentation.logback.appender.v1_0;
import static io.opentelemetry.javaagent.instrumentation.logback.appender.v1_0.LogbackSingletons.mapper;
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
import static net.bytebuddy.matcher.ElementMatchers.isPublic;
import static net.bytebuddy.matcher.ElementMatchers.named;
@ -13,7 +14,6 @@ import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
import ch.qos.logback.classic.spi.ILoggingEvent;
import io.opentelemetry.instrumentation.api.appender.internal.LogEmitterProvider;
import io.opentelemetry.instrumentation.logback.appender.v1_0.internal.LoggingEventMapper;
import io.opentelemetry.javaagent.bootstrap.AgentLogEmitterProvider;
import io.opentelemetry.javaagent.bootstrap.CallDepth;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
@ -51,7 +51,7 @@ class LogbackInstrumentation implements TypeInstrumentation {
// logging framework delegates to another
callDepth = CallDepth.forClass(LogEmitterProvider.class);
if (callDepth.getAndIncrement() == 0) {
LoggingEventMapper.INSTANCE.emit(AgentLogEmitterProvider.get(), event);
mapper().emit(AgentLogEmitterProvider.get(), event);
}
}

View File

@ -0,0 +1,35 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.logback.appender.v1_0;
import static java.util.Collections.emptyList;
import io.opentelemetry.instrumentation.logback.appender.v1_0.internal.LoggingEventMapper;
import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig;
import java.util.List;
public class LogbackSingletons {
private static final LoggingEventMapper mapper;
static {
InstrumentationConfig config = InstrumentationConfig.get();
boolean captureExperimentalAttributes =
config.getBoolean(
"otel.instrumentation.logback-appender.experimental-log-attributes", false);
List<String> captureMdcAttributes =
config.getList(
"otel.instrumentation.logback-appender.experimental.capture-mdc-attributes",
emptyList());
mapper = new LoggingEventMapper(captureExperimentalAttributes, captureMdcAttributes);
}
public static LoggingEventMapper mapper() {
return mapper;
}
}

View File

@ -11,8 +11,3 @@ dependencies {
testImplementation("io.opentelemetry:opentelemetry-sdk-logs")
testImplementation("io.opentelemetry:opentelemetry-sdk-testing")
}
tasks.withType<Test>().configureEach {
// TODO run tests both with and without experimental log attributes
jvmArgs("-Dotel.instrumentation.logback-appender.experimental.capture-mdc-attributes=*")
}

View File

@ -5,6 +5,8 @@
package io.opentelemetry.instrumentation.logback.appender.v1_0;
import static java.util.Collections.emptyList;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.UnsynchronizedAppenderBase;
import io.opentelemetry.instrumentation.api.appender.internal.LogEmitterProvider;
@ -12,17 +14,32 @@ import io.opentelemetry.instrumentation.api.appender.internal.LogEmitterProvider
import io.opentelemetry.instrumentation.logback.appender.v1_0.internal.LoggingEventMapper;
import io.opentelemetry.instrumentation.sdk.appender.internal.DelegatingLogEmitterProvider;
import io.opentelemetry.sdk.logs.SdkLogEmitterProvider;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.slf4j.MDC;
public class OpenTelemetryAppender extends UnsynchronizedAppenderBase<ILoggingEvent> {
private static final LogEmitterProviderHolder logEmitterProviderHolder =
new LogEmitterProviderHolder();
private volatile boolean captureExperimentalAttributes = false;
private volatile List<String> captureMdcAttributes = emptyList();
private volatile LoggingEventMapper mapper;
public OpenTelemetryAppender() {}
@Override
public void start() {
mapper = new LoggingEventMapper(captureExperimentalAttributes, captureMdcAttributes);
super.start();
}
@Override
protected void append(ILoggingEvent event) {
LoggingEventMapper.INSTANCE.emit(logEmitterProviderHolder.get(), event);
mapper.emit(logEmitterProviderHolder.get(), event);
}
/**
@ -36,6 +53,24 @@ public class OpenTelemetryAppender extends UnsynchronizedAppenderBase<ILoggingEv
logEmitterProviderHolder.set(DelegatingLogEmitterProvider.from(sdkLogEmitterProvider));
}
/**
* Sets whether experimental attributes should be set to logs. These attributes may be changed or
* removed in the future, so only enable this if you know you do not require attributes filled by
* this instrumentation to be stable across versions.
*/
public void setCaptureExperimentalAttributes(boolean captureExperimentalAttributes) {
this.captureExperimentalAttributes = captureExperimentalAttributes;
}
/** Configures the {@link MDC} attributes that will be copied to logs. */
public void setCaptureMdcAttributes(String attributes) {
if (attributes != null) {
captureMdcAttributes = filterBlanksAndNulls(attributes.split(","));
} else {
captureMdcAttributes = emptyList();
}
}
/**
* Unsets the global {@link LogEmitterProvider}. This is only meant to be used from tests which
* need to reconfigure {@link LogEmitterProvider}.
@ -43,4 +78,12 @@ public class OpenTelemetryAppender extends UnsynchronizedAppenderBase<ILoggingEv
public static void resetSdkLogEmitterProviderForTest() {
logEmitterProviderHolder.resetForTest();
}
// copied from SDK's DefaultConfigProperties
private static List<String> filterBlanksAndNulls(String[] values) {
return Arrays.stream(values)
.map(String::trim)
.filter(s -> !s.isEmpty())
.collect(Collectors.toList());
}
}

View File

@ -5,8 +5,6 @@
package io.opentelemetry.instrumentation.logback.appender.v1_0.internal;
import static java.util.Collections.emptyList;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.ThrowableProxy;
@ -17,7 +15,6 @@ import io.opentelemetry.context.Context;
import io.opentelemetry.instrumentation.api.appender.internal.LogBuilder;
import io.opentelemetry.instrumentation.api.appender.internal.LogEmitterProvider;
import io.opentelemetry.instrumentation.api.appender.internal.Severity;
import io.opentelemetry.instrumentation.api.config.Config;
import io.opentelemetry.instrumentation.api.internal.cache.Cache;
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
import java.io.PrintWriter;
@ -32,29 +29,15 @@ import java.util.concurrent.TimeUnit;
*/
public final class LoggingEventMapper {
public static final LoggingEventMapper INSTANCE = new LoggingEventMapper();
private static final boolean captureExperimentalAttributes =
Config.get()
.getBoolean("otel.instrumentation.logback-appender.experimental-log-attributes", false);
private static final Cache<String, AttributeKey<String>> mdcAttributeKeys = Cache.bounded(100);
private final boolean captureExperimentalAttributes;
private final List<String> captureMdcAttributes;
// cached as an optimization
private final boolean captureAllMdcAttributes;
private LoggingEventMapper() {
this(
Config.get()
.getList(
"otel.instrumentation.logback-appender.experimental.capture-mdc-attributes",
emptyList()));
}
// visible for testing
LoggingEventMapper(List<String> captureMdcAttributes) {
public LoggingEventMapper(
boolean captureExperimentalAttributes, List<String> captureMdcAttributes) {
this.captureExperimentalAttributes = captureExperimentalAttributes;
this.captureMdcAttributes = captureMdcAttributes;
this.captureAllMdcAttributes =
captureMdcAttributes.size() == 1 && captureMdcAttributes.get(0).equals("*");

View File

@ -22,7 +22,7 @@ class LoggingEventMapperTest {
@Test
void testDefault() {
// given
LoggingEventMapper mapper = new LoggingEventMapper(emptyList());
LoggingEventMapper mapper = new LoggingEventMapper(false, emptyList());
Map<String, String> contextData = new HashMap<>();
contextData.put("key1", "value1");
contextData.put("key2", "value2");
@ -38,7 +38,7 @@ class LoggingEventMapperTest {
@Test
void testSome() {
// given
LoggingEventMapper mapper = new LoggingEventMapper(singletonList("key2"));
LoggingEventMapper mapper = new LoggingEventMapper(false, singletonList("key2"));
Map<String, String> contextData = new HashMap<>();
contextData.put("key1", "value1");
contextData.put("key2", "value2");
@ -55,7 +55,7 @@ class LoggingEventMapperTest {
@Test
void testAll() {
// given
LoggingEventMapper mapper = new LoggingEventMapper(singletonList("*"));
LoggingEventMapper mapper = new LoggingEventMapper(false, singletonList("*"));
Map<String, String> contextData = new HashMap<>();
contextData.put("key1", "value1");
contextData.put("key2", "value2");

View File

@ -8,12 +8,15 @@
</pattern>
</encoder>
</appender>
<appender name="OpenTelemetry" class="io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender">
<appender name="OpenTelemetry"
class="io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender">
<captureExperimentalAttributes>false</captureExperimentalAttributes>
<captureMdcAttributes>*</captureMdcAttributes>
</appender>
<root level="INFO">
<appender-ref ref="console"/>
<appender-ref ref="OpenTelemetry" />
<appender-ref ref="OpenTelemetry"/>
</root>
</configuration>