You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by vy...@apache.org on 2021/07/07 09:03:00 UTC

[logging-log4j2] 01/04: LOG4J2-3074 Add replacement parameter to ReadOnlyStringMapResolver.

This is an automated email from the ASF dual-hosted git repository.

vy pushed a commit to branch release-2.x
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git

commit 3a86977ecf0e1ea27ec59c587284b8617875b65e
Author: Volkan Yazici <vo...@gmail.com>
AuthorDate: Wed Jul 7 10:47:12 2021 +0200

    LOG4J2-3074 Add replacement parameter to ReadOnlyStringMapResolver.
---
 .../json/resolver/ReadOnlyStringMapResolver.java   |  87 +++++++++++++----
 .../resolver/ReadOnlyStringMapResolverTest.java    | 107 +++++++++++++++++++++
 src/changes/changes.xml                            |   3 +
 .../asciidoc/manual/json-template-layout.adoc.vm   |  51 ++++++++--
 4 files changed, 222 insertions(+), 26 deletions(-)

diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ReadOnlyStringMapResolver.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ReadOnlyStringMapResolver.java
index 3735017..788fa5d 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ReadOnlyStringMapResolver.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ReadOnlyStringMapResolver.java
@@ -25,6 +25,7 @@ import org.apache.logging.log4j.util.TriConsumer;
 
 import java.util.Map;
 import java.util.function.Function;
+import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 /**
@@ -39,8 +40,9 @@ import java.util.regex.Pattern;
  * key           = "key" -> string
  * stringified   = "stringified" -> boolean
  *
- * multiAccess   = [ pattern ] , [ flatten ] , [ stringified ]
+ * multiAccess   = [ pattern ] , [ replacement ] , [ flatten ] , [ stringified ]
  * pattern       = "pattern" -> string
+ * replacement   = "replacement" -> string
  * flatten       = "flatten" -> ( boolean | flattenConfig )
  * flattenConfig = [ flattenPrefix ]
  * flattenPrefix = "prefix" -> string
@@ -54,13 +56,20 @@ import java.util.regex.Pattern;
  * Enabling <tt>stringified</tt> flag converts each value to its string
  * representation.
  * <p>
- * Regex provided in the `pattern` is used to match against the keys.
+ * Regex provided in the <tt>pattern</tt> is used to match against the keys.
+ * If provided, <tt>replacement</tt> will be used to replace the matched keys.
+ * These two are effectively equivalent to
+ * <tt>Pattern.compile(pattern).matcher(key).matches()</tt> and
+ * <tt>Pattern.compile(pattern).matcher(key).replaceAll(replacement)</tt> calls.
  *
  * <h3>Garbage Footprint</h3>
  *
  * <tt>stringified</tt> allocates a new <tt>String</tt> for values that are not
  * of type <tt>String</tt>.
  * <p>
+ * <tt>pattern</tt> and <tt>replacement</tt> incur pattern matcher allocation
+ * costs.
+ * <p>
  * Writing certain non-primitive values (e.g., <tt>BigDecimal</tt>,
  * <tt>Set</tt>, etc.) to JSON generates garbage, though most (e.g.,
  * <tt>int</tt>, <tt>long</tt>, <tt>String</tt>, <tt>List</tt>,
@@ -72,21 +81,21 @@ import java.util.regex.Pattern;
  * defined by the actual resolver, e.g., {@link MapResolver},
  * {@link ThreadContextDataResolver}.
  * <p>
- * Resolve the value of the field keyed with <tt>userRole</tt>:
+ * Resolve the value of the field keyed with <tt>user:role</tt>:
  *
  * <pre>
  * {
  *   "$resolver": "…",
- *   "key": "userRole"
+ *   "key": "user:role"
  * }
  * </pre>
  *
- * Resolve the string representation of the <tt>userRank</tt> field value:
+ * Resolve the string representation of the <tt>user:rank</tt> field value:
  *
  * <pre>
  * {
  *   "$resolver": "…",
- *   "key": "userRank",
+ *   "key": "user:rank",
  *   "stringified": true
  * }
  * </pre>
@@ -109,14 +118,35 @@ import java.util.regex.Pattern;
  * }
  * </pre>
  *
+ * Resolve all fields whose keys match with the <tt>user:(role|rank)</tt> regex
+ * into an object:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "…",
+ *   "pattern": "user:(role|rank)"
+ * }
+ * </pre>
+ *
+ * Resolve all fields whose keys match with the <tt>user:(role|rank)</tt> regex
+ * into an object after removing the <tt>user:</tt> prefix in the key:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "…",
+ *   "pattern": "user:(role|rank)",
+ *   "replacement": "$1"
+ * }
+ * </pre>
+ *
  * Merge all fields whose keys are matching with the
- * <tt>user(Role|Rank)</tt> regex into the parent:
+ * <tt>user:(role|rank)</tt> regex into the parent:
  *
  * <pre>
  * {
  *   "$resolver": "…",
  *   "flatten": true,
- *   "pattern": "user(Role|Rank)"
+ *   "pattern": "user:(role|rank)"
  * }
  * </pre>
  *
@@ -162,15 +192,24 @@ class ReadOnlyStringMapResolver implements EventResolver {
         } else {
             throw new IllegalArgumentException("invalid flatten option: " + config);
         }
-        final String key = config.getString("key");
         final String prefix = config.getString(new String[] {"flatten", "prefix"});
+        final String key = config.getString("key");
+        if (key != null && flatten) {
+            throw new IllegalArgumentException(
+                    "key and flatten options cannot be combined: " + config);
+        }
         final String pattern = config.getString("pattern");
+        if (pattern != null && key != null) {
+            throw new IllegalArgumentException(
+                    "pattern and key options cannot be combined: " + config);
+        }
+        final String replacement = config.getString("replacement");
+        if (pattern == null && replacement != null) {
+            throw new IllegalArgumentException(
+                    "replacement cannot be provided without a pattern: " + config);
+        }
         final boolean stringified = config.getBoolean("stringified", false);
         if (key != null) {
-            if (flatten) {
-                throw new IllegalArgumentException(
-                        "both key and flatten options cannot be supplied: " + config);
-            }
             return createKeyResolver(key, stringified, mapAccessor);
         } else {
             final RecyclerFactory recyclerFactory = context.getRecyclerFactory();
@@ -179,6 +218,7 @@ class ReadOnlyStringMapResolver implements EventResolver {
                     flatten,
                     prefix,
                     pattern,
+                    replacement,
                     stringified,
                     mapAccessor);
         }
@@ -216,6 +256,7 @@ class ReadOnlyStringMapResolver implements EventResolver {
             final boolean flatten,
             final String prefix,
             final String pattern,
+            final String replacement,
             final boolean stringified,
             final Function<LogEvent, ReadOnlyStringMap> mapAccessor) {
 
@@ -234,6 +275,7 @@ class ReadOnlyStringMapResolver implements EventResolver {
                         loopContext.prefixedKey = new StringBuilder(prefix);
                     }
                     loopContext.pattern = compiledPattern;
+                    loopContext.replacement = replacement;
                     loopContext.stringified = stringified;
                     return loopContext;
                 });
@@ -262,7 +304,7 @@ class ReadOnlyStringMapResolver implements EventResolver {
 
             @Override
             public void resolve(final LogEvent value, final JsonWriter jsonWriter) {
-                throw new UnsupportedOperationException();
+                resolve(value, jsonWriter, false);
             }
 
             @Override
@@ -310,6 +352,8 @@ class ReadOnlyStringMapResolver implements EventResolver {
 
         private Pattern pattern;
 
+        private String replacement;
+
         private boolean stringified;
 
         private JsonWriter jsonWriter;
@@ -329,10 +373,15 @@ class ReadOnlyStringMapResolver implements EventResolver {
                 final String key,
                 final Object value,
                 final LoopContext loopContext) {
-            final boolean keyMatched =
-                    loopContext.pattern == null ||
-                            loopContext.pattern.matcher(key).matches();
+            final Matcher matcher = loopContext.pattern != null
+                    ? loopContext.pattern.matcher(key)
+                    : null;
+            final boolean keyMatched = matcher == null || matcher.matches();
             if (keyMatched) {
+                final String replacedKey =
+                        matcher != null && loopContext.replacement != null
+                                ? matcher.replaceAll(loopContext.replacement)
+                                : key;
                 final boolean succeedingEntry =
                         loopContext.succeedingEntry ||
                                 loopContext.initJsonWriterStringBuilderLength <
@@ -341,10 +390,10 @@ class ReadOnlyStringMapResolver implements EventResolver {
                     loopContext.jsonWriter.writeSeparator();
                 }
                 if (loopContext.prefix == null) {
-                    loopContext.jsonWriter.writeObjectKey(key);
+                    loopContext.jsonWriter.writeObjectKey(replacedKey);
                 } else {
                     loopContext.prefixedKey.setLength(loopContext.prefix.length());
-                    loopContext.prefixedKey.append(key);
+                    loopContext.prefixedKey.append(replacedKey);
                     loopContext.jsonWriter.writeObjectKey(loopContext.prefixedKey);
                 }
                 if (loopContext.stringified && !(value instanceof String)) {
diff --git a/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/ReadOnlyStringMapResolverTest.java b/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/ReadOnlyStringMapResolverTest.java
new file mode 100644
index 0000000..baaedef
--- /dev/null
+++ b/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/ReadOnlyStringMapResolverTest.java
@@ -0,0 +1,107 @@
+package org.apache.logging.log4j.layout.template.json.resolver;
+
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.impl.Log4jLogEvent;
+import org.apache.logging.log4j.layout.template.json.JsonTemplateLayout;
+import org.apache.logging.log4j.util.SortedArrayStringMap;
+import org.apache.logging.log4j.util.StringMap;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.regex.PatternSyntaxException;
+
+import static org.apache.logging.log4j.layout.template.json.TestHelpers.*;
+import static org.assertj.core.api.Assertions.assertThat;
+
+class ReadOnlyStringMapResolverTest {
+
+    @Test
+    void key_should_not_be_allowed_with_flatten() {
+        verifyConfigFailure(
+                writeJson(asMap(
+                        "$resolver", "mdc",
+                        "key", "foo",
+                        "flatten", true)),
+                IllegalArgumentException.class,
+                "key and flatten options cannot be combined");
+    }
+
+    @Test
+    void invalid_pattern_should_fail() {
+        verifyConfigFailure(
+                writeJson(asMap(
+                        "$resolver", "mdc",
+                        "pattern", "[1")),
+                PatternSyntaxException.class,
+                "Unclosed character");
+    }
+
+    @Test
+    void pattern_should_not_be_allowed_with_key() {
+        verifyConfigFailure(
+                writeJson(asMap(
+                        "$resolver", "mdc",
+                        "key", "foo",
+                        "pattern", "bar")),
+                IllegalArgumentException.class,
+                "pattern and key options cannot be combined");
+    }
+
+    @Test
+    void replacement_should_not_be_allowed_without_pattern() {
+        verifyConfigFailure(
+                writeJson(asMap(
+                        "$resolver", "mdc",
+                        "replacement", "$1")),
+                IllegalArgumentException.class,
+                "replacement cannot be provided without a pattern");
+    }
+
+    private void verifyConfigFailure(
+            final String eventTemplate,
+            final Class<? extends Throwable> failureClass,
+            final String failureMessage) {
+        Assertions
+                .assertThatThrownBy(() -> JsonTemplateLayout
+                        .newBuilder()
+                        .setConfiguration(CONFIGURATION)
+                        .setEventTemplate(eventTemplate)
+                        .build())
+                .isInstanceOf(failureClass)
+                .hasMessageContaining(failureMessage);
+    }
+
+    @Test
+    void pattern_replacement_should_work() {
+
+        // Create the event template.
+        final String eventTemplate = writeJson(asMap(
+                "$resolver", "mdc",
+                "pattern", "user:(role|rank)",
+                "replacement", "$1"));
+
+        // Create the layout.
+        final JsonTemplateLayout layout = JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setEventTemplate(eventTemplate)
+                .build();
+
+        // Create the log event.
+        final StringMap contextData = new SortedArrayStringMap();
+        contextData.putValue("user:role", "engineer");
+        contextData.putValue("user:rank", "senior");
+        final LogEvent logEvent = Log4jLogEvent
+                .newBuilder()
+                .setContextData(contextData)
+                .build();
+
+        // Check the serialized event.
+        usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+            assertThat(accessor.getString("role")).isEqualTo("engineer");
+            assertThat(accessor.getString("rank")).isEqualTo("senior");
+        });
+
+    }
+
+}
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 6b35a21..bbddae2 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -31,6 +31,9 @@
     -->
     <release version="2.15.0" date="2021-MM-DD" description="GA Release 2.15.0">
       <!-- ADDS -->
+      <action issue="LOG4J2-3074" dev="vy" type="add">
+        Add replacement parameter to ReadOnlyStringMapResolver.
+      </action>
       <action issue="LOG4J2-3051" dev="vy" type="add">
         Add CaseConverterResolver to JsonTemplateLayout.
       </action>
diff --git a/src/site/asciidoc/manual/json-template-layout.adoc.vm b/src/site/asciidoc/manual/json-template-layout.adoc.vm
index 97e9a05..768e222 100644
--- a/src/site/asciidoc/manual/json-template-layout.adoc.vm
+++ b/src/site/asciidoc/manual/json-template-layout.adoc.vm
@@ -1279,8 +1279,9 @@ singleAccess  = key , [ stringified ]
 key           = "key" -> string
 stringified   = "stringified" -> boolean
 
-multiAccess   = [ pattern ] , [ flatten ] , [ stringified ]
+multiAccess   = [ pattern ] , [ replacement ] , [ flatten ] , [ stringified ]
 pattern       = "pattern" -> string
+replacement   = "replacement" -> string
 flatten       = "flatten" -> ( boolean | flattenConfig )
 flattenConfig = [ flattenPrefix ]
 flattenPrefix = "prefix" -> string
@@ -1290,32 +1291,45 @@ flattenPrefix = "prefix" -> string
 multitude of fields. If `flatten` is provided, `multiAccess` merges the fields
 with the parent, otherwise creates a new JSON object containing the values.
 
+Enabling `stringified` flag converts each value to its string representation.
+
+Regex provided in the `pattern` is used to match against the keys. If provided,
+`replacement` will be used to replace the matched keys. These two are
+effectively equivalent to `Pattern.compile(pattern).matcher(key).matches()` and
+`Pattern.compile(pattern).matcher(key).replaceAll(replacement)` calls.
+
 [WARNING]
 ====
 Regarding garbage footprint, `stringified` flag translates to
 `String.valueOf(value)`, hence mind not-`String`-typed values.
+
+`pattern` and `replacement` incur pattern matcher allocation costs.
+
+Writing certain non-primitive values (e.g., `BigDecimal`, `Set`, etc.) to JSON
+generates garbage, though most (e.g., `int`, `long`, `String`, `List`,
+`boolean[]`, etc.) don't.
 ====
 
 `"${dollar}resolver"` is left out in the following examples, since it is to be
 defined by the actual resolver, e.g., `map`, `mdc`.
 
-Resolve the value of the field keyed with `userRole`:
+Resolve the value of the field keyed with `user:role`:
 
 [source,json]
 ----
 {
   "$resolver": "…",
-  "key": "userRole"
+  "key": "user:role"
 }
 ----
 
-Resolve the string representation of the `userRank` field value:
+Resolve the string representation of the `user:rank` field value:
 
 [source,json]
 ----
 {
   "$resolver": "…",
-  "key": "userRank",
+  "key": "user:rank",
   "stringified": true
 }
 ----
@@ -1339,7 +1353,30 @@ Resolve all fields into an object such that values are converted to string:
 }
 ----
 
-Merge all fields whose keys are matching with the `user(Role|Rank)` regex into
+Resolve all fields whose keys match with the `user:(role|rank)` regex into an
+object:
+
+[source,json]
+----
+{
+  "$resolver": "…",
+  "pattern": "user:(role|rank)"
+}
+----
+
+Resolve all fields whose keys match with the `user:(role|rank)` regex into an
+object after removing the `user:` prefix in the key:
+
+[source,json]
+----
+{
+  "$resolver": "…",
+  "pattern": "user:(role|rank)",
+  "replacement": "$1"
+}
+----
+
+Merge all fields whose keys are matching with the `user:(role|rank)` regex into
 the parent:
 
 [source,json]
@@ -1347,7 +1384,7 @@ the parent:
 {
   "$resolver": "…",
   "flatten": true,
-  "pattern": "user(Role|Rank)"
+  "pattern": "user:(role|rank)"
 }
 ----