diff --git a/instrumentation/logback/logback-appender-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/logback/appender/v1_0/LogbackSingletons.java b/instrumentation/logback/logback-appender-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/logback/appender/v1_0/LogbackSingletons.java index a3d1c6d90688..76a99383ec1b 100644 --- a/instrumentation/logback/logback-appender-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/logback/appender/v1_0/LogbackSingletons.java +++ b/instrumentation/logback/logback-appender-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/logback/appender/v1_0/LogbackSingletons.java @@ -39,6 +39,10 @@ public final class LogbackSingletons { boolean captureArguments = config.getBoolean( "otel.instrumentation.logback-appender.experimental.capture-arguments", false); + boolean captureLogstashAttributes = + config.getBoolean( + "otel.instrumentation.logback-appender.experimental.capture-logstash-attributes", + false); List captureMdcAttributes = config.getList( "otel.instrumentation.logback-appender.experimental.capture-mdc-attributes", @@ -53,6 +57,7 @@ public final class LogbackSingletons { .setCaptureKeyValuePairAttributes(captureKeyValuePairAttributes) .setCaptureLoggerContext(captureLoggerContext) .setCaptureArguments(captureArguments) + .setCaptureLogstashAttributes(captureLogstashAttributes) .build(); } diff --git a/instrumentation/logback/logback-appender-1.0/library/build.gradle.kts b/instrumentation/logback/logback-appender-1.0/library/build.gradle.kts index 87a21f3aa5ea..8d1f7ea591dd 100644 --- a/instrumentation/logback/logback-appender-1.0/library/build.gradle.kts +++ b/instrumentation/logback/logback-appender-1.0/library/build.gradle.kts @@ -19,6 +19,11 @@ dependencies { strictly("2.0.0") } } + compileOnly("net.logstash.logback:logstash-logback-encoder") { + version { + strictly("3.0") + } + } if (findProperty("testLatestDeps") as Boolean) { testImplementation("ch.qos.logback:logback-classic:+") @@ -75,6 +80,7 @@ testing { if (latestDepTest) { implementation("ch.qos.logback:logback-classic:+") implementation("org.slf4j:slf4j-api:+") + implementation("net.logstash.logback:logstash-logback-encoder:+") } else { implementation("ch.qos.logback:logback-classic") { version { @@ -86,6 +92,11 @@ testing { strictly("2.0.0") } } + implementation("net.logstash.logback:logstash-logback-encoder") { + version { + strictly("3.0") + } + } } } } diff --git a/instrumentation/logback/logback-appender-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/appender/v1_0/OpenTelemetryAppender.java b/instrumentation/logback/logback-appender-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/appender/v1_0/OpenTelemetryAppender.java index 4a21b6c4b529..0fc7602a013b 100644 --- a/instrumentation/logback/logback-appender-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/appender/v1_0/OpenTelemetryAppender.java +++ b/instrumentation/logback/logback-appender-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/appender/v1_0/OpenTelemetryAppender.java @@ -34,6 +34,7 @@ public class OpenTelemetryAppender extends UnsynchronizedAppenderBase captureMdcAttributes = emptyList(); private volatile OpenTelemetry openTelemetry; @@ -81,6 +82,7 @@ public void start() { .setCaptureKeyValuePairAttributes(captureKeyValuePairAttributes) .setCaptureLoggerContext(captureLoggerContext) .setCaptureArguments(captureArguments) + .setCaptureLogstashAttributes(captureLogstashAttributes) .build(); eventsToReplay = new ArrayBlockingQueue<>(numLogsCapturedBeforeOtelInstall); super.start(); @@ -175,6 +177,15 @@ public void setCaptureArguments(boolean captureArguments) { this.captureArguments = captureArguments; } + /** + * Sets whether the Logstash attributes should be set to logs. + * + * @param captureLogstashAttributes To enable or disable capturing Logstash attributes + */ + public void setCaptureLogstashAttributes(boolean captureLogstashAttributes) { + this.captureLogstashAttributes = captureLogstashAttributes; + } + /** Configures the {@link MDC} attributes that will be copied to logs. */ public void setCaptureMdcAttributes(String attributes) { if (attributes != null) { diff --git a/instrumentation/logback/logback-appender-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/appender/v1_0/internal/LoggingEventMapper.java b/instrumentation/logback/logback-appender-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/appender/v1_0/internal/LoggingEventMapper.java index 8f0af6048202..8f634e396045 100644 --- a/instrumentation/logback/logback-appender-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/appender/v1_0/internal/LoggingEventMapper.java +++ b/instrumentation/logback/logback-appender-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/appender/v1_0/internal/LoggingEventMapper.java @@ -23,12 +23,18 @@ import io.opentelemetry.semconv.ExceptionAttributes; import java.io.PrintWriter; import java.io.StringWriter; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import javax.annotation.Nullable; +import net.logstash.logback.marker.LogstashMarker; +import net.logstash.logback.marker.MapEntriesAppendingMarker; +import net.logstash.logback.marker.SingleFieldAppendingMarker; import org.slf4j.Marker; import org.slf4j.event.KeyValuePair; @@ -50,6 +56,7 @@ public final class LoggingEventMapper { private static final boolean supportsInstant = supportsInstant(); private static final boolean supportsKeyValuePairs = supportsKeyValuePairs(); private static final boolean supportsMultipleMarkers = supportsMultipleMarkers(); + private static final boolean supportsLogstashMarkers = supportsLogstashMarkers(); private static final Cache> mdcAttributeKeys = Cache.bounded(100); private static final Cache> attributeKeys = Cache.bounded(100); @@ -60,6 +67,8 @@ public final class LoggingEventMapper { private static final AttributeKey> LOG_BODY_PARAMETERS = AttributeKey.stringArrayKey("log.body.parameters"); + private static final Cache, Field> valueField = Cache.bounded(20); + private final boolean captureExperimentalAttributes; private final List captureMdcAttributes; private final boolean captureAllMdcAttributes; @@ -68,6 +77,7 @@ public final class LoggingEventMapper { private final boolean captureKeyValuePairAttributes; private final boolean captureLoggerContext; private final boolean captureArguments; + private final boolean captureLogstashAttributes; private LoggingEventMapper(Builder builder) { this.captureExperimentalAttributes = builder.captureExperimentalAttributes; @@ -77,6 +87,7 @@ private LoggingEventMapper(Builder builder) { this.captureKeyValuePairAttributes = builder.captureKeyValuePairAttributes; this.captureLoggerContext = builder.captureLoggerContext; this.captureArguments = builder.captureArguments; + this.captureLogstashAttributes = builder.captureLogstashAttributes; this.captureAllMdcAttributes = builder.captureMdcAttributes.size() == 1 && builder.captureMdcAttributes.get(0).equals("*"); } @@ -170,7 +181,8 @@ private void mapLoggingEvent( } if (captureMarkerAttribute) { - captureMarkerAttribute(attributes, loggingEvent); + boolean skipLogstashMarkers = supportsLogstashMarkers && captureLogstashAttributes; + captureMarkerAttribute(attributes, loggingEvent, skipLogstashMarkers); } if (supportsKeyValuePairs && captureKeyValuePairAttributes) { @@ -187,6 +199,10 @@ private void mapLoggingEvent( captureArguments(attributes, loggingEvent.getMessage(), loggingEvent.getArgumentArray()); } + if (supportsLogstashMarkers && captureLogstashAttributes) { + captureLogstashAttributes(attributes, loggingEvent); + } + builder.setAllAttributes(attributes.build()); // span context @@ -326,31 +342,35 @@ private static boolean supportsKeyValuePairs() { } private static void captureMarkerAttribute( - AttributesBuilder attributes, ILoggingEvent loggingEvent) { + AttributesBuilder attributes, ILoggingEvent loggingEvent, boolean skipLogstashMarkers) { if (supportsMultipleMarkers && hasMultipleMarkers(loggingEvent)) { - captureMultipleMarkerAttributes(attributes, loggingEvent); + captureMultipleMarkerAttributes(attributes, loggingEvent, skipLogstashMarkers); } else { - captureSingleMarkerAttribute(attributes, loggingEvent); + captureSingleMarkerAttribute(attributes, loggingEvent, skipLogstashMarkers); } } @SuppressWarnings("deprecation") // getMarker is deprecate since 1.3.0 private static void captureSingleMarkerAttribute( - AttributesBuilder attributes, ILoggingEvent loggingEvent) { + AttributesBuilder attributes, ILoggingEvent loggingEvent, boolean skipLogstashMarkers) { Marker marker = loggingEvent.getMarker(); - if (marker != null) { + if (marker != null && (!skipLogstashMarkers || !isLogstashMarker(marker))) { attributes.put(LOG_MARKER, marker.getName()); } } @NoMuzzle private static void captureMultipleMarkerAttributes( - AttributesBuilder attributes, ILoggingEvent loggingEvent) { + AttributesBuilder attributes, ILoggingEvent loggingEvent, boolean skipLogstashMarkers) { List markerNames = new ArrayList<>(loggingEvent.getMarkerList().size()); for (Marker marker : loggingEvent.getMarkerList()) { - markerNames.add(marker.getName()); + if (!skipLogstashMarkers || !isLogstashMarker(marker)) { + markerNames.add(marker.getName()); + } + } + if (!markerNames.isEmpty()) { + attributes.put(LOG_MARKER, markerNames.toArray(new String[0])); } - attributes.put(LOG_MARKER, markerNames.toArray(new String[0])); } @NoMuzzle @@ -369,6 +389,173 @@ private static boolean supportsMultipleMarkers() { return true; } + private static void captureLogstashAttributes( + AttributesBuilder attributes, ILoggingEvent loggingEvent) { + try { + if (supportsMultipleMarkers && hasMultipleMarkers(loggingEvent)) { + captureMultipleLogstashAttributes(attributes, loggingEvent); + } else { + captureSingleLogstashAttribute(attributes, loggingEvent); + } + } catch (Throwable e) { + // ignore + } + } + + private static boolean isLogstashMarker(Marker marker) { + return marker instanceof LogstashMarker; + } + + @NoMuzzle + @SuppressWarnings("deprecation") // getMarker is deprecate since 1.3.0 + private static void captureSingleLogstashAttribute( + AttributesBuilder attributes, ILoggingEvent loggingEvent) { + Marker marker = loggingEvent.getMarker(); + if (isLogstashMarker(marker)) { + LogstashMarker logstashMarker = (LogstashMarker) marker; + captureLogstashMarker(attributes, logstashMarker); + } + } + + @NoMuzzle + private static void captureMultipleLogstashAttributes( + AttributesBuilder attributes, ILoggingEvent loggingEvent) { + for (Marker marker : loggingEvent.getMarkerList()) { + if (isLogstashMarker(marker)) { + LogstashMarker logstashMarker = (LogstashMarker) marker; + captureLogstashMarker(attributes, logstashMarker); + } + } + } + + private static void captureLogstashMarker( + AttributesBuilder attributes, LogstashMarker logstashMarker) { + captureLogstashMarkerAttributes(attributes, logstashMarker); + + if (logstashMarker.hasReferences()) { + for (Iterator it = logstashMarker.iterator(); it.hasNext(); ) { + Marker referenceMarker = it.next(); + if (isLogstashMarker(referenceMarker)) { + LogstashMarker referenceLogstashMarker = (LogstashMarker) referenceMarker; + captureLogstashMarker(attributes, referenceLogstashMarker); + } + } + } + } + + @NoMuzzle + private static void captureLogstashMarkerAttributes( + AttributesBuilder attributes, LogstashMarker logstashMarker) { + if (logstashMarker instanceof SingleFieldAppendingMarker) { + SingleFieldAppendingMarker singleFieldAppendingMarker = + (SingleFieldAppendingMarker) logstashMarker; + String fieldName = singleFieldAppendingMarker.getFieldName(); + String fieldValue = extractFieldValue(singleFieldAppendingMarker); + if (fieldName != null) { + if (fieldValue != null) { + attributes.put(fieldName, fieldValue); + } else { + attributes.put(fieldName, ""); + } + } + } else if (logstashMarker instanceof MapEntriesAppendingMarker) { + MapEntriesAppendingMarker mapEntriesAppendingMarker = + (MapEntriesAppendingMarker) logstashMarker; + Map map = extractMapValue(mapEntriesAppendingMarker); + if (map != null) { + for (Map.Entry entry : map.entrySet()) { + Object key = entry.getKey(); + Object value = entry.getValue(); + if (key != null) { + if (value != null) { + attributes.put(key.toString(), value.toString()); + } else { + attributes.put(key.toString(), ""); + } + } + } + } + } + } + + @Nullable + private static String extractFieldValue(SingleFieldAppendingMarker singleFieldAppendingMarker) { + // ObjectAppendingMarker.fieldValue since v7.0 + // ObjectAppendingMarker.object since v3.0 + // RawJsonAppendingMarker.rawJson since v3.0 + Field field = + valueField.computeIfAbsent( + singleFieldAppendingMarker.getClass(), + clazz -> findValueField(clazz, new String[] {"fieldValue", "object", "rawJson"})); + if (field != null) { + try { + Object value = field.get(singleFieldAppendingMarker); + if (value != null) { + return value.toString(); + } + } catch (IllegalAccessException e) { + // ignore + } + } + return null; + } + + @Nullable + private static Map extractMapValue(MapEntriesAppendingMarker mapEntriesAppendingMarker) { + // MapEntriesAppendingMarker.map since v3.0 + Field field = + valueField.computeIfAbsent( + mapEntriesAppendingMarker.getClass(), + clazz -> findValueField(clazz, new String[] {"map"})); + if (field != null) { + try { + Object value = field.get(mapEntriesAppendingMarker); + if (value instanceof Map) { + return (Map) value; + } + } catch (IllegalAccessException e) { + // ignore + } + } + return null; + } + + @Nullable + private static Field findValueField(Class clazz, String[] fieldNames) { + for (String fieldName : fieldNames) { + try { + Field field = clazz.getDeclaredField(fieldName); + field.setAccessible(true); + return field; + } catch (NoSuchFieldException e) { + // ignore + } + } + return null; + } + + private static boolean supportsLogstashMarkers() { + try { + Class.forName("net.logstash.logback.marker.LogstashMarker"); + } catch (ClassNotFoundException e) { + return false; + } + + try { + Class.forName("net.logstash.logback.marker.SingleFieldAppendingMarker"); + } catch (ClassNotFoundException e) { + return false; + } + + try { + Class.forName("net.logstash.logback.marker.MapEntriesAppendingMarker"); + } catch (ClassNotFoundException e) { + return false; + } + + return true; + } + /** * This class is internal and is hence not for public use. Its APIs are unstable and can change at * any time. @@ -381,6 +568,7 @@ public static final class Builder { private boolean captureKeyValuePairAttributes; private boolean captureLoggerContext; private boolean captureArguments; + private boolean captureLogstashAttributes; Builder() {} @@ -426,6 +614,12 @@ public Builder setCaptureArguments(boolean captureArguments) { return this; } + @CanIgnoreReturnValue + public Builder setCaptureLogstashAttributes(boolean captureLogstashAttributes) { + this.captureLogstashAttributes = captureLogstashAttributes; + return this; + } + public LoggingEventMapper build() { return new LoggingEventMapper(this); } diff --git a/instrumentation/logback/logback-appender-1.0/library/src/slf4j2ApiTest/java/io/opentelemetry/instrumentation/logback/appender/v1_0/Slf4j2Test.java b/instrumentation/logback/logback-appender-1.0/library/src/slf4j2ApiTest/java/io/opentelemetry/instrumentation/logback/appender/v1_0/Slf4j2Test.java index ddccdeb5a84a..a1318b282fb3 100644 --- a/instrumentation/logback/logback-appender-1.0/library/src/slf4j2ApiTest/java/io/opentelemetry/instrumentation/logback/appender/v1_0/Slf4j2Test.java +++ b/instrumentation/logback/logback-appender-1.0/library/src/slf4j2ApiTest/java/io/opentelemetry/instrumentation/logback/appender/v1_0/Slf4j2Test.java @@ -12,6 +12,9 @@ import io.opentelemetry.sdk.common.InstrumentationScopeInfo; import io.opentelemetry.sdk.resources.Resource; import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import net.logstash.logback.marker.Markers; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -125,4 +128,30 @@ void arguments() { AttributeKey.stringKey("log.body.template"), "log message {} and {}, bool {}, long {}"))); } + + @Test + void logstash() { + Map entries = new HashMap<>(); + entries.put("field2", 2); + entries.put("field3", "value3"); + + logger + .atInfo() + .setMessage("log message 1") + .addMarker(Markers.append("field1", "value1")) + .addMarker(Markers.appendEntries(entries)) + .log(); + + testing.waitAndAssertLogRecords( + logRecord -> + logRecord + .hasResource(resource) + .hasInstrumentationScope(instrumentationScopeInfo) + .hasBody("log message 1") + .hasTotalAttributeCount(7) // 4 code attributes + 3 markers + .hasAttributesSatisfying( + equalTo(AttributeKey.stringKey("field1"), "value1"), + equalTo(AttributeKey.stringKey("field2"), "2"), + equalTo(AttributeKey.stringKey("field3"), "value3"))); + } } diff --git a/instrumentation/logback/logback-appender-1.0/library/src/slf4j2ApiTest/resources/logback-test.xml b/instrumentation/logback/logback-appender-1.0/library/src/slf4j2ApiTest/resources/logback-test.xml index 366678be3369..aa3a4517bd73 100644 --- a/instrumentation/logback/logback-appender-1.0/library/src/slf4j2ApiTest/resources/logback-test.xml +++ b/instrumentation/logback/logback-appender-1.0/library/src/slf4j2ApiTest/resources/logback-test.xml @@ -15,6 +15,7 @@ true true true + true * diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/logging/LogbackAppenderInstaller.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/logging/LogbackAppenderInstaller.java index c2ef98d65f1d..899a5ea93afa 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/logging/LogbackAppenderInstaller.java +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/logging/LogbackAppenderInstaller.java @@ -122,6 +122,14 @@ private static void initializeOpenTelemetryAppenderFromProperties( openTelemetryAppender.setCaptureArguments(captureArguments.booleanValue()); } + Boolean captureLogstashAttributes = + evaluateBooleanProperty( + applicationEnvironmentPreparedEvent, + "otel.instrumentation.logback-appender.experimental.capture-logstash-attributes"); + if (captureLogstashAttributes != null) { + openTelemetryAppender.setCaptureLogstashAttributes(captureLogstashAttributes.booleanValue()); + } + String mdcAttributeProperty = applicationEnvironmentPreparedEvent .getEnvironment()