diff --git a/instrumentation/log4j/log4j-appender-2.17/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/appender/v2_17/Log4jHelper.java b/instrumentation/log4j/log4j-appender-2.17/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/appender/v2_17/Log4jHelper.java index 6fc308cc1dd9..329dc67016e5 100644 --- a/instrumentation/log4j/log4j-appender-2.17/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/appender/v2_17/Log4jHelper.java +++ b/instrumentation/log4j/log4j-appender-2.17/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/appender/v2_17/Log4jHelper.java @@ -30,7 +30,7 @@ public final class Log4jHelper { - private static final LogEventMapper> mapper; + private static final LogEventMapper> mapper; private static final boolean captureExperimentalAttributes; private static final MethodHandle stackTraceMethodHandle = getStackTraceMethodHandle(); @@ -147,17 +147,17 @@ private static MethodHandle getStackTraceMethodHandle() { } } - private enum ContextDataAccessorImpl implements ContextDataAccessor> { + private enum ContextDataAccessorImpl implements ContextDataAccessor, Object> { INSTANCE; @Override @Nullable - public String getValue(Map contextData, String key) { + public Object getValue(Map contextData, String key) { return contextData.get(key); } @Override - public void forEach(Map contextData, BiConsumer action) { + public void forEach(Map contextData, BiConsumer action) { contextData.forEach(action); } } diff --git a/instrumentation/log4j/log4j-appender-2.17/library/build.gradle.kts b/instrumentation/log4j/log4j-appender-2.17/library/build.gradle.kts index aabb6ce4a876..a384aed65a01 100644 --- a/instrumentation/log4j/log4j-appender-2.17/library/build.gradle.kts +++ b/instrumentation/log4j/log4j-appender-2.17/library/build.gradle.kts @@ -7,6 +7,7 @@ dependencies { annotationProcessor("org.apache.logging.log4j:log4j-core:2.17.0") implementation(project(":instrumentation:log4j:log4j-context-data:log4j-context-data-2.17:library-autoconfigure")) + implementation("io.opentelemetry:opentelemetry-api-incubator") testImplementation("io.opentelemetry:opentelemetry-sdk-testing") testLibrary("com.lmax:disruptor:3.3.4") diff --git a/instrumentation/log4j/log4j-appender-2.17/library/src/main/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/OpenTelemetryAppender.java b/instrumentation/log4j/log4j-appender-2.17/library/src/main/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/OpenTelemetryAppender.java index ff980b3be35a..0d1ebcee682a 100644 --- a/instrumentation/log4j/log4j-appender-2.17/library/src/main/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/OpenTelemetryAppender.java +++ b/instrumentation/log4j/log4j-appender-2.17/library/src/main/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/OpenTelemetryAppender.java @@ -300,11 +300,13 @@ private void emit(OpenTelemetry openTelemetry, LogEvent event) { // when using async logger we'll be executing on a different thread than what started logging // reconstruct the context from context data if (context == Context.root()) { - ContextDataAccessor contextDataAccessor = ContextDataAccessorImpl.INSTANCE; - String traceId = contextDataAccessor.getValue(contextData, ContextDataKeys.TRACE_ID_KEY); - String spanId = contextDataAccessor.getValue(contextData, ContextDataKeys.SPAN_ID_KEY); + ContextDataAccessor contextDataAccessor = + ContextDataAccessorImpl.INSTANCE; + String traceId = + contextDataAccessor.getStringValue(contextData, ContextDataKeys.TRACE_ID_KEY); + String spanId = contextDataAccessor.getStringValue(contextData, ContextDataKeys.SPAN_ID_KEY); String traceFlags = - contextDataAccessor.getValue(contextData, ContextDataKeys.TRACE_FLAGS_KEY); + contextDataAccessor.getStringValue(contextData, ContextDataKeys.TRACE_FLAGS_KEY); if (traceId != null && spanId != null && traceFlags != null) { context = Context.root() @@ -340,17 +342,17 @@ private void emit(OpenTelemetry openTelemetry, LogEvent event) { builder.emit(); } - private enum ContextDataAccessorImpl implements ContextDataAccessor { + private enum ContextDataAccessorImpl implements ContextDataAccessor { INSTANCE; @Override @Nullable - public String getValue(ReadOnlyStringMap contextData, String key) { + public Object getValue(ReadOnlyStringMap contextData, String key) { return contextData.getValue(key); } @Override - public void forEach(ReadOnlyStringMap contextData, BiConsumer action) { + public void forEach(ReadOnlyStringMap contextData, BiConsumer action) { contextData.forEach(action::accept); } } diff --git a/instrumentation/log4j/log4j-appender-2.17/library/src/main/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/internal/ContextDataAccessor.java b/instrumentation/log4j/log4j-appender-2.17/library/src/main/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/internal/ContextDataAccessor.java index e1552abe8045..65172cf149e1 100644 --- a/instrumentation/log4j/log4j-appender-2.17/library/src/main/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/internal/ContextDataAccessor.java +++ b/instrumentation/log4j/log4j-appender-2.17/library/src/main/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/internal/ContextDataAccessor.java @@ -12,10 +12,18 @@ * This class is internal and is hence not for public use. Its APIs are unstable and can change at * any time. */ -public interface ContextDataAccessor { +public interface ContextDataAccessor { @Nullable - String getValue(T contextData, String key); + V getValue(T contextData, String key); - void forEach(T contextData, BiConsumer action); + default String getStringValue(T contextData, String key) { + Object value = getValue(contextData, key); + if (value != null) { + return value.toString(); + } + return null; + } + + void forEach(T contextData, BiConsumer action); } diff --git a/instrumentation/log4j/log4j-appender-2.17/library/src/main/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/internal/LogEventMapper.java b/instrumentation/log4j/log4j-appender-2.17/library/src/main/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/internal/LogEventMapper.java index e26958f5215b..981f3d6f013d 100644 --- a/instrumentation/log4j/log4j-appender-2.17/library/src/main/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/internal/LogEventMapper.java +++ b/instrumentation/log4j/log4j-appender-2.17/library/src/main/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/internal/LogEventMapper.java @@ -6,8 +6,12 @@ package io.opentelemetry.instrumentation.log4j.appender.v2_17.internal; import io.opentelemetry.api.common.AttributeKey; -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.incubator.common.ExtendedAttributeKey; +import io.opentelemetry.api.incubator.common.ExtendedAttributeType; +import io.opentelemetry.api.incubator.common.ExtendedAttributes; +import io.opentelemetry.api.incubator.common.ExtendedAttributesBuilder; +import io.opentelemetry.api.incubator.internal.InternalExtendedAttributeKeyImpl; +import io.opentelemetry.api.incubator.logs.ExtendedLogRecordBuilder; import io.opentelemetry.api.logs.LogRecordBuilder; import io.opentelemetry.api.logs.Severity; import io.opentelemetry.context.Context; @@ -15,8 +19,15 @@ import io.opentelemetry.semconv.ExceptionAttributes; import java.io.PrintWriter; import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Consumer; import java.util.function.Supplier; +import java.util.stream.Collectors; import javax.annotation.Nullable; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.Marker; @@ -42,14 +53,14 @@ public final class LogEventMapper { private static final String SPECIAL_MAP_MESSAGE_ATTRIBUTE = "message"; - private static final Cache> contextDataAttributeKeyCache = + private static final Cache> contextDataAttributeKeyCache = Cache.bounded(100); - private static final Cache> mapMessageAttributeKeyCache = + private static final Cache> mapMessageAttributeKeyCache = Cache.bounded(100); private static final AttributeKey LOG_MARKER = AttributeKey.stringKey("log4j.marker"); - private final ContextDataAccessor contextDataAccessor; + private final ContextDataAccessor contextDataAccessor; private final boolean captureExperimentalAttributes; private final boolean captureCodeAttributes; @@ -59,7 +70,7 @@ public final class LogEventMapper { private final boolean captureAllContextDataAttributes; public LogEventMapper( - ContextDataAccessor contextDataAccessor, + ContextDataAccessor contextDataAccessor, boolean captureExperimentalAttributes, boolean captureCodeAttributes, boolean captureMapMessageAttributes, @@ -98,7 +109,7 @@ public void mapLogEvent( Supplier sourceSupplier, Context context) { - AttributesBuilder attributes = Attributes.builder(); + ExtendedAttributesBuilder attributes = ExtendedAttributes.builder(); captureMessage(builder, attributes, message); @@ -134,19 +145,24 @@ public void mapLogEvent( } attributes.put(CODE_NAMESPACE, source.getClassName()); attributes.put(CODE_FUNCTION, source.getMethodName()); - int lineNumber = source.getLineNumber(); + long lineNumber = source.getLineNumber(); if (lineNumber > 0) { attributes.put(CODE_LINENO, lineNumber); } } } - builder.setAllAttributes(attributes.build()); + if (builder instanceof ExtendedLogRecordBuilder) { + ((ExtendedLogRecordBuilder) builder).setAllAttributes(attributes.build()); + } else { + builder.setAllAttributes(attributes.build().asAttributes()); + } builder.setContext(context); } // visible for testing - void captureMessage(LogRecordBuilder builder, AttributesBuilder attributes, Message message) { + void captureMessage( + LogRecordBuilder builder, ExtendedAttributesBuilder attributes, Message message) { if (message == null) { return; } @@ -169,51 +185,206 @@ void captureMessage(LogRecordBuilder builder, AttributesBuilder attributes, Mess if (captureMapMessageAttributes) { // TODO (trask) this could be optimized in 2.9 and later by calling MapMessage.forEach() - mapMessage - .getData() - .forEach( - (key, value) -> { - if (value != null - && (!checkSpecialMapMessageAttribute - || !key.equals(SPECIAL_MAP_MESSAGE_ATTRIBUTE))) { - attributes.put(getMapMessageAttributeKey(key), value.toString()); - } - }); + consumeAttributes( + mapMessage.getData()::forEach, + attributes, + checkSpecialMapMessageAttribute, + LogEventMapper::getMapMessageAttributeKey); } } - // visible for testing - void captureContextDataAttributes(AttributesBuilder attributes, T contextData) { + private static void consumeAttributes( + // Consumes an action on an entry, like map::forEach + Consumer> entryActionConsumer, + ExtendedAttributesBuilder attributes, + boolean checkSpecialMapMessageAttribute, + BiFunction> keyProvider) { + entryActionConsumer.accept( + (key, value) -> { + if (value != null + && (!checkSpecialMapMessageAttribute || !key.equals(SPECIAL_MAP_MESSAGE_ATTRIBUTE))) { + consumeEntry(key, value, attributes, keyProvider); + } + }); + } + + @SuppressWarnings({"unchecked", "ReturnValueIgnored"}) + private static void consumeEntry( + String key, + Object value, + ExtendedAttributesBuilder attributes, + BiFunction> keyProvider) { + if (value instanceof String) { + attributes.put( + (ExtendedAttributeKey) keyProvider.apply(key, ExtendedAttributeType.STRING), + (String) value); + } else if (value instanceof Boolean) { + attributes.put( + (ExtendedAttributeKey) keyProvider.apply(key, ExtendedAttributeType.BOOLEAN), + (Boolean) value); + } else if (value instanceof Byte + || value instanceof Short + || value instanceof Integer + || value instanceof Long) { + attributes.put( + (ExtendedAttributeKey) keyProvider.apply(key, ExtendedAttributeType.LONG), + ((Number) value).longValue()); + } else if (value instanceof Float || value instanceof Double) { + attributes.put( + (ExtendedAttributeKey) keyProvider.apply(key, ExtendedAttributeType.DOUBLE), + ((Number) value).doubleValue()); + } else if (value instanceof List) { + List list = (List) value; + if (list.isEmpty()) { + return; + } + + Object first = list.get(0); + for (int idx = 1; idx < list.size(); ++idx) { + try { + first.getClass().cast(list.get(idx)); + } catch (ClassCastException exception) { + // fallback to a list of strings + list = list.stream().map(Object::toString).collect(Collectors.toList()); + first = list.get(0); + break; + } + } + + if (first instanceof String) { + attributes.put( + (ExtendedAttributeKey>) + keyProvider.apply(key, ExtendedAttributeType.STRING_ARRAY), + (List) list); + } else if (first instanceof Boolean) { + attributes.put( + (ExtendedAttributeKey>) + keyProvider.apply(key, ExtendedAttributeType.BOOLEAN_ARRAY), + (List) list); + } else if (first instanceof Integer) { + attributes.put( + (ExtendedAttributeKey>) + keyProvider.apply(key, ExtendedAttributeType.LONG_ARRAY), + ((List) list).stream().map(Integer::longValue).collect(Collectors.toList())); + } else if (first instanceof Long) { + attributes.put( + (ExtendedAttributeKey>) + keyProvider.apply(key, ExtendedAttributeType.LONG_ARRAY), + (List) list); + } else if (first instanceof Float) { + attributes.put( + (ExtendedAttributeKey>) + keyProvider.apply(key, ExtendedAttributeType.DOUBLE_ARRAY), + ((List) list).stream().map(Float::doubleValue).collect(Collectors.toList())); + } else if (first instanceof Double) { + attributes.put( + (ExtendedAttributeKey>) + keyProvider.apply(key, ExtendedAttributeType.DOUBLE_ARRAY), + (List) list); + } + } else if (value instanceof String[]) { + attributes.put( + (ExtendedAttributeKey>) + keyProvider.apply(key, ExtendedAttributeType.STRING_ARRAY), + Arrays.asList((String[]) value)); + } else if (value instanceof boolean[]) { + boolean[] arr = (boolean[]) value; + List list = new ArrayList<>(arr.length); + for (boolean f : arr) { + list.add(f); + } + attributes.put( + (ExtendedAttributeKey>) + keyProvider.apply(key, ExtendedAttributeType.BOOLEAN_ARRAY), + list); + } else if (value instanceof long[]) { + List list = Arrays.stream((long[]) value).boxed().collect(Collectors.toList()); + attributes.put( + (ExtendedAttributeKey>) + keyProvider.apply(key, ExtendedAttributeType.LONG_ARRAY), + list); + } else if (value instanceof int[]) { + List list = + Arrays.stream((int[]) value).mapToLong(i -> i).boxed().collect(Collectors.toList()); + attributes.put( + (ExtendedAttributeKey>) + keyProvider.apply(key, ExtendedAttributeType.LONG_ARRAY), + list); + } else if (value instanceof double[]) { + List list = Arrays.stream((double[]) value).boxed().collect(Collectors.toList()); + attributes.put( + (ExtendedAttributeKey>) + keyProvider.apply(key, ExtendedAttributeType.DOUBLE_ARRAY), + list); + } else if (value instanceof float[]) { + float[] arr = (float[]) value; + List list = new ArrayList<>(arr.length); + for (float f : arr) { + list.add((double) f); + } + attributes.put( + (ExtendedAttributeKey>) + keyProvider.apply(key, ExtendedAttributeType.DOUBLE_ARRAY), + list); + } else if ((value instanceof Map)) { + ExtendedAttributesBuilder nestedAttribute = ExtendedAttributes.builder(); + Map nestedMap = (Map) value; + consumeAttributes( + consumer -> nestedMap.forEach((k, v) -> consumer.accept(k.toString(), v)), + nestedAttribute, + false, + keyProvider); + attributes.put( + (ExtendedAttributeKey) + keyProvider.apply(key, ExtendedAttributeType.EXTENDED_ATTRIBUTES), + nestedAttribute.build()); + } else { + throw new IllegalArgumentException("Unrecognized value type: " + value.getClass()); + } + } + // visible for testing + void captureContextDataAttributes(ExtendedAttributesBuilder attributes, T contextData) { if (captureAllContextDataAttributes) { - contextDataAccessor.forEach( - contextData, - (key, value) -> { - if (value != null) { - attributes.put(getContextDataAttributeKey(key), value); - } - }); + consumeAttributes( + entryConsumer -> contextDataAccessor.forEach(contextData, entryConsumer), + attributes, + false, + LogEventMapper::getContextDataAttributeKey); return; } for (String key : captureContextDataAttributes) { - String value = contextDataAccessor.getValue(contextData, key); + Object value = contextDataAccessor.getValue(contextData, key); if (value != null) { - attributes.put(getContextDataAttributeKey(key), value); + consumeEntry(key, value, attributes, LogEventMapper::getContextDataAttributeKey); } } } - public static AttributeKey getContextDataAttributeKey(String key) { - return contextDataAttributeKeyCache.computeIfAbsent(key, AttributeKey::stringKey); + private static ExtendedAttributeKey getContextDataAttributeKey( + String key, ExtendedAttributeType type) { + return getAttributeKey(key, type, contextDataAttributeKeyCache); + } + + private static ExtendedAttributeKey getMapMessageAttributeKey( + String key, ExtendedAttributeType type) { + return getAttributeKey("log4j.map_message." + key, type, mapMessageAttributeKeyCache); } - public static AttributeKey getMapMessageAttributeKey(String key) { - return mapMessageAttributeKeyCache.computeIfAbsent( - key, k -> AttributeKey.stringKey("log4j.map_message." + k)); + @SuppressWarnings("unchecked") + private static ExtendedAttributeKey getAttributeKey( + String key, ExtendedAttributeType type, Cache> cache) { + ExtendedAttributeKey output = + cache.computeIfAbsent(key, k -> InternalExtendedAttributeKeyImpl.create(key, type)); + if (output.getType() != type) { + output = InternalExtendedAttributeKeyImpl.create(key, type); + cache.put(key, output); + } + return (ExtendedAttributeKey) output; } - private static void setThrowable(AttributesBuilder attributes, Throwable throwable) { + private static void setThrowable(ExtendedAttributesBuilder attributes, Throwable throwable) { // TODO (trask) extract method for recording exception into // io.opentelemetry:opentelemetry-api attributes.put(ExceptionAttributes.EXCEPTION_TYPE, throwable.getClass().getName()); diff --git a/instrumentation/log4j/log4j-appender-2.17/library/src/test/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/internal/LogEventMapperTest.java b/instrumentation/log4j/log4j-appender-2.17/library/src/test/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/internal/LogEventMapperTest.java index ce374ff97b9b..afce689ca97e 100644 --- a/instrumentation/log4j/log4j-appender-2.17/library/src/test/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/internal/LogEventMapperTest.java +++ b/instrumentation/log4j/log4j-appender-2.17/library/src/test/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/internal/LogEventMapperTest.java @@ -14,15 +14,18 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.incubator.common.ExtendedAttributeKey; +import io.opentelemetry.api.incubator.common.ExtendedAttributes; +import io.opentelemetry.api.incubator.common.ExtendedAttributesBuilder; import io.opentelemetry.api.logs.LogRecordBuilder; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.function.BiConsumer; import javax.annotation.Nullable; import org.apache.logging.log4j.message.StringMapMessage; import org.apache.logging.log4j.message.StructuredDataMessage; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; class LogEventMapperTest { @@ -30,62 +33,62 @@ class LogEventMapperTest { @Test void testDefault() { // given - LogEventMapper> mapper = + LogEventMapper> mapper = new LogEventMapper<>( ContextDataAccessorImpl.INSTANCE, false, false, false, false, emptyList()); - Map contextData = new HashMap<>(); + Map contextData = new HashMap<>(); contextData.put("key1", "value1"); contextData.put("key2", "value2"); - AttributesBuilder attributes = Attributes.builder(); + ExtendedAttributesBuilder attributes = ExtendedAttributes.builder(); // when mapper.captureContextDataAttributes(attributes, contextData); // then - assertThat(attributes.build()).isEmpty(); + assertThat(attributes.build().asAttributes()).isEmpty(); } @Test void testSome() { // given - LogEventMapper> mapper = + LogEventMapper> mapper = new LogEventMapper<>( ContextDataAccessorImpl.INSTANCE, false, false, false, false, singletonList("key2")); - Map contextData = new HashMap<>(); + Map contextData = new HashMap<>(); contextData.put("key1", "value1"); contextData.put("key2", "value2"); - AttributesBuilder attributes = Attributes.builder(); + ExtendedAttributesBuilder attributes = ExtendedAttributes.builder(); // when mapper.captureContextDataAttributes(attributes, contextData); // then - assertThat(attributes.build()).containsOnly(attributeEntry("key2", "value2")); + assertThat(attributes.build().asAttributes()).containsOnly(attributeEntry("key2", "value2")); } @Test void testAll() { // given - LogEventMapper> mapper = + LogEventMapper> mapper = new LogEventMapper<>( ContextDataAccessorImpl.INSTANCE, false, false, false, false, singletonList("*")); - Map contextData = new HashMap<>(); + Map contextData = new HashMap<>(); contextData.put("key1", "value1"); contextData.put("key2", "value2"); - AttributesBuilder attributes = Attributes.builder(); + ExtendedAttributesBuilder attributes = ExtendedAttributes.builder(); // when mapper.captureContextDataAttributes(attributes, contextData); // then - assertThat(attributes.build()) + assertThat(attributes.build().asAttributes()) .containsOnly(attributeEntry("key1", "value1"), attributeEntry("key2", "value2")); } @Test void testCaptureMapMessageDisabled() { // given - LogEventMapper> mapper = + LogEventMapper> mapper = new LogEventMapper<>( ContextDataAccessorImpl.INSTANCE, false, false, false, false, singletonList("*")); @@ -94,20 +97,20 @@ void testCaptureMapMessageDisabled() { message.put("message", "value2"); LogRecordBuilder logRecordBuilder = mock(LogRecordBuilder.class); - AttributesBuilder attributes = Attributes.builder(); + ExtendedAttributesBuilder attributes = ExtendedAttributes.builder(); // when mapper.captureMessage(logRecordBuilder, attributes, message); // then verify(logRecordBuilder).setBody("value2"); - assertThat(attributes.build()).isEmpty(); + assertThat(attributes.build().asAttributes()).isEmpty(); } @Test void testCaptureMapMessageWithSpecialAttribute() { // given - LogEventMapper> mapper = + LogEventMapper> mapper = new LogEventMapper<>( ContextDataAccessorImpl.INSTANCE, false, false, true, false, singletonList("*")); @@ -116,20 +119,21 @@ void testCaptureMapMessageWithSpecialAttribute() { message.put("message", "value2"); LogRecordBuilder logRecordBuilder = mock(LogRecordBuilder.class); - AttributesBuilder attributes = Attributes.builder(); + ExtendedAttributesBuilder attributes = ExtendedAttributes.builder(); // when mapper.captureMessage(logRecordBuilder, attributes, message); // then verify(logRecordBuilder).setBody("value2"); - assertThat(attributes.build()).containsOnly(attributeEntry("log4j.map_message.key1", "value1")); + assertThat(attributes.build().asAttributes()) + .containsOnly(attributeEntry("log4j.map_message.key1", "value1")); } @Test void testCaptureMapMessageWithoutSpecialAttribute() { // given - LogEventMapper> mapper = + LogEventMapper> mapper = new LogEventMapper<>( ContextDataAccessorImpl.INSTANCE, false, false, true, false, singletonList("*")); @@ -138,14 +142,14 @@ void testCaptureMapMessageWithoutSpecialAttribute() { message.put("key2", "value2"); LogRecordBuilder logRecordBuilder = mock(LogRecordBuilder.class); - AttributesBuilder attributes = Attributes.builder(); + ExtendedAttributesBuilder attributes = ExtendedAttributes.builder(); // when mapper.captureMessage(logRecordBuilder, attributes, message); // then verify(logRecordBuilder, never()).setBody(anyString()); - assertThat(attributes.build()) + assertThat(attributes.build().asAttributes()) .containsOnly( attributeEntry("log4j.map_message.key1", "value1"), attributeEntry("log4j.map_message.key2", "value2")); @@ -154,7 +158,7 @@ void testCaptureMapMessageWithoutSpecialAttribute() { @Test void testCaptureStructuredDataMessage() { // given - LogEventMapper> mapper = + LogEventMapper> mapper = new LogEventMapper<>( ContextDataAccessorImpl.INSTANCE, false, false, true, false, singletonList("*")); @@ -163,30 +167,143 @@ void testCaptureStructuredDataMessage() { message.put("message", "value2"); LogRecordBuilder logRecordBuilder = mock(LogRecordBuilder.class); - AttributesBuilder attributes = Attributes.builder(); + ExtendedAttributesBuilder attributes = ExtendedAttributes.builder(); // when mapper.captureMessage(logRecordBuilder, attributes, message); // then verify(logRecordBuilder).setBody("a message"); - assertThat(attributes.build()) + assertThat(attributes.build().asAttributes()) .containsOnly( attributeEntry("log4j.map_message.key1", "value1"), attributeEntry("log4j.map_message.message", "value2")); } - private enum ContextDataAccessorImpl implements ContextDataAccessor> { + @Test + void testObjectsInContextData() { + // given + LogEventMapper> mapper = + new LogEventMapper<>( + ContextDataAccessorImpl.INSTANCE, false, false, true, false, singletonList("*")); + + Map contextData = new HashMap<>(); + + // scalars + contextData.put("string", "value"); + contextData.put("int", 10); + contextData.put("long", 11L); + contextData.put("float", 12f); + contextData.put("double", 13d); + contextData.put("boolean", false); + + // arrays + contextData.put("stringArray", new String[] {"one", "two", "three"}); + contextData.put("intArray", new int[] {1, 2, 3}); + contextData.put("longArray", new long[] {4L, 5L, 6L}); + contextData.put("floatArray", new float[] {7f, 8f, 9f}); + contextData.put("doubleArray", new double[] {10d, 11d, 12d}); + contextData.put("booleanArray", new boolean[] {true, false, false}); + + // lists + contextData.put("stringList", Arrays.asList("one", "two", "three")); + contextData.put("intList", Arrays.asList(1, 2, 3)); + contextData.put("longList", Arrays.asList(4L, 5L, 6L)); + contextData.put("floatList", Arrays.asList(7f, 8f, 9f)); + contextData.put("doubleList", Arrays.asList(10d, 11d, 12d)); + contextData.put("booleanList", Arrays.asList(true, false, false)); + + Map map = new HashMap<>(); + map.put("entry1", "value1"); + map.put("entry2", 28); + map.put("entry3", new int[] {1, 2, 3}); + contextData.put("map", map); + + ExtendedAttributesBuilder attributes = ExtendedAttributes.builder(); + + // when + mapper.captureContextDataAttributes(attributes, contextData); + + // then + ExtendedAttributes result = attributes.build(); + + // scalars + assertThat(result.get(ExtendedAttributeKey.stringKey("string"))).isEqualTo("value"); + assertThat(result.get(ExtendedAttributeKey.longKey("int"))).isEqualTo(10L); + assertThat(result.get(ExtendedAttributeKey.longKey("long"))).isEqualTo(11L); + assertThat(result.get(ExtendedAttributeKey.doubleKey("float"))).isEqualTo(12d); + assertThat(result.get(ExtendedAttributeKey.doubleKey("double"))).isEqualTo(13f); + assertThat(result.get(ExtendedAttributeKey.booleanKey("boolean"))).isEqualTo(false); + + assertThat(result.get(ExtendedAttributeKey.stringArrayKey("stringArray"))) + .isEqualTo(Arrays.asList("one", "two", "three")); + assertThat(result.get(ExtendedAttributeKey.longArrayKey("intArray"))) + .isEqualTo(Arrays.asList(1L, 2L, 3L)); + assertThat(result.get(ExtendedAttributeKey.longArrayKey("longArray"))) + .isEqualTo(Arrays.asList(4L, 5L, 6L)); + assertThat(result.get(ExtendedAttributeKey.doubleArrayKey("floatArray"))) + .isEqualTo(Arrays.asList(7d, 8d, 9d)); + assertThat(result.get(ExtendedAttributeKey.doubleArrayKey("doubleArray"))) + .isEqualTo(Arrays.asList(10d, 11d, 12d)); + assertThat(result.get(ExtendedAttributeKey.booleanArrayKey("booleanArray"))) + .isEqualTo(Arrays.asList(true, false, false)); + + assertThat(result.get(ExtendedAttributeKey.stringArrayKey("stringList"))) + .isEqualTo(Arrays.asList("one", "two", "three")); + assertThat(result.get(ExtendedAttributeKey.longArrayKey("intList"))) + .isEqualTo(Arrays.asList(1L, 2L, 3L)); + assertThat(result.get(ExtendedAttributeKey.longArrayKey("longList"))) + .isEqualTo(Arrays.asList(4L, 5L, 6L)); + assertThat(result.get(ExtendedAttributeKey.doubleArrayKey("floatList"))) + .isEqualTo(Arrays.asList(7d, 8d, 9d)); + assertThat(result.get(ExtendedAttributeKey.doubleArrayKey("doubleList"))) + .isEqualTo(Arrays.asList(10d, 11d, 12d)); + assertThat(result.get(ExtendedAttributeKey.booleanArrayKey("booleanList"))) + .isEqualTo(Arrays.asList(true, false, false)); + + Map, Object> expected = new HashMap<>(); + expected.put(ExtendedAttributeKey.stringKey("entry1"), "value1"); + expected.put(ExtendedAttributeKey.longKey("entry2"), 28L); + expected.put(ExtendedAttributeKey.longArrayKey("entry3"), Arrays.asList(1L, 2L, 3L)); + + ExtendedAttributes actual = result.get(ExtendedAttributeKey.extendedAttributesKey("map")); + assertThat(actual).isNotNull(); + assertThat(actual.asMap()).containsExactlyInAnyOrderEntriesOf(expected); + } + + @Test + void testMixedTypeListAttribute() { + // given + LogEventMapper> mapper = + new LogEventMapper<>( + ContextDataAccessorImpl.INSTANCE, false, false, true, false, singletonList("*")); + + Map contextData = new HashMap<>(); + contextData.put("stringList", Arrays.asList("one", 2, "three")); + + ExtendedAttributesBuilder attributes = ExtendedAttributes.builder(); + + // when + mapper.captureContextDataAttributes(attributes, contextData); + + // then + ExtendedAttributes result = attributes.build(); + + Assertions.assertThat(result.get(ExtendedAttributeKey.stringArrayKey("stringList"))) + .isEqualTo(Arrays.asList("one", "2", "three")); + } + + private enum ContextDataAccessorImpl implements ContextDataAccessor, Object> { INSTANCE; @Override @Nullable - public String getValue(Map contextData, String key) { + public Object getValue(Map contextData, String key) { return contextData.get(key); } @Override - public void forEach(Map contextData, BiConsumer action) { + public void forEach(Map contextData, BiConsumer action) { contextData.forEach(action); } }