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/09 13:38:05 UTC
[logging-log4j2] 01/05: 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 master
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git
commit 37d865a03eb538f43021b2fd1f190e58d8ad8012
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 0b7c3e0..3222b34 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -170,6 +170,9 @@
</release>
<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 8fad470..d3204f6 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)"
}
----