diff --git a/instrumentation/logback/logback-appender-1.0/javaagent/README.md b/instrumentation/logback/logback-appender-1.0/javaagent/README.md index 2a962ae4e739..581211cddb91 100644 --- a/instrumentation/logback/logback-appender-1.0/javaagent/README.md +++ b/instrumentation/logback/logback-appender-1.0/javaagent/README.md @@ -1,13 +1,14 @@ # Settings for the Logback Appender instrumentation -| System property | Type | Default | Description | -|----------------------------------------------------------------------------------------|---------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------| -| `otel.instrumentation.logback-appender.experimental-log-attributes` | Boolean | `false` | Enable the capture of experimental log attributes `thread.name` and `thread.id`. | -| `otel.instrumentation.logback-appender.experimental.capture-code-attributes` | Boolean | `false` | Enable the capture of [source code attributes]. Note that capturing source code attributes at logging sites might add a performance overhead. | -| `otel.instrumentation.logback-appender.experimental.capture-marker-attribute` | Boolean | `false` | Enable the capture of Logback markers as attributes. | -| `otel.instrumentation.logback-appender.experimental.capture-key-value-pair-attributes` | Boolean | `false` | Enable the capture of Logback key value pairs as attributes. | -| `otel.instrumentation.logback-appender.experimental.capture-logger-context-attributes` | Boolean | `false` | Enable the capture of Logback logger context properties as attributes. | -| `otel.instrumentation.logback-appender.experimental.capture-arguments` | Boolean | `false` | Enable the capture of Logback logger arguments. | -| `otel.instrumentation.logback-appender.experimental.capture-mdc-attributes` | String | | Comma separated list of MDC attributes to capture. Use the wildcard character `*` to capture all attributes. | +| System property | Type | Default | Description | +|----------------------------------------------------------------------------------------|---------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `otel.instrumentation.logback-appender.experimental-log-attributes` | Boolean | `false` | Enable the capture of experimental log attributes `thread.name` and `thread.id`. | +| `otel.instrumentation.logback-appender.experimental.capture-code-attributes` | Boolean | `false` | Enable the capture of [source code attributes]. Note that capturing source code attributes at logging sites might add a performance overhead. | +| `otel.instrumentation.logback-appender.experimental.capture-marker-attribute` | Boolean | `false` | Enable the capture of Logback markers as attributes. | +| `otel.instrumentation.logback-appender.experimental.capture-key-value-pair-attributes` | Boolean | `false` | Enable the capture of Logback key value pairs as attributes. | +| `otel.instrumentation.logback-appender.experimental.capture-logger-context-attributes` | Boolean | `false` | Enable the capture of Logback logger context properties as attributes. | +| `otel.instrumentation.logback-appender.experimental.capture-arguments` | Boolean | `false` | Enable the capture of Logback logger arguments. | +| `otel.instrumentation.logback-appender.experimental.capture-logstash-attributes` | Boolean | `false` | Enable the capture of Logstash attributes, supported are those added to logs via `Markers.append()`, `Markers.appendEntries()`, `Markers.appendArray()` and `Markers.appendRaw()` methods. | +| `otel.instrumentation.logback-appender.experimental.capture-mdc-attributes` | String | | Comma separated list of MDC attributes to capture. Use the wildcard character `*` to capture all attributes. | [source code attributes]: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/general/attributes.md#source-code-attributes 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/README.md b/instrumentation/logback/logback-appender-1.0/library/README.md index 14c515071d76..659d32ae5d47 100644 --- a/instrumentation/logback/logback-appender-1.0/library/README.md +++ b/instrumentation/logback/logback-appender-1.0/library/README.md @@ -100,7 +100,8 @@ The available settings are: | `captureMarkerAttribute` | Boolean | `false` | Enable the capture of Logback markers as attributes. | | `captureKeyValuePairAttributes` | Boolean | `false` | Enable the capture of Logback key value pairs as attributes. | | `captureLoggerContext` | Boolean | `false` | Enable the capture of Logback logger context properties as attributes. | -| `captureArguments` | Boolean | `false` | Enable the capture of Logback logger arguments. | +| `captureArguments` | Boolean | `false` | Enable the capture of Logback logger arguments. | +| `captureLogstashAttributes` | Boolean | `false` | Enable the capture of Logstash attributes, supported are those added to logs via `Markers.append()`, `Markers.appendEntries()`, `Markers.appendArray()` and `Markers.appendRaw()` methods. | | `captureMdcAttributes` | String | | Comma separated list of MDC attributes to capture. Use the wildcard character `*` to capture all attributes. | | `numLogsCapturedBeforeOtelInstall` | Integer | 1000 | Log telemetry is emitted after the initialization of the OpenTelemetry Logback appender with an OpenTelemetry object. This setting allows you to modify the size of the cache used to replay the first logs. thread.id attribute is not captured. | 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 a57a9fd41959..3a96bc7b5814 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 9707126d51dd..c918dd101840 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,21 @@ import io.opentelemetry.semconv.ExceptionAttributes; import java.io.PrintWriter; import java.io.StringWriter; +import java.lang.reflect.Array; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; +import java.util.function.Function; 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 +59,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 +70,14 @@ public final class LoggingEventMapper { private static final AttributeKey> LOG_BODY_PARAMETERS = AttributeKey.stringArrayKey("log.body.parameters"); + private static final ClassValue valueField = + new ClassValue() { + @Override + protected FieldReader computeValue(Class type) { + return createFieldReader(type); + } + }; + private final boolean captureExperimentalAttributes; private final List captureMdcAttributes; private final boolean captureAllMdcAttributes; @@ -68,6 +86,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 +96,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("*"); } @@ -161,7 +181,8 @@ private void mapLoggingEvent( } if (captureMarkerAttribute) { - captureMarkerAttribute(attributes, loggingEvent); + boolean skipLogstashMarkers = supportsLogstashMarkers && captureLogstashAttributes; + captureMarkerAttribute(attributes, loggingEvent, skipLogstashMarkers); } if (supportsKeyValuePairs && captureKeyValuePairAttributes) { @@ -178,6 +199,10 @@ private void mapLoggingEvent( captureArguments(attributes, loggingEvent.getMessage(), loggingEvent.getArgumentArray()); } + if (supportsLogstashMarkers && captureLogstashAttributes) { + captureLogstashAttributes(attributes, loggingEvent); + } + builder.setAllAttributes(attributes.build()); // span context @@ -269,27 +294,84 @@ private static void captureKeyValuePairAttributes( List keyValuePairs = loggingEvent.getKeyValuePairs(); if (keyValuePairs != null) { for (KeyValuePair keyValuePair : keyValuePairs) { - Object value = keyValuePair.value; - if (value != null) { - String key = keyValuePair.key; - // preserve type for boolean and numeric values, everything else is converted to String - if (value instanceof Boolean) { - attributes.put(key, (Boolean) value); - } else if (value instanceof Byte - || value instanceof Integer - || value instanceof Long - || value instanceof Short) { - attributes.put(key, ((Number) value).longValue()); - } else if (value instanceof Double || value instanceof Float) { - attributes.put(key, ((Number) value).doubleValue()); - } else { - attributes.put(getAttributeKey(key), value.toString()); - } + captureAttribute(attributes, keyValuePair.key, keyValuePair.value); + } + } + } + + // visible for testing + static void captureAttribute(AttributesBuilder attributes, Object key, Object value) { + // empty values are not serialized + if (key != null && value != null) { + String keyStr = key.toString(); + // preserve type for boolean and numeric values, everything else is converted to String + if (value instanceof Boolean) { + attributes.put(keyStr, (Boolean) value); + } else if (value instanceof Byte + || value instanceof Integer + || value instanceof Long + || value instanceof Short) { + attributes.put(keyStr, ((Number) value).longValue()); + } else if (value instanceof Double || value instanceof Float) { + attributes.put(keyStr, ((Number) value).doubleValue()); + } else if (value.getClass().isArray()) { + if (value instanceof boolean[] || value instanceof Boolean[]) { + captureArrayValueAttribute( + attributes, AttributeKey.booleanArrayKey(keyStr), value, o -> (Boolean) o); + } else if (value instanceof byte[] + || value instanceof Byte[] + || value instanceof int[] + || value instanceof Integer[] + || value instanceof long[] + || value instanceof Long[] + || value instanceof short[] + || value instanceof Short[]) { + captureArrayValueAttribute( + attributes, AttributeKey.longArrayKey(keyStr), value, o -> ((Number) o).longValue()); + } else if (value instanceof float[] + || value instanceof Float[] + || value instanceof double[] + || value instanceof Double[]) { + captureArrayValueAttribute( + attributes, + AttributeKey.doubleArrayKey(keyStr), + value, + o -> ((Number) o).doubleValue()); + } else { + captureArrayValueAttribute( + attributes, AttributeKey.stringArrayKey(keyStr), value, String::valueOf); } + } else if (value instanceof Collection) { + captureArrayValueAttribute( + attributes, + AttributeKey.stringArrayKey(keyStr), + ((Collection) value).toArray(), + String::valueOf); + } else { + attributes.put(getAttributeKey(keyStr), String.valueOf(value)); } } } + private static void captureArrayValueAttribute( + AttributesBuilder attributes, + AttributeKey> key, + Object array, + Function extractor) { + List list = new ArrayList<>(); + int length = Array.getLength(array); + for (int i = 0; i < length; i++) { + Object value = Array.get(array, i); + if (value != null) { + list.add(extractor.apply(value)); + } + } + // empty lists are not serialized + if (!list.isEmpty()) { + attributes.put(key, list); + } + } + private static void captureLoggerContext( AttributesBuilder attributes, Map loggerContextProperties) { for (Map.Entry entry : loggerContextProperties.entrySet()) { @@ -317,31 +399,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 @@ -360,6 +446,162 @@ private static boolean supportsMultipleMarkers() { return true; } + private static void captureLogstashAttributes( + AttributesBuilder attributes, ILoggingEvent loggingEvent) { + if (supportsMultipleMarkers && hasMultipleMarkers(loggingEvent)) { + captureMultipleLogstashAttributes(attributes, loggingEvent); + } else { + captureSingleLogstashAttribute(attributes, loggingEvent); + } + } + + @NoMuzzle + private static boolean isLogstashMarker(Marker marker) { + return marker instanceof LogstashMarker; + } + + @SuppressWarnings("deprecation") // getMarker is deprecate since 1.3.0 + private static void captureSingleLogstashAttribute( + AttributesBuilder attributes, ILoggingEvent loggingEvent) { + Marker marker = loggingEvent.getMarker(); + if (isLogstashMarker(marker)) { + captureLogstashMarker(attributes, marker); + } + } + + @NoMuzzle + private static void captureMultipleLogstashAttributes( + AttributesBuilder attributes, ILoggingEvent loggingEvent) { + for (Marker marker : loggingEvent.getMarkerList()) { + if (isLogstashMarker(marker)) { + captureLogstashMarker(attributes, marker); + } + } + } + + @NoMuzzle + private static void captureLogstashMarker(AttributesBuilder attributes, Marker marker) { + LogstashMarker logstashMarker = (LogstashMarker) marker; + captureLogstashMarkerAttributes(attributes, logstashMarker); + + if (logstashMarker.hasReferences()) { + for (Iterator it = logstashMarker.iterator(); it.hasNext(); ) { + Marker referenceMarker = it.next(); + if (isLogstashMarker(referenceMarker)) { + captureLogstashMarker(attributes, referenceMarker); + } + } + } + } + + private static void captureLogstashMarkerAttributes( + AttributesBuilder attributes, Object logstashMarker) { + FieldReader fieldReader = valueField.get(logstashMarker.getClass()); + if (fieldReader != null) { + fieldReader.read(attributes, logstashMarker); + } + } + + @NoMuzzle + private static boolean isSingleFieldAppendingMarker(Class type) { + return SingleFieldAppendingMarker.class.isAssignableFrom(type); + } + + @NoMuzzle + private static boolean isMapEntriesAppendingMarker(Class type) { + return MapEntriesAppendingMarker.class.isAssignableFrom(type); + } + + private static FieldReader createFieldReader(Class type) { + if (isSingleFieldAppendingMarker(type)) { + // ObjectAppendingMarker.fieldValue since v7.0 + // ObjectAppendingMarker.object since v3.0 + // RawJsonAppendingMarker.rawJson since v3.0 + return createStringReader(findValueField(type, "fieldValue", "object", "rawJson")); + } else if (isMapEntriesAppendingMarker(type)) { + // MapEntriesAppendingMarker.map since v3.0 + return createMapReader(findValueField(type, "map")); + } + return null; + } + + @NoMuzzle + private static String getSingleFieldAppendingMarkerName(Object logstashMarker) { + SingleFieldAppendingMarker singleFieldAppendingMarker = + (SingleFieldAppendingMarker) logstashMarker; + return singleFieldAppendingMarker.getFieldName(); + } + + @Nullable + private static FieldReader createStringReader(Field field) { + if (field == null) { + return null; + } + return (attributes, logstashMarker) -> { + String fieldName = getSingleFieldAppendingMarkerName(logstashMarker); + Object fieldValue = extractFieldValue(field, logstashMarker); + captureAttribute(attributes, fieldName, fieldValue); + }; + } + + @Nullable + private static FieldReader createMapReader(Field field) { + if (field == null) { + return null; + } + return (attributes, logstashMarker) -> { + Object fieldValue = extractFieldValue(field, logstashMarker); + if (fieldValue instanceof Map) { + Map map = (Map) fieldValue; + for (Map.Entry entry : map.entrySet()) { + Object key = entry.getKey(); + Object value = entry.getValue(); + captureAttribute(attributes, key, value); + } + } + }; + } + + @Nullable + private static Object extractFieldValue(Field field, Object logstashMarker) { + try { + return field.get(logstashMarker); + } 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"); + Class.forName("net.logstash.logback.marker.SingleFieldAppendingMarker"); + Class.forName("net.logstash.logback.marker.MapEntriesAppendingMarker"); + } catch (ClassNotFoundException e) { + return false; + } + + return true; + } + + private interface FieldReader { + void read(AttributesBuilder attributes, Object logstashMarker); + } + /** * This class is internal and is hence not for public use. Its APIs are unstable and can change at * any time. @@ -372,6 +614,7 @@ public static final class Builder { private boolean captureKeyValuePairAttributes; private boolean captureLoggerContext; private boolean captureArguments; + private boolean captureLogstashAttributes; Builder() {} @@ -417,6 +660,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..d1c72d0af99c 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,110 @@ 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.longKey("field2"), 2L), + equalTo(AttributeKey.stringKey("field3"), "value3"))); + } + + @Test + void logstashVariousValues() { + Map entries = new HashMap<>(); + entries.put("map1", 1); + entries.put("map2", 2.0); + entries.put("map3", "text-5"); + entries.put("map4", null); + + logger + .atInfo() + .setMessage("log message 1") + .addMarker(Markers.append("field1", 1)) + .addMarker(Markers.append("field2", 2.0)) + .addMarker(Markers.append("field3", "text-1")) + .addMarker(Markers.append("field4", true)) + .addMarker(Markers.append("field5", new Integer[] {1, null, 2, 3})) + .addMarker(Markers.append("field6", new double[] {1.0, 2.0, 3.0})) + .addMarker(Markers.append("field7", new String[] {"text-2", "text-3", "text-4", null})) + .addMarker(Markers.append("field8", new Boolean[] {true, false, true})) + .addMarker(Markers.appendArray("field9", 1, 2.0, true, "text")) + .addMarker(Markers.appendRaw("field10", "raw value")) + .addMarker(Markers.append("field11", Arrays.asList(1, 2, 3))) + .addMarker(Markers.appendEntries(entries)) + .log(); + + testing.waitAndAssertLogRecords( + logRecord -> + logRecord + .hasResource(resource) + .hasInstrumentationScope(instrumentationScopeInfo) + .hasBody("log message 1") + .hasTotalAttributeCount(18) // 4 code attributes + 14 fields (including map keys) + .hasAttributesSatisfying( + equalTo(AttributeKey.longKey("field1"), 1L), + equalTo(AttributeKey.doubleKey("field2"), 2.0), + equalTo(AttributeKey.stringKey("field3"), "text-1"), + equalTo(AttributeKey.booleanKey("field4"), true), + equalTo(AttributeKey.longArrayKey("field5"), Arrays.asList(1L, 2L, 3L)), + equalTo(AttributeKey.doubleArrayKey("field6"), Arrays.asList(1.0, 2.0, 3.0)), + equalTo( + AttributeKey.stringArrayKey("field7"), + Arrays.asList("text-2", "text-3", "text-4")), + equalTo( + AttributeKey.booleanArrayKey("field8"), Arrays.asList(true, false, true)), + equalTo( + AttributeKey.stringArrayKey("field9"), + Arrays.asList("1", "2.0", "true", "text")), + equalTo(AttributeKey.stringKey("field10"), "raw value"), + equalTo(AttributeKey.stringArrayKey("field11"), Arrays.asList("1", "2", "3")), + equalTo(AttributeKey.longKey("map1"), 1L), + equalTo(AttributeKey.doubleKey("map2"), 2.0), + equalTo(AttributeKey.stringKey("map3"), "text-5"))); + } + + @Test + void logstashEmptyAndNullValues() { + Map noEntries = new HashMap<>(); + + logger + .atInfo() + .setMessage("log message 1") + .addMarker(Markers.appendEntries(noEntries)) + .addMarker(Markers.append("field2", null)) + .addMarker(Markers.append("field3", new int[0])) + .addMarker(Markers.append("field4", new String[0])) + .addMarker(Markers.appendArray("field5")) + .addMarker(Markers.appendArray("field6", (Object) null)) + .addMarker(Markers.appendArray("field7", null, null, null)) + .log(); + + testing.waitAndAssertLogRecords( + logRecord -> + logRecord + .hasResource(resource) + .hasInstrumentationScope(instrumentationScopeInfo) + .hasBody("log message 1") + .hasTotalAttributeCount(4) // 4 code attributes + ); + } } 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/logback/logback-appender-1.0/library/src/test/java/io/opentelemetry/instrumentation/logback/appender/v1_0/internal/LoggingEventMapperTest.java b/instrumentation/logback/logback-appender-1.0/library/src/test/java/io/opentelemetry/instrumentation/logback/appender/v1_0/internal/LoggingEventMapperTest.java index 1f43c1df62cc..36da781f25c7 100644 --- a/instrumentation/logback/logback-appender-1.0/library/src/test/java/io/opentelemetry/instrumentation/logback/appender/v1_0/internal/LoggingEventMapperTest.java +++ b/instrumentation/logback/logback-appender-1.0/library/src/test/java/io/opentelemetry/instrumentation/logback/appender/v1_0/internal/LoggingEventMapperTest.java @@ -12,6 +12,7 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.AttributesBuilder; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.Test; @@ -70,4 +71,54 @@ void testAll() { entry(AttributeKey.stringKey("key1"), "value1"), entry(AttributeKey.stringKey("key2"), "value2")); } + + @Test + void testCaptureAttributeArray() { + AttributesBuilder builder = Attributes.builder(); + + LoggingEventMapper.captureAttribute(builder, "booleanArray", new boolean[] {true}); + LoggingEventMapper.captureAttribute(builder, "BooleanArray", new Boolean[] {true}); + + LoggingEventMapper.captureAttribute(builder, "byteArray", new byte[] {2}); + LoggingEventMapper.captureAttribute(builder, "ByteArray", new Byte[] {2}); + + LoggingEventMapper.captureAttribute(builder, "shortArray", new short[] {2}); + LoggingEventMapper.captureAttribute(builder, "ShortArray", new Short[] {2}); + + LoggingEventMapper.captureAttribute(builder, "intArray", new int[] {2}); + LoggingEventMapper.captureAttribute(builder, "IntegerArray", new Integer[] {2}); + + LoggingEventMapper.captureAttribute(builder, "longArray", new long[] {2}); + LoggingEventMapper.captureAttribute(builder, "LongArray", new Long[] {2L}); + + LoggingEventMapper.captureAttribute(builder, "floatArray", new float[] {2.0f}); + LoggingEventMapper.captureAttribute(builder, "FloatArray", new Float[] {2.0f}); + + LoggingEventMapper.captureAttribute(builder, "doubleArray", new double[] {2.0}); + LoggingEventMapper.captureAttribute(builder, "DoubleArray", new Double[] {2.0}); + + LoggingEventMapper.captureAttribute(builder, "ObjectArray", new Object[] {"test"}); + LoggingEventMapper.captureAttribute(builder, "List", Collections.singletonList("test")); + LoggingEventMapper.captureAttribute(builder, "Set", Collections.singleton("test")); + + assertThat(builder.build()) + .containsOnly( + entry(AttributeKey.booleanArrayKey("booleanArray"), singletonList(Boolean.TRUE)), + entry(AttributeKey.booleanArrayKey("BooleanArray"), singletonList(Boolean.TRUE)), + entry(AttributeKey.longArrayKey("byteArray"), singletonList(2L)), + entry(AttributeKey.longArrayKey("ByteArray"), singletonList(2L)), + entry(AttributeKey.longArrayKey("shortArray"), singletonList(2L)), + entry(AttributeKey.longArrayKey("ShortArray"), singletonList(2L)), + entry(AttributeKey.longArrayKey("intArray"), singletonList(2L)), + entry(AttributeKey.longArrayKey("IntegerArray"), singletonList(2L)), + entry(AttributeKey.longArrayKey("longArray"), singletonList(2L)), + entry(AttributeKey.longArrayKey("LongArray"), singletonList(2L)), + entry(AttributeKey.doubleArrayKey("floatArray"), singletonList(2.0)), + entry(AttributeKey.doubleArrayKey("FloatArray"), singletonList(2.0)), + entry(AttributeKey.doubleArrayKey("doubleArray"), singletonList(2.0)), + entry(AttributeKey.doubleArrayKey("DoubleArray"), singletonList(2.0)), + entry(AttributeKey.stringArrayKey("ObjectArray"), singletonList("test")), + entry(AttributeKey.stringArrayKey("List"), singletonList("test")), + entry(AttributeKey.stringArrayKey("Set"), singletonList("test"))); + } } 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 06cc2eb09a3d..4e8af4b11703 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 @@ -143,6 +143,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() diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index f3e0820a79aa..bafa172b4853 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -411,6 +411,12 @@ "description": "Enable the capture of Logback logger context properties as attributes.", "defaultValue": false }, + { + "name": "otel.instrumentation.logback-appender.experimental.capture-logstash-attributes", + "type": "java.lang.Boolean", + "description": "Enable the capture of Logstash attributes, supported are those added to logs via `Markers.append()`, `Markers.appendEntries()`, `Markers.appendArray()` and `Markers.appendRaw()` methods.", + "defaultValue": false + }, { "name": "otel.instrumentation.logback-appender.experimental.capture-mdc-attributes", "type": "java.util.List",