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 2020/11/24 15:07:04 UTC

[logging-log4j2] branch release-2.x updated: LOG4J2-2962 Enrich "map" resolver by unifying its backend with "mdc" resolver.

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


The following commit(s) were added to refs/heads/release-2.x by this push:
     new a71403e  LOG4J2-2962 Enrich "map" resolver by unifying its backend with "mdc" resolver.
a71403e is described below

commit a71403ee2e5694a2699d68ddf09e8df6ccfc8b57
Author: Volkan Yazıcı <vo...@gmail.com>
AuthorDate: Tue Nov 17 22:54:19 2020 +0100

    LOG4J2-2962 Enrich "map" resolver by unifying its backend with "mdc" resolver.
---
 .../layout/template/json/resolver/MapResolver.java |  74 ++---
 .../template/json/resolver/MapResolverFactory.java |   2 +-
 ...esolver.java => ReadOnlyStringMapResolver.java} | 121 +++++---
 .../json/resolver/ThreadContextDataResolver.java   | 325 +--------------------
 .../template/json/JsonTemplateLayoutTest.java      | 179 ++++++++++--
 src/changes/changes.xml                            |   3 +
 .../asciidoc/manual/json-template-layout.adoc.vm   | 260 ++++++++---------
 7 files changed, 375 insertions(+), 589 deletions(-)

diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MapResolver.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MapResolver.java
index f26bf86..5cc07eb 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MapResolver.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MapResolver.java
@@ -17,75 +17,35 @@
 package org.apache.logging.log4j.layout.template.json.resolver;
 
 import org.apache.logging.log4j.core.LogEvent;
-import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
 import org.apache.logging.log4j.message.MapMessage;
 import org.apache.logging.log4j.message.Message;
-import org.apache.logging.log4j.util.IndexedReadOnlyStringMap;
+import org.apache.logging.log4j.util.ReadOnlyStringMap;
 
 /**
- * {@link MapMessage} field resolver.
+ * {@link MapMessage} resolver.
  *
- * <h3>Configuration</h3>
- *
- * <pre>
- * config      = key , [ stringified ]
- * key         = "key" -> string
- * stringified = "stringified" -> boolean
- * </pre>
- *
- * <h3>Examples</h3>
- *
- * Resolve the <tt>userRole</tt> field of the message:
- *
- * <pre>
- * {
- *   "$resolver": "map",
- *   "key": "userRole"
- * }
- * </pre>
+ * @see ReadOnlyStringMapResolver
  */
-final class MapResolver implements EventResolver {
-
-    private final String key;
+final class MapResolver extends ReadOnlyStringMapResolver {
 
-    private final boolean stringified;
-
-    static String getName() {
-        return "map";
+    MapResolver(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
+        super(context, config, MapResolver::toMap);
     }
 
-    MapResolver(final TemplateResolverConfig config) {
-        this.key = config.getString("key");
-        this.stringified = config.getBoolean("stringified", false);
-        if (key == null) {
-            throw new IllegalArgumentException("missing key: " + config);
-        }
-    }
-
-    @Override
-    public boolean isResolvable(final LogEvent logEvent) {
-        return logEvent.getMessage() instanceof MapMessage;
-    }
-
-    @Override
-    public void resolve(
-            final LogEvent logEvent,
-            final JsonWriter jsonWriter) {
+    private static ReadOnlyStringMap toMap(final LogEvent logEvent) {
         final Message message = logEvent.getMessage();
         if (!(message instanceof MapMessage)) {
-            jsonWriter.writeNull();
-        } else {
-            @SuppressWarnings("unchecked")
-            MapMessage<?, Object> mapMessage = (MapMessage<?, Object>) message;
-            final IndexedReadOnlyStringMap map = mapMessage.getIndexedReadOnlyStringMap();
-            final Object value = map.getValue(key);
-            if (stringified) {
-                final String stringifiedValue = String.valueOf(value);
-                jsonWriter.writeString(stringifiedValue);
-            } else {
-                jsonWriter.writeValue(value);
-            }
+            return null;
         }
+        @SuppressWarnings("unchecked")
+        final MapMessage<?, Object> mapMessage = (MapMessage<?, Object>) message;
+        return mapMessage.getIndexedReadOnlyStringMap();
+    }
+
+    static String getName() {
+        return "map";
     }
 
 }
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MapResolverFactory.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MapResolverFactory.java
index a639719..53092c9 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MapResolverFactory.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MapResolverFactory.java
@@ -35,7 +35,7 @@ final class MapResolverFactory implements EventResolverFactory<MapResolver> {
     public MapResolver create(
             final EventResolverContext context,
             final TemplateResolverConfig config) {
-        return new MapResolver(config);
+        return new MapResolver(context, config);
     }
 
 }
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ThreadContextDataResolver.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ReadOnlyStringMapResolver.java
similarity index 72%
copy from log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ThreadContextDataResolver.java
copy to log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ReadOnlyStringMapResolver.java
index dda41d3..3735017 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ThreadContextDataResolver.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ReadOnlyStringMapResolver.java
@@ -24,10 +24,11 @@ import org.apache.logging.log4j.util.ReadOnlyStringMap;
 import org.apache.logging.log4j.util.TriConsumer;
 
 import java.util.Map;
+import java.util.function.Function;
 import java.util.regex.Pattern;
 
 /**
- * Mapped Diagnostic Context (MDC), aka. Thread Context Data, resolver.
+ * {@link ReadOnlyStringMap} resolver.
  *
  * <h3>Configuration</h3>
  *
@@ -45,87 +46,111 @@ import java.util.regex.Pattern;
  * flattenPrefix = "prefix" -> string
  * </pre>
  *
- * Note that <tt>singleAccess</tt> resolves the MDC value as is, whilst
- * <tt>multiAccess</tt> resolves a multitude of MDC values. If <tt>flatten</tt>
- * is provided, <tt>multiAccess</tt> merges the values with the parent,
+ * Note that <tt>singleAccess</tt> resolves a single field, whilst
+ * <tt>multiAccess</tt> resolves a multitude of fields. If <tt>flatten</tt>
+ * is provided, <tt>multiAccess</tt> merges the fields with the parent,
  * otherwise creates a new JSON object containing the values.
+ * <p>
+ * 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.
+ *
+ * <h3>Garbage Footprint</h3>
+ *
+ * <tt>stringified</tt> allocates a new <tt>String</tt> for values that are not
+ * of type <tt>String</tt>.
+ * <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>,
+ * <tt>boolean[]</tt>, etc.) don't.
  *
  * <h3>Examples</h3>
  *
- * Resolve the <tt>userRole</tt> MDC value:
+ * <tt>"$resolver"</tt> is left out in the following examples, since it is to be
+ * defined by the actual resolver, e.g., {@link MapResolver},
+ * {@link ThreadContextDataResolver}.
+ * <p>
+ * Resolve the value of the field keyed with <tt>userRole</tt>:
  *
  * <pre>
  * {
- *   "$resolver": "mdc",
+ *   "$resolver": "…",
  *   "key": "userRole"
  * }
  * </pre>
  *
- * Resolve the string representation of the <tt>userRank</tt> MDC value:
+ * Resolve the string representation of the <tt>userRank</tt> field value:
  *
  * <pre>
  * {
- *   "$resolver": "mdc",
+ *   "$resolver": "…",
  *   "key": "userRank",
  *   "stringified": true
  * }
  * </pre>
  *
- * Resolve all MDC entries into an object:
+ * Resolve all fields into an object:
  *
  * <pre>
  * {
- *   "$resolver": "mdc"
+ *   "$resolver": "…"
  * }
  * </pre>
  *
- * Resolve all MDC entries into an object such that values are converted to
+ * Resolve all fields into an object such that values are converted to
  * string:
  *
  * <pre>
  * {
- *   "$resolver": "mdc",
+ *   "$resolver": "…",
  *   "stringified": true
  * }
  * </pre>
  *
- * Merge all MDC entries whose keys are matching with the
+ * Merge all fields whose keys are matching with the
  * <tt>user(Role|Rank)</tt> regex into the parent:
  *
  * <pre>
  * {
- *   "$resolver": "mdc",
+ *   "$resolver": "…",
  *   "flatten": true,
  *   "pattern": "user(Role|Rank)"
  * }
  * </pre>
  *
- * After converting the corresponding entries to string, merge all MDC entries
+ * After converting the corresponding field values to string, merge all fields
  * to parent such that keys are prefixed with <tt>_</tt>:
  *
  * <pre>
  * {
- *   "$resolver": "mdc",
+ *   "$resolver": "…",
  *   "stringified": true,
  *   "flatten": {
  *     "prefix": "_"
  *   }
  * }
  * </pre>
+ *
+ * @see MapResolver
+ * @see ThreadContextDataResolver
  */
-final class ThreadContextDataResolver implements EventResolver {
+class ReadOnlyStringMapResolver implements EventResolver {
 
     private final EventResolver internalResolver;
 
-    ThreadContextDataResolver(
+    ReadOnlyStringMapResolver(
             final EventResolverContext context,
-            final TemplateResolverConfig config) {
-        this.internalResolver = createResolver(context, config);
+            final TemplateResolverConfig config,
+            final Function<LogEvent, ReadOnlyStringMap> mapAccessor) {
+        this.internalResolver = createResolver(context, config, mapAccessor);
     }
 
     private static EventResolver createResolver(
             final EventResolverContext context,
-            final TemplateResolverConfig config) {
+            final TemplateResolverConfig config,
+            final Function<LogEvent, ReadOnlyStringMap> mapAccessor) {
         final Object flattenObject = config.getObject("flatten");
         final boolean flatten;
         if (flattenObject == null) {
@@ -146,28 +171,35 @@ final class ThreadContextDataResolver implements EventResolver {
                 throw new IllegalArgumentException(
                         "both key and flatten options cannot be supplied: " + config);
             }
-            return createKeyResolver(key, stringified);
+            return createKeyResolver(key, stringified, mapAccessor);
         } else {
             final RecyclerFactory recyclerFactory = context.getRecyclerFactory();
-            return createResolver(recyclerFactory, flatten, prefix, pattern, stringified);
+            return createResolver(
+                    recyclerFactory,
+                    flatten,
+                    prefix,
+                    pattern,
+                    stringified,
+                    mapAccessor);
         }
     }
 
     private static EventResolver createKeyResolver(
             final String key,
-            final boolean stringified) {
+            final boolean stringified,
+            final Function<LogEvent, ReadOnlyStringMap> mapAccessor) {
         return new EventResolver() {
 
             @Override
             public boolean isResolvable(final LogEvent logEvent) {
-                final ReadOnlyStringMap contextData = logEvent.getContextData();
-                return contextData != null && contextData.containsKey(key);
+                final ReadOnlyStringMap map = mapAccessor.apply(logEvent);
+                return map != null && map.containsKey(key);
             }
 
             @Override
             public void resolve(final LogEvent logEvent, final JsonWriter jsonWriter) {
-                final ReadOnlyStringMap contextData = logEvent.getContextData();
-                final Object value = contextData == null ? null : contextData.getValue(key);
+                final ReadOnlyStringMap map = mapAccessor.apply(logEvent);
+                final Object value = map == null ? null : map.getValue(key);
                 if (stringified) {
                     final String valueString = String.valueOf(value);
                     jsonWriter.writeString(valueString);
@@ -184,7 +216,8 @@ final class ThreadContextDataResolver implements EventResolver {
             final boolean flatten,
             final String prefix,
             final String pattern,
-            final boolean stringified) {
+            final boolean stringified,
+            final Function<LogEvent, ReadOnlyStringMap> mapAccessor) {
 
         // Compile the pattern.
         final Pattern compiledPattern =
@@ -206,13 +239,14 @@ final class ThreadContextDataResolver implements EventResolver {
                 });
 
         // Create the resolver.
-        return createResolver(flatten, loopContextRecycler);
+        return createResolver(flatten, loopContextRecycler, mapAccessor);
 
     }
 
     private static EventResolver createResolver(
             final boolean flatten,
-            final Recycler<LoopContext> loopContextRecycler) {
+            final Recycler<LoopContext> loopContextRecycler,
+            final Function<LogEvent, ReadOnlyStringMap> mapAccessor) {
         return new EventResolver() {
 
             @Override
@@ -222,8 +256,8 @@ final class ThreadContextDataResolver implements EventResolver {
 
             @Override
             public boolean isResolvable(final LogEvent logEvent) {
-                final ReadOnlyStringMap contextData = logEvent.getContextData();
-                return contextData != null && !contextData.isEmpty();
+                final ReadOnlyStringMap map = mapAccessor.apply(logEvent);
+                return map != null && !map.isEmpty();
             }
 
             @Override
@@ -237,16 +271,16 @@ final class ThreadContextDataResolver implements EventResolver {
                     final JsonWriter jsonWriter,
                     final boolean succeedingEntry) {
 
-                // Retrieve the context data.
-                final ReadOnlyStringMap contextData = logEvent.getContextData();
-                if (contextData == null || contextData.isEmpty()) {
+                // Retrieve the map.
+                final ReadOnlyStringMap map = mapAccessor.apply(logEvent);
+                if (map == null || map.isEmpty()) {
                     if (!flatten) {
                         jsonWriter.writeNull();
                     }
                     return;
                 }
 
-                // Resolve the context data.
+                // Resolve the map.
                 if (!flatten) {
                     jsonWriter.writeObjectStart();
                 }
@@ -255,7 +289,7 @@ final class ThreadContextDataResolver implements EventResolver {
                 loopContext.initJsonWriterStringBuilderLength = jsonWriter.getStringBuilder().length();
                 loopContext.succeedingEntry = flatten && succeedingEntry;
                 try {
-                    contextData.forEach(LoopMethod.INSTANCE, loopContext);
+                    map.forEach(LoopMethod.INSTANCE, loopContext);
                 } finally {
                     loopContextRecycler.release(loopContext);
                 }
@@ -286,9 +320,9 @@ final class ThreadContextDataResolver implements EventResolver {
 
     }
 
-    private static final class LoopMethod implements TriConsumer<String, Object, LoopContext> {
+    private enum LoopMethod implements TriConsumer<String, Object, LoopContext> {
 
-        private static final LoopMethod INSTANCE = new LoopMethod();
+        INSTANCE;
 
         @Override
         public void accept(
@@ -324,10 +358,6 @@ final class ThreadContextDataResolver implements EventResolver {
 
     }
 
-    static String getName() {
-        return "mdc";
-    }
-
     @Override
     public boolean isFlattening() {
         return internalResolver.isFlattening();
@@ -335,8 +365,7 @@ final class ThreadContextDataResolver implements EventResolver {
 
     @Override
     public boolean isResolvable(final LogEvent logEvent) {
-        final ReadOnlyStringMap contextData = logEvent.getContextData();
-        return contextData != null && !contextData.isEmpty();
+        return internalResolver.isResolvable(logEvent);
     }
 
     @Override
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ThreadContextDataResolver.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ThreadContextDataResolver.java
index dda41d3..95e3677 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ThreadContextDataResolver.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ThreadContextDataResolver.java
@@ -17,341 +17,22 @@
 package org.apache.logging.log4j.layout.template.json.resolver;
 
 import org.apache.logging.log4j.core.LogEvent;
-import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
-import org.apache.logging.log4j.layout.template.json.util.Recycler;
-import org.apache.logging.log4j.layout.template.json.util.RecyclerFactory;
-import org.apache.logging.log4j.util.ReadOnlyStringMap;
-import org.apache.logging.log4j.util.TriConsumer;
-
-import java.util.Map;
-import java.util.regex.Pattern;
 
 /**
  * Mapped Diagnostic Context (MDC), aka. Thread Context Data, resolver.
  *
- * <h3>Configuration</h3>
- *
- * <pre>
- * config        = singleAccess | multiAccess
- *
- * singleAccess  = key , [ stringified ]
- * key           = "key" -> string
- * stringified   = "stringified" -> boolean
- *
- * multiAccess   = [ pattern ] , [ flatten ] , [ stringified ]
- * pattern       = "pattern" -> string
- * flatten       = "flatten" -> ( boolean | flattenConfig )
- * flattenConfig = [ flattenPrefix ]
- * flattenPrefix = "prefix" -> string
- * </pre>
- *
- * Note that <tt>singleAccess</tt> resolves the MDC value as is, whilst
- * <tt>multiAccess</tt> resolves a multitude of MDC values. If <tt>flatten</tt>
- * is provided, <tt>multiAccess</tt> merges the values with the parent,
- * otherwise creates a new JSON object containing the values.
- *
- * <h3>Examples</h3>
- *
- * Resolve the <tt>userRole</tt> MDC value:
- *
- * <pre>
- * {
- *   "$resolver": "mdc",
- *   "key": "userRole"
- * }
- * </pre>
- *
- * Resolve the string representation of the <tt>userRank</tt> MDC value:
- *
- * <pre>
- * {
- *   "$resolver": "mdc",
- *   "key": "userRank",
- *   "stringified": true
- * }
- * </pre>
- *
- * Resolve all MDC entries into an object:
- *
- * <pre>
- * {
- *   "$resolver": "mdc"
- * }
- * </pre>
- *
- * Resolve all MDC entries into an object such that values are converted to
- * string:
- *
- * <pre>
- * {
- *   "$resolver": "mdc",
- *   "stringified": true
- * }
- * </pre>
- *
- * Merge all MDC entries whose keys are matching with the
- * <tt>user(Role|Rank)</tt> regex into the parent:
- *
- * <pre>
- * {
- *   "$resolver": "mdc",
- *   "flatten": true,
- *   "pattern": "user(Role|Rank)"
- * }
- * </pre>
- *
- * After converting the corresponding entries to string, merge all MDC entries
- * to parent such that keys are prefixed with <tt>_</tt>:
- *
- * <pre>
- * {
- *   "$resolver": "mdc",
- *   "stringified": true,
- *   "flatten": {
- *     "prefix": "_"
- *   }
- * }
- * </pre>
+ * @see ReadOnlyStringMapResolver
  */
-final class ThreadContextDataResolver implements EventResolver {
-
-    private final EventResolver internalResolver;
+final class ThreadContextDataResolver extends ReadOnlyStringMapResolver {
 
     ThreadContextDataResolver(
             final EventResolverContext context,
             final TemplateResolverConfig config) {
-        this.internalResolver = createResolver(context, config);
-    }
-
-    private static EventResolver createResolver(
-            final EventResolverContext context,
-            final TemplateResolverConfig config) {
-        final Object flattenObject = config.getObject("flatten");
-        final boolean flatten;
-        if (flattenObject == null) {
-            flatten = false;
-        } else if (flattenObject instanceof Boolean) {
-            flatten = (boolean) flattenObject;
-        } else if (flattenObject instanceof Map) {
-            flatten = true;
-        } 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 pattern = config.getString("pattern");
-        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);
-        } else {
-            final RecyclerFactory recyclerFactory = context.getRecyclerFactory();
-            return createResolver(recyclerFactory, flatten, prefix, pattern, stringified);
-        }
-    }
-
-    private static EventResolver createKeyResolver(
-            final String key,
-            final boolean stringified) {
-        return new EventResolver() {
-
-            @Override
-            public boolean isResolvable(final LogEvent logEvent) {
-                final ReadOnlyStringMap contextData = logEvent.getContextData();
-                return contextData != null && contextData.containsKey(key);
-            }
-
-            @Override
-            public void resolve(final LogEvent logEvent, final JsonWriter jsonWriter) {
-                final ReadOnlyStringMap contextData = logEvent.getContextData();
-                final Object value = contextData == null ? null : contextData.getValue(key);
-                if (stringified) {
-                    final String valueString = String.valueOf(value);
-                    jsonWriter.writeString(valueString);
-                } else {
-                    jsonWriter.writeValue(value);
-                }
-            }
-
-        };
-    }
-
-    private static EventResolver createResolver(
-            final RecyclerFactory recyclerFactory,
-            final boolean flatten,
-            final String prefix,
-            final String pattern,
-            final boolean stringified) {
-
-        // Compile the pattern.
-        final Pattern compiledPattern =
-                pattern == null
-                        ? null
-                        : Pattern.compile(pattern);
-
-        // Create the recycler for the loop context.
-        final Recycler<LoopContext> loopContextRecycler =
-                recyclerFactory.create(() -> {
-                    final LoopContext loopContext = new LoopContext();
-                    if (prefix != null) {
-                        loopContext.prefix = prefix;
-                        loopContext.prefixedKey = new StringBuilder(prefix);
-                    }
-                    loopContext.pattern = compiledPattern;
-                    loopContext.stringified = stringified;
-                    return loopContext;
-                });
-
-        // Create the resolver.
-        return createResolver(flatten, loopContextRecycler);
-
-    }
-
-    private static EventResolver createResolver(
-            final boolean flatten,
-            final Recycler<LoopContext> loopContextRecycler) {
-        return new EventResolver() {
-
-            @Override
-            public boolean isFlattening() {
-                return flatten;
-            }
-
-            @Override
-            public boolean isResolvable(final LogEvent logEvent) {
-                final ReadOnlyStringMap contextData = logEvent.getContextData();
-                return contextData != null && !contextData.isEmpty();
-            }
-
-            @Override
-            public void resolve(final LogEvent value, final JsonWriter jsonWriter) {
-                throw new UnsupportedOperationException();
-            }
-
-            @Override
-            public void resolve(
-                    final LogEvent logEvent,
-                    final JsonWriter jsonWriter,
-                    final boolean succeedingEntry) {
-
-                // Retrieve the context data.
-                final ReadOnlyStringMap contextData = logEvent.getContextData();
-                if (contextData == null || contextData.isEmpty()) {
-                    if (!flatten) {
-                        jsonWriter.writeNull();
-                    }
-                    return;
-                }
-
-                // Resolve the context data.
-                if (!flatten) {
-                    jsonWriter.writeObjectStart();
-                }
-                final LoopContext loopContext = loopContextRecycler.acquire();
-                loopContext.jsonWriter = jsonWriter;
-                loopContext.initJsonWriterStringBuilderLength = jsonWriter.getStringBuilder().length();
-                loopContext.succeedingEntry = flatten && succeedingEntry;
-                try {
-                    contextData.forEach(LoopMethod.INSTANCE, loopContext);
-                } finally {
-                    loopContextRecycler.release(loopContext);
-                }
-                if (!flatten) {
-                    jsonWriter.writeObjectEnd();
-                }
-
-            }
-
-        };
-    }
-
-    private static final class LoopContext {
-
-        private String prefix;
-
-        private StringBuilder prefixedKey;
-
-        private Pattern pattern;
-
-        private boolean stringified;
-
-        private JsonWriter jsonWriter;
-
-        private int initJsonWriterStringBuilderLength;
-
-        private boolean succeedingEntry;
-
-    }
-
-    private static final class LoopMethod implements TriConsumer<String, Object, LoopContext> {
-
-        private static final LoopMethod INSTANCE = new LoopMethod();
-
-        @Override
-        public void accept(
-                final String key,
-                final Object value,
-                final LoopContext loopContext) {
-            final boolean keyMatched =
-                    loopContext.pattern == null ||
-                            loopContext.pattern.matcher(key).matches();
-            if (keyMatched) {
-                final boolean succeedingEntry =
-                        loopContext.succeedingEntry ||
-                                loopContext.initJsonWriterStringBuilderLength <
-                                        loopContext.jsonWriter.getStringBuilder().length();
-                if (succeedingEntry) {
-                    loopContext.jsonWriter.writeSeparator();
-                }
-                if (loopContext.prefix == null) {
-                    loopContext.jsonWriter.writeObjectKey(key);
-                } else {
-                    loopContext.prefixedKey.setLength(loopContext.prefix.length());
-                    loopContext.prefixedKey.append(key);
-                    loopContext.jsonWriter.writeObjectKey(loopContext.prefixedKey);
-                }
-                if (loopContext.stringified && !(value instanceof String)) {
-                    final String valueString = String.valueOf(value);
-                    loopContext.jsonWriter.writeString(valueString);
-                } else {
-                    loopContext.jsonWriter.writeValue(value);
-                }
-            }
-        }
-
+        super(context, config, LogEvent::getContextData);
     }
 
     static String getName() {
         return "mdc";
     }
 
-    @Override
-    public boolean isFlattening() {
-        return internalResolver.isFlattening();
-    }
-
-    @Override
-    public boolean isResolvable(final LogEvent logEvent) {
-        final ReadOnlyStringMap contextData = logEvent.getContextData();
-        return contextData != null && !contextData.isEmpty();
-    }
-
-    @Override
-    public void resolve(
-            final LogEvent logEvent,
-            final JsonWriter jsonWriter) {
-        internalResolver.resolve(logEvent, jsonWriter);
-    }
-
-    @Override
-    public void resolve(
-            final LogEvent logEvent,
-            final JsonWriter jsonWriter,
-            final boolean succeedingEntry) {
-        internalResolver.resolve(logEvent, jsonWriter, succeedingEntry);
-    }
-
 }
diff --git a/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/JsonTemplateLayoutTest.java b/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/JsonTemplateLayoutTest.java
index 075dfb5..0fdf369 100644
--- a/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/JsonTemplateLayoutTest.java
+++ b/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/JsonTemplateLayoutTest.java
@@ -708,9 +708,7 @@ public class JsonTemplateLayoutTest {
         final String mdcDirectlyAccessedValue = "mdcValue1";
         contextData.putValue(mdcDirectlyAccessedKey, mdcDirectlyAccessedValue);
         final String mdcDirectlyAccessedNullPropertyKey = "mdcKey2";
-        final String mdcDirectlyAccessedNullPropertyValue = null;
-        // noinspection ConstantConditions
-        contextData.putValue(mdcDirectlyAccessedNullPropertyKey, mdcDirectlyAccessedNullPropertyValue);
+        contextData.putValue(mdcDirectlyAccessedNullPropertyKey, null);
         final LogEvent logEvent = Log4jLogEvent
                 .newBuilder()
                 .setLoggerName(LOGGER_NAME)
@@ -719,14 +717,57 @@ public class JsonTemplateLayoutTest {
                 .setContextData(contextData)
                 .build();
 
+        // Check the serialized event.
+        testReadOnlyStringMapKeyAccess(
+                mdcDirectlyAccessedKey,
+                mdcDirectlyAccessedValue,
+                mdcDirectlyAccessedNullPropertyKey,
+                logEvent,
+                "mdc");
+
+    }
+
+    @Test
+    public void test_map_key_access() {
+
+        // Create the log event.
+        final String directlyAccessedKey = "mapKey1";
+        final String directlyAccessedValue = "mapValue1";
+        final String directlyAccessedNullPropertyKey = "mapKey2";
+        final Message message = new StringMapMessage()
+                .with(directlyAccessedKey, directlyAccessedValue);
+        final LogEvent logEvent = Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(LOGGER_NAME)
+                .setLevel(Level.INFO)
+                .setMessage(message)
+                .build();
+
+        // Check the serialized event.
+        testReadOnlyStringMapKeyAccess(
+                directlyAccessedKey,
+                directlyAccessedValue,
+                directlyAccessedNullPropertyKey,
+                logEvent,
+                "map");
+
+    }
+
+    private static void testReadOnlyStringMapKeyAccess(
+            final String directlyAccessedKey,
+            final String directlyAccessedValue,
+            final String directlyAccessedNullPropertyKey,
+            final LogEvent logEvent,
+            final String resolverName) {
+
         // Create the event template.
         String eventTemplate = writeJson(Map(
-                mdcDirectlyAccessedKey, Map(
-                        "$resolver", "mdc",
-                        "key", mdcDirectlyAccessedKey),
-                mdcDirectlyAccessedNullPropertyKey, Map(
-                        "$resolver", "mdc",
-                        "key", mdcDirectlyAccessedNullPropertyKey)));
+                directlyAccessedKey, Map(
+                        "$resolver", resolverName,
+                        "key", directlyAccessedKey),
+                directlyAccessedNullPropertyKey, Map(
+                        "$resolver", resolverName,
+                        "key", directlyAccessedNullPropertyKey)));
 
         // Create the layout.
         final JsonTemplateLayout layout = JsonTemplateLayout
@@ -738,8 +779,8 @@ public class JsonTemplateLayoutTest {
 
         // Check the serialized event.
         usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
-            assertThat(accessor.getString(mdcDirectlyAccessedKey)).isEqualTo(mdcDirectlyAccessedValue);
-            assertThat(accessor.getString(mdcDirectlyAccessedNullPropertyKey)).isNull();
+            assertThat(accessor.getString(directlyAccessedKey)).isEqualTo(directlyAccessedValue);
+            assertThat(accessor.getString(directlyAccessedNullPropertyKey)).isNull();
         });
 
     }
@@ -764,12 +805,57 @@ public class JsonTemplateLayoutTest {
                 .setContextData(contextData)
                 .build();
 
+        // Check the serialized event.
+        testReadOnlyStringMapPattern(
+                mdcPatternMatchedKey,
+                mdcPatternMatchedValue,
+                mdcPatternMismatchedKey,
+                logEvent,
+                "mdc");
+
+    }
+
+    @Test
+    public void test_map_pattern() {
+
+        // Create the log event.
+        final String patternMatchedKey = "mapKey1";
+        final String patternMatchedValue = "mapValue1";
+        final String patternMismatchedKey = "mapKey2";
+        final String patternMismatchedValue = "mapValue2";
+        final Message message = new StringMapMessage()
+                .with(patternMatchedKey, patternMatchedValue)
+                .with(patternMismatchedKey, patternMismatchedValue);
+        final LogEvent logEvent = Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(LOGGER_NAME)
+                .setLevel(Level.INFO)
+                .setMessage(message)
+                .build();
+
+        // Check the serialized event.
+        testReadOnlyStringMapPattern(
+                patternMatchedKey,
+                patternMatchedValue,
+                patternMismatchedKey,
+                logEvent,
+                "map");
+
+    }
+
+    private static void testReadOnlyStringMapPattern(
+            final String patternMatchedKey,
+            final String patternMatchedValue,
+            final String patternMismatchedKey,
+            final LogEvent logEvent,
+            final String resolverName) {
+
         // Create the event template.
-        final String mdcFieldName = "mdc";
+        final String mapFieldName = "map";
         final String eventTemplate = writeJson(Map(
-                mdcFieldName, Map(
-                        "$resolver", "mdc",
-                        "pattern", mdcPatternMatchedKey)));
+                mapFieldName, Map(
+                        "$resolver", resolverName,
+                        "pattern", patternMatchedKey)));
 
         // Create the layout.
         final JsonTemplateLayout layout = JsonTemplateLayout
@@ -781,8 +867,8 @@ public class JsonTemplateLayoutTest {
 
         // Check the serialized event.
         usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
-            assertThat(accessor.getString(new String[]{mdcFieldName, mdcPatternMatchedKey})).isEqualTo(mdcPatternMatchedValue);
-            assertThat(accessor.exists(new String[]{mdcFieldName, mdcPatternMismatchedKey})).isFalse();
+            assertThat(accessor.getString(new String[]{mapFieldName, patternMatchedKey})).isEqualTo(patternMatchedValue);
+            assertThat(accessor.exists(new String[]{mapFieldName, patternMismatchedKey})).isFalse();
         });
 
     }
@@ -807,13 +893,58 @@ public class JsonTemplateLayoutTest {
                 .setContextData(contextData)
                 .build();
 
+        // Check the serialized event.
+        testReadOnlyStringMapFlatten(
+                mdcPatternMatchedKey,
+                mdcPatternMatchedValue,
+                mdcPatternMismatchedKey,
+                logEvent,
+                "mdc");
+
+    }
+
+    @Test
+    public void test_map_flatten() {
+
+        // Create the log event.
+        final String patternMatchedKey = "mapKey1";
+        final String patternMatchedValue = "mapValue1";
+        final String patternMismatchedKey = "mapKey2";
+        final String patternMismatchedValue = "mapValue2";
+        final Message message = new StringMapMessage()
+                .with(patternMatchedKey, patternMatchedValue)
+                .with(patternMismatchedKey, patternMismatchedValue);
+        final LogEvent logEvent = Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(LOGGER_NAME)
+                .setLevel(Level.INFO)
+                .setMessage(message)
+                .build();
+
+        // Check the serialized event.
+        testReadOnlyStringMapFlatten(
+                patternMatchedKey,
+                patternMatchedValue,
+                patternMismatchedKey,
+                logEvent,
+                "map");
+
+    }
+
+    private static void testReadOnlyStringMapFlatten(
+            final String patternMatchedKey,
+            final String patternMatchedValue,
+            final String patternMismatchedKey,
+            final LogEvent logEvent,
+            final String resolverName) {
+
         // Create the event template.
-        final String mdcPrefix = "_mdc.";
+        final String prefix = "_map.";
         final String eventTemplate = writeJson(Map(
                 "ignoredFieldName", Map(
-                        "$resolver", "mdc",
-                        "pattern", mdcPatternMatchedKey,
-                        "flatten", Map("prefix", mdcPrefix))));
+                        "$resolver", resolverName,
+                        "pattern", patternMatchedKey,
+                        "flatten", Map("prefix", prefix))));
 
         // Create the layout.
         final JsonTemplateLayout layout = JsonTemplateLayout
@@ -825,8 +956,8 @@ public class JsonTemplateLayoutTest {
 
         // Check the serialized event.
         usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
-            assertThat(accessor.getString(mdcPrefix + mdcPatternMatchedKey)).isEqualTo(mdcPatternMatchedValue);
-            assertThat(accessor.exists(mdcPrefix + mdcPatternMismatchedKey)).isFalse();
+            assertThat(accessor.getString(prefix + patternMatchedKey)).isEqualTo(patternMatchedValue);
+            assertThat(accessor.exists(prefix + patternMismatchedKey)).isFalse();
         });
 
     }
@@ -1793,7 +1924,7 @@ public class JsonTemplateLayoutTest {
         testMessageParameterResolver(ReusableMessageFactory.INSTANCE);
     }
 
-    private void testMessageParameterResolver(MessageFactory messageFactory) {
+    private static void testMessageParameterResolver(MessageFactory messageFactory) {
 
         // Create the event template.
         final String eventTemplate = writeJson(Map(
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 1a0ea85..100fba9 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -30,6 +30,9 @@
          - "remove" - Removed
     -->
     <release version="2.14.1" date="2020-MM-DD" description="GA Release 2.14.1">
+      <action issue="LOG4J2-2962" dev="vy" type="add">
+        Enrich "map" resolver by unifying its backend with "mdc" resolver.
+      </action>
       <action issue="LOG4J2-2961" dev="vy" type="fix">
         Fix reading of JsonTemplateLayout event additional fields from config.
       </action>
diff --git a/src/site/asciidoc/manual/json-template-layout.adoc.vm b/src/site/asciidoc/manual/json-template-layout.adoc.vm
index 2536e9b..c50bb0a 100644
--- a/src/site/asciidoc/manual/json-template-layout.adoc.vm
+++ b/src/site/asciidoc/manual/json-template-layout.adoc.vm
@@ -596,147 +596,18 @@ Resolve the argument coming right after `--userId`:
 ----
 
 | map
-a|
-[source]
-----
-config      = key , [ stringified ]
-key         = "key" -> string
-stringified = "stringified" -> boolean
-----
-| resolves the given `key` of ``MapMessage``s
-| `stringified` flag translates to `String.valueOf(value)`, hence mind
-  not-`String`-typed values.
-a|
-Resolve the `userRole` field of the message:
-
-[source,json]
-----
-{
-  "$resolver": "map",
-  "key": "userRole"
-}
-----
-
-| marker
-a|
-[source]
-----
-config = "field" -> "name"
-----
-| `logEvent.getMarker().getName()`
-| none
-a|
-Resolve the marker name:
-
-[source,json]
-----
-{
-  "$resolver": "marker",
-  "field": "name"
-}
-----
+| see link:#map-resolver-template[Map Resolver Template]
+| resolves ``MapMessage``s
+| see link:#map-resolver-template[Map Resolver Template]
+| see link:#map-resolver-template[Map Resolver Template]
 
 | mdc
-a|
-[source]
-----
-config        = singleAccess \| multiAccess
-
-singleAccess  = key , [ stringified ]
-key           = "key" -> string
-stringified   = "stringified" -> boolean
-
-multi-access  = [ pattern ] , [ flatten ] , [ stringified ]
-pattern       = "pattern" -> string
-flatten       = "flatten" -> ( boolean \| flattenConfig )
-flattenConfig = [ flattenPrefix ]
-flattenPrefix = "prefix" -> string
-----
-a| Mapped Diagnostic Context (MDC), aka. Thread Context Data, resolver.
-
-`singleAccess` resolves the MDC value as is, whilst `multiAccess` resolves a
-multitude of MDC values. If `flatten` is provided, `multiAccess` merges the
-values 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.
-a|
-`log4j2.garbagefreeThreadContextMap` flag needs to be turned on to iterate
-the map without allocations.
-
-`stringified` allocates a new `String` for values that are not of type `String`.
-
-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.
-a|
-Resolve the `userRole` MDC value:
-
-[source,json]
-----
-{
-  "$resolver": "mdc",
-  "key": "userRole"
-}
-----
-
-Resolve the string representation of the `userRank` MDC value:
-
-[source,json]
-----
-{
-  "$resolver": "mdc",
-  "key": "userRank",
-  "stringified": true
-}
-----
-
-Resolve all MDC entries into an object:
-
-[source,json]
-----
-{
-  "$resolver": "mdc"
-}
-----
-
-Resolve all MDC entries into an object such that values are converted to string:
-
-[source,json]
-----
-{
-  "$resolver": "mdc",
-  "stringified": true
-}
-----
-
-Merge all MDC entries whose keys are matching with the `user(Role\|Rank)` regex
-into the parent:
-
-[source,json]
-----
-{
-  "$resolver": "mdc",
-  "flatten": true,
-  "pattern": "user(Role\|Rank)"
-}
-----
-
-After converting the corresponding entries to string, merge all MDC entries to
-parent such that keys are prefixed with `_`:
-
-[source,json]
-----
-{
-  "$resolver": "mdc",
-  "stringified": true,
-  "flatten": {
-    "prefix": "_"
-  }
-}
-----
+| see link:#map-resolver-template[Map Resolver Template]
+| resolves Mapped Diagnostic Context (MDC), aka. Thread Context Data
+| `log4j2.garbagefreeThreadContextMap` flag needs to be turned on to iterate
+  the map without allocations. See
+  link:#map-resolver-template[Map Resolver Template] for other details.
+| see link:#map-resolver-template[Map Resolver Template]
 
 | message
 a|
@@ -1109,6 +980,117 @@ a!
 !===
 |===
 
+[#map-resolver-template]
+==== Map Resolver Template
+
+`ReadOnlyStringMap` is Log4j's `Map<String, Object>` equivalent with
+garbage-free accessors and heavily employed throughout the code base. It is the
+data structure backing both Mapped Diagnostic Context (MDC), aka. Thread Context
+Data and `MapMessage` implementations. Hence template resolvers for both of
+these are provided by a single backend: `ReadOnlyStringMapResolver`. Put another
+way, both `mdc` and `map` resolvers support identical configuration, behaviour,
+and garbage footprint, which are detailed below.
+
+[#stringmap-template-resolver]
+.`ReadOnlyStringMap` template resolver
+[cols="3,2,2,4"]
+|===
+| Syntax
+| Description
+| Garbage Footprint
+| Examples
+
+a|
+[source]
+----
+config        = singleAccess \| multiAccess
+
+singleAccess  = key , [ stringified ]
+key           = "key" -> string
+stringified   = "stringified" -> boolean
+
+multiAccess   = [ pattern ] , [ flatten ] , [ stringified ]
+pattern       = "pattern" -> string
+flatten       = "flatten" -> ( boolean \| flattenConfig )
+flattenConfig = [ flattenPrefix ]
+flattenPrefix = "prefix" -> string
+----
+| `singleAccess` resolves a single field, whilst `multiAccess` resolves a
+  multitude of fields. If `flatten` is provided, `multiAccess` merges the fields
+  with the parent, otherwise creates a new JSON object containing the values.
+| `stringified` flag translates to `String.valueOf(value)`, hence mind
+  not-`String`-typed values.
+a|
+`"${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`:
+
+[source,json]
+----
+{
+  "$resolver": "…",
+  "key": "userRole"
+}
+----
+
+Resolve the string representation of the `userRank` field value:
+
+[source,json]
+----
+{
+  "$resolver": "…",
+  "key": "userRank",
+  "stringified": true
+}
+----
+
+Resolve all fields into an object:
+
+[source,json]
+----
+{
+  "$resolver": "…"
+}
+----
+
+Resolve all fields into an object such that values are converted to string:
+
+[source,json]
+----
+{
+  "$resolver": "…",
+  "stringified": true
+}
+----
+
+Merge all fields whose keys are matching with the `user(Role\|Rank)` regex into
+the parent:
+
+[source,json]
+----
+{
+  "$resolver": "…",
+  "flatten": true,
+  "pattern": "user(Role\|Rank)"
+}
+----
+
+After converting the corresponding field values to string, merge all fields to
+parent such that keys are prefixed with `_`:
+
+[source,json]
+----
+{
+  "$resolver": "…",
+  "stringified": true,
+  "flatten": {
+    "prefix": "_"
+  }
+}
+----
+|===
+
 [#stack-trace-element-templates]
 === Stack Trace Element Templates