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/03/22 20:11:51 UTC

[logging-log4j2] 01/01: LOG4J2-3004 Add plugin support to JsonTemplateLayout.

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

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

commit 74d0b56f5542895c9f665eb3cbcad1cd3cad80b5
Author: Volkan Yazici <vo...@gmail.com>
AuthorDate: Mon Mar 22 20:57:48 2021 +0100

    LOG4J2-3004 Add plugin support to JsonTemplateLayout.
---
 .../plugins/convert/TypeConverterRegistry.java     |  59 +-
 .../log4j/core/config/plugins/util/PluginUtil.java |  96 ++++
 .../plugins/convert/TypeConverterRegistryTest.java |  83 ++-
 log4j-layout-template-json/revapi.json             | 279 ++++++++++
 .../layout/template/json/JsonTemplateLayout.java   |  60 +-
 .../template/json/resolver/EndOfBatchResolver.java |   5 +-
 .../json/resolver/EndOfBatchResolverFactory.java   |  15 +-
 .../resolver/EventAdditionalFieldInterceptor.java  |  98 ++++
 .../template/json/resolver/EventResolver.java      |   5 +-
 .../json/resolver/EventResolverContext.java        | 110 ++--
 .../json/resolver/EventResolverFactories.java      |  49 +-
 .../json/resolver/EventResolverFactory.java        |  21 +-
 ...Resolver.java => EventResolverInterceptor.java} |  18 +-
 ...esolver.java => EventResolverInterceptors.java} |  19 +-
 ...er.java => EventResolverStringSubstitutor.java} |  38 +-
 .../resolver/EventRootObjectKeyInterceptor.java    |  53 ++
 .../template/json/resolver/ExceptionResolver.java  | 151 ++++-
 .../json/resolver/ExceptionResolverFactory.java    |  13 +-
 .../json/resolver/ExceptionRootCauseResolver.java  |   8 +-
 .../ExceptionRootCauseResolverFactory.java         |  15 +-
 .../template/json/resolver/LevelResolver.java      |  29 +-
 .../json/resolver/LevelResolverFactory.java        |  12 +-
 .../template/json/resolver/LoggerResolver.java     |   9 +-
 .../json/resolver/LoggerResolverFactory.java       |  12 +-
 .../template/json/resolver/MainMapResolver.java    |   2 +-
 .../json/resolver/MainMapResolverFactory.java      |  12 +-
 .../layout/template/json/resolver/MapResolver.java |   2 +-
 .../template/json/resolver/MapResolverFactory.java |  12 +-
 .../template/json/resolver/MarkerResolver.java     |   2 +-
 .../json/resolver/MarkerResolverFactory.java       |  16 +-
 .../json/resolver/MessageParameterResolver.java    |   2 +-
 .../resolver/MessageParameterResolverFactory.java  |  15 +-
 .../template/json/resolver/MessageResolver.java    |   2 +-
 .../json/resolver/MessageResolverFactory.java      |  12 +-
 .../template/json/resolver/PatternResolver.java    |   2 +-
 .../json/resolver/PatternResolverFactory.java      |  12 +-
 .../template/json/resolver/SourceResolver.java     |  22 +-
 .../json/resolver/SourceResolverFactory.java       |  12 +-
 .../StackTraceElementObjectResolverContext.java    |  93 ----
 .../StackTraceElementObjectResolverFactories.java  |  41 --
 ...esolver.java => StackTraceElementResolver.java} |  27 +-
 .../resolver/StackTraceElementResolverContext.java | 121 ++++
 ....java => StackTraceElementResolverFactory.java} |  33 +-
 ...tackTraceElementResolverStringSubstitutor.java} |  39 +-
 .../json/resolver/StackTraceObjectResolver.java    |   3 +
 .../template/json/resolver/StackTraceResolver.java |   3 +
 .../json/resolver/StackTraceStringResolver.java    |   3 +
 .../json/resolver/TemplateResolverConfig.java      |  37 +-
 .../json/resolver/TemplateResolverContext.java     |  32 +-
 .../json/resolver/TemplateResolverFactories.java   | 130 +++++
 .../json/resolver/TemplateResolverFactory.java     |  25 +-
 .../json/resolver/TemplateResolverInterceptor.java |  56 ++
 .../resolver/TemplateResolverInterceptors.java     | 131 +++++
 ...java => TemplateResolverStringSubstitutor.java} |  22 +-
 .../template/json/resolver/TemplateResolvers.java  | 109 +---
 .../json/resolver/ThreadContextDataResolver.java   |   2 +-
 .../resolver/ThreadContextDataResolverFactory.java |  13 +-
 .../json/resolver/ThreadContextStackResolver.java  |   2 +-
 .../ThreadContextStackResolverFactory.java         |  17 +-
 .../template/json/resolver/ThreadResolver.java     |  12 +-
 .../json/resolver/ThreadResolverFactory.java       |  12 +-
 .../template/json/resolver/TimestampResolver.java  |   4 +-
 .../json/resolver/TimestampResolverFactory.java    |  12 +-
 .../template/json/util/RecyclerFactories.java      |  11 -
 .../RecyclerFactoryConverter.java}                 |  29 +-
 .../src/main/resources/EcsLayout.json              |   1 +
 .../log4j/layout/template/json/EcsLayoutTest.java  |  10 +
 .../template/json/JsonTemplateLayoutTest.java      | 124 ++++-
 .../log4j/layout/template/json/LogstashIT.java     |   3 +-
 .../json/JsonTemplateLayoutBenchmarkState.java     |  11 +-
 pom.xml                                            |   2 +-
 src/changes/changes.xml                            |   5 +
 ...layout.vm.adoc => json-template-layout.adoc.vm} | 616 ++++++++++++++++-----
 src/site/xdoc/manual/layouts.xml.vm                |   7 +-
 src/site/xdoc/manual/plugins.xml                   |   3 +
 75 files changed, 2499 insertions(+), 684 deletions(-)

diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/convert/TypeConverterRegistry.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/convert/TypeConverterRegistry.java
index 5088f15..3964370 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/convert/TypeConverterRegistry.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/convert/TypeConverterRegistry.java
@@ -86,8 +86,9 @@ public class TypeConverterRegistry {
             if (clazz.isEnum()) {
                 @SuppressWarnings({"unchecked","rawtypes"})
                 final EnumConverter<? extends Enum> converter = new EnumConverter(clazz.asSubclass(Enum.class));
-                registry.putIfAbsent(type, converter);
-                return converter;
+                synchronized (INSTANCE_LOCK) {
+                    return registerConverter(type, converter);
+                }
             }
         }
         // look for compatible converters
@@ -96,8 +97,9 @@ public class TypeConverterRegistry {
             if (TypeUtil.isAssignable(type, key)) {
                 LOGGER.debug("Found compatible TypeConverter<{}> for type [{}].", key, type);
                 final TypeConverter<?> value = entry.getValue();
-                registry.putIfAbsent(type, value);
-                return value;
+                synchronized (INSTANCE_LOCK) {
+                    return registerConverter(type, value);
+                }
             }
         }
         throw new UnknownFormatConversionException(type.toString());
@@ -119,11 +121,52 @@ public class TypeConverterRegistry {
                 final Class<? extends TypeConverter> pluginClass =  clazz.asSubclass(TypeConverter.class);
                 final Type conversionType = getTypeConverterSupportedType(pluginClass);
                 final TypeConverter<?> converter = ReflectionUtil.instantiate(pluginClass);
-                if (registry.putIfAbsent(conversionType, converter) != null) {
-                    LOGGER.warn("Found a TypeConverter [{}] for type [{}] that already exists.", converter,
-                        conversionType);
-                }
+                registerConverter(conversionType, converter);
+            }
+        }
+    }
+
+    /**
+     * Attempts to register the given converter and returns the effective
+     * converter associated with the given type.
+     * <p>
+     * Registration will fail if there already exists a converter for the given
+     * type and neither the existing, nor the provided converter extends from {@link Comparable}.
+     */
+    private TypeConverter<?> registerConverter(
+            final Type conversionType,
+            final TypeConverter<?> converter) {
+        final TypeConverter<?> conflictingConverter = registry.get(conversionType);
+        if (conflictingConverter != null) {
+            final boolean overridable;
+            if (converter instanceof Comparable) {
+                @SuppressWarnings("unchecked")
+                final Comparable<TypeConverter<?>> comparableConverter =
+                        (Comparable<TypeConverter<?>>) converter;
+                overridable = comparableConverter.compareTo(conflictingConverter) < 0;
+            } else if (conflictingConverter instanceof Comparable) {
+                @SuppressWarnings("unchecked")
+                final Comparable<TypeConverter<?>> comparableConflictingConverter =
+                        (Comparable<TypeConverter<?>>) conflictingConverter;
+                overridable = comparableConflictingConverter.compareTo(converter) > 0;
+            } else {
+                overridable = false;
+            }
+            if (overridable) {
+                LOGGER.debug(
+                        "Replacing TypeConverter [{}] for type [{}] with [{}] after comparison.",
+                        conflictingConverter, conversionType, converter);
+                registry.put(conversionType, converter);
+                return converter;
+            } else {
+                LOGGER.warn(
+                        "Ignoring TypeConverter [{}] for type [{}] that conflicts with [{}], since they are not comparable.",
+                        converter, conversionType, conflictingConverter);
+                return conflictingConverter;
             }
+        } else {
+            registry.put(conversionType, converter);
+            return converter;
         }
     }
 
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/util/PluginUtil.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/util/PluginUtil.java
new file mode 100644
index 0000000..0f1c92b
--- /dev/null
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/util/PluginUtil.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.core.config.plugins.util;
+
+import org.apache.logging.log4j.core.config.plugins.PluginFactory;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * {@link org.apache.logging.log4j.core.config.plugins.Plugin} utilities.
+ *
+ * @see PluginManager
+ */
+public final class PluginUtil {
+
+    private PluginUtil() {}
+
+    /**
+     * Shortcut for collecting plugins matching with the given {@code category}.
+     */
+    public static Map<String, PluginType<?>> collectPluginsByCategory(final String category) {
+        Objects.requireNonNull(category, "category");
+        return collectPluginsByCategoryAndPackage(category, Collections.emptyList());
+    }
+
+    /**
+     * Short for collecting plugins matching with the given {@code category} in provided {@code packages}.
+     */
+    public static Map<String, PluginType<?>> collectPluginsByCategoryAndPackage(
+            final String category,
+            final List<String> packages) {
+        Objects.requireNonNull(category, "category");
+        Objects.requireNonNull(packages, "packages");
+        final PluginManager pluginManager = new PluginManager(category);
+        pluginManager.collectPlugins(packages);
+        return pluginManager.getPlugins();
+    }
+
+    /**
+     * Instantiates the given plugin using its no-arg {@link PluginFactory}-annotated static method.
+     * @throws IllegalStateException if instantiation fails
+     */
+    public static <V> V instantiatePlugin(Class<V> pluginClass) {
+        Objects.requireNonNull(pluginClass, "pluginClass");
+        final Method pluginFactoryMethod = findPluginFactoryMethod(pluginClass);
+        try {
+            @SuppressWarnings("unchecked")
+            final V instance = (V) pluginFactoryMethod.invoke(null);
+            return instance;
+        } catch (IllegalAccessException | InvocationTargetException error) {
+            final String message = String.format(
+                    "failed to instantiate plugin of type %s using the factory method %s",
+                    pluginClass, pluginFactoryMethod);
+            throw new IllegalStateException(message, error);
+        }
+    }
+
+    /**
+     * Finds the {@link PluginFactory}-annotated static method of the given class.
+     * @throws IllegalStateException if no such method could be found
+     */
+    public static Method findPluginFactoryMethod(final Class<?> pluginClass) {
+        Objects.requireNonNull(pluginClass, "pluginClass");
+        for (final Method method : pluginClass.getDeclaredMethods()) {
+            final boolean methodAnnotated = method.isAnnotationPresent(PluginFactory.class);
+            if (methodAnnotated) {
+                final boolean methodStatic = Modifier.isStatic(method.getModifiers());
+                if (methodStatic) {
+                    return method;
+                }
+            }
+        }
+        throw new IllegalStateException("no factory method found for class " + pluginClass);
+    }
+
+}
diff --git a/log4j-core/src/test/java/org/apache/logging/log4j/core/config/plugins/convert/TypeConverterRegistryTest.java b/log4j-core/src/test/java/org/apache/logging/log4j/core/config/plugins/convert/TypeConverterRegistryTest.java
index f9e757d..e00ce11 100644
--- a/log4j-core/src/test/java/org/apache/logging/log4j/core/config/plugins/convert/TypeConverterRegistryTest.java
+++ b/log4j-core/src/test/java/org/apache/logging/log4j/core/config/plugins/convert/TypeConverterRegistryTest.java
@@ -16,6 +16,7 @@
  */
 package org.apache.logging.log4j.core.config.plugins.convert;
 
+import org.apache.logging.log4j.core.config.plugins.Plugin;
 import org.junit.Test;
 
 import static org.hamcrest.Matchers.instanceOf;
@@ -24,7 +25,7 @@ import static org.junit.Assert.*;
 public class TypeConverterRegistryTest {
 
     @Test(expected = NullPointerException.class)
-    public void testFindNullConverter() throws Exception {
+    public void testFindNullConverter() {
         TypeConverterRegistry.getInstance().findCompatibleConverter(null);
     }
 
@@ -63,7 +64,7 @@ public class TypeConverterRegistryTest {
         // TODO: is there a specific converter this should return?
     }
 
-    public static enum Foo {
+    public enum Foo {
         I, PITY, THE
     }
 
@@ -77,4 +78,82 @@ public class TypeConverterRegistryTest {
         assertEquals(Foo.PITY, fooTypeConverter.convert("pity"));
         assertEquals(Foo.THE, fooTypeConverter.convert("THE"));
     }
+
+    public static final class CustomTestClass1 {
+
+        private CustomTestClass1() {}
+
+    }
+
+    @Plugin(name = "CustomTestClass1Converter1", category = TypeConverters.CATEGORY)
+    public static final class CustomTestClass1Converter1
+            implements TypeConverter<CustomTestClass1> {
+
+        @Override
+        public CustomTestClass1 convert(final String ignored) {
+            return new CustomTestClass1();
+        }
+
+    }
+
+    @Plugin(name = "CustomTestClass1Converter2", category = TypeConverters.CATEGORY)
+    public static final class CustomTestClass1Converter2
+            implements TypeConverter<CustomTestClass1>, Comparable<Object> {
+
+        @Override
+        public CustomTestClass1 convert(final String ignored) {
+            return new CustomTestClass1();
+        }
+
+        @Override
+        public int compareTo(@SuppressWarnings("NullableProblems") final Object converter) {
+            return -1;
+        }
+
+    }
+
+    @Test
+    public void testMultipleComparableConverters() {
+        final TypeConverter<?> converter = TypeConverterRegistry
+                .getInstance()
+                .findCompatibleConverter(CustomTestClass1.class);
+        assertThat(converter, instanceOf(CustomTestClass1Converter2.class));
+    }
+
+    public static final class CustomTestClass2 {
+
+        private CustomTestClass2() {}
+
+    }
+
+    @Plugin(name = "CustomTestClass2Converter1", category = TypeConverters.CATEGORY)
+    public static final class CustomTestClass2Converter1
+            implements TypeConverter<CustomTestClass2> {
+
+        @Override
+        public CustomTestClass2 convert(final String ignored) {
+            return new CustomTestClass2();
+        }
+
+    }
+
+    @Plugin(name = "CustomTestClass2Converter2", category = TypeConverters.CATEGORY)
+    public static final class CustomTestClass2Converter2
+            implements TypeConverter<CustomTestClass2> {
+
+        @Override
+        public CustomTestClass2 convert(final String ignored) {
+            return new CustomTestClass2();
+        }
+
+    }
+
+    @Test
+    public void testMultipleIncomparableConverters() {
+        final TypeConverter<?> converter = TypeConverterRegistry
+                .getInstance()
+                .findCompatibleConverter(CustomTestClass2.class);
+        assertThat(converter, instanceOf(CustomTestClass2Converter1.class));
+    }
+
 }
diff --git a/log4j-layout-template-json/revapi.json b/log4j-layout-template-json/revapi.json
index 6f6096d..62e4e7d 100644
--- a/log4j-layout-template-json/revapi.json
+++ b/log4j-layout-template-json/revapi.json
@@ -449,6 +449,285 @@
         "code": "java.method.removed",
         "old": "method org.apache.logging.log4j.layout.template.json.JsonTemplateLayout.EventTemplateAdditionalField.Type org.apache.logging.log4j.layout.template.json.JsonTemplateLayout.EventTemplateAdditionalField::getType()",
         "justification": "LOG4J2-2973 Rename EventTemplateAdditionalField#type (conflicting with properties file parser) to #format."
+      },
+      {
+        "code": "java.annotation.added",
+        "old": "method org.apache.logging.log4j.layout.template.json.resolver.EndOfBatchResolverFactory org.apache.logging.log4j.layout.template.json.resolver.EndOfBatchResolverFactory::getInstance()",
+        "new": "method org.apache.logging.log4j.layout.template.json.resolver.EndOfBatchResolverFactory org.apache.logging.log4j.layout.template.json.resolver.EndOfBatchResolverFactory::getInstance()",
+        "annotation": "@org.apache.logging.log4j.core.config.plugins.PluginFactory",
+        "justification": "LOG4J2-3004 Add plugin support."
+      },
+      {
+        "code": "java.annotation.added",
+        "old": "class org.apache.logging.log4j.layout.template.json.resolver.EndOfBatchResolverFactory",
+        "new": "class org.apache.logging.log4j.layout.template.json.resolver.EndOfBatchResolverFactory",
+        "annotation": "@org.apache.logging.log4j.core.config.plugins.Plugin(name = \"EndOfBatchResolverFactory\", category = \"JsonTemplateResolverFactory\")",
+        "justification": "LOG4J2-3004 Add plugin support."
+      },
+      {
+        "code": "java.method.removed",
+        "old": "method java.lang.String org.apache.logging.log4j.layout.template.json.resolver.EventResolverContext.Builder::getTruncatedStringSuffix()",
+        "justification": "LOG4J2-3004 Add plugin support."
+      },
+      {
+        "code": "java.method.removed",
+        "old": "method org.apache.logging.log4j.layout.template.json.resolver.EventResolverContext.Builder org.apache.logging.log4j.layout.template.json.resolver.EventResolverContext.Builder::setStackTraceElementObjectResolver(org.apache.logging.log4j.layout.template.json.resolver.TemplateResolver<java.lang.StackTraceElement>)",
+        "justification": "LOG4J2-3004 Add plugin support."
+      },
+      {
+        "code": "java.method.parameterTypeChanged",
+        "old": "parameter org.apache.logging.log4j.layout.template.json.resolver.EventResolverContext.Builder org.apache.logging.log4j.layout.template.json.resolver.EventResolverContext.Builder::setSubstitutor(===org.apache.logging.log4j.core.lookup.StrSubstitutor===)",
+        "new": "parameter org.apache.logging.log4j.layout.template.json.resolver.EventResolverContext.Builder org.apache.logging.log4j.layout.template.json.resolver.EventResolverContext.Builder::setSubstitutor(===org.apache.logging.log4j.layout.template.json.resolver.EventResolverStringSubstitutor===)",
+        "justification": "LOG4J2-3004 Add plugin support."
+      },
+      {
+        "code": "java.method.returnTypeTypeParametersChanged",
+        "old": "method java.util.Map<java.lang.String, org.apache.logging.log4j.layout.template.json.resolver.TemplateResolverFactory<org.apache.logging.log4j.core.LogEvent, org.apache.logging.log4j.layout.template.json.resolver.EventResolverContext, ? extends org.apache.logging.log4j.layout.template.json.resolver.TemplateResolver<org.apache.logging.log4j.core.LogEvent>>> org.apache.logging.log4j.layout.template.json.resolver.EventResolverContext::getResolverFactoryByName()",
+        "new": "method java.util.Map<java.lang.String, org.apache.logging.log4j.layout.template.json.resolver.EventResolverFactory> org.apache.logging.log4j.layout.template.json.resolver.EventResolverContext::getResolverFactoryByName()",
+        "justification": "LOG4J2-3004 Add plugin support."
+      },
+      {
+        "code": "java.method.returnTypeChanged",
+        "old": "method org.apache.logging.log4j.core.lookup.StrSubstitutor org.apache.logging.log4j.layout.template.json.resolver.EventResolverContext::getSubstitutor()",
+        "new": "method org.apache.logging.log4j.layout.template.json.resolver.EventResolverStringSubstitutor org.apache.logging.log4j.layout.template.json.resolver.EventResolverContext::getSubstitutor()",
+        "justification": "LOG4J2-3004 Add plugin support."
+      },
+      {
+        "code": "java.annotation.added",
+        "old": "method org.apache.logging.log4j.layout.template.json.resolver.ExceptionResolverFactory org.apache.logging.log4j.layout.template.json.resolver.ExceptionResolverFactory::getInstance()",
+        "new": "method org.apache.logging.log4j.layout.template.json.resolver.ExceptionResolverFactory org.apache.logging.log4j.layout.template.json.resolver.ExceptionResolverFactory::getInstance()",
+        "annotation": "@org.apache.logging.log4j.core.config.plugins.PluginFactory",
+        "justification": "LOG4J2-3004 Add plugin support."
+      },
+      {
+        "code": "java.annotation.added",
+        "old": "class org.apache.logging.log4j.layout.template.json.resolver.ExceptionResolverFactory",
+        "new": "class org.apache.logging.log4j.layout.template.json.resolver.ExceptionResolverFactory",
+        "annotation": "@org.apache.logging.log4j.core.config.plugins.Plugin(name = \"ExceptionResolverFactory\", category = \"JsonTemplateResolverFactory\")",
+        "justification": "LOG4J2-3004 Add plugin support."
+      },
+      {
+        "code": "java.annotation.added",
+        "old": "method org.apache.logging.log4j.layout.template.json.resolver.ExceptionRootCauseResolverFactory org.apache.logging.log4j.layout.template.json.resolver.ExceptionRootCauseResolverFactory::getInstance()",
+        "new": "method org.apache.logging.log4j.layout.template.json.resolver.ExceptionRootCauseResolverFactory org.apache.logging.log4j.layout.template.json.resolver.ExceptionRootCauseResolverFactory::getInstance()",
+        "annotation": "@org.apache.logging.log4j.core.config.plugins.PluginFactory",
+        "justification": "LOG4J2-3004 Add plugin support."
+      },
+      {
+        "code": "java.annotation.added",
+        "old": "class org.apache.logging.log4j.layout.template.json.resolver.ExceptionRootCauseResolverFactory",
+        "new": "class org.apache.logging.log4j.layout.template.json.resolver.ExceptionRootCauseResolverFactory",
+        "annotation": "@org.apache.logging.log4j.core.config.plugins.Plugin(name = \"ExceptionRootCauseResolverFactory\", category = \"JsonTemplateResolverFactory\")",
+        "justification": "LOG4J2-3004 Add plugin support."
+      },
+      {
+        "code": "java.annotation.added",
+        "old": "method org.apache.logging.log4j.layout.template.json.resolver.LevelResolverFactory org.apache.logging.log4j.layout.template.json.resolver.LevelResolverFactory::getInstance()",
+        "new": "method org.apache.logging.log4j.layout.template.json.resolver.LevelResolverFactory org.apache.logging.log4j.layout.template.json.resolver.LevelResolverFactory::getInstance()",
+        "annotation": "@org.apache.logging.log4j.core.config.plugins.PluginFactory",
+        "justification": "LOG4J2-3004 Add plugin support."
+      },
+      {
+        "code": "java.annotation.added",
+        "old": "class org.apache.logging.log4j.layout.template.json.resolver.LevelResolverFactory",
+        "new": "class org.apache.logging.log4j.layout.template.json.resolver.LevelResolverFactory",
+        "annotation": "@org.apache.logging.log4j.core.config.plugins.Plugin(name = \"LevelResolverFactory\", category = \"JsonTemplateResolverFactory\")",
+        "justification": "LOG4J2-3004 Add plugin support."
+      },
+      {
+        "code": "java.annotation.added",
+        "old": "method org.apache.logging.log4j.layout.template.json.resolver.LoggerResolverFactory org.apache.logging.log4j.layout.template.json.resolver.LoggerResolverFactory::getInstance()",
+        "new": "method org.apache.logging.log4j.layout.template.json.resolver.LoggerResolverFactory org.apache.logging.log4j.layout.template.json.resolver.LoggerResolverFactory::getInstance()",
+        "annotation": "@org.apache.logging.log4j.core.config.plugins.PluginFactory",
+        "justification": "LOG4J2-3004 Add plugin support."
+      },
+      {
+        "code": "java.annotation.added",
+        "old": "class org.apache.logging.log4j.layout.template.json.resolver.LoggerResolverFactory",
+        "new": "class org.apache.logging.log4j.layout.template.json.resolver.LoggerResolverFactory",
+        "annotation": "@org.apache.logging.log4j.core.config.plugins.Plugin(name = \"LoggerResolverFactory\", category = \"JsonTemplateResolverFactory\")",
+        "justification": "LOG4J2-3004 Add plugin support."
+      },
+      {
+        "code": "java.annotation.added",
+        "old": "method org.apache.logging.log4j.layout.template.json.resolver.MainMapResolverFactory org.apache.logging.log4j.layout.template.json.resolver.MainMapResolverFactory::getInstance()",
+        "new": "method org.apache.logging.log4j.layout.template.json.resolver.MainMapResolverFactory org.apache.logging.log4j.layout.template.json.resolver.MainMapResolverFactory::getInstance()",
+        "annotation": "@org.apache.logging.log4j.core.config.plugins.PluginFactory",
+        "justification": "LOG4J2-3004 Add plugin support."
+      },
+      {
+        "code": "java.annotation.added",
+        "old": "class org.apache.logging.log4j.layout.template.json.resolver.MainMapResolverFactory",
+        "new": "class org.apache.logging.log4j.layout.template.json.resolver.MainMapResolverFactory",
+        "annotation": "@org.apache.logging.log4j.core.config.plugins.Plugin(name = \"MainMapResolverFactory\", category = \"JsonTemplateResolverFactory\")",
+        "justification": "LOG4J2-3004 Add plugin support."
+      },
+      {
+        "code": "java.annotation.added",
+        "old": "method org.apache.logging.log4j.layout.template.json.resolver.MapResolverFactory org.apache.logging.log4j.layout.template.json.resolver.MapResolverFactory::getInstance()",
+        "new": "method org.apache.logging.log4j.layout.template.json.resolver.MapResolverFactory org.apache.logging.log4j.layout.template.json.resolver.MapResolverFactory::getInstance()",
+        "annotation": "@org.apache.logging.log4j.core.config.plugins.PluginFactory",
+        "justification": "LOG4J2-3004 Add plugin support."
+      },
+      {
+        "code": "java.annotation.added",
+        "old": "class org.apache.logging.log4j.layout.template.json.resolver.MapResolverFactory",
+        "new": "class org.apache.logging.log4j.layout.template.json.resolver.MapResolverFactory",
+        "annotation": "@org.apache.logging.log4j.core.config.plugins.Plugin(name = \"MapResolverFactory\", category = \"JsonTemplateResolverFactory\")",
+        "justification": "LOG4J2-3004 Add plugin support."
+      },
+      {
+        "code": "java.annotation.added",
+        "old": "method org.apache.logging.log4j.layout.template.json.resolver.MarkerResolverFactory org.apache.logging.log4j.layout.template.json.resolver.MarkerResolverFactory::getInstance()",
+        "new": "method org.apache.logging.log4j.layout.template.json.resolver.MarkerResolverFactory org.apache.logging.log4j.layout.template.json.resolver.MarkerResolverFactory::getInstance()",
+        "annotation": "@org.apache.logging.log4j.core.config.plugins.PluginFactory",
+        "justification": "LOG4J2-3004 Add plugin support."
+      },
+      {
+        "code": "java.annotation.added",
+        "old": "class org.apache.logging.log4j.layout.template.json.resolver.MarkerResolverFactory",
+        "new": "class org.apache.logging.log4j.layout.template.json.resolver.MarkerResolverFactory",
+        "annotation": "@org.apache.logging.log4j.core.config.plugins.Plugin(name = \"MarkerResolverFactory\", category = \"JsonTemplateResolverFactory\")",
+        "justification": "LOG4J2-3004 Add plugin support."
+      },
+      {
+        "code": "java.annotation.added",
+        "old": "method org.apache.logging.log4j.layout.template.json.resolver.MessageParameterResolverFactory org.apache.logging.log4j.layout.template.json.resolver.MessageParameterResolverFactory::getInstance()",
+        "new": "method org.apache.logging.log4j.layout.template.json.resolver.MessageParameterResolverFactory org.apache.logging.log4j.layout.template.json.resolver.MessageParameterResolverFactory::getInstance()",
+        "annotation": "@org.apache.logging.log4j.core.config.plugins.PluginFactory",
+        "justification": "LOG4J2-3004 Add plugin support."
+      },
+      {
+        "code": "java.annotation.added",
+        "old": "class org.apache.logging.log4j.layout.template.json.resolver.MessageParameterResolverFactory",
+        "new": "class org.apache.logging.log4j.layout.template.json.resolver.MessageParameterResolverFactory",
+        "annotation": "@org.apache.logging.log4j.core.config.plugins.Plugin(name = \"MessageParameterResolverFactory\", category = \"JsonTemplateResolverFactory\")",
+        "justification": "LOG4J2-3004 Add plugin support."
+      },
+      {
+        "code": "java.annotation.added",
+        "old": "method org.apache.logging.log4j.layout.template.json.resolver.MessageResolverFactory org.apache.logging.log4j.layout.template.json.resolver.MessageResolverFactory::getInstance()",
+        "new": "method org.apache.logging.log4j.layout.template.json.resolver.MessageResolverFactory org.apache.logging.log4j.layout.template.json.resolver.MessageResolverFactory::getInstance()",
+        "annotation": "@org.apache.logging.log4j.core.config.plugins.PluginFactory",
+        "justification": "LOG4J2-3004 Add plugin support."
+      },
+      {
+        "code": "java.annotation.added",
+        "old": "class org.apache.logging.log4j.layout.template.json.resolver.MessageResolverFactory",
+        "new": "class org.apache.logging.log4j.layout.template.json.resolver.MessageResolverFactory",
+        "annotation": "@org.apache.logging.log4j.core.config.plugins.Plugin(name = \"MessageResolverFactory\", category = \"JsonTemplateResolverFactory\")",
+        "justification": "LOG4J2-3004 Add plugin support."
+      },
+      {
+        "code": "java.annotation.added",
+        "old": "method org.apache.logging.log4j.layout.template.json.resolver.PatternResolverFactory org.apache.logging.log4j.layout.template.json.resolver.PatternResolverFactory::getInstance()",
+        "new": "method org.apache.logging.log4j.layout.template.json.resolver.PatternResolverFactory org.apache.logging.log4j.layout.template.json.resolver.PatternResolverFactory::getInstance()",
+        "annotation": "@org.apache.logging.log4j.core.config.plugins.PluginFactory",
+        "justification": "LOG4J2-3004 Add plugin support."
+      },
+      {
+        "code": "java.annotation.added",
+        "old": "class org.apache.logging.log4j.layout.template.json.resolver.PatternResolverFactory",
+        "new": "class org.apache.logging.log4j.layout.template.json.resolver.PatternResolverFactory",
+        "annotation": "@org.apache.logging.log4j.core.config.plugins.Plugin(name = \"PatternResolverFactory\", category = \"JsonTemplateResolverFactory\")",
+        "justification": "LOG4J2-3004 Add plugin support."
+      },
+      {
+        "code": "java.annotation.added",
+        "old": "method org.apache.logging.log4j.layout.template.json.resolver.SourceResolverFactory org.apache.logging.log4j.layout.template.json.resolver.SourceResolverFactory::getInstance()",
+        "new": "method org.apache.logging.log4j.layout.template.json.resolver.SourceResolverFactory org.apache.logging.log4j.layout.template.json.resolver.SourceResolverFactory::getInstance()",
+        "annotation": "@org.apache.logging.log4j.core.config.plugins.PluginFactory",
+        "justification": "LOG4J2-3004 Add plugin support."
+      },
+      {
+        "code": "java.annotation.added",
+        "old": "class org.apache.logging.log4j.layout.template.json.resolver.SourceResolverFactory",
+        "new": "class org.apache.logging.log4j.layout.template.json.resolver.SourceResolverFactory",
+        "annotation": "@org.apache.logging.log4j.core.config.plugins.Plugin(name = \"SourceResolverFactory\", category = \"JsonTemplateResolverFactory\")",
+        "justification": "LOG4J2-3004 Add plugin support."
+      },
+      {
+        "code": "java.class.removed",
+        "old": "class org.apache.logging.log4j.layout.template.json.resolver.StackTraceElementObjectResolverContext",
+        "justification": "LOG4J2-3004 Add plugin support."
+      },
+      {
+        "code": "java.annotation.added",
+        "old": "method org.apache.logging.log4j.layout.template.json.resolver.ThreadContextDataResolverFactory org.apache.logging.log4j.layout.template.json.resolver.ThreadContextDataResolverFactory::getInstance()",
+        "new": "method org.apache.logging.log4j.layout.template.json.resolver.ThreadContextDataResolverFactory org.apache.logging.log4j.layout.template.json.resolver.ThreadContextDataResolverFactory::getInstance()",
+        "annotation": "@org.apache.logging.log4j.core.config.plugins.PluginFactory",
+        "justification": "LOG4J2-3004 Add plugin support."
+      },
+      {
+        "code": "java.annotation.added",
+        "old": "class org.apache.logging.log4j.layout.template.json.resolver.ThreadContextDataResolverFactory",
+        "new": "class org.apache.logging.log4j.layout.template.json.resolver.ThreadContextDataResolverFactory",
+        "annotation": "@org.apache.logging.log4j.core.config.plugins.Plugin(name = \"ThreadContextDataResolverFactory\", category = \"JsonTemplateResolverFactory\")",
+        "justification": "LOG4J2-3004 Add plugin support."
+      },
+      {
+        "code": "java.annotation.added",
+        "old": "method org.apache.logging.log4j.layout.template.json.resolver.ThreadContextStackResolverFactory org.apache.logging.log4j.layout.template.json.resolver.ThreadContextStackResolverFactory::getInstance()",
+        "new": "method org.apache.logging.log4j.layout.template.json.resolver.ThreadContextStackResolverFactory org.apache.logging.log4j.layout.template.json.resolver.ThreadContextStackResolverFactory::getInstance()",
+        "annotation": "@org.apache.logging.log4j.core.config.plugins.PluginFactory",
+        "justification": "LOG4J2-3004 Add plugin support."
+      },
+      {
+        "code": "java.annotation.added",
+        "old": "class org.apache.logging.log4j.layout.template.json.resolver.ThreadContextStackResolverFactory",
+        "new": "class org.apache.logging.log4j.layout.template.json.resolver.ThreadContextStackResolverFactory",
+        "annotation": "@org.apache.logging.log4j.core.config.plugins.Plugin(name = \"ThreadContextDataResolverFactory\", category = \"JsonTemplateResolverFactory\")",
+        "justification": "LOG4J2-3004 Add plugin support."
+      },
+      {
+        "code": "java.annotation.added",
+        "old": "method org.apache.logging.log4j.layout.template.json.resolver.ThreadResolverFactory org.apache.logging.log4j.layout.template.json.resolver.ThreadResolverFactory::getInstance()",
+        "new": "method org.apache.logging.log4j.layout.template.json.resolver.ThreadResolverFactory org.apache.logging.log4j.layout.template.json.resolver.ThreadResolverFactory::getInstance()",
+        "annotation": "@org.apache.logging.log4j.core.config.plugins.PluginFactory",
+        "justification": "LOG4J2-3004 Add plugin support."
+      },
+      {
+        "code": "java.annotation.added",
+        "old": "class org.apache.logging.log4j.layout.template.json.resolver.ThreadResolverFactory",
+        "new": "class org.apache.logging.log4j.layout.template.json.resolver.ThreadResolverFactory",
+        "annotation": "@org.apache.logging.log4j.core.config.plugins.Plugin(name = \"ThreadResolverFactory\", category = \"JsonTemplateResolverFactory\")",
+        "justification": "LOG4J2-3004 Add plugin support."
+      },
+      {
+        "code": "java.annotation.added",
+        "old": "method org.apache.logging.log4j.layout.template.json.resolver.TimestampResolverFactory org.apache.logging.log4j.layout.template.json.resolver.TimestampResolverFactory::getInstance()",
+        "new": "method org.apache.logging.log4j.layout.template.json.resolver.TimestampResolverFactory org.apache.logging.log4j.layout.template.json.resolver.TimestampResolverFactory::getInstance()",
+        "annotation": "@org.apache.logging.log4j.core.config.plugins.PluginFactory",
+        "justification": "LOG4J2-3004 Add plugin support."
+      },
+      {
+        "code": "java.annotation.added",
+        "old": "class org.apache.logging.log4j.layout.template.json.resolver.TimestampResolverFactory",
+        "new": "class org.apache.logging.log4j.layout.template.json.resolver.TimestampResolverFactory",
+        "annotation": "@org.apache.logging.log4j.core.config.plugins.Plugin(name = \"TimestampResolverFactory\", category = \"JsonTemplateResolverFactory\")",
+        "justification": "LOG4J2-3004 Add plugin support."
+      },
+      {
+        "code": "java.annotation.added",
+        "old": "class org.apache.logging.log4j.layout.template.json.resolver.ThreadContextStackResolverFactory",
+        "new": "class org.apache.logging.log4j.layout.template.json.resolver.ThreadContextStackResolverFactory",
+        "annotation": "@org.apache.logging.log4j.core.config.plugins.Plugin(name = \"ThreadContextStackResolverFactory\", category = \"JsonTemplateResolverFactory\")",
+        "justification": "LOG4J2-3004 Add plugin support."
+      },
+      {
+        "code": "java.annotation.attributeValueChanged",
+        "old": "class org.apache.logging.log4j.layout.template.json.util.RecyclerFactories.RecyclerFactoryConverter",
+        "new": "class org.apache.logging.log4j.layout.template.json.util.RecyclerFactories.RecyclerFactoryConverter",
+        "annotationType": "org.apache.logging.log4j.core.config.plugins.Plugin",
+        "attribute": "name",
+        "oldValue": "\"RecyclerFactory\"",
+        "newValue": "\"RecyclerFactoryConverter\"",
+        "justification": "LOG4J2-3004 Add plugin support."
+      },
+      {
+        "code": "java.class.removed",
+        "old": "class org.apache.logging.log4j.layout.template.json.util.RecyclerFactories.RecyclerFactoryConverter",
+        "justification": "LOG4J2-3004 Add plugin support."
       }
     ]
   }
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/JsonTemplateLayout.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/JsonTemplateLayout.java
index b0a2b66..9276fd3 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/JsonTemplateLayout.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/JsonTemplateLayout.java
@@ -29,11 +29,14 @@ import org.apache.logging.log4j.core.config.plugins.PluginElement;
 import org.apache.logging.log4j.core.layout.ByteBufferDestination;
 import org.apache.logging.log4j.core.layout.Encoder;
 import org.apache.logging.log4j.core.layout.LockingStringBuilderEncoder;
-import org.apache.logging.log4j.core.lookup.StrSubstitutor;
 import org.apache.logging.log4j.core.util.Constants;
 import org.apache.logging.log4j.core.util.StringEncoder;
 import org.apache.logging.log4j.layout.template.json.resolver.EventResolverContext;
-import org.apache.logging.log4j.layout.template.json.resolver.StackTraceElementObjectResolverContext;
+import org.apache.logging.log4j.layout.template.json.resolver.EventResolverFactories;
+import org.apache.logging.log4j.layout.template.json.resolver.EventResolverFactory;
+import org.apache.logging.log4j.layout.template.json.resolver.EventResolverInterceptor;
+import org.apache.logging.log4j.layout.template.json.resolver.EventResolverInterceptors;
+import org.apache.logging.log4j.layout.template.json.resolver.EventResolverStringSubstitutor;
 import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolver;
 import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolvers;
 import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
@@ -44,6 +47,7 @@ import org.apache.logging.log4j.util.Strings;
 
 import java.nio.charset.Charset;
 import java.util.Collections;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.function.Supplier;
@@ -93,59 +97,56 @@ public class JsonTemplateLayout implements StringLayout {
         final String eventDelimiterSuffix = builder.isNullEventDelimiterEnabled() ? "\0" : "";
         this.eventDelimiter = builder.eventDelimiter + eventDelimiterSuffix;
         final Configuration configuration = builder.configuration;
-        final StrSubstitutor substitutor = configuration.getStrSubstitutor();
         final JsonWriter jsonWriter = JsonWriter
                 .newBuilder()
                 .setMaxStringLength(builder.maxStringLength)
                 .setTruncatedStringSuffix(builder.truncatedStringSuffix)
                 .build();
-        final TemplateResolver<StackTraceElement> stackTraceElementObjectResolver =
-                builder.stackTraceEnabled
-                        ? createStackTraceElementResolver(builder, substitutor, jsonWriter)
-                        : null;
         this.eventResolver = createEventResolver(
                 builder,
                 configuration,
-                substitutor,
                 charset,
-                jsonWriter,
-                stackTraceElementObjectResolver);
+                jsonWriter);
         this.contextRecycler = createContextRecycler(builder, jsonWriter);
     }
 
-    private static TemplateResolver<StackTraceElement> createStackTraceElementResolver(
-            final Builder builder,
-            final StrSubstitutor substitutor,
-            final JsonWriter jsonWriter) {
-        final StackTraceElementObjectResolverContext stackTraceElementObjectResolverContext =
-                StackTraceElementObjectResolverContext
-                        .newBuilder()
-                        .setSubstitutor(substitutor)
-                        .setJsonWriter(jsonWriter)
-                        .build();
-        final String stackTraceElementTemplate = readStackTraceElementTemplate(builder);
-        return TemplateResolvers.ofTemplate(stackTraceElementObjectResolverContext, stackTraceElementTemplate);
-    }
-
     private TemplateResolver<LogEvent> createEventResolver(
             final Builder builder,
             final Configuration configuration,
-            final StrSubstitutor substitutor,
             final Charset charset,
-            final JsonWriter jsonWriter,
-            final TemplateResolver<StackTraceElement> stackTraceElementObjectResolver) {
+            final JsonWriter jsonWriter) {
+
+        // Inject resolver factory and interceptor plugins.
+        final List<String> pluginPackages = configuration.getPluginPackages();
+        final Map<String, EventResolverFactory> resolverFactoryByName =
+                EventResolverFactories.populateResolverFactoryByName(pluginPackages);
+        final List<EventResolverInterceptor> resolverInterceptors =
+                EventResolverInterceptors.populateInterceptors(pluginPackages);
+        final EventResolverStringSubstitutor substitutor =
+                new EventResolverStringSubstitutor(configuration.getStrSubstitutor());
+
+        // Read event and stack trace element templates.
         final String eventTemplate = readEventTemplate(builder);
+        final String stackTraceElementTemplate = readStackTraceElementTemplate(builder);
+
+        // Determine the max. string byte count.
         final float maxByteCountPerChar = builder.charset.newEncoder().maxBytesPerChar();
         final int maxStringByteCount =
                 Math.toIntExact(Math.round(Math.ceil(
                         maxByteCountPerChar * builder.maxStringLength)));
+
+        // Replace null event template additional fields with an empty array.
         final EventTemplateAdditionalField[] eventTemplateAdditionalFields =
                 builder.eventTemplateAdditionalFields != null
                         ? builder.eventTemplateAdditionalFields
                         : new EventTemplateAdditionalField[0];
+
+        // Create the resolver context.
         final EventResolverContext resolverContext = EventResolverContext
                 .newBuilder()
                 .setConfiguration(configuration)
+                .setResolverFactoryByName(resolverFactoryByName)
+                .setResolverInterceptors(resolverInterceptors)
                 .setSubstitutor(substitutor)
                 .setCharset(charset)
                 .setJsonWriter(jsonWriter)
@@ -154,11 +155,14 @@ public class JsonTemplateLayout implements StringLayout {
                 .setTruncatedStringSuffix(builder.truncatedStringSuffix)
                 .setLocationInfoEnabled(builder.locationInfoEnabled)
                 .setStackTraceEnabled(builder.stackTraceEnabled)
-                .setStackTraceElementObjectResolver(stackTraceElementObjectResolver)
+                .setStackTraceElementTemplate(stackTraceElementTemplate)
                 .setEventTemplateRootObjectKey(builder.eventTemplateRootObjectKey)
                 .setEventTemplateAdditionalFields(eventTemplateAdditionalFields)
                 .build();
+
+        // Compile the resolver template.
         return TemplateResolvers.ofTemplate(resolverContext, eventTemplate);
+
     }
 
     private static String readEventTemplate(final Builder builder) {
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/EndOfBatchResolver.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/EndOfBatchResolver.java
index 4a716c8..9ff9b3f 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/EndOfBatchResolver.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/EndOfBatchResolver.java
@@ -19,7 +19,10 @@ 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;
 
-final class EndOfBatchResolver implements EventResolver {
+/**
+ * {@code endOfBatch} indicator resolver.
+ */
+public final class EndOfBatchResolver implements EventResolver {
 
     private static final EndOfBatchResolver INSTANCE = new EndOfBatchResolver();
 
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/EndOfBatchResolverFactory.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/EndOfBatchResolverFactory.java
index f059bbb..69d0079 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/EndOfBatchResolverFactory.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/EndOfBatchResolverFactory.java
@@ -16,13 +16,22 @@
  */
 package org.apache.logging.log4j.layout.template.json.resolver;
 
-final class EndOfBatchResolverFactory implements EventResolverFactory<EndOfBatchResolver> {
+import org.apache.logging.log4j.core.config.plugins.Plugin;
+import org.apache.logging.log4j.core.config.plugins.PluginFactory;
 
-    private static final EndOfBatchResolverFactory INSTANCE = new EndOfBatchResolverFactory();
+/**
+ * {@link EndOfBatchResolver} factory.
+ */
+@Plugin(name = "EndOfBatchResolverFactory", category = TemplateResolverFactory.CATEGORY)
+public final class EndOfBatchResolverFactory implements EventResolverFactory {
+
+    private static final EndOfBatchResolverFactory INSTANCE =
+            new EndOfBatchResolverFactory();
 
     private EndOfBatchResolverFactory() {}
 
-    static EndOfBatchResolverFactory getInstance() {
+    @PluginFactory
+    public static EndOfBatchResolverFactory getInstance() {
         return INSTANCE;
     }
 
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/EventAdditionalFieldInterceptor.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/EventAdditionalFieldInterceptor.java
new file mode 100644
index 0000000..951d8df
--- /dev/null
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/EventAdditionalFieldInterceptor.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.layout.template.json.resolver;
+
+import org.apache.logging.log4j.core.config.plugins.Plugin;
+import org.apache.logging.log4j.core.config.plugins.PluginFactory;
+import org.apache.logging.log4j.layout.template.json.JsonTemplateLayout.EventTemplateAdditionalField;
+import org.apache.logging.log4j.layout.template.json.util.JsonReader;
+
+import java.util.Map;
+
+/**
+ * Interceptor to add {@link EventTemplateAdditionalField
+ * additional fields} after event template read.
+ */
+@Plugin(name = "EventAdditionalFieldInterceptor", category = TemplateResolverInterceptor.CATEGORY)
+public class EventAdditionalFieldInterceptor implements EventResolverInterceptor {
+
+    private static final EventAdditionalFieldInterceptor INSTANCE =
+            new EventAdditionalFieldInterceptor();
+
+    private EventAdditionalFieldInterceptor() {}
+
+    @PluginFactory
+    public static EventAdditionalFieldInterceptor getInstance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public Object processTemplateBeforeResolverInjection(
+            final EventResolverContext context,
+            final Object node) {
+
+        // Short-circuit if there are no additional fields.
+        final EventTemplateAdditionalField[] additionalFields =
+                context.getEventTemplateAdditionalFields();
+        if (additionalFields.length == 0) {
+            return node;
+        }
+
+        // Check that the root is an object node.
+        final Map<String, Object> objectNode;
+        try {
+            @SuppressWarnings("unchecked") final Map<String, Object> map = (Map<String, Object>) node;
+            objectNode = map;
+        } catch (final ClassCastException error) {
+            final String message = String.format(
+                    "was expecting an object to merge additional fields: %s",
+                    node.getClass().getName());
+            throw new IllegalArgumentException(message);
+        }
+
+        // Merge additional fields.
+        for (final EventTemplateAdditionalField additionalField : additionalFields) {
+            final String additionalFieldKey = additionalField.getKey();
+            final Object additionalFieldValue;
+            final EventTemplateAdditionalField.Format additionalFieldFormat =
+                    additionalField.getFormat();
+            if (EventTemplateAdditionalField.Format.STRING.equals(additionalFieldFormat)) {
+                additionalFieldValue = additionalField.getValue();
+            } else if (EventTemplateAdditionalField.Format.JSON.equals(additionalFieldFormat)) {
+                try {
+                    additionalFieldValue = JsonReader.read(additionalField.getValue());
+                } catch (final Exception error) {
+                    final String message = String.format(
+                            "failed reading JSON provided by additional field: %s",
+                            additionalFieldKey);
+                    throw new IllegalArgumentException(message, error);
+                }
+            } else {
+                final String message = String.format(
+                        "unknown format %s for additional field: %s",
+                        additionalFieldKey, additionalFieldFormat);
+                throw new IllegalArgumentException(message);
+            }
+            objectNode.put(additionalFieldKey, additionalFieldValue);
+        }
+
+        // Return the modified node.
+        return node;
+
+    }
+
+}
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/EventResolver.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/EventResolver.java
index 8427d6d..f34adb7 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/EventResolver.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/EventResolver.java
@@ -18,4 +18,7 @@ package org.apache.logging.log4j.layout.template.json.resolver;
 
 import org.apache.logging.log4j.core.LogEvent;
 
-interface EventResolver extends TemplateResolver<LogEvent> {}
+/**
+ * {@link TemplateResolver} specialized for {@link LogEvent}s.
+ */
+public interface EventResolver extends TemplateResolver<LogEvent> {}
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/EventResolverContext.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/EventResolverContext.java
index e1d2cb6..8ec2317 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/EventResolverContext.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/EventResolverContext.java
@@ -18,20 +18,31 @@ package org.apache.logging.log4j.layout.template.json.resolver;
 
 import org.apache.logging.log4j.core.LogEvent;
 import org.apache.logging.log4j.core.config.Configuration;
-import org.apache.logging.log4j.core.lookup.StrSubstitutor;
 import org.apache.logging.log4j.layout.template.json.JsonTemplateLayout.EventTemplateAdditionalField;
 import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
 import org.apache.logging.log4j.layout.template.json.util.RecyclerFactory;
+import org.apache.logging.log4j.util.Strings;
 
 import java.nio.charset.Charset;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 
+/**
+ * {@link TemplateResolverContext} specialized for {@link LogEvent}s.
+ *
+ * @see EventResolver
+ * @see EventResolverFactory
+ */
 public final class EventResolverContext implements TemplateResolverContext<LogEvent, EventResolverContext> {
 
     private final Configuration configuration;
 
-    private final StrSubstitutor substitutor;
+    private final Map<String, EventResolverFactory> resolverFactoryByName;
+
+    private final List<EventResolverInterceptor> resolverInterceptors;
+
+    private final EventResolverStringSubstitutor substitutor;
 
     private final Charset charset;
 
@@ -47,14 +58,16 @@ public final class EventResolverContext implements TemplateResolverContext<LogEv
 
     private final boolean stackTraceEnabled;
 
-    private final TemplateResolver<Throwable> stackTraceObjectResolver;
+    private final String stackTraceElementTemplate;
 
     private final String eventTemplateRootObjectKey;
 
-    private final EventTemplateAdditionalField[] additionalFields;
+    private final EventTemplateAdditionalField[] eventTemplateAdditionalFields;
 
     private EventResolverContext(final Builder builder) {
         this.configuration = builder.configuration;
+        this.resolverFactoryByName = builder.resolverFactoryByName;
+        this.resolverInterceptors = builder.resolverInterceptors;
         this.substitutor = builder.substitutor;
         this.charset = builder.charset;
         this.jsonWriter = builder.jsonWriter;
@@ -63,29 +76,32 @@ public final class EventResolverContext implements TemplateResolverContext<LogEv
         this.truncatedStringSuffix = builder.truncatedStringSuffix;
         this.locationInfoEnabled = builder.locationInfoEnabled;
         this.stackTraceEnabled = builder.stackTraceEnabled;
-        this.stackTraceObjectResolver = stackTraceEnabled
-                ? new StackTraceObjectResolver(builder.stackTraceElementObjectResolver)
-                : null;
+        this.stackTraceElementTemplate = builder.stackTraceElementTemplate;
         this.eventTemplateRootObjectKey = builder.eventTemplateRootObjectKey;
-        this.additionalFields = builder.eventTemplateAdditionalFields;
+        this.eventTemplateAdditionalFields = builder.eventTemplateAdditionalFields;
     }
 
     @Override
-    public Class<EventResolverContext> getContextClass() {
+    public final Class<EventResolverContext> getContextClass() {
         return EventResolverContext.class;
     }
 
+    public Configuration getConfiguration() {
+        return configuration;
+    }
+
     @Override
-    public Map<String, TemplateResolverFactory<LogEvent, EventResolverContext, ? extends TemplateResolver<LogEvent>>> getResolverFactoryByName() {
-        return EventResolverFactories.getResolverFactoryByName();
+    public Map<String, EventResolverFactory> getResolverFactoryByName() {
+        return resolverFactoryByName;
     }
 
-    public Configuration getConfiguration() {
-        return configuration;
+    @Override
+    public List<EventResolverInterceptor> getResolverInterceptors() {
+        return resolverInterceptors;
     }
 
     @Override
-    public StrSubstitutor getSubstitutor() {
+    public EventResolverStringSubstitutor getSubstitutor() {
         return substitutor;
     }
 
@@ -98,36 +114,36 @@ public final class EventResolverContext implements TemplateResolverContext<LogEv
         return jsonWriter;
     }
 
-    RecyclerFactory getRecyclerFactory() {
+    public RecyclerFactory getRecyclerFactory() {
         return recyclerFactory;
     }
 
-    int getMaxStringByteCount() {
+    public int getMaxStringByteCount() {
         return maxStringByteCount;
     }
 
-    String getTruncatedStringSuffix() {
+    public String getTruncatedStringSuffix() {
         return truncatedStringSuffix;
     }
 
-    boolean isLocationInfoEnabled() {
+    public boolean isLocationInfoEnabled() {
         return locationInfoEnabled;
     }
 
-    boolean isStackTraceEnabled() {
+    public boolean isStackTraceEnabled() {
         return stackTraceEnabled;
     }
 
-    TemplateResolver<Throwable> getStackTraceObjectResolver() {
-        return stackTraceObjectResolver;
+    public String getStackTraceElementTemplate() {
+        return stackTraceElementTemplate;
     }
 
-    String getEventTemplateRootObjectKey() {
+    public String getEventTemplateRootObjectKey() {
         return eventTemplateRootObjectKey;
     }
 
-    EventTemplateAdditionalField[] getAdditionalFields() {
-        return additionalFields;
+    public EventTemplateAdditionalField[] getEventTemplateAdditionalFields() {
+        return eventTemplateAdditionalFields;
     }
 
     public static Builder newBuilder() {
@@ -138,7 +154,11 @@ public final class EventResolverContext implements TemplateResolverContext<LogEv
 
         private Configuration configuration;
 
-        private StrSubstitutor substitutor;
+        private Map<String, EventResolverFactory> resolverFactoryByName;
+
+        private List<EventResolverInterceptor> resolverInterceptors;
+
+        private EventResolverStringSubstitutor substitutor;
 
         private Charset charset;
 
@@ -154,7 +174,7 @@ public final class EventResolverContext implements TemplateResolverContext<LogEv
 
         private boolean stackTraceEnabled;
 
-        private TemplateResolver<StackTraceElement> stackTraceElementObjectResolver;
+        private String stackTraceElementTemplate;
 
         private String eventTemplateRootObjectKey;
 
@@ -169,7 +189,19 @@ public final class EventResolverContext implements TemplateResolverContext<LogEv
             return this;
         }
 
-        public Builder setSubstitutor(final StrSubstitutor substitutor) {
+        public Builder setResolverFactoryByName(
+                final Map<String, EventResolverFactory> resolverFactoryByName) {
+            this.resolverFactoryByName = resolverFactoryByName;
+            return this;
+        }
+
+        public Builder setResolverInterceptors(
+                final List<EventResolverInterceptor> resolverInterceptors) {
+            this.resolverInterceptors = resolverInterceptors;
+            return this;
+        }
+
+        public Builder setSubstitutor(final EventResolverStringSubstitutor substitutor) {
             this.substitutor = substitutor;
             return this;
         }
@@ -194,10 +226,6 @@ public final class EventResolverContext implements TemplateResolverContext<LogEv
             return this;
         }
 
-        public String getTruncatedStringSuffix() {
-            return truncatedStringSuffix;
-        }
-
         public Builder setTruncatedStringSuffix(final String truncatedStringSuffix) {
             this.truncatedStringSuffix = truncatedStringSuffix;
             return this;
@@ -213,9 +241,8 @@ public final class EventResolverContext implements TemplateResolverContext<LogEv
             return this;
         }
 
-        public Builder setStackTraceElementObjectResolver(
-                final TemplateResolver<StackTraceElement> stackTraceElementObjectResolver) {
-            this.stackTraceElementObjectResolver = stackTraceElementObjectResolver;
+        public Builder setStackTraceElementTemplate(final String stackTraceElementTemplate) {
+            this.stackTraceElementTemplate = stackTraceElementTemplate;
             return this;
         }
 
@@ -237,6 +264,11 @@ public final class EventResolverContext implements TemplateResolverContext<LogEv
 
         private void validate() {
             Objects.requireNonNull(configuration, "configuration");
+            Objects.requireNonNull(resolverFactoryByName, "resolverFactoryByName");
+            if (resolverFactoryByName.isEmpty()) {
+                throw new IllegalArgumentException("empty resolverFactoryByName");
+            }
+            Objects.requireNonNull(resolverInterceptors, "resolverInterceptors");
             Objects.requireNonNull(substitutor, "substitutor");
             Objects.requireNonNull(charset, "charset");
             Objects.requireNonNull(jsonWriter, "jsonWriter");
@@ -246,11 +278,13 @@ public final class EventResolverContext implements TemplateResolverContext<LogEv
                         "was expecting maxStringByteCount > 0: " +
                                 maxStringByteCount);
             }
-            if (stackTraceEnabled) {
-                Objects.requireNonNull(
-                        stackTraceElementObjectResolver,
-                        "stackTraceElementObjectResolver");
+            Objects.requireNonNull(truncatedStringSuffix, "truncatedStringSuffix");
+            if (stackTraceEnabled && Strings.isBlank(stackTraceElementTemplate)) {
+                throw new IllegalArgumentException(
+                        "stackTraceElementTemplate cannot be blank when stackTraceEnabled is set to true");
             }
+            Objects.requireNonNull(stackTraceElementTemplate, "stackTraceElementTemplate");
+            Objects.requireNonNull(eventTemplateAdditionalFields, "eventTemplateAdditionalFields");
         }
 
     }
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/EventResolverFactories.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/EventResolverFactories.java
index 0bfcd76..b35cd22 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/EventResolverFactories.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/EventResolverFactories.java
@@ -18,51 +18,22 @@ package org.apache.logging.log4j.layout.template.json.resolver;
 
 import org.apache.logging.log4j.core.LogEvent;
 
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 
-final class EventResolverFactories {
+/**
+ * Utility class for {@link EventResolverFactory}.
+ */
+public final class EventResolverFactories {
 
     private EventResolverFactories() {}
 
-    private static final Map<String, TemplateResolverFactory<LogEvent, EventResolverContext, ? extends TemplateResolver<LogEvent>>> RESOLVER_FACTORY_BY_NAME =
-            createResolverFactoryByName();
-
-    private static Map<String, TemplateResolverFactory<LogEvent, EventResolverContext, ? extends TemplateResolver<LogEvent>>> createResolverFactoryByName() {
-
-        // Collect resolver factories.
-        final List<EventResolverFactory<? extends EventResolver>> resolverFactories = Arrays.asList(
-                ThreadContextDataResolverFactory.getInstance(),
-                ThreadContextStackResolverFactory.getInstance(),
-                EndOfBatchResolverFactory.getInstance(),
-                ExceptionResolverFactory.getInstance(),
-                ExceptionRootCauseResolverFactory.getInstance(),
-                LevelResolverFactory.getInstance(),
-                LoggerResolverFactory.getInstance(),
-                MainMapResolverFactory.getInstance(),
-                MapResolverFactory.getInstance(),
-                MarkerResolverFactory.getInstance(),
-                MessageResolverFactory.getInstance(),
-                MessageParameterResolverFactory.getInstance(),
-                PatternResolverFactory.getInstance(),
-                SourceResolverFactory.getInstance(),
-                ThreadResolverFactory.getInstance(),
-                TimestampResolverFactory.getInstance());
-
-        // Convert collection to map.
-        final Map<String, TemplateResolverFactory<LogEvent, EventResolverContext, ? extends TemplateResolver<LogEvent>>> resolverFactoryByName = new LinkedHashMap<>();
-        for (final EventResolverFactory<? extends EventResolver> resolverFactory : resolverFactories) {
-            resolverFactoryByName.put(resolverFactory.getName(), resolverFactory);
-        }
-        return Collections.unmodifiableMap(resolverFactoryByName);
-
-    }
-
-    static Map<String, TemplateResolverFactory<LogEvent, EventResolverContext, ? extends TemplateResolver<LogEvent>>> getResolverFactoryByName() {
-        return RESOLVER_FACTORY_BY_NAME;
+    public static Map<String, EventResolverFactory> populateResolverFactoryByName(
+            final List<String> pluginPackages) {
+        return TemplateResolverFactories.populateFactoryByName(
+                pluginPackages,
+                LogEvent.class,
+                EventResolverContext.class);
     }
 
 }
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/EventResolverFactory.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/EventResolverFactory.java
index 11c236e..2783bff 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/EventResolverFactory.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/EventResolverFactory.java
@@ -18,4 +18,23 @@ package org.apache.logging.log4j.layout.template.json.resolver;
 
 import org.apache.logging.log4j.core.LogEvent;
 
-interface EventResolverFactory<R extends TemplateResolver<LogEvent>> extends TemplateResolverFactory<LogEvent, EventResolverContext, R> {}
+/**
+ * {@link TemplateResolverFactory} specialized for {@link LogEvent}s.
+ *
+ * @see EventResolver
+ * @see EventResolverContext
+ */
+public interface EventResolverFactory
+        extends TemplateResolverFactory<LogEvent, EventResolverContext> {
+
+    @Override
+    default Class<LogEvent> getValueClass() {
+        return LogEvent.class;
+    }
+
+    @Override
+    default Class<EventResolverContext> getContextClass() {
+        return EventResolverContext.class;
+    }
+
+}
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/EventResolver.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/EventResolverInterceptor.java
similarity index 68%
copy from log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/EventResolver.java
copy to log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/EventResolverInterceptor.java
index 8427d6d..126ac46 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/EventResolver.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/EventResolverInterceptor.java
@@ -18,4 +18,20 @@ package org.apache.logging.log4j.layout.template.json.resolver;
 
 import org.apache.logging.log4j.core.LogEvent;
 
-interface EventResolver extends TemplateResolver<LogEvent> {}
+/**
+ * {@link TemplateResolverInterceptor} specialized for {@link LogEvent}s.
+ */
+public interface EventResolverInterceptor
+        extends TemplateResolverInterceptor<LogEvent, EventResolverContext> {
+
+    @Override
+    default Class<LogEvent> getValueClass() {
+        return LogEvent.class;
+    }
+
+    @Override
+    default Class<EventResolverContext> getContextClass() {
+        return EventResolverContext.class;
+    }
+
+}
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/EventResolver.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/EventResolverInterceptors.java
similarity index 65%
copy from log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/EventResolver.java
copy to log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/EventResolverInterceptors.java
index 8427d6d..15b35f2 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/EventResolver.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/EventResolverInterceptors.java
@@ -18,4 +18,21 @@ package org.apache.logging.log4j.layout.template.json.resolver;
 
 import org.apache.logging.log4j.core.LogEvent;
 
-interface EventResolver extends TemplateResolver<LogEvent> {}
+import java.util.List;
+
+/**
+ * Utility class for {@link EventResolverInterceptor}.
+ */
+public final class EventResolverInterceptors {
+
+    private EventResolverInterceptors() {}
+
+    public static List<EventResolverInterceptor> populateInterceptors(
+            final List<String> pluginPackages) {
+        return TemplateResolverInterceptors.populateInterceptors(
+                pluginPackages,
+                LogEvent.class,
+                EventResolverContext.class);
+    }
+
+}
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ExceptionRootCauseResolver.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/EventResolverStringSubstitutor.java
similarity index 55%
copy from log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ExceptionRootCauseResolver.java
copy to log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/EventResolverStringSubstitutor.java
index 37119ca..cb109af 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ExceptionRootCauseResolver.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/EventResolverStringSubstitutor.java
@@ -17,33 +17,35 @@
 package org.apache.logging.log4j.layout.template.json.resolver;
 
 import org.apache.logging.log4j.core.LogEvent;
-import org.apache.logging.log4j.core.util.Throwables;
-import org.apache.logging.log4j.layout.template.json.JsonTemplateLayout;
+import org.apache.logging.log4j.core.lookup.StrSubstitutor;
+
+import java.util.Objects;
 
 /**
- * Exception root cause resolver.
- *
- * Note that this resolver is toggled by {@link
- * JsonTemplateLayout.Builder#setStackTraceEnabled(boolean)}.
- *
- * @see ExceptionResolver
+ * {@link TemplateResolverStringSubstitutor} specialized for {@link LogEvent}s.
  */
-final class ExceptionRootCauseResolver extends ExceptionResolver {
+public final class EventResolverStringSubstitutor
+        implements TemplateResolverStringSubstitutor<LogEvent> {
+
+    private final StrSubstitutor substitutor;
 
-    ExceptionRootCauseResolver(
-            final EventResolverContext context,
-            final TemplateResolverConfig config) {
-        super(context, config);
+    public EventResolverStringSubstitutor(final StrSubstitutor substitutor) {
+        this.substitutor = Objects.requireNonNull(substitutor, "substitutor");
     }
 
-    static String getName() {
-        return "exceptionRootCause";
+    @Override
+    public StrSubstitutor getInternalSubstitutor() {
+        return substitutor;
+    }
+
+    @Override
+    public boolean isStable() {
+        return false;
     }
 
     @Override
-    Throwable extractThrowable(final LogEvent logEvent) {
-        final Throwable thrown = logEvent.getThrown();
-        return thrown != null ? Throwables.getRootCause(thrown) : null;
+    public String replace(final LogEvent logEvent, final String source) {
+        return substitutor.replace(logEvent, source);
     }
 
 }
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/EventRootObjectKeyInterceptor.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/EventRootObjectKeyInterceptor.java
new file mode 100644
index 0000000..f601c66
--- /dev/null
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/EventRootObjectKeyInterceptor.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.layout.template.json.resolver;
+
+import org.apache.logging.log4j.core.config.plugins.Plugin;
+import org.apache.logging.log4j.core.config.plugins.PluginFactory;
+import org.apache.logging.log4j.layout.template.json.JsonTemplateLayout;
+
+import java.util.Collections;
+
+/**
+ * Interceptor to add a root object key to the event template.
+ *
+ * @see JsonTemplateLayout.Builder#getEventTemplateRootObjectKey()
+ */
+@Plugin(name = "EventRootObjectKeyInterceptor", category = TemplateResolverInterceptor.CATEGORY)
+public class EventRootObjectKeyInterceptor implements EventResolverInterceptor {
+
+    private static final EventRootObjectKeyInterceptor INSTANCE =
+            new EventRootObjectKeyInterceptor();
+
+    private EventRootObjectKeyInterceptor() {}
+
+    @PluginFactory
+    public static EventRootObjectKeyInterceptor getInstance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public Object processTemplateBeforeResolverInjection(
+            final EventResolverContext context,
+            final Object node) {
+        String eventTemplateRootObjectKey = context.getEventTemplateRootObjectKey();
+        return eventTemplateRootObjectKey != null
+                ? Collections.singletonMap(eventTemplateRootObjectKey, node)
+                : node;
+    }
+
+}
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ExceptionResolver.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ExceptionResolver.java
index 4cdcd28..e2a538a 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ExceptionResolver.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ExceptionResolver.java
@@ -25,11 +25,17 @@ import org.apache.logging.log4j.status.StatusLogger;
 
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 import java.util.regex.Pattern;
 import java.util.regex.PatternSyntaxException;
 
 /**
  * Exception resolver.
+ * <p>
+ * Note that this resolver is toggled by {@link
+ * JsonTemplateLayout.Builder#setStackTraceEnabled(boolean) stackTraceEnabled}
+ * layout configuration, which is by default populated from <tt>log4j.layout.jsonTemplate.stackTraceEnabled</tt>
+ * system property.
  *
  * <h3>Configuration</h3>
  *
@@ -37,7 +43,11 @@ import java.util.regex.PatternSyntaxException;
  * config              = field , [ stringified ] , [ stackTrace ]
  * field               = "field" -> ( "className" | "message" | "stackTrace" )
  *
- * stackTrace          = "stackTrace" -> stringified
+ * stackTrace          = "stackTrace" -> (
+ *                         [ stringified ]
+ *                       , [ elementTemplate ]
+ *                       )
+ *
  * stringified         = "stringified" -> ( boolean | truncation )
  * truncation          = "truncation" -> (
  *                         [ suffix ]
@@ -47,6 +57,8 @@ import java.util.regex.PatternSyntaxException;
  * suffix              = "suffix" -> string
  * pointMatcherStrings = "pointMatcherStrings" -> string[]
  * pointMatcherRegexes = "pointMatcherRegexes" -> string[]
+ *
+ * elementTemplate     = "elementTemplate" -> object
  * </pre>
  *
  * <tt>stringified</tt> is set to <tt>false</tt> by default.
@@ -61,6 +73,28 @@ import java.util.regex.PatternSyntaxException;
  * If a stringified stack trace truncation takes place, it will be indicated
  * with <tt>suffix</tt>, which by default is set to the configured
  * <tt>truncatedStringSuffix</tt> in the layout, unless explicitly provided.
+ * <p>
+ * <tt>elementTemplate</tt> is an object describing the template to be used
+ * while resolving the {@link StackTraceElement} array. If <tt>stringified</tt>
+ * is set to <tt>true</tt>, <tt>elementTemplate</tt> will be discarded. By
+ * default, <tt>elementTemplate</tt> is set to <tt>null</tt> and rather
+ * populated from the layout configuration. That is, the stack trace element
+ * template can also be provided using {@link JsonTemplateLayout.Builder#setStackTraceElementTemplate(String) stackTraceElementTemplate}
+ * and {@link JsonTemplateLayout.Builder#setStackTraceElementTemplateUri(String) setStackTraceElementTemplateUri}
+ * layout configuration parameters. The template to be employed is determined
+ * in the following order:
+ * <ol>
+ * <li><tt>elementTemplate</tt> provided in the resolver configuration
+ * <li><tt>stackTraceElementTemplate</tt> parameter from layout configuration
+ * (the default is populated from <tt>log4j.layout.jsonTemplate.stackTraceElementTemplate</tt>
+ * system property)
+ * <li><tt>stackTraceElementTemplateUri</tt> parameter from layout configuration
+ * (the default is populated from <tt>log4j.layout.jsonTemplate.stackTraceElementTemplateUri</tt>
+ * system property)
+ * </ol>
+ * <p>
+ * See {@link StackTraceElementResolver}
+ * for the list of available resolvers in a stack trace element template.
  *
  * <h3>Examples</h3>
  *
@@ -95,7 +129,7 @@ import java.util.regex.PatternSyntaxException;
  * </pre>
  *
  * Resolve the stack trace into a string field
- * such that the content will be truncated by the given point matcher:
+ * such that the content will be truncated after the given point matcher:
  *
  * <pre>
  *  {
@@ -112,11 +146,46 @@ import java.util.regex.PatternSyntaxException;
  * }
  * </pre>
  *
+ * Resolve the stack trace into an object described by the provided stack trace
+ * element template:
+ *
+ * <pre>
+ *  {
+ *   "$resolver": "exception",
+ *   "field": "stackTrace",
+ *   "stackTrace": {
+ *     "elementTemplate": {
+ *       "class": {
+ *         "$resolver": "stackTraceElement",
+ *         "field": "className"
+ *       },
+ *       "method": {
+ *         "$resolver": "stackTraceElement",
+ *         "field": "methodName"
+ *       },
+ *       "file": {
+ *         "$resolver": "stackTraceElement",
+ *         "field": "fileName"
+ *       },
+ *       "line": {
+ *         "$resolver": "stackTraceElement",
+ *         "field": "lineNumber"
+ *       }
+ *     }
+ *   }
+ * }
+ * </pre>
+ *
  * @see JsonTemplateLayout.Builder#getTruncatedStringSuffix()
  * @see JsonTemplateLayoutDefaults#getTruncatedStringSuffix()
+ * @see JsonTemplateLayout.Builder#getStackTraceElementTemplate()
+ * @see JsonTemplateLayoutDefaults#getStackTraceElementTemplate()
+ * @see JsonTemplateLayout.Builder#getStackTraceElementTemplateUri()
+ * @see JsonTemplateLayoutDefaults#getStackTraceElementTemplateUri()
  * @see ExceptionRootCauseResolver
+ * @see StackTraceElementResolver
  */
-class ExceptionResolver implements EventResolver {
+public class ExceptionResolver implements EventResolver {
 
     private static final Logger LOGGER = StatusLogger.getLogger();
 
@@ -138,13 +207,14 @@ class ExceptionResolver implements EventResolver {
             final EventResolverContext context,
             final TemplateResolverConfig config) {
         final String fieldName = config.getString("field");
-        switch (fieldName) {
-            case "className": return createClassNameResolver();
-            case "message": return createMessageResolver();
-            case "stackTrace": return createStackTraceResolver(context, config);
+        if ("className".equals(fieldName)) {
+            return createClassNameResolver();
+        } else if ("message".equals(fieldName)) {
+            return createMessageResolver();
+        } else if ("stackTrace".equals(fieldName)) {
+            return createStackTraceResolver(context, config);
         }
         throw new IllegalArgumentException("unknown field: " + config);
-
     }
 
     private EventResolver createClassNameResolver() {
@@ -180,7 +250,7 @@ class ExceptionResolver implements EventResolver {
         final boolean stringified = isStackTraceStringified(config);
         return stringified
                 ? createStackTraceStringResolver(context, config)
-                : createStackTraceObjectResolver(context);
+                : createStackTraceObjectResolver(context, config);
     }
 
     private static boolean isStackTraceStringified(
@@ -285,20 +355,71 @@ class ExceptionResolver implements EventResolver {
 
     }
 
+    private static final Map<String, StackTraceElementResolverFactory> STACK_TRACE_ELEMENT_RESOLVER_FACTORY_BY_NAME;
+
+    static {
+        final StackTraceElementResolverFactory stackTraceElementResolverFactory =
+                StackTraceElementResolverFactory.getInstance();
+        STACK_TRACE_ELEMENT_RESOLVER_FACTORY_BY_NAME =
+                Collections.singletonMap(
+                        stackTraceElementResolverFactory.getName(),
+                        stackTraceElementResolverFactory);
+    }
+
     private EventResolver createStackTraceObjectResolver(
-            final EventResolverContext context) {
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
+        final TemplateResolver<StackTraceElement> stackTraceElementResolver =
+                createStackTraceElementResolver(context, config);
+        final StackTraceObjectResolver stackTraceResolver =
+                new StackTraceObjectResolver(stackTraceElementResolver);
         return (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
-            final Throwable exception = extractThrowable(logEvent);
-            if (exception == null) {
+            final Throwable throwable = extractThrowable(logEvent);
+            if (throwable == null) {
                 jsonWriter.writeNull();
             } else {
-                context
-                        .getStackTraceObjectResolver()
-                        .resolve(exception, jsonWriter);
+                stackTraceResolver.resolve(throwable, jsonWriter);
             }
         };
     }
 
+    private static TemplateResolver<StackTraceElement> createStackTraceElementResolver(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
+        final StackTraceElementResolverStringSubstitutor substitutor =
+                new StackTraceElementResolverStringSubstitutor(
+                        context.getSubstitutor().getInternalSubstitutor());
+        final StackTraceElementResolverContext stackTraceElementResolverContext =
+                StackTraceElementResolverContext
+                        .newBuilder()
+                        .setResolverFactoryByName(STACK_TRACE_ELEMENT_RESOLVER_FACTORY_BY_NAME)
+                        .setSubstitutor(substitutor)
+                        .setJsonWriter(context.getJsonWriter())
+                        .build();
+        final String stackTraceElementTemplate =
+                findEffectiveStackTraceElementTemplate(context, config);
+        return TemplateResolvers.ofTemplate(
+                stackTraceElementResolverContext,
+                stackTraceElementTemplate);
+    }
+
+    private static String findEffectiveStackTraceElementTemplate(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
+
+        // First, check the template configured in the resolver configuration.
+        final Object stackTraceElementTemplateObject =
+                config.getObject(new String[]{"stackTrace", "elementTemplate"});
+        if (stackTraceElementTemplateObject != null) {
+            final JsonWriter jsonWriter = context.getJsonWriter();
+            return jsonWriter.use(() -> jsonWriter.writeValue(stackTraceElementTemplateObject));
+        }
+
+        // Otherwise, use the template provided in the context.
+        return context.getStackTraceElementTemplate();
+
+    }
+
     Throwable extractThrowable(final LogEvent logEvent) {
         return logEvent.getThrown();
     }
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ExceptionResolverFactory.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ExceptionResolverFactory.java
index 89ced7f..a58a4e7 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ExceptionResolverFactory.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ExceptionResolverFactory.java
@@ -16,15 +16,22 @@
  */
 package org.apache.logging.log4j.layout.template.json.resolver;
 
-final class ExceptionResolverFactory
-        implements EventResolverFactory<ExceptionResolver> {
+import org.apache.logging.log4j.core.config.plugins.Plugin;
+import org.apache.logging.log4j.core.config.plugins.PluginFactory;
+
+/**
+ * {@link ExceptionResolver} factory.
+ */
+@Plugin(name = "ExceptionResolverFactory", category = TemplateResolverFactory.CATEGORY)
+public final class ExceptionResolverFactory implements EventResolverFactory {
 
     private static final ExceptionResolverFactory INSTANCE =
             new ExceptionResolverFactory();
 
     private ExceptionResolverFactory() {}
 
-    static ExceptionResolverFactory getInstance() {
+    @PluginFactory
+    public static ExceptionResolverFactory getInstance() {
         return INSTANCE;
     }
 
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ExceptionRootCauseResolver.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ExceptionRootCauseResolver.java
index 37119ca..9d630b7 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ExceptionRootCauseResolver.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ExceptionRootCauseResolver.java
@@ -22,13 +22,15 @@ import org.apache.logging.log4j.layout.template.json.JsonTemplateLayout;
 
 /**
  * Exception root cause resolver.
- *
+ * <p>
  * Note that this resolver is toggled by {@link
- * JsonTemplateLayout.Builder#setStackTraceEnabled(boolean)}.
+ * JsonTemplateLayout.Builder#setStackTraceEnabled(boolean) stackTraceEnabled}
+ * layout configuration, which is by default populated from <tt>log4j.layout.jsonTemplate.stackTraceEnabled</tt>
+ * system property.
  *
  * @see ExceptionResolver
  */
-final class ExceptionRootCauseResolver extends ExceptionResolver {
+public final class ExceptionRootCauseResolver extends ExceptionResolver {
 
     ExceptionRootCauseResolver(
             final EventResolverContext context,
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ExceptionRootCauseResolverFactory.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ExceptionRootCauseResolverFactory.java
index 7dcea5f..756b98c 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ExceptionRootCauseResolverFactory.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ExceptionRootCauseResolverFactory.java
@@ -16,13 +16,22 @@
  */
 package org.apache.logging.log4j.layout.template.json.resolver;
 
-final class ExceptionRootCauseResolverFactory implements EventResolverFactory<ExceptionRootCauseResolver> {
+import org.apache.logging.log4j.core.config.plugins.Plugin;
+import org.apache.logging.log4j.core.config.plugins.PluginFactory;
 
-    private static final ExceptionRootCauseResolverFactory INSTANCE = new ExceptionRootCauseResolverFactory();
+/**
+ * {@link ExceptionRootCauseResolver} factory.
+ */
+@Plugin(name = "ExceptionRootCauseResolverFactory", category = TemplateResolverFactory.CATEGORY)
+public final class ExceptionRootCauseResolverFactory implements EventResolverFactory {
+
+    private static final ExceptionRootCauseResolverFactory INSTANCE =
+            new ExceptionRootCauseResolverFactory();
 
     private ExceptionRootCauseResolverFactory() {}
 
-    static ExceptionRootCauseResolverFactory getInstance() {
+    @PluginFactory
+    public static ExceptionRootCauseResolverFactory getInstance() {
         return INSTANCE;
     }
 
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/LevelResolver.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/LevelResolver.java
index 2952eb4..cc04c4a 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/LevelResolver.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/LevelResolver.java
@@ -72,9 +72,9 @@ import java.util.stream.Collectors;
  * }
  * </pre>
  */
-final class LevelResolver implements EventResolver {
+public final class LevelResolver implements EventResolver {
 
-    private static String[] SEVERITY_CODE_RESOLUTION_BY_STANDARD_LEVEL_ORDINAL;
+    private static final String[] SEVERITY_CODE_RESOLUTION_BY_STANDARD_LEVEL_ORDINAL;
 
     static {
         final int levelCount = Level.values().length;
@@ -113,21 +113,20 @@ final class LevelResolver implements EventResolver {
             final TemplateResolverConfig config) {
         final JsonWriter jsonWriter = context.getJsonWriter();
         final String fieldName = config.getString("field");
-        switch (fieldName) {
-            case "name": return createNameResolver(jsonWriter);
-            case "severity": {
-                final String severityFieldName =
-                        config.getString(new String[]{"severity", "field"});
-                switch (severityFieldName) {
-                    case "keyword": return createSeverityKeywordResolver(jsonWriter);
-                    case "code": return SEVERITY_CODE_RESOLVER;
-                    default:
-                        throw new IllegalArgumentException(
-                                "unknown severity field: " + config);
-                }
+        if ("name".equals(fieldName)) {
+            return createNameResolver(jsonWriter);
+        } else if ("severity".equals(fieldName)) {
+            final String severityFieldName =
+                    config.getString(new String[]{"severity", "field"});
+            if ("keyword".equals(severityFieldName)) {
+                return createSeverityKeywordResolver(jsonWriter);
+            } else if ("code".equals(severityFieldName)) {
+                return SEVERITY_CODE_RESOLVER;
             }
-            default: throw new IllegalArgumentException("unknown field: " + config);
+            throw new IllegalArgumentException(
+                    "unknown severity field: " + config);
         }
+        throw new IllegalArgumentException("unknown field: " + config);
     }
 
     private static EventResolver createNameResolver(
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/LevelResolverFactory.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/LevelResolverFactory.java
index 2e8dc60..819dd87 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/LevelResolverFactory.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/LevelResolverFactory.java
@@ -16,13 +16,21 @@
  */
 package org.apache.logging.log4j.layout.template.json.resolver;
 
-final class LevelResolverFactory implements EventResolverFactory<LevelResolver> {
+import org.apache.logging.log4j.core.config.plugins.Plugin;
+import org.apache.logging.log4j.core.config.plugins.PluginFactory;
+
+/**
+ * {@link LevelResolver} factory.
+ */
+@Plugin(name = "LevelResolverFactory", category = TemplateResolverFactory.CATEGORY)
+public final class LevelResolverFactory implements EventResolverFactory {
 
     private static final LevelResolverFactory INSTANCE = new LevelResolverFactory();
 
     private LevelResolverFactory() {}
 
-    static LevelResolverFactory getInstance() {
+    @PluginFactory
+    public static LevelResolverFactory getInstance() {
         return INSTANCE;
     }
 
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/LoggerResolver.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/LoggerResolver.java
index 6a47531..13088fc 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/LoggerResolver.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/LoggerResolver.java
@@ -48,7 +48,7 @@ import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
  * }
  * </pre>
  */
-final class LoggerResolver implements EventResolver {
+public final class LoggerResolver implements EventResolver {
 
     private static final EventResolver NAME_RESOLVER =
             (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
@@ -71,9 +71,10 @@ final class LoggerResolver implements EventResolver {
     private static EventResolver createInternalResolver(
             final TemplateResolverConfig config) {
         final String fieldName = config.getString("field");
-        switch (fieldName) {
-            case "name": return NAME_RESOLVER;
-            case "fqcn": return FQCN_RESOLVER;
+        if ("name".equals(fieldName)) {
+            return NAME_RESOLVER;
+        } else if ("fqcn".equals(fieldName)) {
+            return FQCN_RESOLVER;
         }
         throw new IllegalArgumentException("unknown field: " + config);
     }
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/LoggerResolverFactory.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/LoggerResolverFactory.java
index 08b6fb9..7e889f9 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/LoggerResolverFactory.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/LoggerResolverFactory.java
@@ -16,13 +16,21 @@
  */
 package org.apache.logging.log4j.layout.template.json.resolver;
 
-final class LoggerResolverFactory implements EventResolverFactory<LoggerResolver> {
+import org.apache.logging.log4j.core.config.plugins.Plugin;
+import org.apache.logging.log4j.core.config.plugins.PluginFactory;
+
+/**
+ * {@link LoggerResolver} factory.
+ */
+@Plugin(name = "LoggerResolverFactory", category = TemplateResolverFactory.CATEGORY)
+public final class LoggerResolverFactory implements EventResolverFactory {
 
     private static final LoggerResolverFactory INSTANCE = new LoggerResolverFactory();
 
     private LoggerResolverFactory() {}
 
-    static LoggerResolverFactory getInstance() {
+    @PluginFactory
+    public static LoggerResolverFactory getInstance() {
         return INSTANCE;
     }
 
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MainMapResolver.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MainMapResolver.java
index 514dd1e..a3edafd 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MainMapResolver.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MainMapResolver.java
@@ -53,7 +53,7 @@ import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
  *
  * @see MainMapResolver
  */
-final class MainMapResolver implements EventResolver {
+public final class MainMapResolver implements EventResolver {
 
     private static final MainMapLookup MAIN_MAP_LOOKUP = new MainMapLookup();
 
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MainMapResolverFactory.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MainMapResolverFactory.java
index ee0ab3f..917b4d6 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MainMapResolverFactory.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MainMapResolverFactory.java
@@ -16,13 +16,21 @@
  */
 package org.apache.logging.log4j.layout.template.json.resolver;
 
-final class MainMapResolverFactory implements EventResolverFactory<MainMapResolver> {
+import org.apache.logging.log4j.core.config.plugins.Plugin;
+import org.apache.logging.log4j.core.config.plugins.PluginFactory;
+
+/**
+ * {@link MainMapResolver} factory.
+ */
+@Plugin(name = "MainMapResolverFactory", category = TemplateResolverFactory.CATEGORY)
+public final class MainMapResolverFactory implements EventResolverFactory {
 
     private static final MainMapResolverFactory INSTANCE = new MainMapResolverFactory();
 
     private MainMapResolverFactory() {}
 
-    static MainMapResolverFactory getInstance() {
+    @PluginFactory
+    public static MainMapResolverFactory getInstance() {
         return INSTANCE;
     }
 
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 5cc07eb..d5801a4 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
@@ -26,7 +26,7 @@ import org.apache.logging.log4j.util.ReadOnlyStringMap;
  *
  * @see ReadOnlyStringMapResolver
  */
-final class MapResolver extends ReadOnlyStringMapResolver {
+public final class MapResolver extends ReadOnlyStringMapResolver {
 
     MapResolver(
             final EventResolverContext context,
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 53092c9..a663d9e 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
@@ -16,13 +16,21 @@
  */
 package org.apache.logging.log4j.layout.template.json.resolver;
 
-final class MapResolverFactory implements EventResolverFactory<MapResolver> {
+import org.apache.logging.log4j.core.config.plugins.Plugin;
+import org.apache.logging.log4j.core.config.plugins.PluginFactory;
+
+/**
+ * {@link MapResolver} factory.
+ */
+@Plugin(name = "MapResolverFactory", category = TemplateResolverFactory.CATEGORY)
+public final class MapResolverFactory implements EventResolverFactory {
 
     private static final MapResolverFactory INSTANCE = new MapResolverFactory();
 
     private MapResolverFactory() {}
 
-    static MapResolverFactory getInstance() {
+    @PluginFactory
+    public static MapResolverFactory getInstance() {
         return INSTANCE;
     }
 
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MarkerResolver.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MarkerResolver.java
index 1448444..fc1c30c 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MarkerResolver.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MarkerResolver.java
@@ -40,7 +40,7 @@ import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
  * }
  * </pre>
  */
-final class MarkerResolver implements EventResolver {
+public final class MarkerResolver implements EventResolver {
 
     private static final TemplateResolver<LogEvent> NAME_RESOLVER =
             (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MarkerResolverFactory.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MarkerResolverFactory.java
index 41b2126..4a10b4d 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MarkerResolverFactory.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MarkerResolverFactory.java
@@ -16,16 +16,24 @@
  */
 package org.apache.logging.log4j.layout.template.json.resolver;
 
-final class MarkerResolverFactory implements EventResolverFactory<MarkerResolver> {
+import org.apache.logging.log4j.core.config.plugins.Plugin;
+import org.apache.logging.log4j.core.config.plugins.PluginFactory;
+
+/**
+ * {@link MarkerResolver} factory.
+ */
+@Plugin(name = "MarkerResolverFactory", category = TemplateResolverFactory.CATEGORY)
+public final class MarkerResolverFactory implements EventResolverFactory {
 
     private static final MarkerResolverFactory INSTANCE = new MarkerResolverFactory();
 
-    static MarkerResolverFactory getInstance() {
+    private MarkerResolverFactory() {}
+
+    @PluginFactory
+    public static MarkerResolverFactory getInstance() {
         return INSTANCE;
     }
 
-    private MarkerResolverFactory() {}
-
     @Override
     public String getName() {
         return MarkerResolver.getName();
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MessageParameterResolver.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MessageParameterResolver.java
index 238f523..9b8182e 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MessageParameterResolver.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MessageParameterResolver.java
@@ -71,7 +71,7 @@ import org.apache.logging.log4j.message.ParameterVisitable;
  * }
  * </pre>
  */
-final class MessageParameterResolver implements EventResolver {
+public final class MessageParameterResolver implements EventResolver {
 
     private final Recycler<ParameterConsumerState> parameterConsumerStateRecycler;
 
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MessageParameterResolverFactory.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MessageParameterResolverFactory.java
index 055071c..15c398c 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MessageParameterResolverFactory.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MessageParameterResolverFactory.java
@@ -16,13 +16,22 @@
  */
 package org.apache.logging.log4j.layout.template.json.resolver;
 
-final class MessageParameterResolverFactory implements EventResolverFactory<MessageParameterResolver> {
+import org.apache.logging.log4j.core.config.plugins.Plugin;
+import org.apache.logging.log4j.core.config.plugins.PluginFactory;
 
-    private static final MessageParameterResolverFactory INSTANCE = new MessageParameterResolverFactory();
+/**
+ * {@link MessageParameterResolver} factory.
+ */
+@Plugin(name = "MessageParameterResolverFactory", category = TemplateResolverFactory.CATEGORY)
+public final class MessageParameterResolverFactory implements EventResolverFactory {
+
+    private static final MessageParameterResolverFactory INSTANCE =
+            new MessageParameterResolverFactory();
 
     private MessageParameterResolverFactory() {}
 
-    static MessageParameterResolverFactory getInstance() {
+    @PluginFactory
+    public static MessageParameterResolverFactory getInstance() {
         return INSTANCE;
     }
 
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MessageResolver.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MessageResolver.java
index f42dbab..2975e66 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MessageResolver.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MessageResolver.java
@@ -79,7 +79,7 @@ import org.apache.logging.log4j.util.StringBuilderFormattable;
  * that both emitted JSONs are of type <tt>object</tt> and have no
  * type-conflicting fields.
  */
-final class MessageResolver implements EventResolver {
+public final class MessageResolver implements EventResolver {
 
     private static final String[] FORMATS = { "JSON" };
 
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MessageResolverFactory.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MessageResolverFactory.java
index 5e2a3db..334edfc 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MessageResolverFactory.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MessageResolverFactory.java
@@ -16,13 +16,21 @@
  */
 package org.apache.logging.log4j.layout.template.json.resolver;
 
-final class MessageResolverFactory implements EventResolverFactory<MessageResolver> {
+import org.apache.logging.log4j.core.config.plugins.Plugin;
+import org.apache.logging.log4j.core.config.plugins.PluginFactory;
+
+/**
+ * @see MessageResolver
+ */
+@Plugin(name = "MessageResolverFactory", category = TemplateResolverFactory.CATEGORY)
+public final class MessageResolverFactory implements EventResolverFactory {
 
     private static final MessageResolverFactory INSTANCE = new MessageResolverFactory();
 
     private MessageResolverFactory() {}
 
-    static MessageResolverFactory getInstance() {
+    @PluginFactory
+    public static MessageResolverFactory getInstance() {
         return INSTANCE;
     }
 
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/PatternResolver.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/PatternResolver.java
index 5bfc4b4..8cd2991 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/PatternResolver.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/PatternResolver.java
@@ -51,7 +51,7 @@ import java.util.Optional;
  * }
  * </pre>
  */
-final class PatternResolver implements EventResolver {
+public final class PatternResolver implements EventResolver {
 
     private final BiConsumer<StringBuilder, LogEvent> emitter;
 
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/PatternResolverFactory.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/PatternResolverFactory.java
index 95c9a95..9c105b4 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/PatternResolverFactory.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/PatternResolverFactory.java
@@ -16,13 +16,21 @@
  */
 package org.apache.logging.log4j.layout.template.json.resolver;
 
-final class PatternResolverFactory implements EventResolverFactory<PatternResolver> {
+import org.apache.logging.log4j.core.config.plugins.Plugin;
+import org.apache.logging.log4j.core.config.plugins.PluginFactory;
+
+/**
+ * {@link PatternResolver} factory.
+ */
+@Plugin(name = "PatternResolverFactory", category = TemplateResolverFactory.CATEGORY)
+public final class PatternResolverFactory implements EventResolverFactory {
 
     private static final PatternResolverFactory INSTANCE = new PatternResolverFactory();
 
     private PatternResolverFactory() {}
 
-    static PatternResolverFactory getInstance() {
+    @PluginFactory
+    public static PatternResolverFactory getInstance() {
         return INSTANCE;
     }
 
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/SourceResolver.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/SourceResolver.java
index b4e63e3..36e5f29 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/SourceResolver.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/SourceResolver.java
@@ -22,10 +22,11 @@ import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
 
 /**
  * Resolver for the {@link StackTraceElement} returned by {@link LogEvent#getSource()}.
- *
+ * <p>
  * Note that this resolver is toggled by {@link
- * JsonTemplateLayout.Builder#setLocationInfoEnabled(boolean)}
- * method.
+ * JsonTemplateLayout.Builder#setLocationInfoEnabled(boolean) locationInfoEnabled}
+ * layout configuration, which is by default populated from {@code log4j.layout.jsonTemplate.locationInfoEnabled}
+ * system property.
  *
  * <h3>Configuration</h3>
  *
@@ -48,7 +49,7 @@ import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
  * }
  * </pre>
  */
-final class SourceResolver implements EventResolver {
+public final class SourceResolver implements EventResolver {
 
     private static final EventResolver NULL_RESOLVER =
             (final LogEvent value, final JsonWriter jsonWriter) ->
@@ -116,11 +117,14 @@ final class SourceResolver implements EventResolver {
             return NULL_RESOLVER;
         }
         final String fieldName = config.getString("field");
-        switch (fieldName) {
-            case "className": return CLASS_NAME_RESOLVER;
-            case "fileName": return FILE_NAME_RESOLVER;
-            case "lineNumber": return LINE_NUMBER_RESOLVER;
-            case "methodName": return METHOD_NAME_RESOLVER;
+        if ("className".equals(fieldName)) {
+            return CLASS_NAME_RESOLVER;
+        } else if ("fileName".equals(fieldName)) {
+            return FILE_NAME_RESOLVER;
+        } else if ("lineNumber".equals(fieldName)) {
+            return LINE_NUMBER_RESOLVER;
+        } else if ("methodName".equals(fieldName)) {
+            return METHOD_NAME_RESOLVER;
         }
         throw new IllegalArgumentException("unknown field: " + config);
     }
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/SourceResolverFactory.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/SourceResolverFactory.java
index 65a78d0..223fbfa 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/SourceResolverFactory.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/SourceResolverFactory.java
@@ -16,13 +16,21 @@
  */
 package org.apache.logging.log4j.layout.template.json.resolver;
 
-final class SourceResolverFactory implements EventResolverFactory<SourceResolver> {
+import org.apache.logging.log4j.core.config.plugins.Plugin;
+import org.apache.logging.log4j.core.config.plugins.PluginFactory;
+
+/**
+ * {@link SourceResolver} factory.
+ */
+@Plugin(name = "SourceResolverFactory", category = TemplateResolverFactory.CATEGORY)
+public final class SourceResolverFactory implements EventResolverFactory {
 
     private static final SourceResolverFactory INSTANCE = new SourceResolverFactory();
 
     private SourceResolverFactory() {}
 
-    static SourceResolverFactory getInstance() {
+    @PluginFactory
+    public static SourceResolverFactory getInstance() {
         return INSTANCE;
     }
 
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceElementObjectResolverContext.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceElementObjectResolverContext.java
deleted file mode 100644
index 31493fd..0000000
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceElementObjectResolverContext.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache license, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the license for the specific language governing permissions and
- * limitations under the license.
- */
-package org.apache.logging.log4j.layout.template.json.resolver;
-
-import org.apache.logging.log4j.core.lookup.StrSubstitutor;
-import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
-
-import java.util.Map;
-import java.util.Objects;
-
-public final class StackTraceElementObjectResolverContext
-        implements TemplateResolverContext<StackTraceElement, StackTraceElementObjectResolverContext> {
-
-    private final StrSubstitutor substitutor;
-
-    private final JsonWriter jsonWriter;
-
-    private StackTraceElementObjectResolverContext(final Builder builder) {
-        this.substitutor = builder.substitutor;
-        this.jsonWriter = builder.jsonWriter;
-    }
-
-    @Override
-    public Class<StackTraceElementObjectResolverContext> getContextClass() {
-        return StackTraceElementObjectResolverContext.class;
-    }
-
-    @Override
-    public Map<String, TemplateResolverFactory<StackTraceElement, StackTraceElementObjectResolverContext, ? extends TemplateResolver<StackTraceElement>>> getResolverFactoryByName() {
-        return StackTraceElementObjectResolverFactories.getResolverFactoryByName();
-    }
-
-    @Override
-    public StrSubstitutor getSubstitutor() {
-        return substitutor;
-    }
-
-    @Override
-    public JsonWriter getJsonWriter() {
-        return jsonWriter;
-    }
-
-    public static Builder newBuilder() {
-        return new Builder();
-    }
-
-    public static class Builder {
-
-        private StrSubstitutor substitutor;
-
-        private JsonWriter jsonWriter;
-
-        private Builder() {
-            // Do nothing.
-        }
-
-        public Builder setSubstitutor(final StrSubstitutor substitutor) {
-            this.substitutor = substitutor;
-            return this;
-        }
-
-        public Builder setJsonWriter(final JsonWriter jsonWriter) {
-            this.jsonWriter = jsonWriter;
-            return this;
-        }
-
-        public StackTraceElementObjectResolverContext build() {
-            validate();
-            return new StackTraceElementObjectResolverContext(this);
-        }
-
-        private void validate() {
-            Objects.requireNonNull(substitutor, "substitutor");
-            Objects.requireNonNull(jsonWriter, "jsonWriter");
-        }
-
-    }
-
-}
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceElementObjectResolverFactories.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceElementObjectResolverFactories.java
deleted file mode 100644
index cf76012..0000000
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceElementObjectResolverFactories.java
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache license, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the license for the specific language governing permissions and
- * limitations under the license.
- */
-package org.apache.logging.log4j.layout.template.json.resolver;
-
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.Map;
-
-final class StackTraceElementObjectResolverFactories {
-
-    private StackTraceElementObjectResolverFactories() {}
-
-    private static final Map<String, TemplateResolverFactory<StackTraceElement, StackTraceElementObjectResolverContext, ? extends TemplateResolver<StackTraceElement>>> RESOLVER_FACTORY_BY_NAME =
-            createResolverFactoryByName();
-
-    private static Map<String, TemplateResolverFactory<StackTraceElement, StackTraceElementObjectResolverContext, ? extends TemplateResolver<StackTraceElement>>> createResolverFactoryByName() {
-        final Map<String, TemplateResolverFactory<StackTraceElement, StackTraceElementObjectResolverContext, ? extends TemplateResolver<StackTraceElement>>> resolverFactoryByName = new LinkedHashMap<>();
-        final StackTraceElementObjectResolverFactory stackTraceElementObjectResolverFactory = StackTraceElementObjectResolverFactory.getInstance();
-        resolverFactoryByName.put(stackTraceElementObjectResolverFactory.getName(), stackTraceElementObjectResolverFactory);
-        return Collections.unmodifiableMap(resolverFactoryByName);
-    }
-
-    static Map<String, TemplateResolverFactory<StackTraceElement, StackTraceElementObjectResolverContext, ? extends TemplateResolver<StackTraceElement>>> getResolverFactoryByName() {
-        return RESOLVER_FACTORY_BY_NAME;
-    }
-
-}
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceElementObjectResolver.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceElementResolver.java
similarity index 84%
rename from log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceElementObjectResolver.java
rename to log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceElementResolver.java
index 3d70082..467c92b 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceElementObjectResolver.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceElementResolver.java
@@ -37,12 +37,12 @@ import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
  *
  * <pre>
  * {
- *   "$resolver": "source",
+ *   "$resolver": "stackTraceElement",
  *   "field": "lineNumber"
  * }
  * </pre>
  */
-final class StackTraceElementObjectResolver implements TemplateResolver<StackTraceElement> {
+final class StackTraceElementResolver implements TemplateResolver<StackTraceElement> {
 
     private static final TemplateResolver<StackTraceElement> CLASS_NAME_RESOLVER =
             (final StackTraceElement stackTraceElement, final JsonWriter jsonWriter) ->
@@ -62,26 +62,29 @@ final class StackTraceElementObjectResolver implements TemplateResolver<StackTra
 
     private final TemplateResolver<StackTraceElement> internalResolver;
 
-    StackTraceElementObjectResolver(final TemplateResolverConfig config) {
+    StackTraceElementResolver(final TemplateResolverConfig config) {
         this.internalResolver = createInternalResolver(config);
     }
 
+    static String getName() {
+        return "stackTraceElement";
+    }
+
     private TemplateResolver<StackTraceElement> createInternalResolver(
             final TemplateResolverConfig config) {
         final String fieldName = config.getString("field");
-        switch (fieldName) {
-            case "className": return CLASS_NAME_RESOLVER;
-            case "methodName": return METHOD_NAME_RESOLVER;
-            case "fileName": return FILE_NAME_RESOLVER;
-            case "lineNumber": return LINE_NUMBER_RESOLVER;
+        if ("className".equals(fieldName)) {
+            return CLASS_NAME_RESOLVER;
+        } else if ("methodName".equals(fieldName)) {
+            return METHOD_NAME_RESOLVER;
+        } else if ("fileName".equals(fieldName)) {
+            return FILE_NAME_RESOLVER;
+        } else if ("lineNumber".equals(fieldName)) {
+            return LINE_NUMBER_RESOLVER;
         }
         throw new IllegalArgumentException("unknown field: " + config);
     }
 
-    static String getName() {
-        return "stackTraceElement";
-    }
-
     @Override
     public void resolve(
             final StackTraceElement stackTraceElement,
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceElementResolverContext.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceElementResolverContext.java
new file mode 100644
index 0000000..fbe8a06
--- /dev/null
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceElementResolverContext.java
@@ -0,0 +1,121 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.layout.template.json.resolver;
+
+import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * {@link TemplateResolverContext} specialized for {@link StackTraceElement}s.
+ *
+ * @see StackTraceElementResolver
+ * @see StackTraceElementResolverFactory
+ */
+final class StackTraceElementResolverContext
+        implements TemplateResolverContext<StackTraceElement, StackTraceElementResolverContext> {
+
+    private final Map<String, StackTraceElementResolverFactory> resolverFactoryByName;
+
+    private final StackTraceElementResolverStringSubstitutor substitutor;
+
+    private final JsonWriter jsonWriter;
+
+    private StackTraceElementResolverContext(final Builder builder) {
+        this.resolverFactoryByName = builder.resolverFactoryByName;
+        this.substitutor = builder.substitutor;
+        this.jsonWriter = builder.jsonWriter;
+    }
+
+    @Override
+    public final Class<StackTraceElementResolverContext> getContextClass() {
+        return StackTraceElementResolverContext.class;
+    }
+
+    @Override
+    public Map<String, StackTraceElementResolverFactory> getResolverFactoryByName() {
+        return resolverFactoryByName;
+    }
+
+    @Override
+    public List<? extends TemplateResolverInterceptor<StackTraceElement, StackTraceElementResolverContext>> getResolverInterceptors() {
+        return Collections.emptyList();
+    }
+
+    @Override
+    public StackTraceElementResolverStringSubstitutor getSubstitutor() {
+        return substitutor;
+    }
+
+    @Override
+    public JsonWriter getJsonWriter() {
+        return jsonWriter;
+    }
+
+    static Builder newBuilder() {
+        return new Builder();
+    }
+
+    static class Builder {
+
+        private Map<String, StackTraceElementResolverFactory> resolverFactoryByName;
+
+        private StackTraceElementResolverStringSubstitutor substitutor;
+
+        private JsonWriter jsonWriter;
+
+        private Builder() {
+            // Do nothing.
+        }
+
+        Builder setResolverFactoryByName(
+                final Map<String, StackTraceElementResolverFactory> resolverFactoryByName) {
+            this.resolverFactoryByName = resolverFactoryByName;
+            return this;
+        }
+
+        Builder setSubstitutor(
+                final StackTraceElementResolverStringSubstitutor substitutor) {
+            this.substitutor = substitutor;
+            return this;
+        }
+
+        Builder setJsonWriter(final JsonWriter jsonWriter) {
+            this.jsonWriter = jsonWriter;
+            return this;
+        }
+
+        StackTraceElementResolverContext build() {
+            validate();
+            return new StackTraceElementResolverContext(this);
+        }
+
+        private void validate() {
+            Objects.requireNonNull(resolverFactoryByName, "resolverFactoryByName");
+            if (resolverFactoryByName.isEmpty()) {
+                throw new IllegalArgumentException("empty resolverFactoryByName");
+            }
+            Objects.requireNonNull(substitutor, "substitutor");
+            Objects.requireNonNull(jsonWriter, "jsonWriter");
+        }
+
+    }
+
+}
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceElementObjectResolverFactory.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceElementResolverFactory.java
similarity index 56%
rename from log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceElementObjectResolverFactory.java
rename to log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceElementResolverFactory.java
index aa64f1c..02c96b2 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceElementObjectResolverFactory.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceElementResolverFactory.java
@@ -16,28 +16,41 @@
  */
 package org.apache.logging.log4j.layout.template.json.resolver;
 
-final class StackTraceElementObjectResolverFactory
-        implements TemplateResolverFactory<StackTraceElement, StackTraceElementObjectResolverContext, StackTraceElementObjectResolver> {
+/**
+ * {@link StackTraceElementResolver} factory.
+ */
+final class StackTraceElementResolverFactory
+        implements TemplateResolverFactory<StackTraceElement, StackTraceElementResolverContext> {
 
-    private static final StackTraceElementObjectResolverFactory INSTANCE =
-            new StackTraceElementObjectResolverFactory();
+    private static final StackTraceElementResolverFactory INSTANCE =
+            new StackTraceElementResolverFactory();
 
-    private StackTraceElementObjectResolverFactory() {}
+    private StackTraceElementResolverFactory() {}
 
-    public static StackTraceElementObjectResolverFactory getInstance() {
+    static StackTraceElementResolverFactory getInstance() {
         return INSTANCE;
     }
 
     @Override
+    public Class<StackTraceElement> getValueClass() {
+        return StackTraceElement.class;
+    }
+
+    @Override
+    public Class<StackTraceElementResolverContext> getContextClass() {
+        return StackTraceElementResolverContext.class;
+    }
+
+    @Override
     public String getName() {
-        return StackTraceElementObjectResolver.getName();
+        return StackTraceElementResolver.getName();
     }
 
     @Override
-    public StackTraceElementObjectResolver create(
-            final StackTraceElementObjectResolverContext context,
+    public StackTraceElementResolver create(
+            final StackTraceElementResolverContext context,
             final TemplateResolverConfig config) {
-        return new StackTraceElementObjectResolver(config);
+        return new StackTraceElementResolver(config);
     }
 
 }
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TemplateResolverContext.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceElementResolverStringSubstitutor.java
similarity index 53%
copy from log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TemplateResolverContext.java
copy to log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceElementResolverStringSubstitutor.java
index fbb0da4..fb59c31 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TemplateResolverContext.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceElementResolverStringSubstitutor.java
@@ -17,18 +17,35 @@
 package org.apache.logging.log4j.layout.template.json.resolver;
 
 import org.apache.logging.log4j.core.lookup.StrSubstitutor;
-import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
 
-import java.util.Map;
+import java.util.Objects;
 
-interface TemplateResolverContext<V, C extends TemplateResolverContext<V, C>> {
-
-    Class<C> getContextClass();
-
-    Map<String, TemplateResolverFactory<V, C, ? extends TemplateResolver<V>>> getResolverFactoryByName();
-
-    StrSubstitutor getSubstitutor();
-
-    JsonWriter getJsonWriter();
+/**
+ * {@link TemplateResolverStringSubstitutor} specialized for {@link StackTraceElement}s.
+ */
+final class StackTraceElementResolverStringSubstitutor
+        implements TemplateResolverStringSubstitutor<StackTraceElement> {
+
+    private final StrSubstitutor substitutor;
+
+    StackTraceElementResolverStringSubstitutor(
+            final StrSubstitutor substitutor) {
+        this.substitutor = Objects.requireNonNull(substitutor, "substitutor");
+    }
+
+    @Override
+    public StrSubstitutor getInternalSubstitutor() {
+        return substitutor;
+    }
+
+    @Override
+    public boolean isStable() {
+        return true;
+    }
+
+    @Override
+    public String replace(final StackTraceElement ignored, final String source) {
+        return substitutor.replace(null, source);
+    }
 
 }
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceObjectResolver.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceObjectResolver.java
index d20ca37..c7279d1 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceObjectResolver.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceObjectResolver.java
@@ -18,6 +18,9 @@ package org.apache.logging.log4j.layout.template.json.resolver;
 
 import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
 
+/**
+ * Exception stack trace to JSON object resolver used by {@link ExceptionResolver}.
+ */
 final class StackTraceObjectResolver implements StackTraceResolver {
 
     private final TemplateResolver<StackTraceElement> stackTraceElementResolver;
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceResolver.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceResolver.java
index 824b6da..697480a 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceResolver.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceResolver.java
@@ -16,4 +16,7 @@
  */
 package org.apache.logging.log4j.layout.template.json.resolver;
 
+/**
+ * {@link TemplateResolver} specialized for {@link Throwable}s.
+ */
 interface StackTraceResolver extends TemplateResolver<Throwable> {}
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceStringResolver.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceStringResolver.java
index 61be39e..0dee8d0 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceStringResolver.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceStringResolver.java
@@ -26,6 +26,9 @@ import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
+/**
+ * Exception stack trace to JSON string resolver used by {@link ExceptionResolver}.
+ */
 final class StackTraceStringResolver implements StackTraceResolver {
 
     private final Recycler<TruncatingBufferedPrintWriter> writerRecycler;
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TemplateResolverConfig.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TemplateResolverConfig.java
index d181b87..1ca0c9b 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TemplateResolverConfig.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TemplateResolverConfig.java
@@ -20,7 +20,42 @@ import org.apache.logging.log4j.layout.template.json.util.MapAccessor;
 
 import java.util.Map;
 
-class TemplateResolverConfig extends MapAccessor {
+/**
+ * Accessor to the resolver configuration JSON object read from the template.
+ * {@link TemplateResolver Template resolvers} can use this class to
+ * read the configuration associated with them.
+ * <p>
+ * For instance, given the following template:
+ * <pre>
+ * {
+ *   "@version": 1,
+ *   "message": {
+ *     "$resolver": "message",
+ *     "stringified": true
+ *   },
+ *   "level": {
+ *     "$resolver": "level",
+ *     "field": "severity",
+ *     "severity": {
+ *       "field": "code"
+ *     }
+ *   }
+ * }
+ * </pre>
+ * {@link LevelResolverFactory#create(EventResolverContext, TemplateResolverConfig)}
+ * will be called with a {@link TemplateResolverConfig} accessor to the
+ * following configuration JSON object block:
+ * <pre>
+ * {
+ *   "$resolver": "level",
+ *   "field": "severity",
+ *   "severity": {
+ *     "field": "code"
+ *   }
+ * }
+ * </pre>
+ */
+public class TemplateResolverConfig extends MapAccessor {
 
     TemplateResolverConfig(final Map<String, Object> map) {
         super(map);
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TemplateResolverContext.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TemplateResolverContext.java
index fbb0da4..7f8cbb8 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TemplateResolverContext.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TemplateResolverContext.java
@@ -16,19 +16,45 @@
  */
 package org.apache.logging.log4j.layout.template.json.resolver;
 
-import org.apache.logging.log4j.core.lookup.StrSubstitutor;
 import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
 
+import java.util.List;
 import java.util.Map;
 
+/**
+ * Context used to compile a template and passed to
+ * {@link TemplateResolverFactory#create(TemplateResolverContext, TemplateResolverConfig)
+ * template resolver factory creator}s.
+ *
+ * @param <V> type of the value passed to the resolver as input
+ * @param <C> type of the context passed to the {@link TemplateResolverFactory resolver factory}
+ *
+ * @see TemplateResolverFactory
+ */
 interface TemplateResolverContext<V, C extends TemplateResolverContext<V, C>> {
 
     Class<C> getContextClass();
 
-    Map<String, TemplateResolverFactory<V, C, ? extends TemplateResolver<V>>> getResolverFactoryByName();
+    Map<String, ? extends TemplateResolverFactory<V, C>> getResolverFactoryByName();
 
-    StrSubstitutor getSubstitutor();
+    List<? extends TemplateResolverInterceptor<V, C>> getResolverInterceptors();
+
+    TemplateResolverStringSubstitutor<V> getSubstitutor();
 
     JsonWriter getJsonWriter();
 
+    /**
+     * Process the read template before compiler (i.e.,
+     * {@link TemplateResolvers#ofTemplate(TemplateResolverContext, String)}
+     * starts injecting resolvers.
+     * <p>
+     * This is the right place to introduce, say, contextual additional fields.
+     *
+     * @param node the root object of the read template
+     * @return the root object of the template to be compiled
+     */
+    default Object processTemplateBeforeResolverInjection(Object node) {
+        return node;
+    }
+
 }
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TemplateResolverFactories.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TemplateResolverFactories.java
new file mode 100644
index 0000000..83270ab
--- /dev/null
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TemplateResolverFactories.java
@@ -0,0 +1,130 @@
+package org.apache.logging.log4j.layout.template.json.resolver;
+
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.core.config.plugins.util.PluginType;
+import org.apache.logging.log4j.core.config.plugins.util.PluginUtil;
+import org.apache.logging.log4j.status.StatusLogger;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Utility class for {@link TemplateResolverFactory}.
+ */
+public final class TemplateResolverFactories {
+
+    private static final Logger LOGGER = StatusLogger.getLogger();
+
+    private TemplateResolverFactories() {}
+
+    /**
+     * Populates plugins implementing
+     * {@link TemplateResolverFactory TemplateResolverFactory&lt;V, C&gt;},
+     * where {@code V} and {@code C} denote the value and context class types,
+     * respectively.
+     */
+    public static <V, C extends TemplateResolverContext<V, C>, F extends TemplateResolverFactory<V, C>> Map<String, F> populateFactoryByName(
+            final List<String> pluginPackages,
+            final Class<V> valueClass,
+            final Class<C> contextClass) {
+
+        // Populate template resolver factories.
+        final Map<String, PluginType<?>> pluginTypeByName =
+                PluginUtil.collectPluginsByCategoryAndPackage(
+                        TemplateResolverFactory.CATEGORY,
+                        pluginPackages);
+        if (LOGGER.isDebugEnabled()) {
+            LOGGER.debug(
+                    "found {} plugins of category \"{}\": {}",
+                    pluginTypeByName.size(),
+                    TemplateResolverFactory.CATEGORY,
+                    pluginTypeByName.keySet());
+        }
+
+        // Filter matching resolver factories.
+        final Map<String, F> factoryByName =
+                populateFactoryByName(pluginTypeByName, valueClass, contextClass);
+        if (LOGGER.isDebugEnabled()) {
+            LOGGER.debug(
+                    "matched {} resolver factories out of {} for value class {} and context class {}: {}",
+                    factoryByName.size(),
+                    pluginTypeByName.size(),
+                    valueClass,
+                    contextClass,
+                    factoryByName.keySet());
+        }
+        return factoryByName;
+
+    }
+
+    private static <V, C extends TemplateResolverContext<V, C>, F extends TemplateResolverFactory<V, C>> Map<String, F> populateFactoryByName(
+            final Map<String, PluginType<?>> pluginTypeByName,
+            final Class<V> valueClass,
+            final Class<C> contextClass) {
+        final Map<String, F> factoryByName = new LinkedHashMap<>();
+        final Set<String> pluginNames = pluginTypeByName.keySet();
+        for (final String pluginName : pluginNames) {
+            final PluginType<?> pluginType = pluginTypeByName.get(pluginName);
+            final Class<?> pluginClass = pluginType.getPluginClass();
+            final boolean pluginClassMatched =
+                    TemplateResolverFactory.class.isAssignableFrom(pluginClass);
+            if (pluginClassMatched) {
+                final TemplateResolverFactory<?, ?> rawFactory =
+                        instantiateFactory(pluginName, pluginClass);
+                final F factory = castFactory(valueClass, contextClass, rawFactory);
+                if (factory != null) {
+                    addFactory(factoryByName, factory);
+                }
+            }
+        }
+        return factoryByName;
+    }
+
+    private static TemplateResolverFactory<?, ?> instantiateFactory(
+            final String pluginName,
+            final Class<?> pluginClass) {
+        try {
+            return (TemplateResolverFactory<?, ?>)
+                    PluginUtil.instantiatePlugin(pluginClass);
+        } catch (final Exception error) {
+            final String message = String.format(
+                    "failed instantiating resolver factory plugin %s of name %s",
+                    pluginClass, pluginName);
+            throw new RuntimeException(message, error);
+        }
+    }
+
+    private static <V, C extends TemplateResolverContext<V, C>, F extends TemplateResolverFactory<V, C>> F castFactory(
+            final Class<V> valueClass,
+            final Class<C> contextClass,
+            final TemplateResolverFactory<?, ?> factory) {
+        final Class<?> factoryValueClass = factory.getValueClass();
+        final Class<?> factoryContextClass = factory.getContextClass();
+        final boolean factoryValueClassMatched =
+                valueClass.isAssignableFrom(factoryValueClass);
+        final boolean factoryContextClassMatched =
+                contextClass.isAssignableFrom(factoryContextClass);
+        if (factoryValueClassMatched && factoryContextClassMatched) {
+            @SuppressWarnings("unchecked")
+            final F typedFactory = (F) factory;
+            return typedFactory;
+        }
+        return null;
+    }
+
+    private static <V, C extends TemplateResolverContext<V, C>, F extends TemplateResolverFactory<V, C>> void addFactory(
+            final Map<String, F> factoryByName,
+            final F factory) {
+        final String factoryName = factory.getName();
+        final F conflictingFactory = factoryByName.putIfAbsent(factoryName, factory);
+        if (conflictingFactory != null) {
+            final String message = String.format(
+                    "found resolver factories with overlapping names: %s (%s and %s)",
+                    factoryName, conflictingFactory, factory);
+            throw new IllegalArgumentException(message);
+        }
+    }
+
+}
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TemplateResolverFactory.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TemplateResolverFactory.java
index a2c8216..93cf611 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TemplateResolverFactory.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TemplateResolverFactory.java
@@ -16,10 +16,31 @@
  */
 package org.apache.logging.log4j.layout.template.json.resolver;
 
-interface TemplateResolverFactory<V, C extends TemplateResolverContext<V, C>, R extends TemplateResolver<V>> {
+/**
+ * {@link TemplateResolver} factory.
+ *
+ * @param <V> type of the value passed to the {@link TemplateResolver resolver}
+ * @param <C> type of the context passed to the {@link TemplateResolverFactory#create(TemplateResolverContext, TemplateResolverConfig)} creator}
+ */
+public interface TemplateResolverFactory<V, C extends TemplateResolverContext<V, C>> {
+
+    /**
+     * Main plugin category for {@link TemplateResolverFactory} implementations.
+     */
+    String CATEGORY = "JsonTemplateResolverFactory";
+
+    /**
+     * The targeted value class.
+     */
+    Class<V> getValueClass();
+
+    /**
+     * The targeted {@link TemplateResolverContext} class.
+     */
+    Class<C> getContextClass();
 
     String getName();
 
-    R create(C context, TemplateResolverConfig config);
+    TemplateResolver<V> create(C context, TemplateResolverConfig config);
 
 }
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TemplateResolverInterceptor.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TemplateResolverInterceptor.java
new file mode 100644
index 0000000..4989521
--- /dev/null
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TemplateResolverInterceptor.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.layout.template.json.resolver;
+
+/**
+ * Main {@link TemplateResolver} compilation interception interface.
+ *
+ * @param <V> type of the value passed to the {@link TemplateResolver resolver}
+ * @param <C> type of the context employed
+ */
+public interface TemplateResolverInterceptor<V, C extends TemplateResolverContext<V, C>> {
+
+    /**
+     * Main plugin category for {@link TemplateResolverInterceptor} implementations.
+     */
+    String CATEGORY = "JsonTemplateResolverInterceptor";
+
+    /**
+     * The targeted value class.
+     */
+    Class<V> getValueClass();
+
+    /**
+     * The targeted {@link TemplateResolverContext} class.
+     */
+    Class<C> getContextClass();
+
+    /**
+     * Intercept the read template before compiler (i.e.,
+     * {@link TemplateResolvers#ofTemplate(TemplateResolverContext, String)}
+     * starts injecting resolvers.
+     * <p>
+     * This is the right place to introduce, say, contextual additional fields.
+     *
+     * @param node the root object of the read template
+     * @return the root object of the template to be compiled
+     */
+    default Object processTemplateBeforeResolverInjection(C context, Object node) {
+        return node;
+    }
+
+}
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TemplateResolverInterceptors.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TemplateResolverInterceptors.java
new file mode 100644
index 0000000..126469d
--- /dev/null
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TemplateResolverInterceptors.java
@@ -0,0 +1,131 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.layout.template.json.resolver;
+
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.core.config.plugins.util.PluginType;
+import org.apache.logging.log4j.core.config.plugins.util.PluginUtil;
+import org.apache.logging.log4j.status.StatusLogger;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Utility class for {@link TemplateResolverInterceptor}.
+ */
+public class TemplateResolverInterceptors {
+
+    private static final Logger LOGGER = StatusLogger.getLogger();
+
+    private TemplateResolverInterceptors() {}
+
+    /**
+     * Populates plugins implementing
+     * {@link TemplateResolverInterceptor TemplateResolverInterceptor&lt;V, C&gt;},
+     * where {@code V} and {@code C} denote the value and context class types,
+     * respectively.
+     */
+    public static <V, C extends TemplateResolverContext<V, C>, I extends TemplateResolverInterceptor<V, C>> List<I> populateInterceptors(
+            final List<String> pluginPackages,
+            final Class<V> valueClass,
+            final Class<C> contextClass) {
+
+        // Populate interceptors.
+        final Map<String, PluginType<?>> pluginTypeByName =
+                PluginUtil.collectPluginsByCategoryAndPackage(
+                        TemplateResolverInterceptor.CATEGORY,
+                        pluginPackages);
+        if (LOGGER.isDebugEnabled()) {
+            LOGGER.debug(
+                    "found {} plugins of category \"{}\": {}",
+                    pluginTypeByName.size(),
+                    TemplateResolverFactory.CATEGORY,
+                    pluginTypeByName.keySet());
+        }
+
+        // Filter matching interceptors.
+        final List<I> interceptors =
+                populateInterceptors(pluginTypeByName, valueClass, contextClass);
+        LOGGER.debug(
+                "{} interceptors matched out of {} for value class {} and context class {}",
+                interceptors.size(),
+                pluginTypeByName.size(),
+                valueClass,
+                contextClass);
+        return interceptors;
+
+    }
+
+    private static <V, C extends TemplateResolverContext<V, C>, I extends TemplateResolverInterceptor<V, C>> List<I> populateInterceptors(
+            final Map<String, PluginType<?>> pluginTypeByName,
+            final Class<V> valueClass,
+            final Class<C> contextClass) {
+        final List<I> interceptors = new LinkedList<>();
+        final Set<String> pluginNames = pluginTypeByName.keySet();
+        for (final String pluginName : pluginNames) {
+            final PluginType<?> pluginType = pluginTypeByName.get(pluginName);
+            final Class<?> pluginClass = pluginType.getPluginClass();
+            final boolean pluginClassMatched =
+                    TemplateResolverInterceptor.class.isAssignableFrom(pluginClass);
+            if (pluginClassMatched) {
+                final TemplateResolverInterceptor<?, ?> rawInterceptor =
+                        instantiateInterceptor(pluginName, pluginClass);
+                final I interceptor =
+                        castInterceptor(valueClass, contextClass, rawInterceptor);
+                if (interceptor != null) {
+                    interceptors.add(interceptor);
+                }
+            }
+        }
+        return interceptors;
+    }
+
+    private static TemplateResolverInterceptor<?, ?> instantiateInterceptor(
+            final String pluginName,
+            final Class<?> pluginClass) {
+        try {
+            return (TemplateResolverInterceptor<?, ?>)
+                    PluginUtil.instantiatePlugin(pluginClass);
+        } catch (final Exception error) {
+            final String message = String.format(
+                    "failed instantiating resolver interceptor plugin %s of name %s",
+                    pluginClass, pluginName);
+            throw new RuntimeException(message, error);
+        }
+    }
+
+    private static <V, C extends TemplateResolverContext<V, C>, I extends TemplateResolverInterceptor<V, C>> I castInterceptor(
+            final Class<V> valueClass,
+            final Class<C> contextClass,
+            final TemplateResolverInterceptor<?, ?> interceptor) {
+        final Class<?> interceptorValueClass = interceptor.getValueClass();
+        final Class<?> interceptorContextClass = interceptor.getContextClass();
+        final boolean interceptorValueClassMatched =
+                valueClass.isAssignableFrom(interceptorValueClass);
+        final boolean interceptorContextClassMatched =
+                contextClass.isAssignableFrom(interceptorContextClass);
+        if (interceptorValueClassMatched && interceptorContextClassMatched) {
+            @SuppressWarnings({"unchecked"})
+            final I typedInterceptor = (I) interceptor;
+            return typedInterceptor;
+        }
+        return null;
+    }
+
+}
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TemplateResolverContext.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TemplateResolverStringSubstitutor.java
similarity index 67%
copy from log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TemplateResolverContext.java
copy to log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TemplateResolverStringSubstitutor.java
index fbb0da4..16a20b4 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TemplateResolverContext.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TemplateResolverStringSubstitutor.java
@@ -17,18 +17,22 @@
 package org.apache.logging.log4j.layout.template.json.resolver;
 
 import org.apache.logging.log4j.core.lookup.StrSubstitutor;
-import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
 
-import java.util.Map;
-
-interface TemplateResolverContext<V, C extends TemplateResolverContext<V, C>> {
-
-    Class<C> getContextClass();
+/**
+ * A contextual {@link StrSubstitutor} abstraction.
+ *
+ * @param <V> {@link TemplateResolver} value
+ */
+public interface TemplateResolverStringSubstitutor<V> {
 
-    Map<String, TemplateResolverFactory<V, C, ? extends TemplateResolver<V>>> getResolverFactoryByName();
+    StrSubstitutor getInternalSubstitutor();
 
-    StrSubstitutor getSubstitutor();
+    /**
+     * A substitutor is stable if the replacement doesn't vary with the provided
+     * value. In such a case, value is always set to {@code null}.
+     */
+    boolean isStable();
 
-    JsonWriter getJsonWriter();
+    String replace(V value, String source);
 
 }
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TemplateResolvers.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TemplateResolvers.java
index b0cede2..27a5b49 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TemplateResolvers.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TemplateResolvers.java
@@ -16,17 +16,17 @@
  */
 package org.apache.logging.log4j.layout.template.json.resolver;
 
-import org.apache.logging.log4j.core.LogEvent;
-import org.apache.logging.log4j.layout.template.json.JsonTemplateLayout.EventTemplateAdditionalField;
 import org.apache.logging.log4j.layout.template.json.util.JsonReader;
 import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
 
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
 
+/**
+ * Main class for compiling {@link TemplateResolver}s from a template.
+ */
 public final class TemplateResolvers {
 
     private TemplateResolvers() {}
@@ -87,19 +87,16 @@ public final class TemplateResolvers {
             throw new RuntimeException(message, error);
         }
 
-        if (context instanceof EventResolverContext) {
-
-            // Append the additional fields.
-            final EventResolverContext eventResolverContext = (EventResolverContext) context;
-            final EventTemplateAdditionalField[] additionalFields = eventResolverContext.getAdditionalFields();
-            appendAdditionalFields(node, additionalFields);
-
-            // Set the root object key, if given.
-            final String rootObjectKey = eventResolverContext.getEventTemplateRootObjectKey();
-            if (rootObjectKey != null) {
-                node = Collections.singletonMap(rootObjectKey, node);
-            }
-
+        // Perform contextual interception.
+        final List<? extends TemplateResolverInterceptor<V, C>> interceptors =
+                context.getResolverInterceptors();
+        // noinspection ForLoopReplaceableByForEach
+        for (int interceptorIndex = 0;
+             interceptorIndex < interceptors.size();
+             interceptorIndex++) {
+            final TemplateResolverInterceptor<V, C> interceptor =
+                    interceptors.get(interceptorIndex);
+            node = interceptor.processTemplateBeforeResolverInjection(context, node);
         }
 
         // Resolve the template.
@@ -107,55 +104,6 @@ public final class TemplateResolvers {
 
     }
 
-    private static void appendAdditionalFields(
-            final Object node,
-            EventTemplateAdditionalField[] additionalFields) {
-        if (additionalFields.length > 0) {
-
-            // Check that the root is an object node.
-            final Map<String, Object> objectNode;
-            try {
-                @SuppressWarnings("unchecked")
-                final Map<String, Object> map = (Map<String, Object>) node;
-                objectNode = map;
-            } catch (final ClassCastException error) {
-                final String message = String.format(
-                        "was expecting an object to merge additional fields: %s",
-                        node.getClass().getName());
-                throw new IllegalArgumentException(message);
-            }
-
-            // Merge additional fields.
-            for (final EventTemplateAdditionalField additionalField : additionalFields) {
-                final String additionalFieldKey = additionalField.getKey();
-                final Object additionalFieldValue;
-                switch (additionalField.getFormat()) {
-                    case STRING:
-                        additionalFieldValue = additionalField.getValue();
-                        break;
-                    case JSON:
-                        try {
-                            additionalFieldValue =  JsonReader.read(additionalField.getValue());
-                        } catch (final Exception error) {
-                            final String message = String.format(
-                                    "failed reading JSON provided by additional field: %s",
-                                    additionalFieldKey);
-                            throw new IllegalArgumentException(message, error);
-                        }
-                        break;
-                    default: {
-                        final String message = String.format(
-                                "unknown format %s for additional field: %s",
-                                additionalFieldKey, additionalField.getFormat());
-                        throw new IllegalArgumentException(message);
-                    }
-                }
-                objectNode.put(additionalFieldKey, additionalFieldValue);
-            }
-
-        }
-    }
-
     private static <V, C extends TemplateResolverContext<V, C>> TemplateResolver<V> ofObject(
             final C context,
             final Object object) {
@@ -352,7 +300,7 @@ public final class TemplateResolvers {
         final String resolverName = (String) resolverNameObject;
 
         // Retrieve the resolver.
-        final TemplateResolverFactory<V, C, ? extends TemplateResolver<V>> resolverFactory =
+        final TemplateResolverFactory<V, C> resolverFactory =
                 context.getResolverFactoryByName().get(resolverName);
         if (resolverFactory == null) {
             throw new IllegalArgumentException("unknown resolver: " + resolverName);
@@ -366,24 +314,16 @@ public final class TemplateResolvers {
             final C context,
             final String fieldValue) {
 
-        // Check if substitution needed at all. (Copied logic from
-        // AbstractJacksonLayout.valueNeedsLookup() method.)
+        // Check if substitution is needed.
         final boolean substitutionNeeded = fieldValue.contains("${");
         final JsonWriter contextJsonWriter = context.getJsonWriter();
         if (substitutionNeeded) {
+            final TemplateResolverStringSubstitutor<V> substitutor = context.getSubstitutor();
 
-            // Use Log4j substitutor with LogEvent.
-            if (EventResolverContext.class.isAssignableFrom(context.getContextClass())) {
-                return (final V value, final JsonWriter jsonWriter) -> {
-                    final LogEvent logEvent = (LogEvent) value;
-                    final String replacedText = context.getSubstitutor().replace(logEvent, fieldValue);
-                    jsonWriter.writeString(replacedText);
-                };
-            }
-
-            // Use standalone Log4j substitutor.
-            else {
-                final String replacedText = context.getSubstitutor().replace(null, fieldValue);
+            // If the substitutor is stable, we can get the replacement right
+            // away and avoid runtime substitution.
+            if (substitutor.isStable()) {
+                final String replacedText = substitutor.replace(null, fieldValue);
                 if (replacedText == null) {
                     @SuppressWarnings("unchecked")
                     final TemplateResolver<V> resolver =
@@ -400,6 +340,15 @@ public final class TemplateResolvers {
                 }
             }
 
+            // Otherwise, the unstable substitutor needs to be invoked always at
+            // runtime.
+            else {
+                return (final V value, final JsonWriter jsonWriter) -> {
+                    final String replacedText = substitutor.replace(value, fieldValue);
+                    jsonWriter.writeString(replacedText);
+                };
+            }
+
         }
 
         // Write the field value as is.
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 95e3677..1875e1f 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
@@ -23,7 +23,7 @@ import org.apache.logging.log4j.core.LogEvent;
  *
  * @see ReadOnlyStringMapResolver
  */
-final class ThreadContextDataResolver extends ReadOnlyStringMapResolver {
+public final class ThreadContextDataResolver extends ReadOnlyStringMapResolver {
 
     ThreadContextDataResolver(
             final EventResolverContext context,
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ThreadContextDataResolverFactory.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ThreadContextDataResolverFactory.java
index 05474a3..0cb2c07 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ThreadContextDataResolverFactory.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ThreadContextDataResolverFactory.java
@@ -16,15 +16,22 @@
  */
 package org.apache.logging.log4j.layout.template.json.resolver;
 
-final class ThreadContextDataResolverFactory
-        implements EventResolverFactory<ThreadContextDataResolver> {
+import org.apache.logging.log4j.core.config.plugins.Plugin;
+import org.apache.logging.log4j.core.config.plugins.PluginFactory;
+
+/**
+ * {@link ThreadContextDataResolver} factory.
+ */
+@Plugin(name = "ThreadContextDataResolverFactory", category = TemplateResolverFactory.CATEGORY)
+public final class ThreadContextDataResolverFactory implements EventResolverFactory {
 
     private static final ThreadContextDataResolverFactory INSTANCE =
             new ThreadContextDataResolverFactory();
 
     private ThreadContextDataResolverFactory() {}
 
-    static ThreadContextDataResolverFactory getInstance() {
+    @PluginFactory
+    public static ThreadContextDataResolverFactory getInstance() {
         return INSTANCE;
     }
 
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ThreadContextStackResolver.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ThreadContextStackResolver.java
index 4f4afa3..7a70e47 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ThreadContextStackResolver.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ThreadContextStackResolver.java
@@ -52,7 +52,7 @@ import java.util.regex.Pattern;
  * }
  * </pre>
  */
-final class ThreadContextStackResolver implements EventResolver {
+public final class ThreadContextStackResolver implements EventResolver {
 
     private final Pattern itemPattern;
 
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ThreadContextStackResolverFactory.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ThreadContextStackResolverFactory.java
index 4946f0d..ad22bb9 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ThreadContextStackResolverFactory.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ThreadContextStackResolverFactory.java
@@ -16,15 +16,22 @@
  */
 package org.apache.logging.log4j.layout.template.json.resolver;
 
-final class ThreadContextStackResolverFactory
-        implements EventResolverFactory<ThreadContextStackResolver> {
+import org.apache.logging.log4j.core.config.plugins.Plugin;
+import org.apache.logging.log4j.core.config.plugins.PluginFactory;
 
-    private static final ThreadContextStackResolverFactory INSTANCE
-            = new ThreadContextStackResolverFactory();
+/**
+ * {@link ThreadContextStackResolver} factory.
+ */
+@Plugin(name = "ThreadContextStackResolverFactory", category = TemplateResolverFactory.CATEGORY)
+public final class ThreadContextStackResolverFactory implements EventResolverFactory {
+
+    private static final ThreadContextStackResolverFactory INSTANCE =
+            new ThreadContextStackResolverFactory();
 
     private ThreadContextStackResolverFactory() {}
 
-    static ThreadContextStackResolverFactory getInstance() {
+    @PluginFactory
+    public static ThreadContextStackResolverFactory getInstance() {
         return INSTANCE;
     }
 
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ThreadResolver.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ThreadResolver.java
index 4ea7d80..eb1e050 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ThreadResolver.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ThreadResolver.java
@@ -39,7 +39,7 @@ import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
  * }
  * </pre>
  */
-final class ThreadResolver implements EventResolver {
+public final class ThreadResolver implements EventResolver {
 
     private static final EventResolver NAME_RESOLVER =
             (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
@@ -68,10 +68,12 @@ final class ThreadResolver implements EventResolver {
     private static EventResolver createInternalResolver(
             final TemplateResolverConfig config) {
         final String fieldName = config.getString("field");
-        switch (fieldName) {
-            case "name": return NAME_RESOLVER;
-            case "id": return ID_RESOLVER;
-            case "priority": return PRIORITY_RESOLVER;
+        if ("name".equals(fieldName)) {
+            return NAME_RESOLVER;
+        } else if ("id".equals(fieldName)) {
+            return ID_RESOLVER;
+        } else if ("priority".equals(fieldName)) {
+            return PRIORITY_RESOLVER;
         }
         throw new IllegalArgumentException("unknown field: " + config);
     }
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ThreadResolverFactory.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ThreadResolverFactory.java
index 69d5e91..ec65c56 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ThreadResolverFactory.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ThreadResolverFactory.java
@@ -16,13 +16,21 @@
  */
 package org.apache.logging.log4j.layout.template.json.resolver;
 
-final class ThreadResolverFactory implements EventResolverFactory<ThreadResolver> {
+import org.apache.logging.log4j.core.config.plugins.Plugin;
+import org.apache.logging.log4j.core.config.plugins.PluginFactory;
+
+/**
+ * {@link ThreadResolver} factory.
+ */
+@Plugin(name = "ThreadResolverFactory", category = TemplateResolverFactory.CATEGORY)
+public final class ThreadResolverFactory implements EventResolverFactory {
 
     private static final ThreadResolverFactory INSTANCE = new ThreadResolverFactory();
 
     private ThreadResolverFactory() {}
 
-    static ThreadResolverFactory getInstance() {
+    @PluginFactory
+    public static ThreadResolverFactory getInstance() {
         return INSTANCE;
     }
 
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TimestampResolver.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TimestampResolver.java
index eea0dd6..9488f0d 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TimestampResolver.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TimestampResolver.java
@@ -191,7 +191,7 @@ import java.util.TimeZone;
  * </tr>
  * </table>
  */
-final class TimestampResolver implements EventResolver {
+public final class TimestampResolver implements EventResolver {
 
     private final EventResolver internalResolver;
 
@@ -381,7 +381,7 @@ final class TimestampResolver implements EventResolver {
 
         private Instant instant;
 
-        private char[] resolution = new char[
+        private final char[] resolution = new char[
                 /* integral: */ MAX_LONG_LENGTH +
                 /* dot: */ 1 +
                 /* fractional: */ MAX_LONG_LENGTH];
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TimestampResolverFactory.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TimestampResolverFactory.java
index 6f4cb6d..054785b 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TimestampResolverFactory.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TimestampResolverFactory.java
@@ -16,13 +16,21 @@
  */
 package org.apache.logging.log4j.layout.template.json.resolver;
 
-final class TimestampResolverFactory implements EventResolverFactory<TimestampResolver> {
+import org.apache.logging.log4j.core.config.plugins.Plugin;
+import org.apache.logging.log4j.core.config.plugins.PluginFactory;
+
+/**
+ * {@link TimestampResolver} factory.
+ */
+@Plugin(name = "TimestampResolverFactory", category = TemplateResolverFactory.CATEGORY)
+public final class TimestampResolverFactory implements EventResolverFactory {
 
     private static final TimestampResolverFactory INSTANCE = new TimestampResolverFactory();
 
     private TimestampResolverFactory() {}
 
-    static TimestampResolverFactory getInstance() {
+    @PluginFactory
+    public static TimestampResolverFactory getInstance() {
         return INSTANCE;
     }
 
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/util/RecyclerFactories.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/util/RecyclerFactories.java
index b839619..22244fb 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/util/RecyclerFactories.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/util/RecyclerFactories.java
@@ -16,9 +16,6 @@
  */
 package org.apache.logging.log4j.layout.template.json.util;
 
-import org.apache.logging.log4j.core.config.plugins.Plugin;
-import org.apache.logging.log4j.core.config.plugins.convert.TypeConverter;
-import org.apache.logging.log4j.core.config.plugins.convert.TypeConverters;
 import org.apache.logging.log4j.core.util.Constants;
 import org.apache.logging.log4j.util.LoaderUtil;
 import org.jctools.queues.MpmcArrayQueue;
@@ -53,14 +50,6 @@ public final class RecyclerFactories {
         }
     }
 
-    @Plugin(name = "RecyclerFactory", category = TypeConverters.CATEGORY)
-    public static final class RecyclerFactoryConverter implements TypeConverter<RecyclerFactory> {
-        @Override
-        public RecyclerFactory convert(final String recyclerFactorySpec) {
-            return ofSpec(recyclerFactorySpec);
-        }
-    }
-
     public static RecyclerFactory ofSpec(final String recyclerFactorySpec) {
 
         // Determine the default capacity.
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/LoggerResolverFactory.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/util/RecyclerFactoryConverter.java
similarity index 55%
copy from log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/LoggerResolverFactory.java
copy to log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/util/RecyclerFactoryConverter.java
index 08b6fb9..3111af5 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/LoggerResolverFactory.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/util/RecyclerFactoryConverter.java
@@ -14,28 +14,21 @@
  * See the license for the specific language governing permissions and
  * limitations under the license.
  */
-package org.apache.logging.log4j.layout.template.json.resolver;
+package org.apache.logging.log4j.layout.template.json.util;
 
-final class LoggerResolverFactory implements EventResolverFactory<LoggerResolver> {
+import org.apache.logging.log4j.core.config.plugins.Plugin;
+import org.apache.logging.log4j.core.config.plugins.convert.TypeConverter;
+import org.apache.logging.log4j.core.config.plugins.convert.TypeConverters;
 
-    private static final LoggerResolverFactory INSTANCE = new LoggerResolverFactory();
-
-    private LoggerResolverFactory() {}
-
-    static LoggerResolverFactory getInstance() {
-        return INSTANCE;
-    }
-
-    @Override
-    public String getName() {
-        return LoggerResolver.getName();
-    }
+/**
+ * The default string (i.e., recycler factory spec) to {@link RecyclerFactory} type converter.
+ */
+@Plugin(name = "RecyclerFactoryConverter", category = TypeConverters.CATEGORY)
+public final class RecyclerFactoryConverter implements TypeConverter<RecyclerFactory> {
 
     @Override
-    public LoggerResolver create(
-            final EventResolverContext context,
-            final TemplateResolverConfig config) {
-        return new LoggerResolver(config);
+    public RecyclerFactory convert(final String recyclerFactorySpec) {
+        return RecyclerFactories.ofSpec(recyclerFactorySpec);
     }
 
 }
diff --git a/log4j-layout-template-json/src/main/resources/EcsLayout.json b/log4j-layout-template-json/src/main/resources/EcsLayout.json
index 708b27b..8d215ab 100644
--- a/log4j-layout-template-json/src/main/resources/EcsLayout.json
+++ b/log4j-layout-template-json/src/main/resources/EcsLayout.json
@@ -6,6 +6,7 @@
       "timeZone": "UTC"
     }
   },
+  "ecs.version": "1.2.0",
   "log.level": {
     "$resolver": "level",
     "field": "name"
diff --git a/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/EcsLayoutTest.java b/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/EcsLayoutTest.java
index 9f806f5..f58e3c4 100644
--- a/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/EcsLayoutTest.java
+++ b/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/EcsLayoutTest.java
@@ -24,6 +24,8 @@ import org.apache.logging.log4j.layout.template.json.JsonTemplateLayout.EventTem
 import org.assertj.core.api.Assertions;
 import org.junit.jupiter.api.Test;
 
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
@@ -34,6 +36,8 @@ class EcsLayoutTest {
 
     private static final Configuration CONFIGURATION = new DefaultConfiguration();
 
+    private static final Charset CHARSET = StandardCharsets.UTF_8;
+
     private static final String SERVICE_NAME = "test";
 
     private static final String EVENT_DATASET = SERVICE_NAME + ".log";
@@ -41,6 +45,7 @@ class EcsLayoutTest {
     private static final JsonTemplateLayout JSON_TEMPLATE_LAYOUT = JsonTemplateLayout
             .newBuilder()
             .setConfiguration(CONFIGURATION)
+            .setCharset(CHARSET)
             .setEventTemplateUri("classpath:EcsLayout.json")
             .setEventTemplateAdditionalFields(
                     new EventTemplateAdditionalField[]{
@@ -65,6 +70,11 @@ class EcsLayoutTest {
             .build();
 
     @Test
+    void test_EcsLayout_charset() {
+        Assertions.assertThat(ECS_LAYOUT.getCharset()).isEqualTo(CHARSET);
+    }
+
+    @Test
     void test_lite_log_events() {
         final List<LogEvent> logEvents = LogEventFixture.createLiteLogEvents(1_000);
         test(logEvents);
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 6c81991..d75e0ab 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
@@ -28,12 +28,20 @@ import org.apache.logging.log4j.core.appender.SocketAppender;
 import org.apache.logging.log4j.core.config.Configuration;
 import org.apache.logging.log4j.core.config.DefaultConfiguration;
 import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilderFactory;
+import org.apache.logging.log4j.core.config.plugins.Plugin;
+import org.apache.logging.log4j.core.config.plugins.PluginFactory;
 import org.apache.logging.log4j.core.impl.Log4jLogEvent;
 import org.apache.logging.log4j.core.layout.ByteBufferDestination;
 import org.apache.logging.log4j.core.lookup.MainMapLookup;
 import org.apache.logging.log4j.core.net.Severity;
 import org.apache.logging.log4j.core.time.MutableInstant;
 import org.apache.logging.log4j.layout.template.json.JsonTemplateLayout.EventTemplateAdditionalField;
+import org.apache.logging.log4j.layout.template.json.resolver.EventResolver;
+import org.apache.logging.log4j.layout.template.json.resolver.EventResolverContext;
+import org.apache.logging.log4j.layout.template.json.resolver.EventResolverFactory;
+import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolver;
+import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolverConfig;
+import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolverFactory;
 import org.apache.logging.log4j.layout.template.json.util.JsonReader;
 import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
 import org.apache.logging.log4j.layout.template.json.util.MapAccessor;
@@ -73,6 +81,7 @@ import java.util.Map;
 import java.util.concurrent.ArrayBlockingQueue;
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Consumer;
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
@@ -1794,6 +1803,115 @@ class JsonTemplateLayoutTest {
     }
 
     @Test
+    void test_inline_stack_trace_element_template() {
+
+        // Create the event template.
+        final String eventTemplate = writeJson(asMap(
+                "stackTrace", asMap(
+                        "$resolver", "exception",
+                        "field", "stackTrace",
+                        "stackTrace", asMap(
+                                "elementTemplate", asMap(
+                                        "$resolver", "stackTraceElement",
+                                        "field", "className")))));
+
+        // Create the layout.
+        final JsonTemplateLayout layout = JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setEventTemplate(eventTemplate)
+                .build();
+
+        // Create the log event.
+        final Throwable error = new RuntimeException("foo");
+        final SimpleMessage message = new SimpleMessage("foo");
+        final LogEvent logEvent = Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(LOGGER_NAME)
+                .setMessage(message)
+                .setThrown(error)
+                .build();
+
+        // Check the serialized log event.
+        final String expectedClassName = JsonTemplateLayoutTest.class.getCanonicalName();
+        usingSerializedLogEventAccessor(layout, logEvent, accessor -> Assertions
+                .assertThat(accessor.getList("stackTrace", String.class))
+                .contains(expectedClassName));
+
+    }
+
+    @Test
+    void test_custom_resolver() {
+
+        // Create the event template.
+        final String eventTemplate = writeJson(asMap(
+                "customField", asMap("$resolver", "custom")));
+
+        // Create the layout.
+        final JsonTemplateLayout layout = JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setEventTemplate(eventTemplate)
+                .build();
+
+        // Create the log event.
+        final SimpleMessage message = new SimpleMessage("foo");
+        final LogEvent logEvent = Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(LOGGER_NAME)
+                .setMessage(message)
+                .build();
+
+        // Check the serialized log event.
+        final String expectedClassName = JsonTemplateLayoutTest.class.getCanonicalName();
+        usingSerializedLogEventAccessor(layout, logEvent, accessor -> Assertions
+                .assertThat(accessor.getString("customField"))
+                .matches("CustomValue-[0-9]+"));
+
+    }
+
+    private static final class CustomResolver implements EventResolver {
+
+        private static final AtomicInteger COUNTER = new AtomicInteger(0);
+
+        private CustomResolver() {}
+
+        @Override
+        public void resolve(
+                final LogEvent value,
+                final JsonWriter jsonWriter) {
+            jsonWriter.writeString("CustomValue-" + COUNTER.getAndIncrement());
+        }
+
+    }
+
+    @Plugin(name = "CustomResolverFactory", category = TemplateResolverFactory.CATEGORY)
+    public static final class CustomResolverFactory implements EventResolverFactory {
+
+        private static final CustomResolverFactory INSTANCE = new CustomResolverFactory();
+
+        private CustomResolverFactory() {}
+
+        @PluginFactory
+        public static CustomResolverFactory getInstance() {
+            return INSTANCE;
+        }
+
+        @Override
+        public String getName() {
+            return "custom";
+        }
+
+        @Override
+        public TemplateResolver<LogEvent> create(
+                final EventResolverContext context,
+                final TemplateResolverConfig config) {
+            return new CustomResolver();
+        }
+
+    }
+
+    @Test
     void test_null_eventDelimiter() {
 
         // Create the event template.
@@ -2291,9 +2409,9 @@ class JsonTemplateLayoutTest {
             final Consumer<MapAccessor> accessorConsumer) {
         final String serializedLogEventJson = layout.toSerializable(logEvent);
         @SuppressWarnings("unchecked")
-        final Map<String, Object> serializedLogEvent =
+        final Map<String, Object> deserializedLogEvent =
                 (Map<String, Object>) readJson(serializedLogEventJson);
-        final MapAccessor serializedLogEventAccessor = new MapAccessor(serializedLogEvent);
+        final MapAccessor serializedLogEventAccessor = new MapAccessor(deserializedLogEvent);
         accessorConsumer.accept(serializedLogEventAccessor);
     }
 
@@ -2304,7 +2422,7 @@ class JsonTemplateLayoutTest {
     private static Map<String, Object> asMap(final Object... pairs) {
         final Map<String, Object> map = new LinkedHashMap<>();
         if (pairs.length % 2 != 0) {
-            throw new IllegalArgumentException("odd number of arguments");
+            throw new IllegalArgumentException("odd number of arguments: " + pairs.length);
         }
         for (int i = 0; i < pairs.length; i += 2) {
             final String key = (String) pairs[i];
diff --git a/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/LogstashIT.java b/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/LogstashIT.java
index f38b823..4b64bec 100644
--- a/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/LogstashIT.java
+++ b/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/LogstashIT.java
@@ -109,10 +109,11 @@ class LogstashIT {
                     })
             .build();
 
+    // Note that EcsLayout doesn't support charset configuration, though it uses
+    // UTF-8 internally.
     private static final EcsLayout ECS_LAYOUT = EcsLayout
             .newBuilder()
             .setConfiguration(CONFIGURATION)
-            .setCharset(CHARSET)
             .setServiceName(SERVICE_NAME)
             .setEventDataset(EVENT_DATASET)
             .build();
diff --git a/log4j-perf/src/main/java/org/apache/logging/log4j/layout/template/json/JsonTemplateLayoutBenchmarkState.java b/log4j-perf/src/main/java/org/apache/logging/log4j/layout/template/json/JsonTemplateLayoutBenchmarkState.java
index 90dd89d..9a46528 100644
--- a/log4j-perf/src/main/java/org/apache/logging/log4j/layout/template/json/JsonTemplateLayoutBenchmarkState.java
+++ b/log4j-perf/src/main/java/org/apache/logging/log4j/layout/template/json/JsonTemplateLayoutBenchmarkState.java
@@ -145,12 +145,19 @@ public class JsonTemplateLayoutBenchmarkState {
     }
 
     private static EcsLayout createEcsLayout() {
-        return EcsLayout
+        final EcsLayout layout = EcsLayout
                 .newBuilder()
                 .setConfiguration(CONFIGURATION)
-                .setCharset(CHARSET)
                 .setServiceName("benchmark")
                 .build();
+        final Charset layoutCharset = layout.getCharset();
+        // Note that EcsLayout doesn't support charset configuration, though it
+        // uses UTF-8 internally.
+        if (CHARSET.equals(layoutCharset)) {
+            throw new IllegalArgumentException(
+                    "invalid EcsLayout charset: " + layoutCharset);
+        }
+        return layout;
     }
 
     private static GelfLayout createGelfLayout() {
diff --git a/pom.xml b/pom.xml
index a46a550..4869e2f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -873,7 +873,7 @@
       <dependency>
         <groupId>co.elastic.logging</groupId>
         <artifactId>log4j2-ecs-layout</artifactId>
-        <version>0.5.2</version>
+        <version>1.0.1</version>
       </dependency>
       <dependency>
         <groupId>org.elasticsearch.client</groupId>
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 088c844..3218bb0 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -30,6 +30,10 @@
          - "remove" - Removed
     -->
     <release version="2.15.0" date="2021-MM-DD" description="GA Release 2.15.0">
+      <!-- ADDS -->
+      <action issue="LOG4J2-3004" dev="vy" type="add">
+        Add plugin support to JsonTemplateLayout.
+      </action>
       <action issue="LOG4J2-3050" dev="rgoers" type="add">
         Allow AdditionalFields to be ignored if their value is null or a zero-length String.
       </action>
@@ -42,6 +46,7 @@
       <action issue="LOG4J2-3044" dev="rgoers" type="add">
         Add RepeatPatternConverter.
       </action>
+      <!-- UPDATES -->
       <action issue="LOG4J2-3041" dev="rgoers" type="update">
         Allow a PatternSelector to be specified on GelfLayout.
       </action>
diff --git a/src/site/asciidoc/manual/json-template-layout.vm.adoc b/src/site/asciidoc/manual/json-template-layout.adoc.vm
similarity index 65%
rename from src/site/asciidoc/manual/json-template-layout.vm.adoc
rename to src/site/asciidoc/manual/json-template-layout.adoc.vm
index c0ff96b..c3a9e05 100644
--- a/src/site/asciidoc/manual/json-template-layout.vm.adoc
+++ b/src/site/asciidoc/manual/json-template-layout.adoc.vm
@@ -19,13 +19,21 @@
 Volkan Yazıcı <vy...@apache.org>
 
 `JsonTemplateLayout` is a customizable, efficient, and garbage-free JSON
-emitting layout. It encodes ``LogEvent``s according to the structure described
+generating layout. It encodes ``LogEvent``s according to the structure described
 by the JSON template provided. In a nutshell, it shines with its
 
 * Customizable JSON structure (see `eventTemplate[Uri]` and
-  `stackTraceElementTemplate[Uri]` parameters)
+  `stackTraceElementTemplate[Uri]` xref:layout-config[layout configuration] parameters)
 
-* Customizable timestamp formatting (see `timestamp` parameter)
+* Customizable timestamp formatting (see xref:event-template-resolver-timestamp[]
+  event template resolver)
+
+* Feature rich exception formatting (see xref:event-template-resolver-exception[]
+  and xref:event-template-resolver-exceptionRootCause[] event template resolvers)
+
+* xref:extending[Extensible plugin support]
+
+* Customizable object xref:recycling-strategy[recycling strategy]
 
 [#usage]
 == Usage
@@ -43,68 +51,58 @@ enough to enable access to `JsonTemplateLayout` in your Log4j configuration:
 ----
 
 For instance, given the following JSON template modelling
-https://github.com/logstash/log4j-jsonevent-layout[the official Logstash
-`JSONEventLayoutV1`] (accessible via `classpath:LogstashJsonEventLayoutV1.json`)
+https://www.elastic.co/guide/en/ecs/current/ecs-reference.html[the Elastic Common Schema (ECS) specification]
+(accessible via `classpath:EcsLayout.json`)
 
 [source,json]
 ----
 {
-  "mdc": {
-    "$resolver": "mdc"
-  },
-  "exception": {
-    "exception_class": {
-      "$resolver": "exception",
-      "field": "className"
-    },
-    "exception_message": {
-      "$resolver": "exception",
-      "field": "message"
-    },
-    "stacktrace": {
-      "$resolver": "exception",
-      "field": "stackTrace",
-      "stackTrace": {
-        "stringified": true
-      }
+  "@timestamp": {
+    "$resolver": "timestamp",
+    "pattern": {
+      "format": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
+      "timeZone": "UTC"
     }
   },
-  "line_number": {
-    "$resolver": "source",
-    "field": "lineNumber"
-  },
-  "class": {
-    "$resolver": "source",
-    "field": "className"
+  "ecs.version": "1.2.0",
+  "log.level": {
+    "$resolver": "level",
+    "field": "name"
   },
-  "@version": 1,
-  "source_host": "${hostName}",
   "message": {
     "$resolver": "message",
     "stringified": true
   },
-  "thread_name": {
+  "process.thread.name": {
     "$resolver": "thread",
     "field": "name"
   },
-  "@timestamp": {
-    "$resolver": "timestamp"
-  },
-  "level": {
-    "$resolver": "level",
+  "log.logger": {
+    "$resolver": "logger",
     "field": "name"
   },
-  "file": {
-    "$resolver": "source",
-    "field": "fileName"
+  "labels": {
+    "$resolver": "mdc",
+    "flatten": true,
+    "stringified": true
   },
-  "method": {
-    "$resolver": "source",
-    "field": "methodName"
+  "tags": {
+    "$resolver": "ndc"
   },
-  "logger_name": {
-    "$resolver": "logger",
-    "field": "name"
+  "error.type": {
+    "$resolver": "exception",
+    "field": "className"
+  },
+  "error.message": {
+    "$resolver": "exception",
+    "field": "message"
+  },
+  "error.stack_trace": {
+    "$resolver": "exception",
+    "field": "stackTrace",
+    "stackTrace": {
+      "stringified": true
+    }
   }
 }
 ----
@@ -113,7 +111,7 @@ in combination with the below `log4j2.xml` configuration:
 
 [source,xml]
 ----
-<JsonTemplateLayout eventTemplateUri="classpath:LogstashJsonEventLayoutV1.json"/>
+<JsonTemplateLayout eventTemplateUri="classpath:EcsLayout.json"/>
 ----
 
 or with the below `log4j2.properties` configuration:
@@ -121,30 +119,23 @@ or with the below `log4j2.properties` configuration:
 [source,ini]
 ----
 appender.console.json.type = JsonTemplateLayout
-appender.console.json.eventTemplateUri = classpath:LogstashJsonEventLayoutV1.json
+appender.console.json.eventTemplateUri = classpath:EcsLayout.json
 ----
 
-`JsonTemplateLayout` emits JSON strings as follows:
+`JsonTemplateLayout` generates JSON as follows:
 
 [source,json]
 ----
 {
-  "exception": {
-    "exception_class": "java.lang.RuntimeException",
-    "exception_message": "test",
-    "stacktrace": "java.lang.RuntimeException: test\n\tat org.apache.logging.log4j.JsonTemplateLayoutDemo.main(JsonTemplateLayoutDemo.java:11)\n"
-  },
-  "line_number": 12,
-  "class": "org.apache.logging.log4j.JsonTemplateLayoutDemo",
-  "@version": 1,
-  "source_host": "varlik",
+  "@timestamp": "2017-05-25T19:56:23.370Z",
+  "ecs.version": "1.2.0",
+  "log.level": "ERROR",
   "message": "Hello, error!",
-  "thread_name": "main",
-  "@timestamp": "2017-05-25T19:56:23.370+02:00",
-  "level": "ERROR",
-  "file": "JsonTemplateLayoutDemo.java",
-  "method": "main",
-  "logger_name": "org.apache.logging.log4j.JsonTemplateLayoutDemo"
+  "process.thread.name": "main",
+  "log.logger": "org.apache.logging.log4j.JsonTemplateLayoutDemo",
+  "error.type": "java.lang.RuntimeException",
+  "error.message": "test",
+  "error.stack_trace": "java.lang.RuntimeException: test\n\tat org.apache.logging.log4j.JsonTemplateLayoutDemo.main(JsonTemplateLayoutDemo.java:11)\n"
 }
 ----
 
@@ -189,12 +180,12 @@ appender.console.json.eventTemplateUri = classpath:LogstashJsonEventLayoutV1.jso
 
 | eventTemplateRootObjectKey
 | String
-| if given, puts the event template into a JSON object composed of a single
-  member with the given key (defaults to `null` set by
+| if present, the event template is put into a JSON object composed of a single
+  member with the provided key (defaults to `null` set by
   `log4j.layout.jsonTemplate.eventTemplateRootObjectKey`
   property)
 
-| eventTemplateAdditionalFields
+| eventTemplateAdditionalField
 | EventTemplateAdditionalField[]
 | additional key-value pairs appended to the root of the event template
 
@@ -206,20 +197,20 @@ appender.console.json.eventTemplateUri = classpath:LogstashJsonEventLayoutV1.jso
 
 | stackTraceElementTemplateUri
 | String
-| JSON template for rendering ``StackTraceElement``s (defaults to
-  `classpath:StackTraceElementLayout.json` set by
+| URI pointing to the JSON template for rendering ``StackTraceElement``s
+  (defaults to `classpath:StackTraceElementLayout.json` set by
   `log4j.layout.jsonTemplate.stackTraceElementTemplateUri` property)
 
 | eventDelimiter
 | String
-| delimiter used for separating emitted ``LogEvent``s (defaults to
+| delimiter used for separating rendered ``LogEvent``s (defaults to
   `System.lineSeparator()` set by `log4j.layout.jsonTemplate.eventDelimiter`
   property)
 
 | nullEventDelimiterEnabled
 | boolean
-| append `\0` (`null`) character to the end of every emitted `eventDelimiter`
-  (defaults to `false` set by
+| append `\0` (`null`) character to the end of every `eventDelimiter`
+  separating rendered ``LogEvent``s (defaults to `false` set by
   `log4j.layout.jsonTemplate.nullEventDelimiterEnabled` property)
 
 | maxStringLength
@@ -240,7 +231,7 @@ appender.console.json.eventTemplateUri = classpath:LogstashJsonEventLayoutV1.jso
 |===
 
 [#additional-event-template-fields]
-=== Additonal event template fields
+=== Additional event template fields
 
 Additional event template fields are a convenient short-cut to add custom fields
 to a template or override the existing ones. Following configuration overrides
@@ -348,7 +339,7 @@ JsonTemplateLayout:
 
 `RecyclerFactory` plays a crucial role for determining the memory footprint of
 the layout. Template resolvers employ it to create recyclers for objects that
-they can reuse. The function of each `RecyclerFactory` and when one should
+they can reuse. The behavior of each `RecyclerFactory` and when one should
 prefer one over another is explained below:
 
 * `dummy` performs no recycling, hence each recycling attempt will result in a
@@ -371,6 +362,7 @@ trying to log), it starts allocating. `queue` is a good strategy where
 otherwise `java.util.concurrent.ArrayBlockingQueue.new`) and `capacity` (of
 type `int`, defaults to `max(8,2*cpuCount+1)`) parameters:
 +
+.Example configurations of `queue` recycling strategy
 [source]
 ----
 queue:supplier=org.jctools.queues.MpmcArrayQueue.new
@@ -381,6 +373,9 @@ queue:supplier=java.util.concurrent.ArrayBlockingQueue.new,capacity=50
 The default `RecyclerFactory` is `threadLocal`, if
 `log4j2.enable.threadlocals=true`; otherwise, `queue`.
 
+See <<extending-recycler>> for details on how to introduce custom
+`RecyclerFactory` implementations.
+
 [#template-config]
 == Template Configuration
 
@@ -389,7 +384,7 @@ parameters:
 
 - `eventTemplate[Uri]` (for serializing ``LogEvent``s)
 - `stackTraceElementTemplate[Uri]` (for serializing ``StackStraceElement``s)
-- `eventTemplateAdditionalFields` (for extending the used event template)
+- `eventTemplateAdditionalField` (for extending the used event template)
 
 [#event-templates]
 === Event Templates
@@ -398,63 +393,7 @@ parameters:
 serialize ``LogEvent``s. The default configuration (accessible by
 `log4j.layout.jsonTemplate.eventTemplate[Uri]` property) is set to
 `classpath:EcsLayout.json` provided by the `log4j-layout-template-json`
-artifact:
-
-[source,json]
-----
-{
-  "@timestamp": {
-    "$resolver": "timestamp",
-    "pattern": {
-      "format": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
-      "timeZone": "UTC"
-    }
-  },
-  "log.level": {
-    "$resolver": "level",
-    "field": "name"
-  },
-  "message": {
-    "$resolver": "message",
-    "stringified": true
-  },
-  "process.thread.name": {
-    "$resolver": "thread",
-    "field": "name"
-  },
-  "log.logger": {
-    "$resolver": "logger",
-    "field": "name"
-  },
-  "labels": {
-    "$resolver": "mdc",
-    "flatten": true,
-    "stringified": true
-  },
-  "tags": {
-    "$resolver": "ndc"
-  },
-  "error.type": {
-    "$resolver": "exception",
-    "field": "className"
-  },
-  "error.message": {
-    "$resolver": "exception",
-    "field": "message"
-  },
-  "error.stack_trace": {
-    "$resolver": "exception",
-    "field": "stackTrace",
-    "stackTrace": {
-      "stringified": true
-    }
-  }
-}
-
-----
-
-`log4j-layout-template-json` artifact contains the following predefined event
-templates:
+artifact, which contains the following predefined event templates:
 
 - https://github.com/apache/logging-log4j2/tree/master/log4j-layout-template-json/src/main/resources/EcsLayout.json[`EcsLayout.json`]
   described by https://www.elastic.co/guide/en/ecs/current/ecs-reference.html[the Elastic Common Schema (ECS) specification]
@@ -467,7 +406,8 @@ templates:
   described by https://docs.graylog.org/en/3.1/pages/gelf.html#gelf-payload-specification[the
   Graylog Extended Log Format (GELF) payload specification] with additional
   `_thread` and `_logger` fields. (Here it is advised to override the obligatory
-  `host` field with a user provided constant via `eventTemplateAdditionalFields`
+  `host` field with a user provided constant via
+  xref:additional-event-template-fields[additional event template fields]
   to avoid `hostName` property lookup at runtime, which incurs an extra cost.)
 
 - https://github.com/apache/logging-log4j2/tree/master/log4j-layout-template-json/src/main/resources/JsonLayout.json[`JsonLayout.json`]
@@ -480,6 +420,40 @@ templates:
 [#event-template-resolvers]
 ==== Event Template Resolvers
 
+Event template resolvers consume a `LogEvent` and render a certain property of
+it at the point of the JSON where they are declared. For instance, `marker`
+resolver renders the marker of the event, `level` resolver renders the level,
+and so on. An event template resolver is denoted with a special object
+containing a`${dollar}resolver` key:
+
+.Example event template demonstrating the usage of `level` resolver
+[source,json]
+----
+{
+  "version": "1.0",
+  "level": {
+    "$resolver": "level",
+    "field": "name"
+  }
+}
+----
+
+Here `version` field will be rendered as is, while `level` field will be
+populated by the `level` resolver. That is, this template will generate JSON
+similar to the following:
+
+.Example JSON generated from the demonstrated event template
+[source,json]
+----
+{
+  "version": "1.0",
+  "level": "INFO"
+}
+----
+
+The complete list of available event template resolvers are provided below in
+detail.
+
 [#event-template-resolver-endOfBatch]
 ===== `endOfBatch`
 
@@ -498,10 +472,14 @@ Resolves `logEvent.isEndOfBatch()` boolean flag:
 [source]
 ----
 config              = field , [ stringified ] , [ stackTrace ]
-field               = "field" -> ( "className" \| "message" \| "stackTrace" )
+field               = "field" -> ( "className" | "message" | "stackTrace" )
+
+stackTrace          = "stackTrace" -> (
+                        [ stringified ]
+                      , [ elementTemplate ]
+                      )
 
-stackTrace          = "stackTrace" -> stringified
-stringified         = "stringified" -> ( boolean \| truncation )
+stringified         = "stringified" -> ( boolean | truncation )
 truncation          = "truncation" -> (
                         [ suffix ]
                       , [ pointMatcherStrings ]
@@ -510,6 +488,8 @@ truncation          = "truncation" -> (
 suffix              = "suffix" -> string
 pointMatcherStrings = "pointMatcherStrings" -> string[]
 pointMatcherRegexes = "pointMatcherRegexes" -> string[]
+
+elementTemplate     = "elementTemplate" -> object
 ----
 
 Resolves fields of the `Throwable` returned by `logEvent.getThrown()`.
@@ -526,6 +506,27 @@ If a stringified stack trace truncation takes place, it will be indicated with
 `suffix`, which by default is set to the configured `truncatedStringSuffix` in
 the layout, unless explicitly provided.
 
+`elementTemplate` is an object describing the template to be used while
+resolving the `StackTraceElement` array. If `stringified` is set to `true`,
+`elementTemplate` will be discarded. By default, `elementTemplate` is set to
+`null` and rather populated from the layout configuration. That is, the stack
+trace element template can also be provided using
+`stackTraceElementTemplate[Uri]` layout configuration parameters. The template
+to be employed is determined in the following order:
+
+. `elementTemplate` provided in the resolver configuration
+
+. `stackTraceElementTemplate` parameter from layout configuration
+(the default is populated from `log4j.layout.jsonTemplate.stackTraceElementTemplate`
+system property)
+
+. `stackTraceElementTemplateUri` parameter from layout configuration
+(the default is populated from `log4j.layout.jsonTemplate.stackTraceElementTemplateUri`
+system property)
+
+See <<stack-trace-element-templates>>
+for the list of available resolvers in a stack trace element template.
+
 Note that this resolver is toggled by
 `log4j.layout.jsonTemplate.stackTraceEnabled` property.
 
@@ -572,7 +573,7 @@ Resolve the stack trace into a string field:
 ----
 
 Resolve the stack trace into a string field such that the content will be
-truncated by the given point matcher:
+truncated after the given point matcher:
 
 [source,json]
 ----
@@ -590,12 +591,46 @@ truncated by the given point matcher:
 }
 ----
 
+Resolve the stack trace into an object described by the provided stack trace
+element template:
+
+[source,json]
+----
+{
+  "$resolver": "exception",
+  "field": "stackTrace",
+  "stackTrace": {
+    "elementTemplate": {
+      "class": {
+       "$resolver": "stackTraceElement",
+       "field": "className"
+      },
+      "method": {
+       "$resolver": "stackTraceElement",
+       "field": "methodName"
+      },
+      "file": {
+       "$resolver": "stackTraceElement",
+       "field": "fileName"
+      },
+      "line": {
+       "$resolver": "stackTraceElement",
+       "field": "lineNumber"
+      }
+    }
+  }
+}
+----
+
+See <<stack-trace-element-templates>> for further details on resolvers available
+for ``StackTraceElement`` templates.
+
 [#event-template-resolver-exceptionRootCause]
 ===== `exceptionRootCause`
 
 Resolves the fields of the innermost `Throwable` returned by
 `logEvent.getThrown()`. Its syntax and garbage-footprint are identical to the
-link:#event-template-exception[`exception`] resolver.
+xref:event-template-resolver-exception[] resolver.
 
 [#event-template-resolver-level]
 ===== `level`
@@ -1206,6 +1241,12 @@ parent such that keys are prefixed with `_`:
 [#stack-trace-element-templates]
 === Stack Trace Element Templates
 
+xref:event-template-resolver-exception[] and
+xref:event-template-resolver-exceptionRootCause[] event template resolvers can
+serialize an exception stack trace (i.e., `StackTraceElement[]` returned by
+`Throwable#getStackTrace()`) into a JSON array. While doing so, JSON templating
+infrastructure is used again.
+
 `stackTraceElement[Uri]` describes the JSON structure `JsonTemplateLayout` uses
 to format ``StackTraceElement``s. The default configuration (accessible by
 `log4j.layout.jsonTemplate.stackTraceElementTemplate[Uri]` property) is set to
@@ -1247,6 +1288,291 @@ config = "field" -> (
 
 All above accesses to `StackTraceElement` is garbage-free.
 
+[#extending]
+== Extending
+
+`JsonTemplateLayout` relies on Log4j link:plugins.html[plugin system] to build
+up the features it provides. This enables feature customization a breeze for
+users. As of this moment, following features are implemented by means of
+plugins:
+
+* Event template resolvers (e.g., `exception`, `message`, `level` event template resolvers)
+* Event template interceptors (e.g., injection of `eventTemplateAdditionalField`)
+* Recycler factories
+
+Following sections cover these in detail.
+
+[#extending-plugins]
+=== Plugin Preliminaries
+
+Log4j plugin system is the de facto extension mechanism embraced by various
+Log4j components, including `JsonTemplateLayout`. Plugins make it possible
+for extensible components _receive_ feature implementations without any explicit
+links in between. It is analogous to a
+https://en.wikipedia.org/wiki/Dependency_injection[dependency injection]
+framework, but curated for Log4j-specific needs.
+
+In a nutshell, you annotate your classes with `@Plugin` and their (`static`)
+creator methods with `@PluginFactory`. Last, you inform the Log4j plugin system
+to discover these custom classes. This can be done either using `packages`
+declared in your Log4j configuration or by various other ways described in
+link:plugins.html[plugin system documentation].
+
+[#extending-event-resolvers]
+=== Extending Event Resolvers
+
+All available xref:event-template-resolvers[event template resolvers] are simple
+plugins employed by `JsonTemplateLayout`. To add new ones, one just needs to
+create their own `EventResolver` and instruct its injection via a
+`@Plugin`-annotated `EventResolverFactory` class.
+
+For demonstration purposes, below we will create a `randomNumber` event resolver.
+Let's start with the actual resolver:
+
+[source,java]
+.Custom random number event resolver
+----
+package com.acme.logging.log4j.layout.template.json;
+
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.layout.template.json.resolver.EventResolver;
+import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
+
+/**
+ * Resolves a random floating point number.
+ *
+ * <h3>Configuration</h3>
+ *
+ * <pre>
+ * config = ( [ range ] )
+ * range  = number[]
+ * </pre>
+ *
+ * {@code range} is a number array with two elements, where the first number
+ * denotes the start (inclusive) and the second denotes the end (exclusive).
+ * {@code range} is optional and by default set to {@code [0, 1]}.
+ *
+ * <h3>Examples</h3>
+ *
+ * Resolve a random number between 0 and 1:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "randomNumber"
+ * }
+ * </pre>
+ *
+ * Resolve a random number between -0.123 and 0.123:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "randomNumber",
+ *   "range": [-0.123, 0.123]
+ * }
+ * </pre>
+ */
+public final class RandomNumberResolver implements EventResolver {
+
+    private final double loIncLimit;
+
+    private final double hiExcLimit;
+
+    RandomNumberResolver(final TemplateResolverConfig config) {
+        final List<Number> rangeArray = config.getList("range", Number.class);
+        if (rangeArray == null) {
+            this.loIncLimit = 0D;
+            this.hiExcLimit = 1D;
+        } else if (rangeArray.size() != 2) {
+            throw new IllegalArgumentException(
+                    "range array must be of size two: " + config);
+        } else {
+            this.loIncLimit = rangeArray.get(0).doubleValue();
+            this.hiExcLimit = rangeArray.get(1).doubleValue();
+            if (loIncLimit > hiExcLimit) {
+                throw new IllegalArgumentException("invalid range: " + config);
+            }
+        }
+    }
+
+    static String getName() {
+        return "randomNumber";
+    }
+
+    @Override
+    public void resolve(
+            final LogEvent value,
+            final JsonWriter jsonWriter) {
+        final double randomNumber =
+                loIncLimit + (hiExcLimit - loIncLimit) * Math.random();
+        jsonWriter.writeNumber(randomNumber);
+    }
+
+}
+----
+
+Next create a `EventResolverFactory` class to register `RandomNumberResolver`
+into the Log4j plugin system.
+
+[source,java]
+.Resolver factory class to register `RandomNumberResolver` into the Log4j plugin system
+----
+package com.acme.logging.log4j.layout.template.json;
+
+import org.apache.logging.log4j.core.config.plugins.Plugin;
+import org.apache.logging.log4j.core.config.plugins.PluginFactory;
+import org.apache.logging.log4j.layout.template.json.resolver.EventResolverContext;
+import org.apache.logging.log4j.layout.template.json.resolver.EventResolverFactory;
+import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolver;
+import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolverConfig;
+import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolverFactory;
+
+/**
+ * {@link RandomNumberResolver} factory.
+ */
+@Plugin(name = "RandomNumberResolverFactory", category = TemplateResolverFactory.CATEGORY)
+public final class RandomNumberResolverFactory implements EventResolverFactory {
+
+    private static final RandomNumberResolverFactory INSTANCE =
+            new RandomNumberResolverFactory();
+
+    private RandomNumberResolverFactory() {}
+
+    @PluginFactory
+    public static RandomNumberResolverFactory getInstance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public String getName() {
+        return RandomNumberResolver.getName();
+    }
+
+    @Override
+    public RandomNumberResolver create(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
+        return new RandomNumberResolver(config);
+    }
+
+}
+----
+
+Almost complete. Last, we need to inform the Log4j plugin system to discover
+these custom classes:
+
+[source,xml]
+.Log4j configuration employing custom `randomNumber` resolver
+----
+<?xml version="1.0" encoding="UTF-8"?>
+<Configuration packages="com.acme.logging.log4j.layout.template.json">
+  <!-- ... -->
+  <JsonTemplateLayout>
+    <EventTemplateAdditionalField
+        key="id"
+        format="JSON"
+        value='{"$resolver": "randomNumber", "range": [0, 1000000]}'/>
+  </JsonTemplateLayout>
+  <!-- ... -->
+</Configuration>
+----
+
+All available event template resolvers are located in
+`org.apache.logging.log4j.layout.template.json.resolver` package. It is a fairly
+rich resource for inspiration while implementing new resolvers.
+
+[#extending-template-resolver]
+=== Intercepting the Template Resolver Compiler
+
+`JsonTemplateLayout` allows interception of the template resolver compilation,
+which is the process converting a template into a Java function performing the
+JSON serialization. This interception mechanism is internally used to implement
+`eventTemplateRootObjectKey` and `eventTemplateAdditionalField` features. In a
+nutshell, one needs to create a `@Plugin`-annotated class extending from
+`EventResolverInterceptor` interface.
+
+To see the interception in action, check out the `EventRootObjectKeyInterceptor`
+class which is responsible for implementing the `eventTemplateRootObjectKey`
+feature:
+
+[source,java]
+.Event interceptor to add `eventTemplateRootObjectKey`, if present
+----
+import org.apache.logging.log4j.layout.template.json.resolver.EventResolverContext;
+import org.apache.logging.log4j.layout.template.json.resolver.EventResolverInterceptor;
+import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolverInterceptor;
+
+/**
+ * Interceptor to add a root object key to the event template.
+ */
+@Plugin(name = "EventRootObjectKeyInterceptor", category = TemplateResolverInterceptor.CATEGORY)
+public class EventRootObjectKeyInterceptor implements EventResolverInterceptor {
+
+    private static final EventRootObjectKeyInterceptor INSTANCE =
+            new EventRootObjectKeyInterceptor();
+
+    private EventRootObjectKeyInterceptor() {}
+
+    @PluginFactory
+    public static EventRootObjectKeyInterceptor getInstance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public Object processTemplateBeforeResolverInjection(
+            final EventResolverContext context,
+            final Object node) {
+        String eventTemplateRootObjectKey = context.getEventTemplateRootObjectKey();
+        return eventTemplateRootObjectKey != null
+                ? Collections.singletonMap(eventTemplateRootObjectKey, node)
+                : node;
+    }
+
+}
+----
+
+Here, `processTemplateBeforeResolverInjection()` method checks if the user has
+provided an `eventTemplateRootObjectKey`. If so, it wraps the root `node` with a
+new object; otherwise, returns the `node` as is. Note that `node` refers to the
+root Java object of the event template read by `JsonReader`.
+
+[#extending-recycler]
+=== Extending Recycler Factories
+
+`recyclerFactory` input `String` read from the layout configuration is converted
+to a `RecyclerFactory` using the default `RecyclerFactoryConverter` extending
+from `TypeConverter<RecyclerFactory>`. If one wants to change this behavior,
+they simply need to add their own `TypeConverter<RecyclerFactory>` implementing
+`Comparable<TypeConverter<?>>` to prioritize their custom converter.
+
+[source,java]
+.Custom `TypeConverter` for `RecyclerFactory`
+----
+package com.acme.logging.log4j.layout.template.json;
+
+import org.apache.logging.log4j.core.config.plugins.Plugin;
+import org.apache.logging.log4j.core.config.plugins.convert.TypeConverter;
+import org.apache.logging.log4j.core.config.plugins.convert.TypeConverters;
+
+@Plugin(name = "AcmeRecyclerFactoryConverter", category = TypeConverters.CATEGORY)
+public final class AcmeRecyclerFactoryConverter
+        implements TypeConverter<RecyclerFactory>, Comparable<TypeConverter<?>> {
+
+    @Override
+    public RecyclerFactory convert(final String recyclerFactorySpec) {
+        return AcmeRecyclerFactory.ofSpec(recyclerFactorySpec);
+    }
+
+    @Override
+    public int compareTo(final TypeConverter<?> ignored) {
+        return -1;
+    }
+
+}
+----
+
+Here note that `compareTo()` always returns -1 to rank it higher compared to
+other matching converters.
+
 [#features]
 == Features
 
@@ -1333,6 +1659,12 @@ alternatives.
 | ✕
 | ✕
 | ✕
+
+| Custom resolvers?
+| ✓
+| ✕
+| ✕
+| ✕
 |===
 
 [#faq]
@@ -1356,7 +1688,7 @@ recursiveCollection[0] = recursiveCollection;
 ----
 
 While the exact exception might vary, you will most like get a
-`StackOverflowError` while trying to render `recursiveCollection` into a
+`StackOverflowError` for trying to render `recursiveCollection` into a
 `String`. Note that this is also the default behaviour for other Java standard
 library methods, e.g., `Arrays.toString()`. Hence mind self references while
 logging.
@@ -1376,9 +1708,9 @@ enabled. Take into account the following caveats:
 
 * Serialization of ``MapMessage``s and ``ObjectMessage``s are mostly
   garbage-free except for certain types (e.g., `BigDecimal`, `BigInteger`,
-  ``Collection``s with the exception of `List`).
+  ``Collection``s, except `List`).
 
 * link:lookups.html[Lookups] (that is, `${...}` variables) are not garbage-free.
 
-Don't forget to checkout link:#event-template-resolvers[the notes on garbage footprint of resolvers]
+Don't forget to check out xref:event-template-resolvers[the notes on garbage footprint of resolvers]
 you employ in templates.
diff --git a/src/site/xdoc/manual/layouts.xml.vm b/src/site/xdoc/manual/layouts.xml.vm
index 9e6916a..0490d73 100644
--- a/src/site/xdoc/manual/layouts.xml.vm
+++ b/src/site/xdoc/manual/layouts.xml.vm
@@ -628,13 +628,14 @@ logger.debug("one={}, two={}, three={}", 1, 2, 3);
     },
     "exception_message": {
       "${dollar}resolver": "exception",
-      "field": "message",
-      "stringified": true
+      "field": "message"
     },
     "stacktrace": {
       "${dollar}resolver": "exception",
       "field": "stackTrace",
-      "stringified": true
+      "stackTrace": {
+        "stringified": true
+      }
     }
   },
   "line_number": {
diff --git a/src/site/xdoc/manual/plugins.xml b/src/site/xdoc/manual/plugins.xml
index c6addc8..54de3d2 100644
--- a/src/site/xdoc/manual/plugins.xml
+++ b/src/site/xdoc/manual/plugins.xml
@@ -240,6 +240,9 @@
             Unlike other plugins, the plugin name of a <code>TypeConverter</code> is purely cosmetic. Appropriate
             type converters are looked up via the <code>Type</code> interface rather than via <code>Class&lt;?&gt;</code>
             objects only. Do note that <code>TypeConverter</code> plugins must have a default constructor.
+            When multiple converters match for a type, the first will be returned.
+            If any extends from <code>Comparable&lt;TypeConverter&lt;?&gt;&gt;</code>,
+            it will be used for determining the order.
           </p>
         </subsection>
       </section>