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 <[email protected]> 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)" } ----
