You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by rg...@apache.org on 2020/02/15 19:28:04 UTC
[logging-log4j2] branch master updated: LOG4J2-2703 Complex data
type support for the default MapMessage JSON formatter. (#339)
This is an automated email from the ASF dual-hosted git repository.
rgoers pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git
The following commit(s) were added to refs/heads/master by this push:
new 79acdd0 LOG4J2-2703 Complex data type support for the default MapMessage JSON formatter. (#339)
79acdd0 is described below
commit 79acdd0a17e009e0e69a19c4c31d0af3ccfe4e6d
Author: Volkan Yazıcı <vo...@gmail.com>
AuthorDate: Sat Feb 15 20:27:56 2020 +0100
LOG4J2-2703 Complex data type support for the default MapMessage JSON formatter. (#339)
---
.../apache/logging/log4j/message/MapMessage.java | 17 +-
.../log4j/message/MapMessageJsonFormatter.java | 403 +++++++++++++++++++++
.../logging/log4j/message/MapMessageTest.java | 98 ++++-
3 files changed, 497 insertions(+), 21 deletions(-)
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/message/MapMessage.java b/log4j-api/src/main/java/org/apache/logging/log4j/message/MapMessage.java
index 1302ef6..2c4dc10 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/message/MapMessage.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/message/MapMessage.java
@@ -424,22 +424,7 @@ public class MapMessage<M extends MapMessage<M, V>, V> implements MultiFormatStr
}
protected void asJson(final StringBuilder sb) {
- sb.append('{');
- for (int i = 0; i < data.size(); i++) {
- if (i > 0) {
- sb.append(", ");
- }
- sb.append(Chars.DQUOTE);
- int start = sb.length();
- sb.append(data.getKeyAt(i));
- StringBuilders.escapeJson(sb, start);
- sb.append(Chars.DQUOTE).append(':').append(Chars.DQUOTE);
- start = sb.length();
- ParameterFormatter.recursiveDeepToString(data.getValueAt(i), sb, null);
- StringBuilders.escapeJson(sb, start);
- sb.append(Chars.DQUOTE);
- }
- sb.append('}');
+ MapMessageJsonFormatter.format(sb, data);
}
protected void asJavaUnquoted(final StringBuilder sb) {
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/message/MapMessageJsonFormatter.java b/log4j-api/src/main/java/org/apache/logging/log4j/message/MapMessageJsonFormatter.java
new file mode 100644
index 0000000..7c40233
--- /dev/null
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/message/MapMessageJsonFormatter.java
@@ -0,0 +1,403 @@
+package org.apache.logging.log4j.message;
+
+import org.apache.logging.log4j.util.IndexedStringMap;
+import org.apache.logging.log4j.util.PropertiesUtil;
+import org.apache.logging.log4j.util.StringBuilderFormattable;
+import org.apache.logging.log4j.util.StringBuilders;
+
+import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * The default JSON formatter for {@link MapMessage}s.
+ * <p>
+ * The following types have specific handlers:
+ * <p>
+ * <ul>
+ * <li>{@link Map}
+ * <li>{@link Collection} ({@link List}, {@link Set}, etc.)
+ * <li>{@link Number} ({@link BigDecimal}, {@link Double}, {@link Long}, {@link Byte}, etc.)
+ * <li>{@link Boolean}
+ * <li>{@link StringBuilderFormattable}
+ * <li><tt>char/boolean/byte/short/int/long/float/double/Object</tt> arrays
+ * <li>{@link String}
+ * </ul>
+ * <p>
+ * It supports nesting up to a maximum depth of 8, which is set by
+ * <tt>log4j2.mapMessage.jsonFormatter.maxDepth</tt> property.
+ */
+enum MapMessageJsonFormatter {;
+
+ public static final int MAX_DEPTH = readMaxDepth();
+
+ private static final char DQUOTE = '"';
+
+ private static final char RBRACE = ']';
+
+ private static final char LBRACE = '[';
+
+ private static final char COMMA = ',';
+
+ private static final char RCURLY = '}';
+
+ private static final char LCURLY = '{';
+
+ private static final char COLON = ':';
+
+ private static int readMaxDepth() {
+ final int maxDepth = PropertiesUtil
+ .getProperties()
+ .getIntegerProperty("log4j2.mapMessage.jsonFormatter.maxDepth", 8);
+ if (maxDepth < 0) {
+ throw new IllegalArgumentException(
+ "was expecting a positive maxDepth, found: " + maxDepth);
+ }
+ return maxDepth;
+ }
+
+ static void format(final StringBuilder sb, final Object object) {
+ format(sb, object, 0);
+ }
+
+ private static void format(
+ final StringBuilder sb,
+ final Object object,
+ final int depth) {
+
+ if (depth >= MAX_DEPTH) {
+ throw new IllegalArgumentException("maxDepth has been exceeded");
+ }
+
+ // null
+ if (object == null) {
+ sb.append("null");
+ }
+
+ // map
+ else if (object instanceof IndexedStringMap) {
+ final IndexedStringMap map = (IndexedStringMap) object;
+ formatIndexedStringMap(sb, map, depth);
+ } else if (object instanceof Map) {
+ @SuppressWarnings("unchecked")
+ final Map<Object, Object> map = (Map<Object, Object>) object;
+ formatMap(sb, map, depth);
+ }
+
+ // list & collection
+ else if (object instanceof List) {
+ @SuppressWarnings("unchecked")
+ final List<Object> list = (List<Object>) object;
+ formatList(sb, list, depth);
+ } else if (object instanceof Collection) {
+ @SuppressWarnings("unchecked")
+ final Collection<Object> collection = (Collection<Object>) object;
+ formatCollection(sb, collection, depth);
+ }
+
+ // number & boolean
+ else if (object instanceof Number) {
+ final Number number = (Number) object;
+ formatNumber(sb, number);
+ } else if (object instanceof Boolean) {
+ final boolean booleanValue = (boolean) object;
+ formatBoolean(sb, booleanValue);
+ }
+
+ // formattable
+ else if (object instanceof StringBuilderFormattable) {
+ final StringBuilderFormattable formattable = (StringBuilderFormattable) object;
+ formatFormattable(sb, formattable);
+ }
+
+ // arrays
+ else if (object instanceof char[]) {
+ final char[] charValues = (char[]) object;
+ formatCharArray(sb, charValues);
+ } else if (object instanceof boolean[]) {
+ final boolean[] booleanValues = (boolean[]) object;
+ formatBooleanArray(sb, booleanValues);
+ } else if (object instanceof byte[]) {
+ final byte[] byteValues = (byte[]) object;
+ formatByteArray(sb, byteValues);
+ } else if (object instanceof short[]) {
+ final short[] shortValues = (short[]) object;
+ formatShortArray(sb, shortValues);
+ } else if (object instanceof int[]) {
+ final int[] intValues = (int[]) object;
+ formatIntArray(sb, intValues);
+ } else if (object instanceof long[]) {
+ final long[] longValues = (long[]) object;
+ formatLongArray(sb, longValues);
+ } else if (object instanceof float[]) {
+ final float[] floatValues = (float[]) object;
+ formatFloatArray(sb, floatValues);
+ } else if (object instanceof double[]) {
+ final double[] doubleValues = (double[]) object;
+ formatDoubleArray(sb, doubleValues);
+ } else if (object instanceof Object[]) {
+ final Object[] objectValues = (Object[]) object;
+ formatObjectArray(sb, objectValues, depth);
+ }
+
+ // string
+ else {
+ formatString(sb, object);
+ }
+
+ }
+
+ private static void formatIndexedStringMap(
+ final StringBuilder sb,
+ final IndexedStringMap map,
+ final int depth) {
+ sb.append(LCURLY);
+ final int nextDepth = depth + 1;
+ for (int entryIndex = 0; entryIndex < map.size(); entryIndex++) {
+ final String key = map.getKeyAt(entryIndex);
+ final Object value = map.getValueAt(entryIndex);
+ if (entryIndex > 0) {
+ sb.append(COMMA);
+ }
+ sb.append(DQUOTE);
+ final int keyStartIndex = sb.length();
+ sb.append(key);
+ StringBuilders.escapeJson(sb, keyStartIndex);
+ sb.append(DQUOTE).append(COLON);
+ format(sb, value, nextDepth);
+ }
+ sb.append(RCURLY);
+ }
+
+ private static void formatMap(
+ final StringBuilder sb,
+ final Map<Object, Object> map,
+ final int depth) {
+ sb.append(LCURLY);
+ final int nextDepth = depth + 1;
+ final boolean[] firstEntry = {true};
+ map.forEach((final Object key, final Object value) -> {
+ if (key == null) {
+ throw new IllegalArgumentException("null keys are not allowed");
+ }
+ if (firstEntry[0]) {
+ firstEntry[0] = false;
+ } else {
+ sb.append(COMMA);
+ }
+ sb.append(DQUOTE);
+ final String keyString = String.valueOf(key);
+ final int keyStartIndex = sb.length();
+ sb.append(keyString);
+ StringBuilders.escapeJson(sb, keyStartIndex);
+ sb.append(DQUOTE).append(COLON);
+ format(sb, value, nextDepth);
+ });
+ sb.append(RCURLY);
+ }
+
+ private static void formatList(
+ final StringBuilder sb,
+ final List<Object> items,
+ final int depth) {
+ sb.append(LBRACE);
+ final int nextDepth = depth + 1;
+ for (int itemIndex = 0; itemIndex < items.size(); itemIndex++) {
+ if (itemIndex > 0) {
+ sb.append(COMMA);
+ }
+ final Object item = items.get(itemIndex);
+ format(sb, item, nextDepth);
+ }
+ sb.append(RBRACE);
+ }
+
+ private static void formatCollection(
+ final StringBuilder sb,
+ final Collection<Object> items,
+ final int depth) {
+ sb.append(LBRACE);
+ final int nextDepth = depth + 1;
+ final boolean[] firstItem = {true};
+ items.forEach((final Object item) -> {
+ if (firstItem[0]) {
+ firstItem[0] = false;
+ } else {
+ sb.append(COMMA);
+ }
+ format(sb, item, nextDepth);
+ });
+ sb.append(RBRACE);
+ }
+
+ private static void formatNumber(final StringBuilder sb, final Number number) {
+ if (number instanceof BigDecimal) {
+ final BigDecimal decimalNumber = (BigDecimal) number;
+ sb.append(decimalNumber.toString());
+ } else if (number instanceof Double) {
+ final double doubleNumber = (Double) number;
+ sb.append(doubleNumber);
+ } else if (number instanceof Float) {
+ final float floatNumber = (float) number;
+ sb.append(floatNumber);
+ } else if (number instanceof Byte ||
+ number instanceof Short ||
+ number instanceof Integer ||
+ number instanceof Long) {
+ final long longNumber = number.longValue();
+ sb.append(longNumber);
+ } else {
+ final long longNumber = number.longValue();
+ final double doubleValue = number.doubleValue();
+ if (Double.compare(longNumber, doubleValue) == 0) {
+ sb.append(longNumber);
+ } else {
+ sb.append(doubleValue);
+ }
+ }
+ }
+
+ private static void formatBoolean(final StringBuilder sb, final boolean booleanValue) {
+ sb.append(booleanValue);
+ }
+
+ private static void formatFormattable(
+ final StringBuilder sb,
+ final StringBuilderFormattable formattable) {
+ sb.append(DQUOTE);
+ final int startIndex = sb.length();
+ formattable.formatTo(sb);
+ StringBuilders.escapeJson(sb, startIndex);
+ sb.append(DQUOTE);
+ }
+
+ private static void formatCharArray(final StringBuilder sb, final char[] items) {
+ sb.append(LBRACE);
+ for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {
+ if (itemIndex > 0) {
+ sb.append(COMMA);
+ }
+ final char item = items[itemIndex];
+ sb.append(DQUOTE);
+ final int startIndex = sb.length();
+ sb.append(item);
+ StringBuilders.escapeJson(sb, startIndex);
+ sb.append(DQUOTE);
+ }
+ sb.append(RBRACE);
+ }
+
+ private static void formatBooleanArray(final StringBuilder sb, final boolean[] items) {
+ sb.append(LBRACE);
+ for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {
+ if (itemIndex > 0) {
+ sb.append(COMMA);
+ }
+ final boolean item = items[itemIndex];
+ sb.append(item);
+ }
+ sb.append(RBRACE);
+ }
+
+ private static void formatByteArray(final StringBuilder sb, final byte[] items) {
+ sb.append(LBRACE);
+ for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {
+ if (itemIndex > 0) {
+ sb.append(COMMA);
+ }
+ final byte item = items[itemIndex];
+ sb.append(item);
+ }
+ sb.append(RBRACE);
+ }
+
+ private static void formatShortArray(final StringBuilder sb, final short[] items) {
+ sb.append(LBRACE);
+ for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {
+ if (itemIndex > 0) {
+ sb.append(COMMA);
+ }
+ final short item = items[itemIndex];
+ sb.append(item);
+ }
+ sb.append(RBRACE);
+ }
+
+ private static void formatIntArray(final StringBuilder sb, final int[] items) {
+ sb.append(LBRACE);
+ for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {
+ if (itemIndex > 0) {
+ sb.append(COMMA);
+ }
+ final int item = items[itemIndex];
+ sb.append(item);
+ }
+ sb.append(RBRACE);
+ }
+
+ private static void formatLongArray(final StringBuilder sb, final long[] items) {
+ sb.append(LBRACE);
+ for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {
+ if (itemIndex > 0) {
+ sb.append(COMMA);
+ }
+ final long item = items[itemIndex];
+ sb.append(item);
+ }
+ sb.append(RBRACE);
+ }
+
+ private static void formatFloatArray(final StringBuilder sb, final float[] items) {
+ sb.append(LBRACE);
+ for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {
+ if (itemIndex > 0) {
+ sb.append(COMMA);
+ }
+ final float item = items[itemIndex];
+ sb.append(item);
+ }
+ sb.append(RBRACE);
+ }
+
+ private static void formatDoubleArray(
+ final StringBuilder sb,
+ final double[] items) {
+ sb.append(LBRACE);
+ for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {
+ if (itemIndex > 0) {
+ sb.append(COMMA);
+ }
+ final double item = items[itemIndex];
+ sb.append(item);
+ }
+ sb.append(RBRACE);
+ }
+
+ private static void formatObjectArray(
+ final StringBuilder sb,
+ final Object[] items,
+ final int depth) {
+ sb.append(LBRACE);
+ final int nextDepth = depth + 1;
+ for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {
+ if (itemIndex > 0) {
+ sb.append(COMMA);
+ }
+ final Object item = items[itemIndex];
+ format(sb, item, nextDepth);
+ }
+ sb.append(RBRACE);
+ }
+
+ private static void formatString(final StringBuilder sb, final Object value) {
+ sb.append(DQUOTE);
+ final int startIndex = sb.length();
+ final String valueString = String.valueOf(value);
+ sb.append(valueString);
+ StringBuilders.escapeJson(sb, startIndex);
+ sb.append(DQUOTE);
+ }
+
+}
diff --git a/log4j-api/src/test/java/org/apache/logging/log4j/message/MapMessageTest.java b/log4j-api/src/test/java/org/apache/logging/log4j/message/MapMessageTest.java
index 44a15aa..c70afa0 100644
--- a/log4j-api/src/test/java/org/apache/logging/log4j/message/MapMessageTest.java
+++ b/log4j-api/src/test/java/org/apache/logging/log4j/message/MapMessageTest.java
@@ -16,10 +16,18 @@
*/
package org.apache.logging.log4j.message;
+import com.google.common.base.Strings;
import org.apache.logging.log4j.util.StringBuilderFormattable;
import org.junit.Test;
-import static org.junit.Assert.*;
+import java.math.BigDecimal;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
/**
*
@@ -79,7 +87,7 @@ public class MapMessageTest {
msg.put("message", testMsg);
msg.put("project", "Log4j");
final String result = msg.getFormattedMessage(new String[]{"JSON"});
- final String expected = "{\"message\":\"Test message {}\", \"project\":\"Log4j\"}";
+ final String expected = "{'message':'Test message {}','project':'Log4j'}".replace('\'', '"');
assertEquals(expected, result);
}
@@ -105,6 +113,87 @@ public class MapMessageTest {
}
@Test
+ public void testJsonFormatterNestedObjectSupport() {
+ final String actualJson = new ObjectMapMessage()
+ .with("key1", "val1")
+ .with("key2", Collections.singletonMap("key2.1", "val2.1"))
+ .with("key3", Arrays.asList(
+ 3,
+ (byte) 127,
+ 4.5D,
+ 4.6F,
+ Arrays.asList(true, false),
+ new BigDecimal(30),
+ Collections.singletonMap("key3.3", "val3.3")))
+ .with("key4", new LinkedHashMap<String, Object>() {{
+ put("chars", new char[]{'a', 'b', 'c'});
+ put("booleans", new boolean[]{true, false});
+ put("bytes", new byte[]{1, 2});
+ put("shorts", new short[]{3, 4});
+ put("ints", new int[]{256, 257});
+ put("longs", new long[]{2147483648L, 2147483649L});
+ put("floats", new float[]{1.0F, 1.1F});
+ put("doubles", new double[]{2.0D, 2.1D});
+ put("objects", new Object[]{"foo", "bar"});
+ }})
+ .getFormattedMessage(new String[]{"JSON"});
+ final String expectedJson = ("{" +
+ "'key1':'val1'," +
+ "'key2':{'key2.1':'val2.1'}," +
+ "'key3':[3,127,4.5,4.6,[true,false],30,{'key3.3':'val3.3'}]," +
+ "'key4':{" +
+ "'chars':['a','b','c']," +
+ "'booleans':[true,false]," +
+ "'bytes':[1,2]," +
+ "'shorts':[3,4]," +
+ "'ints':[256,257]," +
+ "'longs':[2147483648,2147483649]," +
+ "'floats':[1.0,1.1]," +
+ "'doubles':[2.0,2.1]," +
+ "'objects':['foo','bar']" +
+ "}}").replace('\'', '"');
+ assertEquals(expectedJson, actualJson);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testJsonFormatterInfiniteRecursionPrevention() {
+ final List<Object> recursiveValue = Arrays.asList(1, null);
+ // noinspection CollectionAddedToSelf
+ recursiveValue.set(1, recursiveValue);
+ new ObjectMapMessage()
+ .with("key", recursiveValue)
+ .getFormattedMessage(new String[]{"JSON"});
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testJsonFormatterMaxDepthViolation() {
+ testJsonFormatterMaxDepth(MapMessageJsonFormatter.MAX_DEPTH - 1);
+ }
+
+ @Test
+ public void testJsonFormatterMaxDepthConformance() {
+ int depth = MapMessageJsonFormatter.MAX_DEPTH - 2;
+ String expectedJson = String
+ .format("{'key':%s1%s}",
+ Strings.repeat("[", depth),
+ Strings.repeat("]", depth))
+ .replace('\'', '"');
+ String actualJson = testJsonFormatterMaxDepth(depth);
+ assertEquals(expectedJson, actualJson);
+ }
+
+ public static String testJsonFormatterMaxDepth(int depth) {
+ List<Object> list = new LinkedList<>();
+ list.add(1);
+ while (--depth > 0) {
+ list = new LinkedList<>(Collections.singletonList(list));
+ }
+ return new ObjectMapMessage()
+ .with("key", list)
+ .getFormattedMessage(new String[]{"JSON"});
+ }
+
+ @Test
public void testJava() {
final String testMsg = "Test message {}";
final StringMapMessage msg = new StringMapMessage();
@@ -151,10 +240,9 @@ public class MapMessageTest {
@Test
public void testJSONFormatNonStringValue() {
- final ObjectMapMessage msg = new ObjectMapMessage()
- .with("key", 1L);
+ final ObjectMapMessage msg = new ObjectMapMessage().with("key", 1L);
final String result = msg.getFormattedMessage(new String[]{"JSON"});
- final String expected = "{\"key\":\"1\"}";
+ final String expected = "{'key':1}".replace('\'', '"');
assertEquals(expected, result);
}