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/04/03 10:48:30 UTC

[logging-log4j2] branch release-2.x updated: LOG4J2-3004 Add plugin support to JsonTemplateLayout. (#476)

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

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


The following commit(s) were added to refs/heads/release-2.x by this push:
     new 6f950c7  LOG4J2-3004 Add plugin support to JsonTemplateLayout. (#476)
6f950c7 is described below

commit 6f950c7bd5428954e84bb05a345ea7789e86a713
Author: Volkan Yazıcı <vo...@gmail.com>
AuthorDate: Sat Apr 3 12:48:21 2021 +0200

    LOG4J2-3004 Add plugin support to JsonTemplateLayout. (#476)
---
 .../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                            |   8 +-
 ...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, 2501 insertions(+), 685 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 e040217..c5f5597 100644
--- a/pom.xml
+++ b/pom.xml
@@ -874,7 +874,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 dad8f58..cb4efa9 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>
@@ -52,15 +56,17 @@
         applications with a single LoggerContext. This selector avoids classloader lookup
         overhead incurred by the existing AsyncLoggerContextSelector.
       </action>
-      <action issue="LOG4J2-3041" dev="rgoers" type="update">
+      <action issue="LOG4J2-3041" dev="rgoers" type="add">
         Allow a PatternSelector to be specified on GelfLayout.
       </action>
+      <!-- FIXES -->
       <action issue="LOG4J2-3054" dev="ckozak" type="fix">
         BasicContextSelector hasContext and shutdown take the default context into account
       </action>
       <action issue="LOG4J2-2940" dev="ckozak" type="fix">
         Slf4j implementations walk the stack at most once rather than twice to determine the caller's class loader.
       </action>
+      <!-- UPDATES -->
       <action dev="ggregory" type="update">
         - org.eclipse.persistence:javax.persistence ............. 2.1.1 -> 2.2.1
         - org.eclipse.persistence:org.eclipse.persistence.jpa ... 2.6.5 -> 2.6.9
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>

Re: [logging-log4j2] branch release-2.x updated: LOG4J2-3004 Add plugin support to JsonTemplateLayout. (#476)

Posted by Matt Sicker <bo...@gmail.com>.
Sounds like annotation processing has managed to get disabled in your
IDE. IDEs tend to disable that by default due to the ability to run
arbitrary code in annotation processors (but now IntelliJ has a
"trusted project" feature for its own metadata, so maybe that's
changed). Without the processor, you'll usually see errors like an
inability to find any plugins or null pointer exceptions.

On Tue, 6 Apr 2021 at 15:27, Volkan Yazıcı <vo...@gmail.com> wrote:
>
> Ralph, same here. I had tested it before pushing the commit. Now I cannot
> make it work either. JTL cannot find any of its plugins. Even reverting
> back to the point where I committed doesn't help either. I also cannot run
> tests in the IDE anymore, hence it is pretty difficult to debug for me. (I
> have shared this problem in another thread for future reference.)
>
> Please note that what broke release-2.x branch wasn't new functionality,
> but new dummy TypeConverter classes in TypeConverterRegistryTest. I have
> the feeling that there is something else wrong in the core and these new
> test classes just make it surface. @Matt, given you are the plugin expert,
> I presume, do you have any ideas?
>
> On Tue, Apr 6, 2021 at 6:56 AM Ralph Goers <ra...@dslextreme.com>
> wrote:
>
> > OK, release-2.x now seems to be better but I don’t understand what is
> > going on with Master. I am sure I built master several times over the
> > weekend. But now it is getting errors all over the place in Json Template
> > Layout tests. I did two commits that shouldn’t have impacted anything in
> > that.
> >
> > My only concern when code like the registry changes is that all the
> > existing unit tests continue to pass without having to change them (unless
> > they are clearly buggy). We have enough tests that I am pretty confident
> > that if they all pass nothing that is going to affect anything has changed.
> >
> > Ralph
> >
> > > On Apr 5, 2021, at 1:19 PM, Volkan Yazıcı <vo...@gmail.com>
> > wrote:
> > >
> > > Hello Ralph,
> > >
> > > *TypeConverterRegistry changes*
> > >
> > > Sorry for breaking certain tests. I had used the following to test my
> > > changes: `./mvnw clean install -pl
> > > :log4j-core,:log4j-layout-template-json`. I have tested my changes after
> > > purging Log4j `2.*-SNAPSHOT` artifacts from my `~/.m2` and there I indeed
> > > observe the same errors reported in Jenkins:
> > >
> > > org.apache.logging.log4j.core.config.LoggersPluginTest.testEmptyAttribute
> > >
> > org.apache.logging.log4j.core.config.plugins.validation.validators.ValidatingPluginWithFailoverTest.testDoesNotLog_NoParameterThatMatchesElement_message
> > >
> > > (Apparently I have needed an extra bootstrap for annotations to kick in.)
> > >
> > > Though something interesting is going on here. If I revert my changes to
> > > TypeConverterRegistry, *aforementioned tests still do fail*! It is
> > actually
> > > TypeConverterRegistryTest changes that is breaking those changes. I have
> > > experimented with the following combinations:
> > >
> > > - reverted TypeConverterRegistry ⇨ LoggersPluginTest fails
> > > - reverted TypeConverterRegistryTest ⇨ LoggersPluginTest succeeds
> > > - `@Ignore`d added tests to TypeConverterRegistryTest ⇨ LoggersPluginTest
> > > fails
> > >
> > > I have reverted the changes to TypeConverterRegistryTest, though the
> > > TypeConverterRegistry changes are still in. I hope this would suffice to
> > > make Jenkins and GitHub Actions happy.
> > >
> > > *RecyclerFactory customization support*
> > >
> > > In JsonTemplateLayout.Builder, I have a @PluginBuilderAttribute of type
> > > RecyclerFactory. The input string is converted to a RecyclerFactory using
> > > RecyclerFactoryConverter. To allow users to introduce their own
> > > RecyclerFactory implementation in a backward-compatible way, I thought of
> > > supporting multiple TypeConverter's in TypeConverterRegistry. The idea is
> > > to enhance TypeConverterRegistry such that in the presence of multiple
> > > TypeConverter's matching on the same footprint, TypeConverterRegistry
> > will
> > > use the Comparable interface to determine the effective one.
> > >
> > > I think it is a nice feature to support multiple TypeConverter's, as my
> > use
> > > case demonstrates. Do you agree? What else would you recommend me to
> > > implement this customization support needed for
> > > JsonTemplateLayout.Builder#recyclerFactory?
> > >
> > > Kind regards.
> > >
> > > On Sun, Apr 4, 2021 at 8:43 AM Ralph Goers <ra...@dslextreme.com>
> > > wrote:
> > >
> > >> I am now looking at the Jenkins and GitHub Actions builds and it is
> > clear
> > >> that the change I highlighted below has caused at 3 tests to fail.
> > Please
> > >> revert the change to the TypeConverterRegistry. In the future, whenever
> > a
> > >> change is made to the API or core components please make sure you run a
> > >> full build before you commit. This should be standard practice for
> > everyone.
> > >>
> > >> Ralph
> > >>
> > >>> On Apr 3, 2021, at 1:27 PM, Ralph Goers <ra...@dslextreme.com>
> > >> wrote:
> > >>>
> > >>> Volkan,
> > >>>
> > >>> I haven’t looked at the details of what you changed in
> > >> JsonTemplateLayout yet but the below concerns me a bit….
> > >>>
> > >>> Ralph
> > >>>
> > >>>
> > >>>> On Apr 3, 2021, at 3:48 AM, vy@apache.org wrote:
> > >>>>
> > >>>> This is an automated email from the ASF dual-hosted git repository.
> > >>>>
> > >>>
> > >>> Why were changes to the registry required for this? Aren’t you just
> > >> creating new Plugins?
> > >>>
> > >>>> 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));
> > >>>
> > >>> I don’t understand how replacing calling registerConverter with
> > >> putIfAbsent is equivalent. The registerConverterMethod seems to allow
> > >> plugins to be overridden.
> > >>>
> > >>>> -                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;
> > >>>>       }
> > >>>>   }
> > >>>
> > >>>
> > >>>
> > >>
> > >>
> > >>
> >
> >
> >

Re: [logging-log4j2] branch release-2.x updated: LOG4J2-3004 Add plugin support to JsonTemplateLayout. (#476)

Posted by Volkan Yazıcı <vo...@gmail.com>.
Ralph, same here. I had tested it before pushing the commit. Now I cannot
make it work either. JTL cannot find any of its plugins. Even reverting
back to the point where I committed doesn't help either. I also cannot run
tests in the IDE anymore, hence it is pretty difficult to debug for me. (I
have shared this problem in another thread for future reference.)

Please note that what broke release-2.x branch wasn't new functionality,
but new dummy TypeConverter classes in TypeConverterRegistryTest. I have
the feeling that there is something else wrong in the core and these new
test classes just make it surface. @Matt, given you are the plugin expert,
I presume, do you have any ideas?

On Tue, Apr 6, 2021 at 6:56 AM Ralph Goers <ra...@dslextreme.com>
wrote:

> OK, release-2.x now seems to be better but I don’t understand what is
> going on with Master. I am sure I built master several times over the
> weekend. But now it is getting errors all over the place in Json Template
> Layout tests. I did two commits that shouldn’t have impacted anything in
> that.
>
> My only concern when code like the registry changes is that all the
> existing unit tests continue to pass without having to change them (unless
> they are clearly buggy). We have enough tests that I am pretty confident
> that if they all pass nothing that is going to affect anything has changed.
>
> Ralph
>
> > On Apr 5, 2021, at 1:19 PM, Volkan Yazıcı <vo...@gmail.com>
> wrote:
> >
> > Hello Ralph,
> >
> > *TypeConverterRegistry changes*
> >
> > Sorry for breaking certain tests. I had used the following to test my
> > changes: `./mvnw clean install -pl
> > :log4j-core,:log4j-layout-template-json`. I have tested my changes after
> > purging Log4j `2.*-SNAPSHOT` artifacts from my `~/.m2` and there I indeed
> > observe the same errors reported in Jenkins:
> >
> > org.apache.logging.log4j.core.config.LoggersPluginTest.testEmptyAttribute
> >
> org.apache.logging.log4j.core.config.plugins.validation.validators.ValidatingPluginWithFailoverTest.testDoesNotLog_NoParameterThatMatchesElement_message
> >
> > (Apparently I have needed an extra bootstrap for annotations to kick in.)
> >
> > Though something interesting is going on here. If I revert my changes to
> > TypeConverterRegistry, *aforementioned tests still do fail*! It is
> actually
> > TypeConverterRegistryTest changes that is breaking those changes. I have
> > experimented with the following combinations:
> >
> > - reverted TypeConverterRegistry ⇨ LoggersPluginTest fails
> > - reverted TypeConverterRegistryTest ⇨ LoggersPluginTest succeeds
> > - `@Ignore`d added tests to TypeConverterRegistryTest ⇨ LoggersPluginTest
> > fails
> >
> > I have reverted the changes to TypeConverterRegistryTest, though the
> > TypeConverterRegistry changes are still in. I hope this would suffice to
> > make Jenkins and GitHub Actions happy.
> >
> > *RecyclerFactory customization support*
> >
> > In JsonTemplateLayout.Builder, I have a @PluginBuilderAttribute of type
> > RecyclerFactory. The input string is converted to a RecyclerFactory using
> > RecyclerFactoryConverter. To allow users to introduce their own
> > RecyclerFactory implementation in a backward-compatible way, I thought of
> > supporting multiple TypeConverter's in TypeConverterRegistry. The idea is
> > to enhance TypeConverterRegistry such that in the presence of multiple
> > TypeConverter's matching on the same footprint, TypeConverterRegistry
> will
> > use the Comparable interface to determine the effective one.
> >
> > I think it is a nice feature to support multiple TypeConverter's, as my
> use
> > case demonstrates. Do you agree? What else would you recommend me to
> > implement this customization support needed for
> > JsonTemplateLayout.Builder#recyclerFactory?
> >
> > Kind regards.
> >
> > On Sun, Apr 4, 2021 at 8:43 AM Ralph Goers <ra...@dslextreme.com>
> > wrote:
> >
> >> I am now looking at the Jenkins and GitHub Actions builds and it is
> clear
> >> that the change I highlighted below has caused at 3 tests to fail.
> Please
> >> revert the change to the TypeConverterRegistry. In the future, whenever
> a
> >> change is made to the API or core components please make sure you run a
> >> full build before you commit. This should be standard practice for
> everyone.
> >>
> >> Ralph
> >>
> >>> On Apr 3, 2021, at 1:27 PM, Ralph Goers <ra...@dslextreme.com>
> >> wrote:
> >>>
> >>> Volkan,
> >>>
> >>> I haven’t looked at the details of what you changed in
> >> JsonTemplateLayout yet but the below concerns me a bit….
> >>>
> >>> Ralph
> >>>
> >>>
> >>>> On Apr 3, 2021, at 3:48 AM, vy@apache.org wrote:
> >>>>
> >>>> This is an automated email from the ASF dual-hosted git repository.
> >>>>
> >>>
> >>> Why were changes to the registry required for this? Aren’t you just
> >> creating new Plugins?
> >>>
> >>>> 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));
> >>>
> >>> I don’t understand how replacing calling registerConverter with
> >> putIfAbsent is equivalent. The registerConverterMethod seems to allow
> >> plugins to be overridden.
> >>>
> >>>> -                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;
> >>>>       }
> >>>>   }
> >>>
> >>>
> >>>
> >>
> >>
> >>
>
>
>

Re: [logging-log4j2] branch release-2.x updated: LOG4J2-3004 Add plugin support to JsonTemplateLayout. (#476)

Posted by Ralph Goers <ra...@dslextreme.com>.
OK, release-2.x now seems to be better but I don’t understand what is going on with Master. I am sure I built master several times over the weekend. But now it is getting errors all over the place in Json Template Layout tests. I did two commits that shouldn’t have impacted anything in that.

My only concern when code like the registry changes is that all the existing unit tests continue to pass without having to change them (unless they are clearly buggy). We have enough tests that I am pretty confident that if they all pass nothing that is going to affect anything has changed.

Ralph

> On Apr 5, 2021, at 1:19 PM, Volkan Yazıcı <vo...@gmail.com> wrote:
> 
> Hello Ralph,
> 
> *TypeConverterRegistry changes*
> 
> Sorry for breaking certain tests. I had used the following to test my
> changes: `./mvnw clean install -pl
> :log4j-core,:log4j-layout-template-json`. I have tested my changes after
> purging Log4j `2.*-SNAPSHOT` artifacts from my `~/.m2` and there I indeed
> observe the same errors reported in Jenkins:
> 
> org.apache.logging.log4j.core.config.LoggersPluginTest.testEmptyAttribute
> org.apache.logging.log4j.core.config.plugins.validation.validators.ValidatingPluginWithFailoverTest.testDoesNotLog_NoParameterThatMatchesElement_message
> 
> (Apparently I have needed an extra bootstrap for annotations to kick in.)
> 
> Though something interesting is going on here. If I revert my changes to
> TypeConverterRegistry, *aforementioned tests still do fail*! It is actually
> TypeConverterRegistryTest changes that is breaking those changes. I have
> experimented with the following combinations:
> 
> - reverted TypeConverterRegistry ⇨ LoggersPluginTest fails
> - reverted TypeConverterRegistryTest ⇨ LoggersPluginTest succeeds
> - `@Ignore`d added tests to TypeConverterRegistryTest ⇨ LoggersPluginTest
> fails
> 
> I have reverted the changes to TypeConverterRegistryTest, though the
> TypeConverterRegistry changes are still in. I hope this would suffice to
> make Jenkins and GitHub Actions happy.
> 
> *RecyclerFactory customization support*
> 
> In JsonTemplateLayout.Builder, I have a @PluginBuilderAttribute of type
> RecyclerFactory. The input string is converted to a RecyclerFactory using
> RecyclerFactoryConverter. To allow users to introduce their own
> RecyclerFactory implementation in a backward-compatible way, I thought of
> supporting multiple TypeConverter's in TypeConverterRegistry. The idea is
> to enhance TypeConverterRegistry such that in the presence of multiple
> TypeConverter's matching on the same footprint, TypeConverterRegistry will
> use the Comparable interface to determine the effective one.
> 
> I think it is a nice feature to support multiple TypeConverter's, as my use
> case demonstrates. Do you agree? What else would you recommend me to
> implement this customization support needed for
> JsonTemplateLayout.Builder#recyclerFactory?
> 
> Kind regards.
> 
> On Sun, Apr 4, 2021 at 8:43 AM Ralph Goers <ra...@dslextreme.com>
> wrote:
> 
>> I am now looking at the Jenkins and GitHub Actions builds and it is clear
>> that the change I highlighted below has caused at 3 tests to fail. Please
>> revert the change to the TypeConverterRegistry. In the future, whenever a
>> change is made to the API or core components please make sure you run a
>> full build before you commit. This should be standard practice for everyone.
>> 
>> Ralph
>> 
>>> On Apr 3, 2021, at 1:27 PM, Ralph Goers <ra...@dslextreme.com>
>> wrote:
>>> 
>>> Volkan,
>>> 
>>> I haven’t looked at the details of what you changed in
>> JsonTemplateLayout yet but the below concerns me a bit….
>>> 
>>> Ralph
>>> 
>>> 
>>>> On Apr 3, 2021, at 3:48 AM, vy@apache.org wrote:
>>>> 
>>>> This is an automated email from the ASF dual-hosted git repository.
>>>> 
>>> 
>>> Why were changes to the registry required for this? Aren’t you just
>> creating new Plugins?
>>> 
>>>> 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));
>>> 
>>> I don’t understand how replacing calling registerConverter with
>> putIfAbsent is equivalent. The registerConverterMethod seems to allow
>> plugins to be overridden.
>>> 
>>>> -                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;
>>>>       }
>>>>   }
>>> 
>>> 
>>> 
>> 
>> 
>> 



Re: [logging-log4j2] branch release-2.x updated: LOG4J2-3004 Add plugin support to JsonTemplateLayout. (#476)

Posted by Matt Sicker <bo...@gmail.com>.
TypeConverterRegistry updates should be fine, just note I had to
structure some of the code as it is as it can't rely on much else
being set up quite yet. Even with the DI-rewrite, I think this area
would still have limited ability to inject any configuration state as
it's used for parsing and injecting the configuration in the first
place. They're in their own plugin category, so it's independent of
configuration elements anyways.

On Mon, 5 Apr 2021 at 15:19, Volkan Yazıcı <vo...@gmail.com> wrote:
>
> Hello Ralph,
>
> *TypeConverterRegistry changes*
>
> Sorry for breaking certain tests. I had used the following to test my
> changes: `./mvnw clean install -pl
> :log4j-core,:log4j-layout-template-json`. I have tested my changes after
> purging Log4j `2.*-SNAPSHOT` artifacts from my `~/.m2` and there I indeed
> observe the same errors reported in Jenkins:
>
> org.apache.logging.log4j.core.config.LoggersPluginTest.testEmptyAttribute
> org.apache.logging.log4j.core.config.plugins.validation.validators.ValidatingPluginWithFailoverTest.testDoesNotLog_NoParameterThatMatchesElement_message
>
> (Apparently I have needed an extra bootstrap for annotations to kick in.)
>
> Though something interesting is going on here. If I revert my changes to
> TypeConverterRegistry, *aforementioned tests still do fail*! It is actually
> TypeConverterRegistryTest changes that is breaking those changes. I have
> experimented with the following combinations:
>
> - reverted TypeConverterRegistry ⇨ LoggersPluginTest fails
> - reverted TypeConverterRegistryTest ⇨ LoggersPluginTest succeeds
> - `@Ignore`d added tests to TypeConverterRegistryTest ⇨ LoggersPluginTest
> fails
>
> I have reverted the changes to TypeConverterRegistryTest, though the
> TypeConverterRegistry changes are still in. I hope this would suffice to
> make Jenkins and GitHub Actions happy.
>
> *RecyclerFactory customization support*
>
> In JsonTemplateLayout.Builder, I have a @PluginBuilderAttribute of type
> RecyclerFactory. The input string is converted to a RecyclerFactory using
> RecyclerFactoryConverter. To allow users to introduce their own
> RecyclerFactory implementation in a backward-compatible way, I thought of
> supporting multiple TypeConverter's in TypeConverterRegistry. The idea is
> to enhance TypeConverterRegistry such that in the presence of multiple
> TypeConverter's matching on the same footprint, TypeConverterRegistry will
> use the Comparable interface to determine the effective one.
>
> I think it is a nice feature to support multiple TypeConverter's, as my use
> case demonstrates. Do you agree? What else would you recommend me to
> implement this customization support needed for
> JsonTemplateLayout.Builder#recyclerFactory?
>
> Kind regards.
>
> On Sun, Apr 4, 2021 at 8:43 AM Ralph Goers <ra...@dslextreme.com>
> wrote:
>
> > I am now looking at the Jenkins and GitHub Actions builds and it is clear
> > that the change I highlighted below has caused at 3 tests to fail. Please
> > revert the change to the TypeConverterRegistry. In the future, whenever a
> > change is made to the API or core components please make sure you run a
> > full build before you commit. This should be standard practice for everyone.
> >
> > Ralph
> >
> > > On Apr 3, 2021, at 1:27 PM, Ralph Goers <ra...@dslextreme.com>
> > wrote:
> > >
> > > Volkan,
> > >
> > > I haven’t looked at the details of what you changed in
> > JsonTemplateLayout yet but the below concerns me a bit….
> > >
> > > Ralph
> > >
> > >
> > >> On Apr 3, 2021, at 3:48 AM, vy@apache.org wrote:
> > >>
> > >> This is an automated email from the ASF dual-hosted git repository.
> > >>
> > >
> > > Why were changes to the registry required for this? Aren’t you just
> > creating new Plugins?
> > >
> > >> 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));
> > >
> > > I don’t understand how replacing calling registerConverter with
> > putIfAbsent is equivalent. The registerConverterMethod seems to allow
> > plugins to be overridden.
> > >
> > >> -                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;
> > >>        }
> > >>    }
> > >
> > >
> > >
> >
> >
> >

Re: [logging-log4j2] branch release-2.x updated: LOG4J2-3004 Add plugin support to JsonTemplateLayout. (#476)

Posted by Volkan Yazıcı <vo...@gmail.com>.
Hello Ralph,

*TypeConverterRegistry changes*

Sorry for breaking certain tests. I had used the following to test my
changes: `./mvnw clean install -pl
:log4j-core,:log4j-layout-template-json`. I have tested my changes after
purging Log4j `2.*-SNAPSHOT` artifacts from my `~/.m2` and there I indeed
observe the same errors reported in Jenkins:

org.apache.logging.log4j.core.config.LoggersPluginTest.testEmptyAttribute
org.apache.logging.log4j.core.config.plugins.validation.validators.ValidatingPluginWithFailoverTest.testDoesNotLog_NoParameterThatMatchesElement_message

(Apparently I have needed an extra bootstrap for annotations to kick in.)

Though something interesting is going on here. If I revert my changes to
TypeConverterRegistry, *aforementioned tests still do fail*! It is actually
TypeConverterRegistryTest changes that is breaking those changes. I have
experimented with the following combinations:

- reverted TypeConverterRegistry ⇨ LoggersPluginTest fails
- reverted TypeConverterRegistryTest ⇨ LoggersPluginTest succeeds
- `@Ignore`d added tests to TypeConverterRegistryTest ⇨ LoggersPluginTest
fails

I have reverted the changes to TypeConverterRegistryTest, though the
TypeConverterRegistry changes are still in. I hope this would suffice to
make Jenkins and GitHub Actions happy.

*RecyclerFactory customization support*

In JsonTemplateLayout.Builder, I have a @PluginBuilderAttribute of type
RecyclerFactory. The input string is converted to a RecyclerFactory using
RecyclerFactoryConverter. To allow users to introduce their own
RecyclerFactory implementation in a backward-compatible way, I thought of
supporting multiple TypeConverter's in TypeConverterRegistry. The idea is
to enhance TypeConverterRegistry such that in the presence of multiple
TypeConverter's matching on the same footprint, TypeConverterRegistry will
use the Comparable interface to determine the effective one.

I think it is a nice feature to support multiple TypeConverter's, as my use
case demonstrates. Do you agree? What else would you recommend me to
implement this customization support needed for
JsonTemplateLayout.Builder#recyclerFactory?

Kind regards.

On Sun, Apr 4, 2021 at 8:43 AM Ralph Goers <ra...@dslextreme.com>
wrote:

> I am now looking at the Jenkins and GitHub Actions builds and it is clear
> that the change I highlighted below has caused at 3 tests to fail. Please
> revert the change to the TypeConverterRegistry. In the future, whenever a
> change is made to the API or core components please make sure you run a
> full build before you commit. This should be standard practice for everyone.
>
> Ralph
>
> > On Apr 3, 2021, at 1:27 PM, Ralph Goers <ra...@dslextreme.com>
> wrote:
> >
> > Volkan,
> >
> > I haven’t looked at the details of what you changed in
> JsonTemplateLayout yet but the below concerns me a bit….
> >
> > Ralph
> >
> >
> >> On Apr 3, 2021, at 3:48 AM, vy@apache.org wrote:
> >>
> >> This is an automated email from the ASF dual-hosted git repository.
> >>
> >
> > Why were changes to the registry required for this? Aren’t you just
> creating new Plugins?
> >
> >> 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));
> >
> > I don’t understand how replacing calling registerConverter with
> putIfAbsent is equivalent. The registerConverterMethod seems to allow
> plugins to be overridden.
> >
> >> -                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;
> >>        }
> >>    }
> >
> >
> >
>
>
>

Re: [logging-log4j2] branch release-2.x updated: LOG4J2-3004 Add plugin support to JsonTemplateLayout. (#476)

Posted by Ralph Goers <ra...@dslextreme.com>.
I am now looking at the Jenkins and GitHub Actions builds and it is clear that the change I highlighted below has caused at 3 tests to fail. Please revert the change to the TypeConverterRegistry. In the future, whenever a change is made to the API or core components please make sure you run a full build before you commit. This should be standard practice for everyone.

Ralph

> On Apr 3, 2021, at 1:27 PM, Ralph Goers <ra...@dslextreme.com> wrote:
> 
> Volkan,
> 
> I haven’t looked at the details of what you changed in JsonTemplateLayout yet but the below concerns me a bit….
> 
> Ralph
> 
> 
>> On Apr 3, 2021, at 3:48 AM, vy@apache.org wrote:
>> 
>> This is an automated email from the ASF dual-hosted git repository.
>> 
> 
> Why were changes to the registry required for this? Aren’t you just creating new Plugins?
> 
>> 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));
> 
> I don’t understand how replacing calling registerConverter with putIfAbsent is equivalent. The registerConverterMethod seems to allow plugins to be overridden.
> 
>> -                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;
>>        }
>>    }
> 
> 
> 



Re: [logging-log4j2] branch release-2.x updated: LOG4J2-3004 Add plugin support to JsonTemplateLayout. (#476)

Posted by Ralph Goers <ra...@dslextreme.com>.
Volkan,

I haven’t looked at the details of what you changed in JsonTemplateLayout yet but the below concerns me a bit….

Ralph


> On Apr 3, 2021, at 3:48 AM, vy@apache.org wrote:
> 
> This is an automated email from the ASF dual-hosted git repository.
> 

Why were changes to the registry required for this? Aren’t you just creating new Plugins?

> 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));

I don’t understand how replacing calling registerConverter with putIfAbsent is equivalent. The registerConverterMethod seems to allow plugins to be overridden.

> -                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;
>         }
>     }