diff --git a/instrumentation/logback/logback-appender-1.0/javaagent/README.md b/instrumentation/logback/logback-appender-1.0/javaagent/README.md index 2a962ae4e739..c9d661eceb87 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 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..0955fae0efb9 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.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.function.IntFunction; 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,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 +80,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 +90,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 +184,8 @@ private void mapLoggingEvent( } if (captureMarkerAttribute) { - captureMarkerAttribute(attributes, loggingEvent); + boolean skipLogstashMarkers = supportsLogstashMarkers && captureLogstashAttributes; + captureMarkerAttribute(attributes, loggingEvent, skipLogstashMarkers); } if (supportsKeyValuePairs && captureKeyValuePairAttributes) { @@ -187,6 +202,10 @@ private void mapLoggingEvent( captureArguments(attributes, loggingEvent.getMessage(), loggingEvent.getArgumentArray()); } + if (supportsLogstashMarkers && captureLogstashAttributes) { + captureLogstashAttributes(attributes, loggingEvent); + } + builder.setAllAttributes(attributes.build()); // span context @@ -278,25 +297,79 @@ 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()); - } + captureKeyValueAttribute(attributes, keyValuePair.key, keyValuePair.value); + } + } + } + + private static void captureKeyValueAttribute( + 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[]) { + captureKeyArrayValueAttribute(attributes, AttributeKey.booleanArrayKey(keyStr), value, + Boolean[]::new, 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[]) { + captureKeyArrayValueAttribute(attributes, AttributeKey.longArrayKey(keyStr), value, + Long[]::new, o -> ((Number) o).longValue()); + } else if (value instanceof float[] + || value instanceof Float[] + || value instanceof double[] + || value instanceof Double[]) { + captureKeyArrayValueAttribute(attributes, AttributeKey.doubleArrayKey(keyStr), value, + Double[]::new, o -> ((Number) o).doubleValue()); + } else { + captureKeyArrayValueAttribute(attributes, AttributeKey.stringArrayKey(keyStr), value, + String[]::new, String::valueOf); } + } else if (value instanceof Collection) { + captureKeyArrayValueAttribute(attributes, AttributeKey.stringArrayKey(keyStr), + ((Collection) value).toArray(), String[]::new, String::valueOf); + } else { + attributes.put(getAttributeKey(keyStr), String.valueOf(value)); + } + } + } + + private static void captureKeyArrayValueAttribute( + AttributesBuilder attributes, AttributeKey> key, Object array, IntFunction newArray, Function extractor) { + int length = java.lang.reflect.Array.getLength(array); + T[] typedArray = newArray.apply(length); + int offset = 0; + for (int i = 0; i < length; i++) { + Object value = java.lang.reflect.Array.get(array, i); + // empty values are not serialized + if (value != null) { + typedArray[i - offset] = extractor.apply(value); + } else { + offset++; } } + // empty lists are not serialized + if (length != offset) { + attributes.put(key, Arrays.asList( + offset == 0 ? typedArray : Arrays.copyOf(typedArray, length - offset))); + } } private static void captureLoggerContext( @@ -326,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 @@ -369,6 +446,160 @@ 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 + } + } + + @NoMuzzle + private static boolean isLogstashMarker(Marker marker) { + return marker instanceof LogstashMarker; + } + + @SuppressWarnings("deprecation") // getMarker is deprecate since 1.3.0 + @NoMuzzle + 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); + } + } + } + + @NoMuzzle + 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(); + Object fieldValue = extractFieldValue(singleFieldAppendingMarker); + captureKeyValueAttribute(attributes, fieldName, fieldValue); + } 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(); + captureKeyValueAttribute(attributes, key, value); + } + } + } + } + + @Nullable + private static Object 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 { + return field.get(singleFieldAppendingMarker); + } 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 +612,7 @@ public static final class Builder { private boolean captureKeyValuePairAttributes; private boolean captureLoggerContext; private boolean captureArguments; + private boolean captureLogstashAttributes; Builder() {} @@ -426,6 +658,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..236d32d498a4 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,105 @@ 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/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()