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; 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.api.appender.internal.LogBuilder;
import io.opentelemetry.instrumentation.log4j.appender.v2_17.internal.ContextDataAccessor; import io.opentelemetry.instrumentation.log4j.appender.v2_17.internal.ContextDataAccessor;
import io.opentelemetry.instrumentation.log4j.appender.v2_17.internal.LogEventMapper; import io.opentelemetry.instrumentation.log4j.appender.v2_17.internal.LogEventMapper;
import io.opentelemetry.javaagent.bootstrap.AgentLogEmitterProvider; import io.opentelemetry.javaagent.bootstrap.AgentLogEmitterProvider;
import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -19,8 +23,29 @@ import org.apache.logging.log4j.message.Message;
public final class Log4jHelper { public final class Log4jHelper {
private static final LogEventMapper<Map<String, String>> mapper = private static final LogEventMapper<Map<String, String>> mapper;
new LogEventMapper<>(ContextDataAccessorImpl.INSTANCE);
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) { public static void capture(Logger logger, Level level, Message message, Throwable throwable) {
String instrumentationName = logger.getName(); String instrumentationName = logger.getName();

View File

@ -12,9 +12,3 @@ dependencies {
testImplementation("io.opentelemetry:opentelemetry-sdk-logs") testImplementation("io.opentelemetry:opentelemetry-sdk-logs")
testImplementation("io.opentelemetry:opentelemetry-sdk-testing") 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; 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.LogBuilder;
import io.opentelemetry.instrumentation.api.appender.internal.LogEmitterProvider; import io.opentelemetry.instrumentation.api.appender.internal.LogEmitterProvider;
import io.opentelemetry.instrumentation.api.appender.internal.LogEmitterProviderHolder; 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.instrumentation.sdk.appender.internal.DelegatingLogEmitterProvider;
import io.opentelemetry.sdk.logs.SdkLogEmitterProvider; import io.opentelemetry.sdk.logs.SdkLogEmitterProvider;
import java.io.Serializable; import java.io.Serializable;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.stream.Collectors;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import org.apache.logging.log4j.ThreadContext;
import org.apache.logging.log4j.core.Appender; import org.apache.logging.log4j.core.Appender;
import org.apache.logging.log4j.core.Core; import org.apache.logging.log4j.core.Core;
import org.apache.logging.log4j.core.Filter; 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.appender.AbstractAppender;
import org.apache.logging.log4j.core.config.Property; 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.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.config.plugins.PluginBuilderFactory;
import org.apache.logging.log4j.core.time.Instant; import org.apache.logging.log4j.core.time.Instant;
import org.apache.logging.log4j.message.MapMessage;
import org.apache.logging.log4j.util.ReadOnlyStringMap; import org.apache.logging.log4j.util.ReadOnlyStringMap;
@Plugin( @Plugin(
@ -39,8 +47,7 @@ public class OpenTelemetryAppender extends AbstractAppender {
private static final LogEmitterProviderHolder logEmitterProviderHolder = private static final LogEmitterProviderHolder logEmitterProviderHolder =
new LogEmitterProviderHolder(); new LogEmitterProviderHolder();
private static final LogEventMapper<ReadOnlyStringMap> mapper = private final LogEventMapper<ReadOnlyStringMap> mapper;
new LogEventMapper<>(ContextDataAccessorImpl.INSTANCE);
@PluginBuilderFactory @PluginBuilderFactory
public static <B extends Builder<B>> B builder() { 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> static class Builder<B extends Builder<B>> extends AbstractAppender.Builder<B>
implements org.apache.logging.log4j.core.util.Builder<OpenTelemetryAppender> { 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 @Override
public OpenTelemetryAppender build() { public OpenTelemetryAppender build() {
return new OpenTelemetryAppender( 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, Layout<? extends Serializable> layout,
Filter filter, Filter filter,
boolean ignoreExceptions, boolean ignoreExceptions,
Property[] properties) { Property[] properties,
boolean captureExperimentalAttributes,
boolean captureMapMessageAttributes,
String captureContextDataAttributes) {
super(name, filter, layout, ignoreExceptions, properties); 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 @Override

View File

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

View File

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

View File

@ -6,13 +6,12 @@
<PatternLayout <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"/> 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> </Console>
<ListAppender name="ListAppender"/> <!-- TODO run tests both with and without experimental log attributes -->
<OpenTelemetry name="OpenTelemetryAppender"/> <OpenTelemetry name="OpenTelemetryAppender" captureMapMessageAttributes="true" captureContextDataAttributes="*"/>
</Appenders> </Appenders>
<Loggers> <Loggers>
<Logger name="TestLogger" level="All"> <Logger name="TestLogger" level="All">
<AppenderRef ref="OpenTelemetryAppender" level="All"/> <AppenderRef ref="OpenTelemetryAppender" level="All"/>
<AppenderRef ref="ListAppender" level="All"/>
<AppenderRef ref="Console" level="All"/> <AppenderRef ref="Console" level="All"/>
</Logger> </Logger>
<Root> <Root>

View File

@ -5,6 +5,7 @@
package io.opentelemetry.javaagent.instrumentation.logback.appender.v1_0; 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.isMethod;
import static net.bytebuddy.matcher.ElementMatchers.isPublic; import static net.bytebuddy.matcher.ElementMatchers.isPublic;
import static net.bytebuddy.matcher.ElementMatchers.named; 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 ch.qos.logback.classic.spi.ILoggingEvent;
import io.opentelemetry.instrumentation.api.appender.internal.LogEmitterProvider; 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.AgentLogEmitterProvider;
import io.opentelemetry.javaagent.bootstrap.CallDepth; import io.opentelemetry.javaagent.bootstrap.CallDepth;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
@ -51,7 +51,7 @@ class LogbackInstrumentation implements TypeInstrumentation {
// logging framework delegates to another // logging framework delegates to another
callDepth = CallDepth.forClass(LogEmitterProvider.class); callDepth = CallDepth.forClass(LogEmitterProvider.class);
if (callDepth.getAndIncrement() == 0) { 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-logs")
testImplementation("io.opentelemetry:opentelemetry-sdk-testing") 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; 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.classic.spi.ILoggingEvent;
import ch.qos.logback.core.UnsynchronizedAppenderBase; import ch.qos.logback.core.UnsynchronizedAppenderBase;
import io.opentelemetry.instrumentation.api.appender.internal.LogEmitterProvider; 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.logback.appender.v1_0.internal.LoggingEventMapper;
import io.opentelemetry.instrumentation.sdk.appender.internal.DelegatingLogEmitterProvider; import io.opentelemetry.instrumentation.sdk.appender.internal.DelegatingLogEmitterProvider;
import io.opentelemetry.sdk.logs.SdkLogEmitterProvider; 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> { public class OpenTelemetryAppender extends UnsynchronizedAppenderBase<ILoggingEvent> {
private static final LogEmitterProviderHolder logEmitterProviderHolder = private static final LogEmitterProviderHolder logEmitterProviderHolder =
new LogEmitterProviderHolder(); new LogEmitterProviderHolder();
private volatile boolean captureExperimentalAttributes = false;
private volatile List<String> captureMdcAttributes = emptyList();
private volatile LoggingEventMapper mapper;
public OpenTelemetryAppender() {} public OpenTelemetryAppender() {}
@Override
public void start() {
mapper = new LoggingEventMapper(captureExperimentalAttributes, captureMdcAttributes);
super.start();
}
@Override @Override
protected void append(ILoggingEvent event) { 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)); 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 * Unsets the global {@link LogEmitterProvider}. This is only meant to be used from tests which
* need to reconfigure {@link LogEmitterProvider}. * need to reconfigure {@link LogEmitterProvider}.
@ -43,4 +78,12 @@ public class OpenTelemetryAppender extends UnsynchronizedAppenderBase<ILoggingEv
public static void resetSdkLogEmitterProviderForTest() { public static void resetSdkLogEmitterProviderForTest() {
logEmitterProviderHolder.resetForTest(); 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; 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.Level;
import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.ThrowableProxy; 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.LogBuilder;
import io.opentelemetry.instrumentation.api.appender.internal.LogEmitterProvider; import io.opentelemetry.instrumentation.api.appender.internal.LogEmitterProvider;
import io.opentelemetry.instrumentation.api.appender.internal.Severity; 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.instrumentation.api.internal.cache.Cache;
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
import java.io.PrintWriter; import java.io.PrintWriter;
@ -32,29 +29,15 @@ import java.util.concurrent.TimeUnit;
*/ */
public final class LoggingEventMapper { 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 static final Cache<String, AttributeKey<String>> mdcAttributeKeys = Cache.bounded(100);
private final boolean captureExperimentalAttributes;
private final List<String> captureMdcAttributes; private final List<String> captureMdcAttributes;
// cached as an optimization
private final boolean captureAllMdcAttributes; private final boolean captureAllMdcAttributes;
private LoggingEventMapper() { public LoggingEventMapper(
this( boolean captureExperimentalAttributes, List<String> captureMdcAttributes) {
Config.get() this.captureExperimentalAttributes = captureExperimentalAttributes;
.getList(
"otel.instrumentation.logback-appender.experimental.capture-mdc-attributes",
emptyList()));
}
// visible for testing
LoggingEventMapper(List<String> captureMdcAttributes) {
this.captureMdcAttributes = captureMdcAttributes; this.captureMdcAttributes = captureMdcAttributes;
this.captureAllMdcAttributes = this.captureAllMdcAttributes =
captureMdcAttributes.size() == 1 && captureMdcAttributes.get(0).equals("*"); captureMdcAttributes.size() == 1 && captureMdcAttributes.get(0).equals("*");

View File

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

View File

@ -8,12 +8,15 @@
</pattern> </pattern>
</encoder> </encoder>
</appender> </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> </appender>
<root level="INFO"> <root level="INFO">
<appender-ref ref="console"/> <appender-ref ref="console"/>
<appender-ref ref="OpenTelemetry" /> <appender-ref ref="OpenTelemetry"/>
</root> </root>
</configuration> </configuration>