You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by vy...@apache.org on 2020/08/20 13:59:13 UTC

[logging-log4j2] 01/02: #335 Initial import of JsonTemplateLayout from LogstashLayout.

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

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

commit c11ed6f9feb68206bbf1cf689f2d85a1640b2fe3
Author: Volkan Yazıcı <vo...@gmail.com>
AuthorDate: Wed Aug 19 11:01:03 2020 +0200

    #335 Initial import of JsonTemplateLayout from LogstashLayout.
---
 .../org/apache/logging/log4j/util/Strings.java     |   36 +-
 .../org/apache/logging/log4j/util/StringsTest.java |   19 +
 log4j-bom/pom.xml                                  |    6 +
 .../apache/logging/log4j/core/util/Throwables.java |   22 +-
 .../logging/log4j/core/GcFreeLoggingTestUtil.java  |   70 +-
 .../logging/log4j/core/util/ThrowablesTest.java    |   37 +-
 log4j-jpl/pom.xml                                  |   12 -
 log4j-layout-json-template/pom.xml                 |  533 ++++++
 log4j-layout-json-template/revapi.json             |   16 +
 .../layout/json/template/JsonTemplateLayout.java   |  688 +++++++
 .../json/template/JsonTemplateLayoutDefaults.java  |  213 +++
 .../json/template/resolver/EndOfBatchResolver.java |   44 +
 .../resolver/EndOfBatchResolverFactory.java        |   41 +
 .../json/template/resolver/EventResolver.java      |   21 +
 .../template/resolver/EventResolverContext.java    |  228 +++
 .../template/resolver/EventResolverFactories.java  |   65 +
 .../template/resolver/EventResolverFactory.java    |   21 +
 .../resolver/ExceptionInternalResolverFactory.java |   68 +
 .../json/template/resolver/ExceptionResolver.java  |  122 ++
 .../resolver/ExceptionResolverFactory.java         |   43 +
 .../resolver/ExceptionRootCauseResolver.java       |  127 ++
 .../ExceptionRootCauseResolverFactory.java         |   41 +
 .../json/template/resolver/LevelResolver.java      |  176 ++
 .../template/resolver/LevelResolverFactory.java    |   41 +
 .../json/template/resolver/LoggerResolver.java     |   92 +
 .../template/resolver/LoggerResolverFactory.java   |   41 +
 .../json/template/resolver/MainMapResolver.java    |   90 +
 .../template/resolver/MainMapResolverFactory.java  |   41 +
 .../layout/json/template/resolver/MapResolver.java |   91 +
 .../json/template/resolver/MapResolverFactory.java |   41 +
 .../json/template/resolver/MarkerResolver.java     |   86 +
 .../template/resolver/MarkerResolverFactory.java   |   41 +
 .../json/template/resolver/MessageResolver.java    |  223 +++
 .../template/resolver/MessageResolverFactory.java  |   41 +
 .../json/template/resolver/PatternResolver.java    |   87 +
 .../template/resolver/PatternResolverFactory.java  |   41 +
 .../json/template/resolver/SourceResolver.java     |  148 ++
 .../template/resolver/SourceResolverFactory.java   |   41 +
 .../resolver/StackTraceElementObjectResolver.java  |   92 +
 .../StackTraceElementObjectResolverContext.java    |   93 +
 .../StackTraceElementObjectResolverFactories.java  |   39 +
 .../StackTraceElementObjectResolverFactory.java    |   43 +
 .../resolver/StackTraceObjectResolver.java         |   54 +
 .../json/template/resolver/StackTraceResolver.java |   19 +
 .../resolver/StackTraceStringResolver.java         |   51 +
 .../json/template/resolver/TemplateResolver.java   |   42 +
 .../template/resolver/TemplateResolverConfig.java  |   29 +
 .../template/resolver/TemplateResolverContext.java |   34 +
 .../template/resolver/TemplateResolverFactory.java |   25 +
 .../json/template/resolver/TemplateResolvers.java  |  414 +++++
 .../resolver/ThreadContextDataResolver.java        |  357 ++++
 .../resolver/ThreadContextDataResolverFactory.java |   43 +
 .../resolver/ThreadContextStackResolver.java       |  107 ++
 .../ThreadContextStackResolverFactory.java         |   43 +
 .../json/template/resolver/ThreadResolver.java     |   90 +
 .../template/resolver/ThreadResolverFactory.java   |   41 +
 .../json/template/resolver/TimestampResolver.java  |  505 ++++++
 .../resolver/TimestampResolverFactory.java         |   41 +
 .../layout/json/template/util/DummyRecycler.java   |   37 +
 .../json/template/util/DummyRecyclerFactory.java   |   39 +
 .../layout/json/template/util/JsonReader.java      |  447 +++++
 .../layout/json/template/util/JsonWriter.java      |  889 +++++++++
 .../layout/json/template/util/MapAccessor.java     |  139 ++
 .../json/template/util/QueueingRecycler.java       |   61 +
 .../template/util/QueueingRecyclerFactory.java     |   40 +
 .../log4j/layout/json/template/util/Recycler.java  |   25 +
 .../json/template/util/RecyclerFactories.java      |  205 +++
 .../layout/json/template/util/RecyclerFactory.java |   31 +
 .../json/template/util/StringParameterParser.java  |  292 +++
 .../json/template/util/ThreadLocalRecycler.java    |   45 +
 .../template/util/ThreadLocalRecyclerFactory.java  |   40 +
 .../util/TruncatingBufferedPrintWriter.java        |   60 +
 .../template/util/TruncatingBufferedWriter.java    |  208 +++
 .../log4j/layout/json/template/util/Uris.java      |  138 ++
 .../src/main/resources/EcsLayout.json              |   46 +
 .../src/main/resources/GelfLayout.json             |   41 +
 .../src/main/resources/JsonLayout.json             |   83 +
 .../main/resources/LogstashJsonEventLayoutV1.json  |   58 +
 .../main/resources/StackTraceElementLayout.json    |   18 +
 .../src/site/manual/index.md                       |   32 +
 log4j-layout-json-template/src/site/site.xml       |   55 +
 .../template/BlackHoleByteBufferDestination.java   |   50 +
 .../log4j/layout/json/template/EcsLayoutTest.java  |   90 +
 .../log4j/layout/json/template/GelfLayoutTest.java |  109 ++
 .../log4j/layout/json/template/JacksonFixture.java |   29 +
 .../log4j/layout/json/template/JsonLayoutTest.java |   71 +
 .../JsonTemplateLayoutConcurrentEncodeTest.java    |  192 ++
 .../template/JsonTemplateLayoutGcFreeTest.java     |   40 +
 .../JsonTemplateLayoutNullEventDelimiterTest.java  |  127 ++
 .../json/template/JsonTemplateLayoutTest.java      | 1889 ++++++++++++++++++++
 .../json/template/LayoutComparisonHelpers.java     |   19 +
 .../layout/json/template/LogEventFixture.java      |  151 ++
 .../log4j/layout/json/template/LogstashIT.java     |  503 ++++++
 .../layout/json/template/util/JsonReaderTest.java  |  380 ++++
 .../layout/json/template/util/JsonWriterTest.java  |  729 ++++++++
 .../json/template/util/RecyclerFactoriesTest.java  |  120 ++
 .../template/util/StringParameterParserTest.java   |  393 ++++
 .../util/TruncatingBufferedWriterTest.java         |  228 +++
 .../log4j/layout/json/template/util/UrisTest.java  |   65 +
 .../resources/gcFreeJsonTemplateLayoutLogging.xml  |   39 +
 ...nullEventDelimitedJsonTemplateLayoutLogging.xml |   39 +
 .../src/test/resources/testJsonTemplateLayout.json |   68 +
 log4j-perf/pom.xml                                 |   19 +-
 .../json/template/JsonTemplateLayoutBenchmark.java |  185 ++
 .../JsonTemplateLayoutBenchmarkReport.java         |  359 ++++
 .../template/JsonTemplateLayoutBenchmarkState.java |  212 +++
 .../log4j/perf/jmh/ThreadLocalVsPoolBenchmark.java |  252 ++-
 .../src/main/config-repo/log4j2.xml                |   47 +-
 pom.xml                                            |   39 +-
 src/site/asciidoc/manual/json-template-layout.adoc | 1198 +++++++++++++
 src/site/markdown/manual/cloud.md                  |  231 ++-
 src/site/site.xml                                  |    1 +
 src/site/xdoc/manual/garbagefree.xml               |    6 +
 src/site/xdoc/manual/json-template-layout.xml.vm   | 1526 ++++++++++++++++
 src/site/xdoc/manual/layouts.xml.vm                |  101 ++
 115 files changed, 18239 insertions(+), 253 deletions(-)

diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/util/Strings.java b/log4j-api/src/main/java/org/apache/logging/log4j/util/Strings.java
index e7b92a1..c5bdce1 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/util/Strings.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/util/Strings.java
@@ -50,14 +50,23 @@ public final class Strings {
     }
     
     /**
-     * Checks if a String is blank. A blank string is one that is {@code null}, empty, or when trimmed using
-     * {@link String#trim()} is empty.
+     * Checks if a String is blank. A blank string is one that is either
+     * {@code null}, empty, or all characters are {@link Character#isWhitespace(char)}.
      *
      * @param s the String to check, may be {@code null}
-     * @return {@code true} if the String is {@code null}, empty, or trims to empty.
+     * @return {@code true} if the String is {@code null}, empty, or or all characters are {@link Character#isWhitespace(char)}
      */
     public static boolean isBlank(final String s) {
-        return s == null || s.trim().isEmpty();
+        if (s == null || s.isEmpty()) {
+            return true;
+        }
+        for (int i = 0; i < s.length(); i++) {
+            char c = s.charAt(i);
+            if (!Character.isWhitespace(c)) {
+                return false;
+            }
+        }
+        return true;
     }
 
     /**
@@ -273,4 +282,23 @@ public final class Strings {
         return str.toUpperCase(Locale.ROOT);
     }
 
+    /**
+     * Creates a new string repeating given {@code str} {@code count} times.
+     * @param str input string
+     * @param count the repetition count
+     * @return the new string
+     * @throws IllegalArgumentException if either {@code str} is null or {@code count} is negative
+     */
+    public static String repeat(final String str, final int count) {
+        Objects.requireNonNull(str, "str");
+        if (count < 0) {
+            throw new IllegalArgumentException("count");
+        }
+        StringBuilder sb = new StringBuilder(str.length() * count);
+        for (int index = 0; index < count; index++) {
+            sb.append(str);
+        }
+        return sb.toString();
+    }
+
 }
diff --git a/log4j-api/src/test/java/org/apache/logging/log4j/util/StringsTest.java b/log4j-api/src/test/java/org/apache/logging/log4j/util/StringsTest.java
index fc928bc..8e606a3 100644
--- a/log4j-api/src/test/java/org/apache/logging/log4j/util/StringsTest.java
+++ b/log4j-api/src/test/java/org/apache/logging/log4j/util/StringsTest.java
@@ -25,6 +25,25 @@ import org.junit.Test;
 
 public class StringsTest {
 
+    @Test
+    public void testIsEmpty() {
+        Assert.assertTrue(Strings.isEmpty(null));
+        Assert.assertTrue(Strings.isEmpty(""));
+        Assert.assertFalse(Strings.isEmpty(" "));
+        Assert.assertFalse(Strings.isEmpty("a"));
+    }
+
+    @Test
+    public void testIsBlank() {
+        Assert.assertTrue(Strings.isBlank(null));
+        Assert.assertTrue(Strings.isBlank(""));
+        Assert.assertTrue(Strings.isBlank(" "));
+        Assert.assertTrue(Strings.isBlank("\n"));
+        Assert.assertTrue(Strings.isBlank("\r"));
+        Assert.assertTrue(Strings.isBlank("\t"));
+        Assert.assertFalse(Strings.isEmpty("a"));
+    }
+
     /**
      * A sanity test to make sure a typo does not mess up {@link Strings#EMPTY}.
      */
diff --git a/log4j-bom/pom.xml b/log4j-bom/pom.xml
index 547265d..f3d3e11 100644
--- a/log4j-bom/pom.xml
+++ b/log4j-bom/pom.xml
@@ -42,6 +42,12 @@
         <artifactId>log4j-core</artifactId>
         <version>${project.version}</version>
       </dependency>
+      <!-- JSON template layout -->
+      <dependency>
+        <groupId>org.apache.logging.log4j</groupId>
+        <artifactId>log4j-layout-json-template</artifactId>
+        <version>${project.version}</version>
+      </dependency>
       <!-- Legacy Log4j 1.2 API -->
       <dependency>
         <groupId>org.apache.logging.log4j</groupId>
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/Throwables.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/Throwables.java
index 0d56ef1..e6c758e 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/Throwables.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/Throwables.java
@@ -40,12 +40,26 @@ public final class Throwables {
      * @return the deepest throwable or the given throwable
      */
     public static Throwable getRootCause(final Throwable throwable) {
+
+        // Keep a second pointer that slowly walks the causal chain. If the fast
+        // pointer ever catches the slower pointer, then there's a loop.
+        Throwable slowPointer = throwable;
+        boolean advanceSlowPointer = false;
+
+        Throwable parent = throwable;
         Throwable cause;
-        Throwable root = throwable;
-        while ((cause = root.getCause()) != null) {
-            root = cause;
+        while ((cause = parent.getCause()) != null) {
+            parent = cause;
+            if (parent == slowPointer) {
+                throw new IllegalArgumentException("loop in causal chain");
+            }
+            if (advanceSlowPointer) {
+                slowPointer = slowPointer.getCause();
+            }
+            advanceSlowPointer = !advanceSlowPointer; // only advance every other iteration
         }
-        return root;
+        return parent;
+
     }
 
     /**
diff --git a/log4j-core/src/test/java/org/apache/logging/log4j/core/GcFreeLoggingTestUtil.java b/log4j-core/src/test/java/org/apache/logging/log4j/core/GcFreeLoggingTestUtil.java
index 69ffe2c..26ea145 100644
--- a/log4j-core/src/test/java/org/apache/logging/log4j/core/GcFreeLoggingTestUtil.java
+++ b/log4j-core/src/test/java/org/apache/logging/log4j/core/GcFreeLoggingTestUtil.java
@@ -16,35 +16,33 @@
  */
 package org.apache.logging.log4j.core;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
-import java.io.File;
-import java.net.URL;
-import java.nio.charset.Charset;
-import java.nio.file.Files;
-import java.util.List;
-import java.util.concurrent.atomic.AtomicBoolean;
-
+import com.google.monitoring.runtime.instrumentation.AllocationRecorder;
+import com.google.monitoring.runtime.instrumentation.Sampler;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Marker;
 import org.apache.logging.log4j.MarkerManager;
 import org.apache.logging.log4j.ThreadContext;
 import org.apache.logging.log4j.core.util.Constants;
 import org.apache.logging.log4j.message.StringMapMessage;
-import org.apache.logging.log4j.util.Strings;
 
-import com.google.monitoring.runtime.instrumentation.AllocationRecorder;
-import com.google.monitoring.runtime.instrumentation.Sampler;
+import java.io.File;
+import java.net.URL;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.regex.Pattern;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 
 /**
- * Utily methods for the GC-free logging tests.s.
+ * Utility methods for the GC-free logging tests.
  */
-public class GcFreeLoggingTestUtil {
+public enum GcFreeLoggingTestUtil {;
 
     public static void executeLogging(final String configurationFile,
-            final Class<?> testClass) throws Exception {
+                                      final Class<?> testClass) throws Exception {
 
         System.setProperty("log4j2.enable.threadlocals", "true");
         System.setProperty("log4j2.enable.direct.encoders", "true");
@@ -148,16 +146,36 @@ public class GcFreeLoggingTestUtil {
         process.waitFor();
         process.exitValue();
 
-        final String text = new String(Files.readAllBytes(tempFile.toPath()));
-        final List<String> lines = Files.readAllLines(tempFile.toPath(), Charset.defaultCharset());
-        final String className = cls.getSimpleName();
-        assertEquals(text, "FATAL o.a.l.l.c." + className + " [main] value1 {aKey=value1, key2=value2, prop1=value1, prop2=value2} This message is logged to the console",
-                lines.get(0));
+        final AtomicInteger lineCounter = new AtomicInteger(0);
+        Files.lines(tempFile.toPath(), Charset.defaultCharset()).forEach(line -> {
+
+            // Trim the line.
+            line = line.trim();
+
+            // Check the first line.
+            final int lineNumber = lineCounter.incrementAndGet();
+            if (lineNumber == 1) {
+                final String className = cls.getSimpleName();
+                final String firstLinePattern = String.format(
+                        "^FATAL .*\\.%s %s",
+                        className,
+                        Pattern.quote("[main] value1 {aKey=value1, " +
+                                "key2=value2, prop1=value1, prop2=value2} " +
+                                "This message is logged to the console"));
+                assertTrue(
+                        "pattern mismatch at line 1: " + line,
+                        line.matches(firstLinePattern));
+            }
+
+            // Check the rest of the lines.
+            else {
+                assertFalse(
+                        "(allocated|array) pattern matches at line " + lineNumber + ": " + line,
+                        line.contains("allocated") || line.contains("array"));
+            }
+
+        });
 
-        for (int i = 1; i < lines.size(); i++) {
-            final String line = lines.get(i);
-            assertFalse(i + ": " + line + Strings.LINE_SEPARATOR + text, line.contains("allocated") || line.contains("array"));
-        }
     }
 
     private static File agentJar() {
diff --git a/log4j-core/src/test/java/org/apache/logging/log4j/core/util/ThrowablesTest.java b/log4j-core/src/test/java/org/apache/logging/log4j/core/util/ThrowablesTest.java
index e8f82f5..7e354bc 100644
--- a/log4j-core/src/test/java/org/apache/logging/log4j/core/util/ThrowablesTest.java
+++ b/log4j-core/src/test/java/org/apache/logging/log4j/core/util/ThrowablesTest.java
@@ -16,41 +16,54 @@
  */
 package org.apache.logging.log4j.core.util;
 
+import org.junit.Assert;
 import org.junit.Test;
 
 public class ThrowablesTest {
 
     @Test
-    public void testGetRootCauseNone() throws Exception {
+    public void testGetRootCauseNone() {
         final NullPointerException throwable = new NullPointerException();
-        org.junit.Assert.assertEquals(throwable, Throwables.getRootCause(throwable));
+        Assert.assertEquals(throwable, Throwables.getRootCause(throwable));
     }
 
     @Test
-    public void testGetRootCauseDepth1() throws Exception {
-        final NullPointerException throwable = new NullPointerException();
-        org.junit.Assert.assertEquals(throwable, Throwables.getRootCause(new UnsupportedOperationException(throwable)));
+    public void testGetRootCauseDepth1() {
+        final Throwable cause = new NullPointerException();
+        final Throwable error = new UnsupportedOperationException(cause);
+        Assert.assertEquals(cause, Throwables.getRootCause(error));
     }
 
     @Test
-    public void testGetRootCauseDepth2() throws Exception {
-        final NullPointerException throwable = new NullPointerException();
-        org.junit.Assert.assertEquals(throwable,
-                Throwables.getRootCause(new IllegalArgumentException(new UnsupportedOperationException(throwable))));
+    public void testGetRootCauseDepth2() {
+        final Throwable rootCause = new NullPointerException();
+        final Throwable cause = new UnsupportedOperationException(rootCause);
+        final Throwable error = new IllegalArgumentException(cause);
+        Assert.assertEquals(rootCause, Throwables.getRootCause(error));
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testGetRootCauseLoop() {
+        final Throwable cause1 = new RuntimeException();
+        final Throwable cause2 = new RuntimeException(cause1);
+        final Throwable cause3 = new RuntimeException(cause2);
+        cause1.initCause(cause3);
+        // noinspection ThrowableNotThrown
+        Throwables.getRootCause(cause3);
     }
 
     @Test(expected = NullPointerException.class)
-    public void testRethrowRuntimeException() throws Exception {
+    public void testRethrowRuntimeException() {
         Throwables.rethrow(new NullPointerException());
     }
 
     @Test(expected = UnknownError.class)
-    public void testRethrowError() throws Exception {
+    public void testRethrowError() {
         Throwables.rethrow(new UnknownError());
     }
 
     @Test(expected = NoSuchMethodException.class)
-    public void testRethrowCheckedException() throws Exception {
+    public void testRethrowCheckedException() {
         Throwables.rethrow(new NoSuchMethodException());
     }
 }
diff --git a/log4j-jpl/pom.xml b/log4j-jpl/pom.xml
index c589fc0..ca45baf 100644
--- a/log4j-jpl/pom.xml
+++ b/log4j-jpl/pom.xml
@@ -261,18 +261,6 @@
       <reporting>
         <plugins>
           <plugin>
-            <!-- spotbugs is not compatible with toolchain and needs same JDK than one use to compile -->
-            <groupId>com.github.spotbugs</groupId>
-            <artifactId>spotbugs-maven-plugin</artifactId>
-            <configuration>
-              <fork>true</fork>
-              <jvmArgs>-Duser.language=en</jvmArgs>
-              <threshold>Normal</threshold>
-              <effort>Default</effort>
-              <excludeFilterFile>${log4jParentDir}/spotbugs-exclude-filter.xml</excludeFilterFile>
-            </configuration>
-          </plugin>
-          <plugin>
             <!-- pmd is not compatible with toolchain and needs same JDK than one use to compile -->
             <groupId>org.apache.maven.plugins</groupId>
             <artifactId>maven-pmd-plugin</artifactId>
diff --git a/log4j-layout-json-template/pom.xml b/log4j-layout-json-template/pom.xml
new file mode 100644
index 0000000..e060b94
--- /dev/null
+++ b/log4j-layout-json-template/pom.xml
@@ -0,0 +1,533 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.logging.log4j</groupId>
+    <artifactId>log4j</artifactId>
+    <version>2.14.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>log4j-layout-json-template</artifactId>
+  <name>Apache Log4j Layout for JSON template</name>
+  <description>
+    Apache Log4j Layout for JSON template.
+  </description>
+
+  <properties>
+    <log4jParentDir>${basedir}/..</log4jParentDir>
+    <docLabel>Log4j Layout for JSON Template Documentation</docLabel>
+    <projectDir>/log4j-layout-json-template</projectDir>
+    <module.name>org.apache.logging.log4j.layout.json.template</module.name>
+  </properties>
+
+  <dependencies>
+
+    <dependency>
+      <groupId>org.apache.logging.log4j</groupId>
+      <artifactId>log4j-core</artifactId>
+    </dependency>
+
+    <dependency>
+      <groupId>org.apache.logging.log4j</groupId>
+      <artifactId>log4j-core</artifactId>
+      <version>${project.version}</version>
+      <type>test-jar</type>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.jctools</groupId>
+      <artifactId>jctools-core</artifactId>
+      <optional>true</optional>
+    </dependency>
+
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.assertj</groupId>
+      <artifactId>assertj-core</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>com.fasterxml.jackson.core</groupId>
+      <artifactId>jackson-databind</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>co.elastic.logging</groupId>
+      <artifactId>log4j2-ecs-layout</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>com.google.code.java-allocation-instrumenter</groupId>
+      <artifactId>java-allocation-instrumenter</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.elasticsearch.client</groupId>
+      <artifactId>elasticsearch-rest-high-level-client</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.awaitility</groupId>
+      <artifactId>awaitility</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+  </dependencies>
+
+  <build>
+    <plugins>
+
+      <plugin>
+        <groupId>org.apache.felix</groupId>
+        <artifactId>maven-bundle-plugin</artifactId>
+        <configuration>
+          <instructions>
+            <Fragment-Host>org.apache.logging.log4j.layout.json.template</Fragment-Host>
+            <Export-Package>*</Export-Package>
+          </instructions>
+        </configuration>
+      </plugin>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-jar-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>default-jar</id>
+            <goals>
+              <goal>jar</goal>
+            </goals>
+            <configuration combine.self="override">
+              <archive>
+                <manifestFile>${manifestfile}</manifestFile>
+                <manifestEntries>
+                  <Specification-Title>${project.name}</Specification-Title>
+                  <Specification-Version>${project.version}</Specification-Version>
+                  <Specification-Vendor>${project.organization.name}</Specification-Vendor>
+                  <Implementation-Title>${project.name}</Implementation-Title>
+                  <Implementation-Version>${project.version}</Implementation-Version>
+                  <Implementation-Vendor>${project.organization.name}</Implementation-Vendor>
+                  <Implementation-Vendor-Id>org.apache</Implementation-Vendor-Id>
+                  <X-Compile-Source-JDK>${maven.compiler.source}</X-Compile-Source-JDK>
+                  <X-Compile-Target-JDK>${maven.compiler.target}</X-Compile-Target-JDK>
+                  <Multi-Release>true</Multi-Release>
+                </manifestEntries>
+              </archive>
+            </configuration>
+          </execution>
+          <execution>
+            <id>default</id>
+            <goals>
+              <goal>test-jar</goal>
+            </goals>
+            <configuration>
+              <archive>
+                <manifestFile>${manifestfile}</manifestFile>
+                <manifestEntries>
+                  <Specification-Title>${project.name}</Specification-Title>
+                  <Specification-Version>${project.version}</Specification-Version>
+                  <Specification-Vendor>${project.organization.name}</Specification-Vendor>
+                  <Implementation-Title>${project.name}</Implementation-Title>
+                  <Implementation-Version>${project.version}</Implementation-Version>
+                  <Implementation-Vendor>${project.organization.name}</Implementation-Vendor>
+                  <Implementation-Vendor-Id>org.apache</Implementation-Vendor-Id>
+                  <X-Compile-Source-JDK>${maven.compiler.source}</X-Compile-Source-JDK>
+                  <X-Compile-Target-JDK>${maven.compiler.target}</X-Compile-Target-JDK>
+                </manifestEntries>
+              </archive>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-surefire-plugin</artifactId>
+        <configuration>
+          <skip>${maven.test.skip}</skip>
+          <excludes>
+            <exclude>**/JsonTemplateLayoutConcurrentEncodeTest.java</exclude>
+            <exclude>**/JsonTemplateLayoutTest.java</exclude>
+          </excludes>
+          <!-- Enforcing a non-UTF-8 encoding to check that the layout
+               indeed handles everything in UTF-8 without implicitly
+               relying on the system defaults. -->
+          <argLine>-Dfile.encoding=US-ASCII</argLine>
+        </configuration>
+        <executions>
+          <!-- Dummy recycler execution -->
+          <execution>
+            <id>recycler-dummy</id>
+            <goals>
+              <goal>test</goal>
+            </goals>
+            <configuration>
+              <skip>${skipTests}</skip>
+              <systemPropertyVariables>
+                <log4j2.layout.jsonTemplate.recyclerFactory>threadLocal</log4j2.layout.jsonTemplate.recyclerFactory>
+              </systemPropertyVariables>
+              <includes>
+                <include>**/JsonTemplateLayoutConcurrentEncodeTest.java</include>
+                <include>**/JsonTemplateLayoutTest.java</include>
+              </includes>
+            </configuration>
+          </execution>
+          <!-- Thread-Local recycler execution -->
+          <execution>
+            <id>recycler-tl</id>
+            <goals>
+              <goal>test</goal>
+            </goals>
+            <configuration>
+              <skip>${skipTests}</skip>
+              <systemPropertyVariables>
+                <log4j2.layout.jsonTemplate.recyclerFactory>threadLocal</log4j2.layout.jsonTemplate.recyclerFactory>
+              </systemPropertyVariables>
+              <includes>
+                <include>**/JsonTemplateLayoutConcurrentEncodeTest.java</include>
+                <include>**/JsonTemplateLayoutTest.java</include>
+              </includes>
+            </configuration>
+          </execution>
+          <!-- ArrayBlockingQueue recycler execution -->
+          <execution>
+            <id>recycler-abq</id>
+            <goals>
+              <goal>test</goal>
+            </goals>
+            <configuration>
+              <skip>${skipTests}</skip>
+              <systemPropertyVariables>
+                <log4j2.layout.jsonTemplate.recyclerFactory>queue:supplier=java.util.concurrent.ArrayBlockingQueue.new</log4j2.layout.jsonTemplate.recyclerFactory>
+              </systemPropertyVariables>
+              <includes>
+                <include>**/JsonTemplateLayoutConcurrentEncodeTest.java</include>
+                <include>**/JsonTemplateLayoutTest.java</include>
+              </includes>
+            </configuration>
+          </execution>
+          <!-- MpmcArrayQueue recycler execution -->
+          <execution>
+            <id>recycler-mpmc</id>
+            <goals>
+              <goal>test</goal>
+            </goals>
+            <configuration>
+              <skip>${skipTests}</skip>
+              <systemPropertyVariables>
+                <log4j2.layout.jsonTemplate.recyclerFactory>queue:supplier=org.jctools.queues.MpmcArrayQueue.new</log4j2.layout.jsonTemplate.recyclerFactory>
+              </systemPropertyVariables>
+              <includes>
+                <include>**/JsonTemplateLayoutConcurrentEncodeTest.java</include>
+                <include>**/JsonTemplateLayoutTest.java</include>
+              </includes>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+
+      <!-- Disable ITs, which are Docker-dependent, by default. -->
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-failsafe-plugin</artifactId>
+        <executions>
+          <execution>
+            <goals>
+              <goal>integration-test</goal>
+              <goal>verify</goal>
+            </goals>
+          </execution>
+        </executions>
+        <configuration>
+          <skip>true</skip>
+        </configuration>
+      </plugin>
+
+    </plugins>
+  </build>
+
+  <profiles>
+    <profile>
+      <id>docker</id>
+      <activation>
+        <activeByDefault>false</activeByDefault>
+      </activation>
+      <build>
+        <plugins>
+
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-failsafe-plugin</artifactId>
+            <executions>
+              <execution>
+                <goals>
+                  <goal>integration-test</goal>
+                  <goal>verify</goal>
+                </goals>
+              </execution>
+            </executions>
+            <configuration>
+              <skip>${skipTests}</skip>
+              <includes>
+                <include>**/*IT.java</include>
+              </includes>
+            </configuration>
+          </plugin>
+
+          <plugin>
+            <groupId>io.fabric8</groupId>
+            <artifactId>docker-maven-plugin</artifactId>
+            <executions>
+              <execution>
+                <id>start</id>
+                <phase>pre-integration-test</phase>
+                <goals>
+                  <goal>start</goal>
+                </goals>
+              </execution>
+              <execution>
+                <id>stop</id>
+                <phase>post-integration-test</phase>
+                <goals>
+                  <goal>stop</goal>
+                </goals>
+              </execution>
+            </executions>
+            <configuration>
+              <verbose>all</verbose>
+              <startParallel>true</startParallel>
+              <autoCreateCustomNetworks>true</autoCreateCustomNetworks>
+              <images>
+                <image>
+                  <alias>elasticsearch</alias>
+                  <name>elasticsearch:${elastic.version}</name>
+                  <run>
+                    <env>
+                      <discovery.type>single-node</discovery.type>
+                    </env>
+                    <ports>
+                      <port>9200:9200</port>
+                    </ports>
+                    <network>
+                      <mode>custom</mode>
+                      <name>log4j-layout-json-template-network</name>
+                      <alias>elasticsearch</alias>
+                    </network>
+                    <log>
+                      <prefix>[ES]</prefix>
+                      <color>cyan</color>
+                    </log>
+                    <wait>
+                      <log>recovered \[0\] indices into cluster_state</log>
+                      <time>60000</time>
+                    </wait>
+                  </run>
+                </image>
+                <image>
+                  <alias>logstash</alias>
+                  <name>logstash:${elastic.version}</name>
+                  <run>
+                    <dependsOn>
+                      <container>elasticsearch</container>
+                    </dependsOn>
+                    <network>
+                      <mode>custom</mode>
+                      <name>log4j-layout-json-template-network</name>
+                      <alias>logstash</alias>
+                    </network>
+                    <ports>
+                      <port>12222:12222</port>
+                      <port>12345:12345</port>
+                    </ports>
+                    <log>
+                      <prefix>[LS]</prefix>
+                      <color>green</color>
+                    </log>
+                    <entrypoint>
+                      <exec>
+                        <arg>logstash</arg>
+                        <arg>--pipeline.batch.size</arg>
+                        <arg>1</arg>
+                        <arg>-e</arg>
+                        <arg>
+                          input {
+                            gelf {
+                              host => "logstash"
+                              use_tcp => true
+                              use_udp => false
+                              port => 12222
+                              type => "gelf"
+                            }
+                            tcp {
+                              port => 12345
+                              codec => json
+                              type => "tcp"
+                            }
+                          }
+
+                          filter {
+                            if [type] == "gelf" {
+                              # These are GELF/Syslog logging levels as defined in RFC 3164.
+                              # Map the integer level to its human readable format.
+                              translate {
+                                field => "[level]"
+                                destination => "[levelName]"
+                                dictionary => {
+                                  "0" => "EMERG"
+                                  "1" => "ALERT"
+                                  "2" => "CRITICAL"
+                                  "3" => "ERROR"
+                                  "4" => "WARN"
+                                  "5" => "NOTICE"
+                                  "6" => "INFO"
+                                  "7" => "DEBUG"
+                                }
+                              }
+                            }
+                          }
+
+                          output {
+                            # (Un)comment for debugging purposes
+                            # stdout { codec => rubydebug }
+                            elasticsearch {
+                              hosts => ["http://elasticsearch:9200"]
+                              index => "log4j"
+                            }
+                          }
+                        </arg>
+                      </exec>
+                    </entrypoint>
+                    <wait>
+                      <log>Successfully started Logstash API endpoint</log>
+                      <time>60000</time>
+                    </wait>
+                  </run>
+                </image>
+              </images>
+            </configuration>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+  </profiles>
+
+  <reporting>
+    <plugins>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-changes-plugin</artifactId>
+        <version>${changes.plugin.version}</version>
+        <reportSets>
+          <reportSet>
+            <reports>
+              <report>changes-report</report>
+            </reports>
+          </reportSet>
+        </reportSets>
+        <configuration>
+          <issueLinkTemplate>%URL%/show_bug.cgi?id=%ISSUE%</issueLinkTemplate>
+          <useJql>true</useJql>
+        </configuration>
+      </plugin>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-checkstyle-plugin</artifactId>
+        <version>${checkstyle.plugin.version}</version>
+        <configuration>
+          <!--<propertiesLocation>${vfs.parent.dir}/checkstyle.properties</propertiesLocation> -->
+          <configLocation>${log4jParentDir}/checkstyle.xml</configLocation>
+          <suppressionsLocation>${log4jParentDir}/checkstyle-suppressions.xml</suppressionsLocation>
+          <enableRulesSummary>false</enableRulesSummary>
+          <propertyExpansion>basedir=${basedir}</propertyExpansion>
+          <propertyExpansion>licensedir=${log4jParentDir}/checkstyle-header.txt</propertyExpansion>
+        </configuration>
+      </plugin>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-javadoc-plugin</artifactId>
+        <version>${javadoc.plugin.version}</version>
+        <configuration>
+          <bottom><![CDATA[<p align="center">Copyright &#169; {inceptionYear}-{currentYear} {organizationName}. All Rights Reserved.<br />
+            Apache Logging, Apache Log4j, Log4j, Apache, the Apache feather logo, the Apache Logging project logo,
+            and the Apache Log4j logo are trademarks of The Apache Software Foundation.</p>]]></bottom>
+          <!-- module link generation is completely broken in the javadoc plugin for a multi-module non-aggregating project -->
+          <detectOfflineLinks>false</detectOfflineLinks>
+          <linksource>true</linksource>
+        </configuration>
+        <reportSets>
+          <reportSet>
+            <id>non-aggregate</id>
+            <reports>
+              <report>javadoc</report>
+            </reports>
+          </reportSet>
+        </reportSets>
+      </plugin>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-jxr-plugin</artifactId>
+        <version>${jxr.plugin.version}</version>
+        <reportSets>
+          <reportSet>
+            <id>non-aggregate</id>
+            <reports>
+              <report>jxr</report>
+            </reports>
+          </reportSet>
+          <reportSet>
+            <id>aggregate</id>
+            <reports>
+              <report>aggregate</report>
+            </reports>
+          </reportSet>
+        </reportSets>
+      </plugin>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-pmd-plugin</artifactId>
+        <version>${pmd.plugin.version}</version>
+        <configuration>
+          <targetJdk>${maven.compiler.target}</targetJdk>
+        </configuration>
+      </plugin>
+
+    </plugins>
+  </reporting>
+
+</project>
diff --git a/log4j-layout-json-template/revapi.json b/log4j-layout-json-template/revapi.json
new file mode 100644
index 0000000..cf1bfdf
--- /dev/null
+++ b/log4j-layout-json-template/revapi.json
@@ -0,0 +1,16 @@
+[
+  {
+    "extension": "revapi.java",
+    "configuration": {
+      "filter": {
+        "classes": {
+          "exclude": [
+            "org\\.apache\\.logging\\.log4j\\.layout\\.json\\.template\\.resolver\\.TemplateResolverConfig",
+            "org\\.apache\\.logging\\.log4j\\.layout\\.json\\.template\\.resolver\\.TemplateResolverContext",
+            "org\\.apache\\.logging\\.log4j\\.layout\\.json\\.template\\.resolver\\.TemplateResolverFactory"
+          ]
+        }
+      }
+    }
+  }
+]
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayout.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayout.java
new file mode 100644
index 0000000..0f3632a
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayout.java
@@ -0,0 +1,688 @@
+/*
+ * 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.json.template;
+
+import org.apache.logging.log4j.core.Layout;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.StringLayout;
+import org.apache.logging.log4j.core.config.Configuration;
+import org.apache.logging.log4j.core.config.Node;
+import org.apache.logging.log4j.core.config.plugins.Plugin;
+import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute;
+import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
+import org.apache.logging.log4j.core.config.plugins.PluginConfiguration;
+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.json.template.resolver.EventResolverContext;
+import org.apache.logging.log4j.layout.json.template.resolver.StackTraceElementObjectResolverContext;
+import org.apache.logging.log4j.layout.json.template.resolver.TemplateResolver;
+import org.apache.logging.log4j.layout.json.template.resolver.TemplateResolvers;
+import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+import org.apache.logging.log4j.layout.json.template.util.Recycler;
+import org.apache.logging.log4j.layout.json.template.util.RecyclerFactory;
+import org.apache.logging.log4j.layout.json.template.util.Uris;
+import org.apache.logging.log4j.util.Strings;
+
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Supplier;
+
+@Plugin(name = "JsonTemplateLayout",
+        category = Node.CATEGORY,
+        elementType = Layout.ELEMENT_TYPE)
+public class JsonTemplateLayout implements StringLayout {
+
+    private static final Map<String, String> CONTENT_FORMAT =
+            Collections.singletonMap("version", "1");
+
+    private final Charset charset;
+
+    private final String contentType;
+
+    private final TemplateResolver<LogEvent> eventResolver;
+
+    private final String eventDelimiter;
+
+    private final Recycler<Context> contextRecycler;
+
+    // The class and fields are visible for tests.
+    static final class Context implements AutoCloseable {
+
+        final JsonWriter jsonWriter;
+
+        final Encoder<StringBuilder> encoder;
+
+        private Context(
+                final JsonWriter jsonWriter,
+                final Encoder<StringBuilder> encoder) {
+            this.jsonWriter = jsonWriter;
+            this.encoder = encoder;
+        }
+
+        @Override
+        public void close() {
+            jsonWriter.close();
+        }
+
+    }
+
+    private JsonTemplateLayout(final Builder builder) {
+        this.charset = builder.charset;
+        this.contentType = "application/json; charset=" + charset;
+        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);
+        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 String eventTemplate = readEventTemplate(builder);
+        final float maxByteCountPerChar = builder.charset.newEncoder().maxBytesPerChar();
+        final int maxStringByteCount =
+                Math.toIntExact(Math.round(
+                        maxByteCountPerChar * builder.maxStringLength));
+        final EventResolverContext resolverContext = EventResolverContext
+                .newBuilder()
+                .setConfiguration(configuration)
+                .setSubstitutor(substitutor)
+                .setCharset(charset)
+                .setJsonWriter(jsonWriter)
+                .setRecyclerFactory(builder.recyclerFactory)
+                .setMaxStringByteCount(maxStringByteCount)
+                .setLocationInfoEnabled(builder.locationInfoEnabled)
+                .setStackTraceEnabled(builder.stackTraceEnabled)
+                .setStackTraceElementObjectResolver(stackTraceElementObjectResolver)
+                .setEventTemplateAdditionalFields(builder.eventTemplateAdditionalFields.additionalFields)
+                .build();
+        return TemplateResolvers.ofTemplate(resolverContext, eventTemplate);
+    }
+
+    private static String readEventTemplate(final Builder builder) {
+        return readTemplate(
+                builder.eventTemplate,
+                builder.eventTemplateUri,
+                builder.charset);
+    }
+
+    private static String readStackTraceElementTemplate(final Builder builder) {
+        return readTemplate(
+                builder.stackTraceElementTemplate,
+                builder.stackTraceElementTemplateUri,
+                builder.charset);
+    }
+
+    private static String readTemplate(
+            final String template,
+            final String templateUri,
+            final Charset charset) {
+        return Strings.isBlank(template)
+                ? Uris.readUri(templateUri, charset)
+                : template;
+    }
+
+    private static Recycler<Context> createContextRecycler(
+            final Builder builder,
+            final JsonWriter jsonWriter) {
+        final Supplier<Context> supplier =
+                createContextSupplier(builder.charset, jsonWriter);
+        return builder
+                .recyclerFactory
+                .create(supplier, Context::close);
+    }
+
+    private static Supplier<Context> createContextSupplier(
+            final Charset charset,
+            final JsonWriter jsonWriter) {
+        return () -> {
+            final JsonWriter clonedJsonWriter = jsonWriter.clone();
+            final Encoder<StringBuilder> encoder =
+                    Constants.ENABLE_DIRECT_ENCODERS
+                            ? new LockingStringBuilderEncoder(charset)
+                            : null;
+            return new Context(clonedJsonWriter, encoder);
+        };
+    }
+
+    @Override
+    public byte[] toByteArray(final LogEvent event) {
+        final String eventJson = toSerializable(event);
+        return StringEncoder.toBytes(eventJson, charset);
+    }
+
+    @Override
+    public String toSerializable(final LogEvent event) {
+        final Context context = acquireContext();
+        final JsonWriter jsonWriter = context.jsonWriter;
+        final StringBuilder stringBuilder = jsonWriter.getStringBuilder();
+        try {
+            eventResolver.resolve(event, jsonWriter);
+            stringBuilder.append(eventDelimiter);
+            return stringBuilder.toString();
+        } finally {
+            contextRecycler.release(context);
+        }
+    }
+
+    @Override
+    public void encode(final LogEvent event, final ByteBufferDestination destination) {
+
+        // Acquire a context.
+        final Context context = acquireContext();
+        final JsonWriter jsonWriter = context.jsonWriter;
+        final StringBuilder stringBuilder = jsonWriter.getStringBuilder();
+        final Encoder<StringBuilder> encoder = context.encoder;
+
+        try {
+
+            // Render the JSON.
+            eventResolver.resolve(event, jsonWriter);
+            stringBuilder.append(eventDelimiter);
+
+            // Write to the destination.
+            if (encoder == null) {
+                final String eventJson = stringBuilder.toString();
+                final byte[] eventJsonBytes = StringEncoder.toBytes(eventJson, charset);
+                destination.writeBytes(eventJsonBytes, 0, eventJsonBytes.length);
+            } else {
+                encoder.encode(stringBuilder, destination);
+            }
+
+        }
+
+        // Release the context.
+        finally {
+            contextRecycler.release(context);
+        }
+
+    }
+
+    // Visible for tests.
+    Context acquireContext() {
+        return contextRecycler.acquire();
+    }
+
+    @Override
+    public byte[] getFooter() {
+        return null;
+    }
+
+    @Override
+    public byte[] getHeader() {
+        return null;
+    }
+
+    @Override
+    public Charset getCharset() {
+        return charset;
+    }
+
+    @Override
+    public String getContentType() {
+        return contentType;
+    }
+
+    @Override
+    public Map<String, String> getContentFormat() {
+        return CONTENT_FORMAT;
+    }
+
+    @PluginBuilderFactory
+    @SuppressWarnings("WeakerAccess")
+    public static Builder newBuilder() {
+        return new Builder();
+    }
+
+    @SuppressWarnings({"unused", "WeakerAccess"})
+    public static final class Builder
+            implements org.apache.logging.log4j.core.util.Builder<JsonTemplateLayout> {
+
+        @PluginConfiguration
+        private Configuration configuration;
+
+        @PluginBuilderAttribute
+        private Charset charset = JsonTemplateLayoutDefaults.getCharset();
+
+        @PluginBuilderAttribute
+        private boolean locationInfoEnabled =
+                JsonTemplateLayoutDefaults.isLocationInfoEnabled();
+
+        @PluginBuilderAttribute
+        private boolean stackTraceEnabled =
+                JsonTemplateLayoutDefaults.isStackTraceEnabled();
+
+        @PluginBuilderAttribute
+        private String eventTemplate = JsonTemplateLayoutDefaults.getEventTemplate();
+
+        @PluginBuilderAttribute
+        private String eventTemplateUri =
+                JsonTemplateLayoutDefaults.getEventTemplateUri();
+
+        @PluginElement("EventTemplateAdditionalFields")
+        private EventTemplateAdditionalFields eventTemplateAdditionalFields
+                = EventTemplateAdditionalFields.EMPTY;
+
+        @PluginBuilderAttribute
+        private String stackTraceElementTemplate =
+                JsonTemplateLayoutDefaults.getStackTraceElementTemplate();
+
+        @PluginBuilderAttribute
+        private String stackTraceElementTemplateUri =
+                JsonTemplateLayoutDefaults.getStackTraceElementTemplateUri();
+
+        @PluginBuilderAttribute
+        private String eventDelimiter = JsonTemplateLayoutDefaults.getEventDelimiter();
+
+        @PluginBuilderAttribute
+        private boolean nullEventDelimiterEnabled =
+                JsonTemplateLayoutDefaults.isNullEventDelimiterEnabled();
+
+        @PluginBuilderAttribute
+        private int maxStringLength = JsonTemplateLayoutDefaults.getMaxStringLength();
+
+        @PluginBuilderAttribute
+        private String truncatedStringSuffix =
+                JsonTemplateLayoutDefaults.getTruncatedStringSuffix();
+
+        @PluginBuilderAttribute
+        private RecyclerFactory recyclerFactory =
+                JsonTemplateLayoutDefaults.getRecyclerFactory();
+
+        private Builder() {
+            // Do nothing.
+        }
+
+        public Configuration getConfiguration() {
+            return configuration;
+        }
+
+        public Builder setConfiguration(final Configuration configuration) {
+            this.configuration = configuration;
+            return this;
+        }
+
+        public Charset getCharset() {
+            return charset;
+        }
+
+        public Builder setCharset(final Charset charset) {
+            this.charset = charset;
+            return this;
+        }
+
+        public boolean isLocationInfoEnabled() {
+            return locationInfoEnabled;
+        }
+
+        public Builder setLocationInfoEnabled(final boolean locationInfoEnabled) {
+            this.locationInfoEnabled = locationInfoEnabled;
+            return this;
+        }
+
+        public boolean isStackTraceEnabled() {
+            return stackTraceEnabled;
+        }
+
+        public Builder setStackTraceEnabled(final boolean stackTraceEnabled) {
+            this.stackTraceEnabled = stackTraceEnabled;
+            return this;
+        }
+
+        public String getEventTemplate() {
+            return eventTemplate;
+        }
+
+        public Builder setEventTemplate(final String eventTemplate) {
+            this.eventTemplate = eventTemplate;
+            return this;
+        }
+
+        public String getEventTemplateUri() {
+            return eventTemplateUri;
+        }
+
+        public Builder setEventTemplateUri(final String eventTemplateUri) {
+            this.eventTemplateUri = eventTemplateUri;
+            return this;
+        }
+
+        public EventTemplateAdditionalFields getEventTemplateAdditionalFields() {
+            return eventTemplateAdditionalFields;
+        }
+
+        public Builder setEventTemplateAdditionalFields(
+                final EventTemplateAdditionalFields eventTemplateAdditionalFields) {
+            this.eventTemplateAdditionalFields = eventTemplateAdditionalFields;
+            return this;
+        }
+
+        public String getStackTraceElementTemplate() {
+            return stackTraceElementTemplate;
+        }
+
+        public Builder setStackTraceElementTemplate(
+                final String stackTraceElementTemplate) {
+            this.stackTraceElementTemplate = stackTraceElementTemplate;
+            return this;
+        }
+
+        public String getStackTraceElementTemplateUri() {
+            return stackTraceElementTemplateUri;
+        }
+
+        public Builder setStackTraceElementTemplateUri(
+                final String stackTraceElementTemplateUri) {
+            this.stackTraceElementTemplateUri = stackTraceElementTemplateUri;
+            return this;
+        }
+
+        public String getEventDelimiter() {
+            return eventDelimiter;
+        }
+
+        public Builder setEventDelimiter(final String eventDelimiter) {
+            this.eventDelimiter = eventDelimiter;
+            return this;
+        }
+
+        public boolean isNullEventDelimiterEnabled() {
+            return nullEventDelimiterEnabled;
+        }
+
+        public Builder setNullEventDelimiterEnabled(
+                final boolean nullEventDelimiterEnabled) {
+            this.nullEventDelimiterEnabled = nullEventDelimiterEnabled;
+            return this;
+        }
+
+        public int getMaxStringLength() {
+            return maxStringLength;
+        }
+
+        public Builder setMaxStringLength(final int maxStringLength) {
+            this.maxStringLength = maxStringLength;
+            return this;
+        }
+
+        public String getTruncatedStringSuffix() {
+            return truncatedStringSuffix;
+        }
+
+        public Builder setTruncatedStringSuffix(final String truncatedStringSuffix) {
+            this.truncatedStringSuffix = truncatedStringSuffix;
+            return this;
+        }
+
+        public RecyclerFactory getRecyclerFactory() {
+            return recyclerFactory;
+        }
+
+        public Builder setRecyclerFactory(final RecyclerFactory recyclerFactory) {
+            this.recyclerFactory = recyclerFactory;
+            return this;
+        }
+
+        @Override
+        public JsonTemplateLayout build() {
+            validate();
+            return new JsonTemplateLayout(this);
+        }
+
+        private void validate() {
+            Objects.requireNonNull(configuration, "config");
+            if (Strings.isBlank(eventTemplate) && Strings.isBlank(eventTemplateUri)) {
+                    throw new IllegalArgumentException(
+                            "both eventTemplate and eventTemplateUri are blank");
+            }
+            Objects.requireNonNull(eventTemplateAdditionalFields, "eventTemplateAdditionalFields");
+            if (stackTraceEnabled &&
+                    Strings.isBlank(stackTraceElementTemplate)
+                    && Strings.isBlank(stackTraceElementTemplateUri)) {
+                throw new IllegalArgumentException(
+                        "both stackTraceElementTemplate and stackTraceElementTemplateUri are blank");
+            }
+            if (maxStringLength <= 0) {
+                throw new IllegalArgumentException(
+                        "was expecting a non-zero positive maxStringLength: " +
+                                maxStringLength);
+            }
+            Objects.requireNonNull(truncatedStringSuffix, "truncatedStringSuffix");
+            Objects.requireNonNull(recyclerFactory, "recyclerFactory");
+        }
+
+    }
+
+    // We need this ugly model and its builder just to be able to allow
+    // key-value pairs in a dedicated element.
+    @SuppressWarnings({"unused", "WeakerAccess"})
+    @Plugin(name = "EventTemplateAdditionalFields",
+            category = Node.CATEGORY,
+            printObject = true)
+    public static final class EventTemplateAdditionalFields {
+
+        private static final EventTemplateAdditionalFields EMPTY = newBuilder().build();
+
+        private final EventTemplateAdditionalField[] additionalFields;
+
+        private EventTemplateAdditionalFields(final Builder builder) {
+            this.additionalFields = builder.additionalFields != null
+                    ? builder.additionalFields
+                    : new EventTemplateAdditionalField[0];
+        }
+
+        public EventTemplateAdditionalField[] getAdditionalFields() {
+            return additionalFields;
+        }
+
+        @Override
+        public boolean equals(Object object) {
+            if (this == object) return true;
+            if (object == null || getClass() != object.getClass()) return false;
+            EventTemplateAdditionalFields that = (EventTemplateAdditionalFields) object;
+            return Arrays.equals(additionalFields, that.additionalFields);
+        }
+
+        @Override
+        public int hashCode() {
+            return Arrays.hashCode(additionalFields);
+        }
+
+        @Override
+        public String toString() {
+            return Arrays.toString(additionalFields);
+        }
+
+        @PluginBuilderFactory
+        public static Builder newBuilder() {
+            return new Builder();
+        }
+
+        public static class Builder
+                implements org.apache.logging.log4j.core.util.Builder<EventTemplateAdditionalFields> {
+
+            @PluginElement("AdditionalField")
+            private EventTemplateAdditionalField[] additionalFields;
+
+            private Builder() {}
+
+            public EventTemplateAdditionalField[] getAdditionalFields() {
+                return additionalFields;
+            }
+
+            public Builder setAdditionalFields(
+                    final EventTemplateAdditionalField[] additionalFields) {
+                this.additionalFields = additionalFields;
+                return this;
+            }
+
+            @Override
+            public EventTemplateAdditionalFields build() {
+                return new EventTemplateAdditionalFields(this);
+            }
+
+        }
+
+    }
+
+    @Plugin(name = "EventTemplateAdditionalField",
+            category = Node.CATEGORY,
+            printObject = true)
+    public static final class EventTemplateAdditionalField {
+
+        public enum Type { STRING, JSON }
+
+        private final String key;
+
+        private final String value;
+
+        private final Type type;
+
+        private EventTemplateAdditionalField(final Builder builder) {
+            this.key = builder.key;
+            this.value = builder.value;
+            this.type = builder.type;
+        }
+
+        public String getKey() {
+            return key;
+        }
+
+        public String getValue() {
+            return value;
+        }
+
+        public Type getType() {
+            return type;
+        }
+
+        @Override
+        public boolean equals(Object object) {
+            if (this == object) return true;
+            if (object == null || getClass() != object.getClass()) return false;
+            EventTemplateAdditionalField that = (EventTemplateAdditionalField) object;
+            return key.equals(that.key) &&
+                    value.equals(that.value) &&
+                    type == that.type;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(key, value, type);
+        }
+
+        @Override
+        public String toString() {
+            final String formattedValue = Type.STRING.equals(type)
+                    ? String.format("\"%s\"", value)
+                    : value;
+            return String.format("%s=%s", key, formattedValue);
+        }
+
+        @PluginBuilderFactory
+        public static EventTemplateAdditionalField.Builder newBuilder() {
+            return new EventTemplateAdditionalField.Builder();
+        }
+
+        public static class Builder
+                implements org.apache.logging.log4j.core.util.Builder<EventTemplateAdditionalField> {
+
+            private String key;
+
+            private String value;
+
+            private Type type = Type.STRING;
+
+            public Builder setKey(final String key) {
+                this.key = key;
+                return this;
+            }
+
+            public Builder setValue(final String value) {
+                this.value = value;
+                return this;
+            }
+
+            public Builder setType(final Type type) {
+                this.type = type;
+                return this;
+            }
+
+            @Override
+            public EventTemplateAdditionalField build() {
+                validate();
+                return new EventTemplateAdditionalField(this);
+            }
+
+            private void validate() {
+                if (Strings.isBlank(key)) {
+                    throw new IllegalArgumentException("blank key");
+                }
+                if (Strings.isBlank(value)) {
+                    throw new IllegalArgumentException("blank value");
+                }
+                Objects.requireNonNull(type, "type");
+            }
+
+        }
+
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutDefaults.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutDefaults.java
new file mode 100644
index 0000000..7c28b9f
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutDefaults.java
@@ -0,0 +1,213 @@
+/*
+ * 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.json.template;
+
+import org.apache.logging.log4j.layout.json.template.util.RecyclerFactories;
+import org.apache.logging.log4j.layout.json.template.util.RecyclerFactory;
+import org.apache.logging.log4j.util.PropertiesUtil;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.Locale;
+import java.util.TimeZone;
+
+public enum JsonTemplateLayoutDefaults {;
+
+    private static final PropertiesUtil PROPERTIES = PropertiesUtil.getProperties();
+
+    private static final Charset CHARSET = readCharset();
+
+    private static final boolean LOCATION_INFO_ENABLED =
+            PROPERTIES.getBooleanProperty(
+                    "log4j.layout.jsonTemplate.locationInfoEnabled",
+                    false);
+
+    private static final boolean STACK_TRACE_ENABLED =
+            PROPERTIES.getBooleanProperty(
+                    "log4j.layout.jsonTemplate.stackTraceEnabled",
+                    true);
+
+    private static final String TIMESTAMP_FORMAT_PATTERN =
+            PROPERTIES.getStringProperty(
+                    "log4j.layout.jsonTemplate.timestampFormatPattern",
+                    "yyyy-MM-dd'T'HH:mm:ss.SSSZZZ");
+
+    private static final TimeZone TIME_ZONE = readTimeZone();
+
+    private static final Locale LOCALE = readLocale();
+
+    private static final String EVENT_TEMPLATE =
+            PROPERTIES.getStringProperty(
+                    "log4j.layout.jsonTemplate.eventTemplate");
+
+    private static final String EVENT_TEMPLATE_URI =
+            PROPERTIES.getStringProperty(
+                    "log4j.layout.jsonTemplate.eventTemplateUri",
+                    "classpath:EcsLayout.json");
+
+    private static final String STACK_TRACE_ELEMENT_TEMPLATE =
+            PROPERTIES.getStringProperty(
+                    "log4j.layout.jsonTemplate.stackTraceElementTemplate");
+
+    private static final String STACK_TRACE_ELEMENT_TEMPLATE_URI =
+            PROPERTIES.getStringProperty(
+                    "log4j.layout.jsonTemplate.stackTraceElementTemplateUri",
+                    "classpath:StackTraceElementLayout.json");
+
+    private static final String MDC_KEY_PATTERN =
+            PROPERTIES.getStringProperty("log4j.layout.jsonTemplate.mdcKeyPattern");
+
+    private static final String NDC_PATTERN =
+            PROPERTIES.getStringProperty("log4j.layout.jsonTemplate.ndcPattern");
+
+    private static final String EVENT_DELIMITER =
+            PROPERTIES.getStringProperty(
+                    "log4j.layout.jsonTemplate.eventDelimiter",
+                    System.lineSeparator());
+
+    private static final boolean NULL_EVENT_DELIMITER_ENABLED =
+            PROPERTIES.getBooleanProperty(
+                    "log4j.layout.jsonTemplate.nullEventDelimiterEnabled",
+                    false);
+
+    private static final int MAX_STRING_LENGTH = readMaxStringLength();
+
+    private static final String TRUNCATED_STRING_SUFFIX =
+            PROPERTIES.getStringProperty(
+                    "log4j.layout.jsonTemplate.truncatedStringSuffix",
+                    "…");
+
+    private static final RecyclerFactory RECYCLER_FACTORY = readRecyclerFactory();
+
+    private static Charset readCharset() {
+        final String charsetName =
+                PROPERTIES.getStringProperty("log4j.layout.jsonTemplate.charset");
+        return charsetName != null
+                ? Charset.forName(charsetName)
+                : StandardCharsets.UTF_8;
+    }
+
+    private static TimeZone readTimeZone() {
+        final String timeZoneId =
+                PROPERTIES.getStringProperty("log4j.layout.jsonTemplate.timeZone");
+        return timeZoneId != null
+                ? TimeZone.getTimeZone(timeZoneId)
+                : TimeZone.getDefault();
+    }
+
+    private static Locale readLocale() {
+        final String locale =
+                PROPERTIES.getStringProperty("log4j.layout.jsonTemplate.locale");
+        if (locale == null) {
+            return Locale.getDefault();
+        }
+        final String[] localeFields = locale.split("_", 3);
+        switch (localeFields.length) {
+            case 1: return new Locale(localeFields[0]);
+            case 2: return new Locale(localeFields[0], localeFields[1]);
+            case 3: return new Locale(localeFields[0], localeFields[1], localeFields[2]);
+            default: throw new IllegalArgumentException("invalid locale: " + locale);
+        }
+    }
+
+    private static int readMaxStringLength() {
+        final int maxStringLength = PROPERTIES.getIntegerProperty(
+                "log4j.layout.jsonTemplate.maxStringLength",
+                16 * 1_024);
+        if (maxStringLength <= 0) {
+            throw new IllegalArgumentException(
+                    "was expecting a non-zero positive maxStringLength: " +
+                            maxStringLength);
+        }
+        return maxStringLength;
+    }
+
+    private static RecyclerFactory readRecyclerFactory() {
+        final String recyclerFactorySpec = PROPERTIES.getStringProperty(
+                "log4j.layout.jsonTemplate.recyclerFactory");
+        return RecyclerFactories.ofSpec(recyclerFactorySpec);
+    }
+
+    public static Charset getCharset() {
+        return CHARSET;
+    }
+
+    public static boolean isLocationInfoEnabled() {
+        return LOCATION_INFO_ENABLED;
+    }
+
+    public static boolean isStackTraceEnabled() {
+        return STACK_TRACE_ENABLED;
+    }
+
+    public static String getTimestampFormatPattern() {
+        return TIMESTAMP_FORMAT_PATTERN;
+    }
+
+    public static TimeZone getTimeZone() {
+        return TIME_ZONE;
+    }
+
+    public static Locale getLocale() {
+        return LOCALE;
+    }
+
+    public static String getEventTemplate() {
+        return EVENT_TEMPLATE;
+    }
+
+    public static String getEventTemplateUri() {
+        return EVENT_TEMPLATE_URI;
+    }
+
+    public static String getStackTraceElementTemplate() {
+        return STACK_TRACE_ELEMENT_TEMPLATE;
+    }
+
+    public static String getStackTraceElementTemplateUri() {
+        return STACK_TRACE_ELEMENT_TEMPLATE_URI;
+    }
+
+    public static String getMdcKeyPattern() {
+        return MDC_KEY_PATTERN;
+    }
+
+    public static String getNdcPattern() {
+        return NDC_PATTERN;
+    }
+
+    public static String getEventDelimiter() {
+        return EVENT_DELIMITER;
+    }
+
+    public static boolean isNullEventDelimiterEnabled() {
+        return NULL_EVENT_DELIMITER_ENABLED;
+    }
+
+    public static int getMaxStringLength() {
+        return MAX_STRING_LENGTH;
+    }
+
+    public static String getTruncatedStringSuffix() {
+        return TRUNCATED_STRING_SUFFIX;
+    }
+
+    public static RecyclerFactory getRecyclerFactory() {
+        return RECYCLER_FACTORY;
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EndOfBatchResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EndOfBatchResolver.java
new file mode 100644
index 0000000..268df52
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EndOfBatchResolver.java
@@ -0,0 +1,44 @@
+/*
+ * 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.json.template.resolver;
+
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+
+final class EndOfBatchResolver implements EventResolver {
+
+    private static final EndOfBatchResolver INSTANCE = new EndOfBatchResolver();
+
+    private EndOfBatchResolver() {}
+
+    static EndOfBatchResolver getInstance() {
+        return INSTANCE;
+    }
+
+    static String getName() {
+        return "endOfBatch";
+    }
+
+    @Override
+    public void resolve(
+            final LogEvent logEvent,
+            final JsonWriter jsonWriter) {
+        final boolean endOfBatch = logEvent.isEndOfBatch();
+        jsonWriter.writeBoolean(endOfBatch);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EndOfBatchResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EndOfBatchResolverFactory.java
new file mode 100644
index 0000000..0f013a4
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EndOfBatchResolverFactory.java
@@ -0,0 +1,41 @@
+/*
+ * 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.json.template.resolver;
+
+final class EndOfBatchResolverFactory implements EventResolverFactory<EndOfBatchResolver> {
+
+    private static final EndOfBatchResolverFactory INSTANCE = new EndOfBatchResolverFactory();
+
+    private EndOfBatchResolverFactory() {}
+
+    static EndOfBatchResolverFactory getInstance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public String getName() {
+        return EndOfBatchResolver.getName();
+    }
+
+    @Override
+    public EndOfBatchResolver create(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
+        return EndOfBatchResolver.getInstance();
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EventResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EventResolver.java
new file mode 100644
index 0000000..ce21181
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EventResolver.java
@@ -0,0 +1,21 @@
+/*
+ * 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.json.template.resolver;
+
+import org.apache.logging.log4j.core.LogEvent;
+
+interface EventResolver extends TemplateResolver<LogEvent> {}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EventResolverContext.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EventResolverContext.java
new file mode 100644
index 0000000..a83b2c9
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EventResolverContext.java
@@ -0,0 +1,228 @@
+/*
+ * 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.json.template.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.core.util.KeyValuePair;
+import org.apache.logging.log4j.layout.json.template.JsonTemplateLayout;
+import org.apache.logging.log4j.layout.json.template.JsonTemplateLayout.EventTemplateAdditionalField;
+import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+import org.apache.logging.log4j.layout.json.template.util.RecyclerFactory;
+
+import java.nio.charset.Charset;
+import java.util.Map;
+import java.util.Objects;
+
+public final class EventResolverContext implements TemplateResolverContext<LogEvent, EventResolverContext> {
+
+    private final Configuration configuration;
+
+    private final StrSubstitutor substitutor;
+
+    private final Charset charset;
+
+    private final JsonWriter jsonWriter;
+
+    private final RecyclerFactory recyclerFactory;
+
+    private final int maxStringByteCount;
+
+    private final boolean locationInfoEnabled;
+
+    private final boolean stackTraceEnabled;
+
+    private final TemplateResolver<Throwable> stackTraceObjectResolver;
+
+    private final EventTemplateAdditionalField[] additionalFields;
+
+    private EventResolverContext(final Builder builder) {
+        this.configuration = builder.configuration;
+        this.substitutor = builder.substitutor;
+        this.charset = builder.charset;
+        this.jsonWriter = builder.jsonWriter;
+        this.recyclerFactory = builder.recyclerFactory;
+        this.maxStringByteCount = builder.maxStringByteCount;
+        this.locationInfoEnabled = builder.locationInfoEnabled;
+        this.stackTraceEnabled = builder.stackTraceEnabled;
+        this.stackTraceObjectResolver = stackTraceEnabled
+                ? new StackTraceObjectResolver(builder.stackTraceElementObjectResolver)
+                : null;
+        this.additionalFields = builder.eventTemplateAdditionalFields;
+    }
+
+    @Override
+    public Class<EventResolverContext> getContextClass() {
+        return EventResolverContext.class;
+    }
+
+    @Override
+    public Map<String, TemplateResolverFactory<LogEvent, EventResolverContext, ? extends TemplateResolver<LogEvent>>> getResolverFactoryByName() {
+        return EventResolverFactories.getResolverFactoryByName();
+    }
+
+    public Configuration getConfiguration() {
+        return configuration;
+    }
+
+    @Override
+    public StrSubstitutor getSubstitutor() {
+        return substitutor;
+    }
+
+    public Charset getCharset() {
+        return charset;
+    }
+
+    @Override
+    public JsonWriter getJsonWriter() {
+        return jsonWriter;
+    }
+
+    RecyclerFactory getRecyclerFactory() {
+        return recyclerFactory;
+    }
+
+    int getMaxStringByteCount() {
+        return maxStringByteCount;
+    }
+
+    boolean isLocationInfoEnabled() {
+        return locationInfoEnabled;
+    }
+
+    boolean isStackTraceEnabled() {
+        return stackTraceEnabled;
+    }
+
+    TemplateResolver<Throwable> getStackTraceObjectResolver() {
+        return stackTraceObjectResolver;
+    }
+
+    EventTemplateAdditionalField[] getAdditionalFields() {
+        return additionalFields;
+    }
+
+    public static Builder newBuilder() {
+        return new Builder();
+    }
+
+    public static class Builder {
+
+        private Configuration configuration;
+
+        private StrSubstitutor substitutor;
+
+        private Charset charset;
+
+        private JsonWriter jsonWriter;
+
+        private RecyclerFactory recyclerFactory;
+
+        private int maxStringByteCount;
+
+        private boolean locationInfoEnabled;
+
+        private boolean stackTraceEnabled;
+
+        private TemplateResolver<StackTraceElement> stackTraceElementObjectResolver;
+
+        private EventTemplateAdditionalField[] eventTemplateAdditionalFields;
+
+        private Builder() {
+            // Do nothing.
+        }
+
+        public Builder setConfiguration(final Configuration configuration) {
+            this.configuration = configuration;
+            return this;
+        }
+
+        public Builder setSubstitutor(final StrSubstitutor substitutor) {
+            this.substitutor = substitutor;
+            return this;
+        }
+
+        public Builder setCharset(final Charset charset) {
+            this.charset = charset;
+            return this;
+        }
+
+        public Builder setJsonWriter(final JsonWriter jsonWriter) {
+            this.jsonWriter = jsonWriter;
+            return this;
+        }
+
+        public Builder setRecyclerFactory(final RecyclerFactory recyclerFactory) {
+            this.recyclerFactory = recyclerFactory;
+            return this;
+        }
+
+        public Builder setMaxStringByteCount(final int maxStringByteCount) {
+            this.maxStringByteCount = maxStringByteCount;
+            return this;
+        }
+
+        public Builder setLocationInfoEnabled(final boolean locationInfoEnabled) {
+            this.locationInfoEnabled = locationInfoEnabled;
+            return this;
+        }
+
+        public Builder setStackTraceEnabled(final boolean stackTraceEnabled) {
+            this.stackTraceEnabled = stackTraceEnabled;
+            return this;
+        }
+
+        public Builder setStackTraceElementObjectResolver(
+                final TemplateResolver<StackTraceElement> stackTraceElementObjectResolver) {
+            this.stackTraceElementObjectResolver = stackTraceElementObjectResolver;
+            return this;
+        }
+
+        public Builder setEventTemplateAdditionalFields(
+                final EventTemplateAdditionalField[] eventTemplateAdditionalFields) {
+            this.eventTemplateAdditionalFields = eventTemplateAdditionalFields;
+            return this;
+        }
+
+        public EventResolverContext build() {
+            validate();
+            return new EventResolverContext(this);
+        }
+
+        private void validate() {
+            Objects.requireNonNull(configuration, "configuration");
+            Objects.requireNonNull(substitutor, "substitutor");
+            Objects.requireNonNull(charset, "charset");
+            Objects.requireNonNull(jsonWriter, "jsonWriter");
+            Objects.requireNonNull(recyclerFactory, "recyclerFactory");
+            if (maxStringByteCount <= 0) {
+                throw new IllegalArgumentException(
+                        "was expecting maxStringByteCount > 0: " +
+                                maxStringByteCount);
+            }
+            if (stackTraceEnabled) {
+                Objects.requireNonNull(
+                        stackTraceElementObjectResolver,
+                        "stackTraceElementObjectResolver");
+            }
+        }
+
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EventResolverFactories.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EventResolverFactories.java
new file mode 100644
index 0000000..fc8c6e9
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EventResolverFactories.java
@@ -0,0 +1,65 @@
+/*
+ * 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.json.template.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;
+
+enum 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(),
+                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;
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EventResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EventResolverFactory.java
new file mode 100644
index 0000000..3c2f2db
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EventResolverFactory.java
@@ -0,0 +1,21 @@
+/*
+ * 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.json.template.resolver;
+
+import org.apache.logging.log4j.core.LogEvent;
+
+interface EventResolverFactory<R extends TemplateResolver<LogEvent>> extends TemplateResolverFactory<LogEvent, EventResolverContext, R> {}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionInternalResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionInternalResolverFactory.java
new file mode 100644
index 0000000..b6e5ff8
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionInternalResolverFactory.java
@@ -0,0 +1,68 @@
+/*
+ * 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.json.template.resolver;
+
+/**
+ * Exception resolver factory.
+ *
+ * <h3>Configuration</h3>
+ *
+ * <pre>
+ * config      = field , [ stringified ]
+ * field       = "field" -> ( "className" | "message" | "stackTrace" )
+ * stringified = "stringified" -> boolean
+ * </pre>
+ */
+abstract class ExceptionInternalResolverFactory {
+
+    private static final EventResolver NULL_RESOLVER =
+            (ignored, jsonGenerator) -> jsonGenerator.writeNull();
+
+    EventResolver createInternalResolver(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
+        final String fieldName = config.getString("field");
+        switch (fieldName) {
+            case "className": return createClassNameResolver();
+            case "message": return createMessageResolver(context);
+            case "stackTrace": return createStackTraceResolver(context, config);
+        }
+        throw new IllegalArgumentException("unknown field: " + config);
+
+    }
+
+    abstract EventResolver createClassNameResolver();
+
+    abstract EventResolver createMessageResolver(EventResolverContext context);
+
+    private EventResolver createStackTraceResolver(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
+        if (!context.isStackTraceEnabled()) {
+            return NULL_RESOLVER;
+        }
+        final boolean stringified = config.getBoolean("stringified", false);
+        return stringified
+                ? createStackTraceStringResolver(context)
+                : createStackTraceObjectResolver(context);
+    }
+
+    abstract EventResolver createStackTraceStringResolver(EventResolverContext context);
+
+    abstract EventResolver createStackTraceObjectResolver(EventResolverContext context);
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionResolver.java
new file mode 100644
index 0000000..140cc42
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionResolver.java
@@ -0,0 +1,122 @@
+/*
+ * 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.json.template.resolver;
+
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+
+/**
+ * Exception resolver.
+ *
+ * Note that this resolver is toggled by {@link
+ * org.apache.logging.log4j.layout.json.template.JsonTemplateLayout.Builder#setStackTraceEnabled(boolean)}.
+ *
+ * @see ExceptionInternalResolverFactory
+ */
+class ExceptionResolver implements EventResolver {
+
+    private static final ExceptionInternalResolverFactory INTERNAL_RESOLVER_FACTORY =
+            new ExceptionInternalResolverFactory() {
+
+                @Override
+                EventResolver createClassNameResolver() {
+                    return (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
+                        final Throwable exception = logEvent.getThrown();
+                        if (exception == null) {
+                            jsonWriter.writeNull();
+                        } else {
+                            String exceptionClassName = exception.getClass().getCanonicalName();
+                            jsonWriter.writeString(exceptionClassName);
+                        }
+                    };
+                }
+
+                @Override
+                EventResolver createMessageResolver(final EventResolverContext context) {
+                    return (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
+                        final Throwable exception = logEvent.getThrown();
+                        if (exception == null) {
+                            jsonWriter.writeNull();
+                        } else {
+                            String exceptionMessage = exception.getMessage();
+                            jsonWriter.writeString(exceptionMessage);
+                        }
+                    };
+                }
+
+                @Override
+                EventResolver createStackTraceStringResolver(final EventResolverContext context) {
+                    StackTraceStringResolver stackTraceStringResolver =
+                            new StackTraceStringResolver(context);
+                    return (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
+                        final Throwable exception = logEvent.getThrown();
+                        if (exception == null) {
+                            jsonWriter.writeNull();
+                        } else {
+                            stackTraceStringResolver.resolve(exception, jsonWriter);
+                        }
+                    };
+                }
+
+                @Override
+                EventResolver createStackTraceObjectResolver(final EventResolverContext context) {
+                    return (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
+                        final Throwable exception = logEvent.getThrown();
+                        if (exception == null) {
+                            jsonWriter.writeNull();
+                        } else {
+                            context.getStackTraceObjectResolver().resolve(exception, jsonWriter);
+                        }
+                    };
+                }
+
+            };
+
+    private final boolean stackTraceEnabled;
+
+    private final EventResolver internalResolver;
+
+    ExceptionResolver(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
+        this.stackTraceEnabled = context.isStackTraceEnabled();
+        this.internalResolver = INTERNAL_RESOLVER_FACTORY
+                .createInternalResolver(context, config);
+    }
+
+    static String getName() {
+        return "exception";
+    }
+
+    @Override
+    public boolean isResolvable() {
+        return stackTraceEnabled;
+    }
+
+    @Override
+    public boolean isResolvable(final LogEvent logEvent) {
+        return stackTraceEnabled && logEvent.getThrown() != null;
+    }
+
+    @Override
+    public void resolve(
+            final LogEvent logEvent,
+            final JsonWriter jsonWriter) {
+        internalResolver.resolve(logEvent, jsonWriter);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionResolverFactory.java
new file mode 100644
index 0000000..7ca79b0
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionResolverFactory.java
@@ -0,0 +1,43 @@
+/*
+ * 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.json.template.resolver;
+
+final class ExceptionResolverFactory
+        implements EventResolverFactory<ExceptionResolver> {
+
+    private static final ExceptionResolverFactory INSTANCE =
+            new ExceptionResolverFactory();
+
+    private ExceptionResolverFactory() {}
+
+    static ExceptionResolverFactory getInstance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public String getName() {
+        return ExceptionResolver.getName();
+    }
+
+    @Override
+    public ExceptionResolver create(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
+        return new ExceptionResolver(context, config);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionRootCauseResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionRootCauseResolver.java
new file mode 100644
index 0000000..f3d4705
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionRootCauseResolver.java
@@ -0,0 +1,127 @@
+/*
+ * 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.json.template.resolver;
+
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.util.Throwables;
+import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+
+/**
+ * Exception root cause resolver.
+ *
+ * Note that this resolver is toggled by {@link
+ * org.apache.logging.log4j.layout.json.template.JsonTemplateLayout.Builder#setStackTraceEnabled(boolean)}.
+ *
+ * @see ExceptionInternalResolverFactory
+ */
+final class ExceptionRootCauseResolver implements EventResolver {
+
+    private static final ExceptionInternalResolverFactory INTERNAL_RESOLVER_FACTORY =
+            new ExceptionInternalResolverFactory() {
+
+                @Override
+                EventResolver createClassNameResolver() {
+                    return (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
+                        final Throwable exception = logEvent.getThrown();
+                        if (exception == null) {
+                            jsonWriter.writeNull();
+                        } else {
+                            final Throwable rootCause = Throwables.getRootCause(exception);
+                            final String rootCauseClassName = rootCause.getClass().getCanonicalName();
+                            jsonWriter.writeString(rootCauseClassName);
+                        }
+                    };
+                }
+
+                @Override
+                EventResolver createMessageResolver(final EventResolverContext context) {
+                    return (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
+                        final Throwable exception = logEvent.getThrown();
+                        if (exception == null) {
+                            jsonWriter.writeNull();
+                        } else {
+                            final Throwable rootCause = Throwables.getRootCause(exception);
+                            final String rootCauseMessage = rootCause.getMessage();
+                            jsonWriter.writeString(rootCauseMessage);
+                        }
+                    };
+                }
+
+                @Override
+                EventResolver createStackTraceStringResolver(final EventResolverContext context) {
+                    final StackTraceStringResolver stackTraceStringResolver =
+                            new StackTraceStringResolver(context);
+                    return (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
+                        final Throwable exception = logEvent.getThrown();
+                        if (exception == null) {
+                            jsonWriter.writeNull();
+                        } else {
+                            final Throwable rootCause = Throwables.getRootCause(exception);
+                            stackTraceStringResolver.resolve(rootCause, jsonWriter);
+                        }
+                    };
+                }
+
+                @Override
+                EventResolver createStackTraceObjectResolver(EventResolverContext context) {
+                    return (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
+                        final Throwable exception = logEvent.getThrown();
+                        if (exception == null) {
+                            jsonWriter.writeNull();
+                        } else {
+                            final Throwable rootCause = Throwables.getRootCause(exception);
+                            context.getStackTraceObjectResolver().resolve(rootCause, jsonWriter);
+                        }
+                    };
+                }
+
+            };
+
+    private final boolean stackTraceEnabled;
+
+    private final EventResolver internalResolver;
+
+    ExceptionRootCauseResolver(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
+        this.stackTraceEnabled = context.isStackTraceEnabled();
+        this.internalResolver = INTERNAL_RESOLVER_FACTORY
+                .createInternalResolver(context, config);
+    }
+
+    static String getName() {
+        return "exceptionRootCause";
+    }
+
+    @Override
+    public boolean isResolvable() {
+        return stackTraceEnabled;
+    }
+
+    @Override
+    public boolean isResolvable(final LogEvent logEvent) {
+        return stackTraceEnabled && logEvent.getThrown() != null;
+    }
+
+    @Override
+    public void resolve(
+            final LogEvent logEvent,
+            final JsonWriter jsonWriter) {
+        internalResolver.resolve(logEvent, jsonWriter);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionRootCauseResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionRootCauseResolverFactory.java
new file mode 100644
index 0000000..e511f0d
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionRootCauseResolverFactory.java
@@ -0,0 +1,41 @@
+/*
+ * 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.json.template.resolver;
+
+final class ExceptionRootCauseResolverFactory implements EventResolverFactory<ExceptionRootCauseResolver> {
+
+    private static final ExceptionRootCauseResolverFactory INSTANCE = new ExceptionRootCauseResolverFactory();
+
+    private ExceptionRootCauseResolverFactory() {}
+
+    static ExceptionRootCauseResolverFactory getInstance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public String getName() {
+        return ExceptionRootCauseResolver.getName();
+    }
+
+    @Override
+    public ExceptionRootCauseResolver create(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
+        return new ExceptionRootCauseResolver(context, config);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LevelResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LevelResolver.java
new file mode 100644
index 0000000..422e445
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LevelResolver.java
@@ -0,0 +1,176 @@
+/*
+ * 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.json.template.resolver;
+
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.net.Severity;
+import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+
+import java.util.Arrays;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * Level resolver.
+ *
+ * <h3>Configuration</h3>
+ *
+ * <pre>
+ * config         = field , [ severity ]
+ * field          = "field" -> ( "name" | "severity" )
+ * severity       = severity-field
+ * severity-field = "field" -> ( "keyword" | "code" )
+ * </pre>
+ *
+ * <h3>Examples</h3>
+ *
+ * Resolve the level name:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "level",
+ *   "field": "name"
+ * }
+ * </pre>
+ *
+ * Resolve the severity keyword:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "level",
+ *   "field": "severity",
+ *   "severity": {
+ *     "field": "keyword"
+ *   }
+ * }
+ *
+ * Resolve the severity code:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "level",
+ *   "field": "severity",
+ *   "severity": {
+ *     "field": "code"
+ *   }
+ * }
+ * </pre>
+ */
+final class LevelResolver implements EventResolver {
+
+    private static String[] SEVERITY_CODE_RESOLUTION_BY_STANDARD_LEVEL_ORDINAL;
+
+    static {
+        final int levelCount = Level.values().length;
+        final String[] severityCodeResolutionByStandardLevelOrdinal =
+                new String[levelCount + 1];
+        for (final Level level : Level.values()) {
+            final int standardLevelOrdinal = level.getStandardLevel().ordinal();
+            final int severityCode = Severity.getSeverity(level).getCode();
+            severityCodeResolutionByStandardLevelOrdinal[standardLevelOrdinal] =
+                    String.valueOf(severityCode);
+        }
+        SEVERITY_CODE_RESOLUTION_BY_STANDARD_LEVEL_ORDINAL =
+                severityCodeResolutionByStandardLevelOrdinal;
+    }
+
+    private static final EventResolver SEVERITY_CODE_RESOLVER =
+            (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
+                final int standardLevelOrdinal =
+                        logEvent.getLevel().getStandardLevel().ordinal();
+                final String severityCodeResolution =
+                        SEVERITY_CODE_RESOLUTION_BY_STANDARD_LEVEL_ORDINAL[
+                                standardLevelOrdinal];
+                jsonWriter.writeRawString(severityCodeResolution);
+            };
+
+    private final EventResolver internalResolver;
+
+    LevelResolver(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
+        this.internalResolver = createResolver(context, config);
+    }
+
+    private static EventResolver createResolver(
+            final EventResolverContext context,
+            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);
+                }
+            }
+            default: throw new IllegalArgumentException("unknown field: " + config);
+        }
+    }
+
+    private static EventResolver createNameResolver(
+            final JsonWriter contextJsonWriter) {
+        final Map<Level, String> resolutionByLevel = Arrays
+                .stream(Level.values())
+                .collect(Collectors.toMap(
+                        Function.identity(),
+                        (final Level level) -> contextJsonWriter.use(() -> {
+                            final String name = level.name();
+                            contextJsonWriter.writeString(name);
+                        })));
+        return (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
+            final String resolution = resolutionByLevel.get(logEvent.getLevel());
+            jsonWriter.writeRawString(resolution);
+        };
+    }
+
+    private static EventResolver createSeverityKeywordResolver(
+            final JsonWriter contextJsonWriter) {
+        final Map<Level, String> resolutionByLevel = Arrays
+                .stream(Level.values())
+                .collect(Collectors.toMap(
+                        Function.identity(),
+                        (final Level level) -> contextJsonWriter.use(() -> {
+                            final String severityKeyword = Severity.getSeverity(level).name();
+                            contextJsonWriter.writeString(severityKeyword);
+                        })));
+        return (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
+            final String resolution = resolutionByLevel.get(logEvent.getLevel());
+            jsonWriter.writeRawString(resolution);
+        };
+    }
+
+    static String getName() {
+        return "level";
+    }
+
+    @Override
+    public void resolve(
+            final LogEvent logEvent,
+            final JsonWriter jsonWriter) {
+        internalResolver.resolve(logEvent, jsonWriter);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LevelResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LevelResolverFactory.java
new file mode 100644
index 0000000..f5ee519
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LevelResolverFactory.java
@@ -0,0 +1,41 @@
+/*
+ * 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.json.template.resolver;
+
+final class LevelResolverFactory implements EventResolverFactory<LevelResolver> {
+
+    private static final LevelResolverFactory INSTANCE = new LevelResolverFactory();
+
+    private LevelResolverFactory() {}
+
+    static LevelResolverFactory getInstance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public String getName() {
+        return LevelResolver.getName();
+    }
+
+    @Override
+    public LevelResolver create(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
+        return new LevelResolver(context, config);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LoggerResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LoggerResolver.java
new file mode 100644
index 0000000..66f1f87
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LoggerResolver.java
@@ -0,0 +1,92 @@
+/*
+ * 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.json.template.resolver;
+
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+
+/**
+ * Logger resolver.
+ *
+ * <h3>Configuration</h3>
+ *
+ * <pre>
+ * config = "field" -> ( "name" | "fqcn" )
+ * </pre>
+ *
+ * <h3>Examples</h3>
+ *
+ * Resolve the logger name:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "logger",
+ *   "field": "name"
+ * }
+ * </pre>
+ *
+ * Resolve the logger's fully qualified class name:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "logger",
+ *   "field": "fqcn"
+ * }
+ * </pre>
+ */
+final class LoggerResolver implements EventResolver {
+
+    private static final EventResolver NAME_RESOLVER =
+            (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
+                final String loggerName = logEvent.getLoggerName();
+                jsonWriter.writeString(loggerName);
+            };
+
+    private static final EventResolver FQCN_RESOLVER =
+            (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
+                final String loggerFqcn = logEvent.getLoggerFqcn();
+                jsonWriter.writeString(loggerFqcn);
+            };
+
+    private final EventResolver internalResolver;
+
+    LoggerResolver(final TemplateResolverConfig config) {
+        this.internalResolver = createInternalResolver(config);
+    }
+
+    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;
+        }
+        throw new IllegalArgumentException("unknown field: " + config);
+    }
+
+    static String getName() {
+        return "logger";
+    }
+
+    @Override
+    public void resolve(
+            final LogEvent logEvent,
+            final JsonWriter jsonWriter) {
+        internalResolver.resolve(logEvent, jsonWriter);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LoggerResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LoggerResolverFactory.java
new file mode 100644
index 0000000..5539f6e
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LoggerResolverFactory.java
@@ -0,0 +1,41 @@
+/*
+ * 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.json.template.resolver;
+
+final class LoggerResolverFactory implements EventResolverFactory<LoggerResolver> {
+
+    private static final LoggerResolverFactory INSTANCE = new LoggerResolverFactory();
+
+    private LoggerResolverFactory() {}
+
+    static LoggerResolverFactory getInstance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public String getName() {
+        return LoggerResolver.getName();
+    }
+
+    @Override
+    public LoggerResolver create(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
+        return new LoggerResolver(config);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MainMapResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MainMapResolver.java
new file mode 100644
index 0000000..b12821c
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MainMapResolver.java
@@ -0,0 +1,90 @@
+/*
+ * 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.json.template.resolver;
+
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.lookup.MainMapLookup;
+import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+
+/**
+ * An index-based resolver for the <tt>main()</tt> method arguments.
+ *
+ * <h3>Configuration</h3>
+ *
+ * <pre>
+ * config = index | key
+ * index  = "index" -> number
+ * key    = "key" -> string
+ * </pre>
+ *
+ * <h3>Examples</h3>
+ *
+ * Resolve the 1st <tt>main()</tt> method argument:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "main",
+ *   "index": 0
+ * }
+ * </pre>
+ *
+ * Resolve the argument coming right after <tt>--userId</tt>:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "main",
+ *   "key": "--userId"
+ * }
+ * </pre>
+ *
+ * @see MainMapResolver
+ */
+final class MainMapResolver implements EventResolver {
+
+    private static final MainMapLookup MAIN_MAP_LOOKUP = new MainMapLookup();
+
+    private final String key;
+
+    static String getName() {
+        return "main";
+    }
+
+    MainMapResolver(final TemplateResolverConfig config) {
+        final String key = config.getString("key");
+        final Integer index = config.getInteger("index");
+        if (key != null && index != null) {
+            throw new IllegalArgumentException(
+                    "provided both key and index: " + config);
+        }
+        if (key == null && index == null) {
+            throw new IllegalArgumentException(
+                    "either key or index must be provided: " + config);
+        }
+        this.key = index != null
+                ? String.valueOf(index)
+                : key;
+    }
+
+    @Override
+    public void resolve(
+            final LogEvent logEvent,
+            final JsonWriter jsonWriter) {
+        final String value = MAIN_MAP_LOOKUP.lookup(key);
+        jsonWriter.writeString(value);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MainMapResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MainMapResolverFactory.java
new file mode 100644
index 0000000..83b93a1
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MainMapResolverFactory.java
@@ -0,0 +1,41 @@
+/*
+ * 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.json.template.resolver;
+
+final class MainMapResolverFactory implements EventResolverFactory<MainMapResolver> {
+
+    private static final MainMapResolverFactory INSTANCE = new MainMapResolverFactory();
+
+    private MainMapResolverFactory() {}
+
+    static MainMapResolverFactory getInstance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public String getName() {
+        return MainMapResolver.getName();
+    }
+
+    @Override
+    public MainMapResolver create(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
+        return new MainMapResolver(config);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MapResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MapResolver.java
new file mode 100644
index 0000000..21d125c
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MapResolver.java
@@ -0,0 +1,91 @@
+/*
+ * 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.json.template.resolver;
+
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+import org.apache.logging.log4j.message.MapMessage;
+import org.apache.logging.log4j.message.Message;
+import org.apache.logging.log4j.util.IndexedReadOnlyStringMap;
+
+/**
+ * {@link MapMessage} field resolver.
+ *
+ * <h3>Configuration</h3>
+ *
+ * <pre>
+ * config      = key , [ stringified ]
+ * key         = "key" -> string
+ * stringified = "stringified" -> boolean
+ * </pre>
+ *
+ * <h3>Examples</h3>
+ *
+ * Resolve the <tt>userRole</tt> field of the message:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "map",
+ *   "key": "userRole"
+ * }
+ * </pre>
+ */
+final class MapResolver implements EventResolver {
+
+    private final String key;
+
+    private final boolean stringified;
+
+    static String getName() {
+        return "map";
+    }
+
+    MapResolver(final TemplateResolverConfig config) {
+        this.key = config.getString("key");
+        this.stringified = config.getBoolean("stringified", false);
+        if (key == null) {
+            throw new IllegalArgumentException("missing key: " + config);
+        }
+    }
+
+    @Override
+    public boolean isResolvable(final LogEvent logEvent) {
+        return logEvent.getMessage() instanceof MapMessage;
+    }
+
+    @Override
+    public void resolve(
+            final LogEvent logEvent,
+            final JsonWriter jsonWriter) {
+        final Message message = logEvent.getMessage();
+        if (!(message instanceof MapMessage)) {
+            jsonWriter.writeNull();
+        } else {
+            @SuppressWarnings("unchecked")
+            MapMessage<?, Object> mapMessage = (MapMessage<?, Object>) message;
+            final IndexedReadOnlyStringMap map = mapMessage.getIndexedReadOnlyStringMap();
+            final Object value = map.getValue(key);
+            if (stringified) {
+                final String stringifiedValue = String.valueOf(value);
+                jsonWriter.writeString(stringifiedValue);
+            } else {
+                jsonWriter.writeValue(value);
+            }
+        }
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MapResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MapResolverFactory.java
new file mode 100644
index 0000000..df57601
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MapResolverFactory.java
@@ -0,0 +1,41 @@
+/*
+ * 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.json.template.resolver;
+
+final class MapResolverFactory implements EventResolverFactory<MapResolver> {
+
+    private static final MapResolverFactory INSTANCE = new MapResolverFactory();
+
+    private MapResolverFactory() {}
+
+    static MapResolverFactory getInstance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public String getName() {
+        return MapResolver.getName();
+    }
+
+    @Override
+    public MapResolver create(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
+        return new MapResolver(config);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MarkerResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MarkerResolver.java
new file mode 100644
index 0000000..0bef3ff
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MarkerResolver.java
@@ -0,0 +1,86 @@
+/*
+ * 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.json.template.resolver;
+
+import org.apache.logging.log4j.Marker;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+
+/**
+ * A {@link Marker} resolver.
+ *
+ * <h3>Configuration</h3>
+ *
+ * <pre>
+ * config = "field" -> "name"
+ * </pre>
+ *
+ * <h3>Examples</h3>
+ *
+ * Resolve the marker name:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "marker",
+ *   "field": "name"
+ * }
+ * </pre>
+ */
+final class MarkerResolver implements EventResolver {
+
+    private static final TemplateResolver<LogEvent> NAME_RESOLVER =
+            (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
+                final Marker marker = logEvent.getMarker();
+                if (marker == null) {
+                    jsonWriter.writeNull();
+                } else {
+                    jsonWriter.writeString(marker.getName());
+                }
+            };
+
+    private final TemplateResolver<LogEvent> internalResolver;
+
+    MarkerResolver(final TemplateResolverConfig config) {
+        this.internalResolver = createInternalResolver(config);
+    }
+
+    private TemplateResolver<LogEvent> createInternalResolver(
+            final TemplateResolverConfig config) {
+        final String fieldName = config.getString("field");
+        if ("name".equals(fieldName)) {
+            return NAME_RESOLVER;
+        }
+        throw new IllegalArgumentException("unknown field: " + config);
+    }
+
+    static String getName() {
+        return "marker";
+    }
+
+    @Override
+    public boolean isResolvable(final LogEvent logEvent) {
+        return logEvent.getMarker() != null;
+    }
+
+    @Override
+    public void resolve(
+            final LogEvent logEvent,
+            final JsonWriter jsonWriter) {
+        internalResolver.resolve(logEvent, jsonWriter);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MarkerResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MarkerResolverFactory.java
new file mode 100644
index 0000000..2d4a2cb
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MarkerResolverFactory.java
@@ -0,0 +1,41 @@
+/*
+ * 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.json.template.resolver;
+
+final class MarkerResolverFactory implements EventResolverFactory<MarkerResolver> {
+
+    private static final MarkerResolverFactory INSTANCE = new MarkerResolverFactory();
+
+    static MarkerResolverFactory getInstance() {
+        return INSTANCE;
+    }
+
+    private MarkerResolverFactory() {}
+
+    @Override
+    public String getName() {
+        return MarkerResolver.getName();
+    }
+
+    @Override
+    public MarkerResolver create(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
+        return new MarkerResolver(config);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MessageResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MessageResolver.java
new file mode 100644
index 0000000..53dc7d9
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MessageResolver.java
@@ -0,0 +1,223 @@
+/*
+ * 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.json.template.resolver;
+
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+import org.apache.logging.log4j.message.MapMessage;
+import org.apache.logging.log4j.message.Message;
+import org.apache.logging.log4j.message.MultiformatMessage;
+import org.apache.logging.log4j.message.ObjectMessage;
+import org.apache.logging.log4j.message.SimpleMessage;
+import org.apache.logging.log4j.util.StringBuilderFormattable;
+
+/**
+ * {@link Message} resolver.
+ *
+ * <h3>Configuration</h3>
+ *
+ * <pre>
+ * config      = [ stringified ] , [ fallbackKey ]
+ * stringified = "stringified" -> boolean
+ * fallbackKey = "fallbackKey" -> string
+ * </pre>
+ *
+ * <h3>Examples</h3>
+ *
+ * Resolve the message into a string:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "message",
+ *   "stringified": true
+ * }
+ * </pre>
+ *
+ * Resolve the message such that if it is a {@link ObjectMessage} or {@link
+ * MultiformatMessage} with JSON support, its emitted JSON type (string, list,
+ * object, etc.) will be retained:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "message"
+ * }
+ * </pre>
+ *
+ * Given the above configuration, a {@link SimpleMessage} will generate a
+ * <tt>"sample log message"</tt>, whereas a {@link MapMessage} will generate a
+ * <tt>{"action": "login", "sessionId": "87asd97a"}</tt>. Certain indexed log
+ * storage systems (e.g., <a
+ * href="https://www.elastic.co/elasticsearch/">Elasticsearch</a>) will not
+ * allow both values to coexist due to type mismatch: one is a <tt>string</tt>
+ * while the other is an <tt>object</tt>. Here one can use a
+ * <tt>fallbackKey</tt> to work around the problem:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "message",
+ *   "fallbackKey": "formattedMessage"
+ * }
+ * </pre>
+ *
+ * Using this configuration, a {@link SimpleMessage} will generate a
+ * <tt>{"formattedMessage": "sample log message"}</tt> and a {@link MapMessage}
+ * will generate a <tt>{"action": "login", "sessionId": "87asd97a"}</tt>. Note
+ * that both emitted JSONs are of type <tt>object</tt> and have no
+ * type-conflicting fields.
+ */
+final class MessageResolver implements EventResolver {
+
+    private static final String[] FORMATS = { "JSON" };
+
+    private final EventResolver internalResolver;
+
+    MessageResolver(final TemplateResolverConfig config) {
+        this.internalResolver = createInternalResolver(config);
+    }
+
+    static String getName() {
+        return "message";
+    }
+
+    private static EventResolver createInternalResolver(
+            final TemplateResolverConfig config) {
+        final boolean stringified = config.getBoolean("stringified", false);
+        final String fallbackKey = config.getString("fallbackKey");
+        if (stringified && fallbackKey != null) {
+            throw new IllegalArgumentException(
+                    "fallbackKey is not allowed when stringified is enable: " + config);
+        }
+        return stringified
+                ? createStringResolver(fallbackKey)
+                : createObjectResolver(fallbackKey);
+    }
+
+    @Override
+    public void resolve(
+            final LogEvent logEvent,
+            final JsonWriter jsonWriter) {
+        internalResolver.resolve(logEvent, jsonWriter);
+    }
+
+    private static EventResolver createStringResolver(final String fallbackKey) {
+        return (final LogEvent logEvent, final JsonWriter jsonWriter) ->
+                resolveString(fallbackKey, logEvent, jsonWriter);
+    }
+
+    private static void resolveString(
+            final String fallbackKey,
+            final LogEvent logEvent,
+            final JsonWriter jsonWriter) {
+        final Message message = logEvent.getMessage();
+        resolveString(fallbackKey, message, jsonWriter);
+    }
+
+    private static void resolveString(
+            final String fallbackKey,
+            final Message message,
+            final JsonWriter jsonWriter) {
+        if (fallbackKey != null) {
+            jsonWriter.writeObjectStart();
+            jsonWriter.writeObjectKey(fallbackKey);
+        }
+        if (message instanceof StringBuilderFormattable) {
+            final StringBuilderFormattable formattable =
+                    (StringBuilderFormattable) message;
+            jsonWriter.writeString(formattable);
+        } else {
+            final String formattedMessage = message.getFormattedMessage();
+            jsonWriter.writeString(formattedMessage);
+        }
+        if (fallbackKey != null) {
+            jsonWriter.writeObjectEnd();
+        }
+    }
+
+    private static EventResolver createObjectResolver(final String fallbackKey) {
+        return (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
+
+            // Skip custom serializers for SimpleMessage.
+            final Message message = logEvent.getMessage();
+            final boolean simple = message instanceof SimpleMessage;
+            if (!simple) {
+
+                // Try MultiformatMessage serializer.
+                if (writeMultiformatMessage(jsonWriter, message)) {
+                    return;
+                }
+
+                // Try ObjectMessage serializer.
+                if (writeObjectMessage(jsonWriter, message)) {
+                    return;
+                }
+
+            }
+
+            // Fallback to plain String serializer.
+            resolveString(fallbackKey, logEvent, jsonWriter);
+
+        };
+    }
+
+    private static boolean writeMultiformatMessage(
+            final JsonWriter jsonWriter,
+            final Message message) {
+
+        // Check type.
+        if (!(message instanceof MultiformatMessage)) {
+            return false;
+        }
+        final MultiformatMessage multiformatMessage = (MultiformatMessage) message;
+
+        // Check formatter's JSON support.
+        boolean jsonSupported = false;
+        final String[] formats = multiformatMessage.getFormats();
+        for (final String format : formats) {
+            if (FORMATS[0].equalsIgnoreCase(format)) {
+                jsonSupported = true;
+                break;
+            }
+        }
+        if (!jsonSupported) {
+            return false;
+        }
+
+        // Write the formatted JSON.
+        final String messageJson = multiformatMessage.getFormattedMessage(FORMATS);
+        jsonWriter.writeRawString(messageJson);
+        return true;
+
+    }
+
+    private static boolean writeObjectMessage(
+            final JsonWriter jsonWriter,
+            final Message message) {
+
+        // Check type.
+        if (!(message instanceof ObjectMessage)) {
+            return false;
+        }
+
+        // Serialize object.
+        final ObjectMessage objectMessage = (ObjectMessage) message;
+        final Object object = objectMessage.getParameter();
+        jsonWriter.writeValue(object);
+        return true;
+
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MessageResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MessageResolverFactory.java
new file mode 100644
index 0000000..4d46bb5
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MessageResolverFactory.java
@@ -0,0 +1,41 @@
+/*
+ * 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.json.template.resolver;
+
+final class MessageResolverFactory implements EventResolverFactory<MessageResolver> {
+
+    private static final MessageResolverFactory INSTANCE = new MessageResolverFactory();
+
+    private MessageResolverFactory() {}
+
+    static MessageResolverFactory getInstance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public String getName() {
+        return MessageResolver.getName();
+    }
+
+    @Override
+    public MessageResolver create(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
+        return new MessageResolver(config);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/PatternResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/PatternResolver.java
new file mode 100644
index 0000000..a18a85d
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/PatternResolver.java
@@ -0,0 +1,87 @@
+/*
+ * 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.json.template.resolver;
+
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.layout.PatternLayout;
+import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+import org.apache.logging.log4j.util.BiConsumer;
+import org.apache.logging.log4j.util.Strings;
+
+import java.util.Optional;
+
+/**
+ * Resolver delegating to {@link PatternLayout}.
+ *
+ * <h3>Configuration</h3>
+ *
+ * <pre>
+ * config            = pattern , [ stackTraceEnabled ]
+ * pattern           = "pattern" -> string
+ * stackTraceEnabled = "stackTraceEnabled" -> boolean
+ * </pre>
+ *
+ * The default value of <tt>stackTraceEnabled</tt> is inherited from the parent
+ * {@link org.apache.logging.log4j.layout.json.template.JsonTemplateLayout}.
+ *
+ * <h3>Examples</h3>
+ *
+ * Resolve the string produced by <tt>%p %c{1.} [%t] %X{userId} %X %m%ex</tt>
+ * pattern:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "pattern",
+ *   "pattern": "%p %c{1.} [%t] %X{userId} %X %m%ex"
+ * }
+ * </pre>
+ */
+final class PatternResolver implements EventResolver {
+
+    private final BiConsumer<StringBuilder, LogEvent> emitter;
+
+    PatternResolver(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
+        final String pattern = config.getString("pattern");
+        if (Strings.isBlank(pattern)) {
+            throw new IllegalArgumentException("blank pattern: " + config);
+        }
+        final boolean stackTraceEnabled = Optional
+                .ofNullable(config.getBoolean("stackTraceEnabled"))
+                .orElse(context.isStackTraceEnabled());
+        final PatternLayout patternLayout = PatternLayout
+                .newBuilder()
+                .withConfiguration(context.getConfiguration())
+                .withCharset(context.getCharset())
+                .withPattern(pattern)
+                .withAlwaysWriteExceptions(stackTraceEnabled)
+                .build();
+        this.emitter = (final StringBuilder stringBuilder, final LogEvent logEvent) ->
+                patternLayout.serialize(logEvent, stringBuilder);
+    }
+
+    static String getName() {
+        return "pattern";
+    }
+
+    @Override
+    public void resolve(final LogEvent logEvent, final JsonWriter jsonWriter) {
+        jsonWriter.writeString(emitter, logEvent);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/PatternResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/PatternResolverFactory.java
new file mode 100644
index 0000000..e3ddaf9
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/PatternResolverFactory.java
@@ -0,0 +1,41 @@
+/*
+ * 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.json.template.resolver;
+
+final class PatternResolverFactory implements EventResolverFactory<PatternResolver> {
+
+    private static final PatternResolverFactory INSTANCE = new PatternResolverFactory();
+
+    private PatternResolverFactory() {}
+
+    static PatternResolverFactory getInstance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public String getName() {
+        return PatternResolver.getName();
+    }
+
+    @Override
+    public PatternResolver create(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
+        return new PatternResolver(context, config);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/SourceResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/SourceResolver.java
new file mode 100644
index 0000000..8f857d0
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/SourceResolver.java
@@ -0,0 +1,148 @@
+/*
+ * 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.json.template.resolver;
+
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+
+/**
+ * Resolver for the {@link StackTraceElement} returned by {@link LogEvent#getSource()}.
+ *
+ * Note that this resolver is toggled by {@link
+ * org.apache.logging.log4j.layout.json.template.JsonTemplateLayout.Builder#setLocationInfoEnabled(boolean)}
+ * method.
+ *
+ * <h3>Configuration</h3>
+ *
+ * <pre>
+ * config = "field" -> (
+ *            "className"  |
+ *            "fileName"   |
+ *            "methodName" |
+ *            "lineNumber" )
+ * </pre>
+ *
+ * <h3>Examples</h3>
+ *
+ * Resolve the line number:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "source",
+ *   "field": "lineNumber"
+ * }
+ * </pre>
+ */
+final class SourceResolver implements EventResolver {
+
+    private static final EventResolver NULL_RESOLVER =
+            (final LogEvent value, final JsonWriter jsonWriter) ->
+                    jsonWriter.writeNull();
+
+    private static final EventResolver CLASS_NAME_RESOLVER =
+            (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
+                final StackTraceElement logEventSource = logEvent.getSource();
+                if (logEventSource == null) {
+                    jsonWriter.writeNull();
+                } else {
+                    final String sourceClassName = logEventSource.getClassName();
+                    jsonWriter.writeString(sourceClassName);
+                }
+            };
+
+    private static final EventResolver FILE_NAME_RESOLVER =
+            (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
+                final StackTraceElement logEventSource = logEvent.getSource();
+                if (logEventSource == null) {
+                    jsonWriter.writeNull();
+                } else {
+                    final String sourceFileName = logEventSource.getFileName();
+                    jsonWriter.writeString(sourceFileName);
+                }
+            };
+
+    private static final EventResolver LINE_NUMBER_RESOLVER =
+            (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
+                final StackTraceElement logEventSource = logEvent.getSource();
+                if (logEventSource == null) {
+                    jsonWriter.writeNull();
+                } else {
+                    final int sourceLineNumber = logEventSource.getLineNumber();
+                    jsonWriter.writeNumber(sourceLineNumber);
+                }
+            };
+
+    private static final EventResolver METHOD_NAME_RESOLVER =
+            (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
+                final StackTraceElement logEventSource = logEvent.getSource();
+                if (logEventSource == null) {
+                    jsonWriter.writeNull();
+                } else {
+                    final String sourceMethodName = logEventSource.getMethodName();
+                    jsonWriter.writeString(sourceMethodName);
+                }
+            };
+
+    private final boolean locationInfoEnabled;
+
+    private final EventResolver internalResolver;
+
+    SourceResolver(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
+        this.locationInfoEnabled = context.isLocationInfoEnabled();
+        this.internalResolver = createInternalResolver(context, config);
+    }
+
+    private static EventResolver createInternalResolver(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
+        if (!context.isLocationInfoEnabled()) {
+            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;
+        }
+        throw new IllegalArgumentException("unknown field: " + config);
+    }
+
+    static String getName() {
+        return "source";
+    }
+
+    @Override
+    public boolean isResolvable() {
+        return locationInfoEnabled;
+    }
+
+    @Override
+    public boolean isResolvable(final LogEvent logEvent) {
+        return locationInfoEnabled && logEvent.getSource() != null;
+    }
+
+    @Override
+    public void resolve(
+            final LogEvent logEvent,
+            final JsonWriter jsonWriter) {
+        internalResolver.resolve(logEvent, jsonWriter);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/SourceResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/SourceResolverFactory.java
new file mode 100644
index 0000000..3f1e957
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/SourceResolverFactory.java
@@ -0,0 +1,41 @@
+/*
+ * 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.json.template.resolver;
+
+final class SourceResolverFactory implements EventResolverFactory<SourceResolver> {
+
+    private static final SourceResolverFactory INSTANCE = new SourceResolverFactory();
+
+    private SourceResolverFactory() {}
+
+    static SourceResolverFactory getInstance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public String getName() {
+        return SourceResolver.getName();
+    }
+
+    @Override
+    public SourceResolver create(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
+        return new SourceResolver(context, config);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceElementObjectResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceElementObjectResolver.java
new file mode 100644
index 0000000..1c8a483
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceElementObjectResolver.java
@@ -0,0 +1,92 @@
+/*
+ * 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.json.template.resolver;
+
+import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+
+/**
+ * {@link StackTraceElement} resolver.
+ *
+ * <h3>Configuration</h3>
+ *
+ * <pre>
+ * config = "field" -> (
+ *            "className"  |
+ *            "fileName"   |
+ *            "methodName" |
+ *            "lineNumber" )
+ * </pre>
+ *
+ * <h3>Examples</h3>
+ *
+ * Resolve the line number:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "source",
+ *   "field": "lineNumber"
+ * }
+ * </pre>
+ */
+final class StackTraceElementObjectResolver implements TemplateResolver<StackTraceElement> {
+
+    private static final TemplateResolver<StackTraceElement> CLASS_NAME_RESOLVER =
+            (final StackTraceElement stackTraceElement, final JsonWriter jsonWriter) ->
+                    jsonWriter.writeString(stackTraceElement.getClassName());
+
+    private static final TemplateResolver<StackTraceElement> METHOD_NAME_RESOLVER =
+            (final StackTraceElement stackTraceElement, final JsonWriter jsonWriter) ->
+                    jsonWriter.writeString(stackTraceElement.getMethodName());
+
+    private static final TemplateResolver<StackTraceElement> FILE_NAME_RESOLVER =
+            (final StackTraceElement stackTraceElement, final JsonWriter jsonWriter) ->
+                    jsonWriter.writeString(stackTraceElement.getFileName());
+
+    private static final TemplateResolver<StackTraceElement> LINE_NUMBER_RESOLVER =
+            (final StackTraceElement stackTraceElement, final JsonWriter jsonWriter) ->
+                    jsonWriter.writeNumber(stackTraceElement.getLineNumber());
+
+    private final TemplateResolver<StackTraceElement> internalResolver;
+
+    StackTraceElementObjectResolver(final TemplateResolverConfig config) {
+        this.internalResolver = createInternalResolver(config);
+    }
+
+    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;
+        }
+        throw new IllegalArgumentException("unknown field: " + config);
+    }
+
+    static String getName() {
+        return "stackTraceElement";
+    }
+
+    @Override
+    public void resolve(
+            final StackTraceElement stackTraceElement,
+            final JsonWriter jsonWriter) {
+        internalResolver.resolve(stackTraceElement, jsonWriter);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceElementObjectResolverContext.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceElementObjectResolverContext.java
new file mode 100644
index 0000000..6e42237
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceElementObjectResolverContext.java
@@ -0,0 +1,93 @@
+/*
+ * 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.json.template.resolver;
+
+import org.apache.logging.log4j.core.lookup.StrSubstitutor;
+import org.apache.logging.log4j.layout.json.template.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-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceElementObjectResolverFactories.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceElementObjectResolverFactories.java
new file mode 100644
index 0000000..90af9ab
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceElementObjectResolverFactories.java
@@ -0,0 +1,39 @@
+/*
+ * 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.json.template.resolver;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+enum 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-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceElementObjectResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceElementObjectResolverFactory.java
new file mode 100644
index 0000000..a07694c
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceElementObjectResolverFactory.java
@@ -0,0 +1,43 @@
+/*
+ * 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.json.template.resolver;
+
+final class StackTraceElementObjectResolverFactory
+        implements TemplateResolverFactory<StackTraceElement, StackTraceElementObjectResolverContext, StackTraceElementObjectResolver> {
+
+    private static final StackTraceElementObjectResolverFactory INSTANCE =
+            new StackTraceElementObjectResolverFactory();
+
+    private StackTraceElementObjectResolverFactory() {}
+
+    public static StackTraceElementObjectResolverFactory getInstance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public String getName() {
+        return StackTraceElementObjectResolver.getName();
+    }
+
+    @Override
+    public StackTraceElementObjectResolver create(
+            final StackTraceElementObjectResolverContext context,
+            final TemplateResolverConfig config) {
+        return new StackTraceElementObjectResolver(config);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceObjectResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceObjectResolver.java
new file mode 100644
index 0000000..53a9ce4
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceObjectResolver.java
@@ -0,0 +1,54 @@
+/*
+ * 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.json.template.resolver;
+
+import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+
+final class StackTraceObjectResolver implements StackTraceResolver {
+
+    private final TemplateResolver<StackTraceElement> stackTraceElementResolver;
+
+    StackTraceObjectResolver(final TemplateResolver<StackTraceElement> stackTraceElementResolver) {
+        this.stackTraceElementResolver = stackTraceElementResolver;
+    }
+
+    @Override
+    public void resolve(
+            final Throwable throwable,
+            final JsonWriter jsonWriter) {
+        // Following check against the stacktrace element count is not
+        // implemented in isResolvable(), since Throwable#getStackTrace() incurs
+        // a significant cloning cost.
+        final StackTraceElement[] stackTraceElements = throwable.getStackTrace();
+        if (stackTraceElements.length  == 0) {
+            jsonWriter.writeNull();
+        } else {
+            jsonWriter.writeArrayStart();
+            for (int stackTraceElementIndex = 0;
+                 stackTraceElementIndex < stackTraceElements.length;
+                 stackTraceElementIndex++) {
+                if (stackTraceElementIndex > 0) {
+                    jsonWriter.writeSeparator();
+                }
+                final StackTraceElement stackTraceElement = stackTraceElements[stackTraceElementIndex];
+                stackTraceElementResolver.resolve(stackTraceElement, jsonWriter);
+            }
+            jsonWriter.writeArrayEnd();
+        }
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceResolver.java
new file mode 100644
index 0000000..8275193
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceResolver.java
@@ -0,0 +1,19 @@
+/*
+ * 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.json.template.resolver;
+
+interface StackTraceResolver extends TemplateResolver<Throwable> {}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceStringResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceStringResolver.java
new file mode 100644
index 0000000..d744070
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceStringResolver.java
@@ -0,0 +1,51 @@
+/*
+ * 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.json.template.resolver;
+
+import org.apache.logging.log4j.layout.json.template.util.TruncatingBufferedPrintWriter;
+import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+import org.apache.logging.log4j.layout.json.template.util.Recycler;
+
+import java.util.function.Supplier;
+
+final class StackTraceStringResolver implements StackTraceResolver {
+
+    private final Recycler<TruncatingBufferedPrintWriter> writerRecycler;
+
+    StackTraceStringResolver(final EventResolverContext context) {
+        final Supplier<TruncatingBufferedPrintWriter> writerSupplier =
+                () -> TruncatingBufferedPrintWriter.ofCapacity(
+                        context.getMaxStringByteCount());
+        this.writerRecycler = context
+                .getRecyclerFactory()
+                .create(writerSupplier, TruncatingBufferedPrintWriter::close);
+    }
+
+    @Override
+    public void resolve(
+            final Throwable throwable,
+            final JsonWriter jsonWriter) {
+        final TruncatingBufferedPrintWriter writer = writerRecycler.acquire();
+        try {
+            throwable.printStackTrace(writer);
+            jsonWriter.writeString(writer.getBuffer(), 0, writer.getPosition());
+        } finally {
+            writerRecycler.release(writer);
+        }
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolver.java
new file mode 100644
index 0000000..a251075
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolver.java
@@ -0,0 +1,42 @@
+/*
+ * 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.json.template.resolver;
+
+import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+
+@FunctionalInterface
+public interface TemplateResolver<V> {
+
+    default boolean isFlattening() {
+        return false;
+    }
+
+    default boolean isResolvable() {
+        return true;
+    }
+
+    default boolean isResolvable(V value) {
+        return true;
+    }
+
+    void resolve(V value, JsonWriter jsonWriter);
+
+    default void resolve(V value, JsonWriter jsonWriter, boolean succeedingEntry) {
+        resolve(value, jsonWriter);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolverConfig.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolverConfig.java
new file mode 100644
index 0000000..a83fffa
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolverConfig.java
@@ -0,0 +1,29 @@
+/*
+ * 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.json.template.resolver;
+
+import org.apache.logging.log4j.layout.json.template.util.MapAccessor;
+
+import java.util.Map;
+
+class TemplateResolverConfig extends MapAccessor {
+
+    TemplateResolverConfig(final Map<String, Object> map) {
+        super(map);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolverContext.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolverContext.java
new file mode 100644
index 0000000..74687d2
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolverContext.java
@@ -0,0 +1,34 @@
+/*
+ * 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.json.template.resolver;
+
+import org.apache.logging.log4j.core.lookup.StrSubstitutor;
+import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+
+import java.util.Map;
+
+interface TemplateResolverContext<V, C extends TemplateResolverContext<V, C>> {
+
+    Class<C> getContextClass();
+
+    Map<String, TemplateResolverFactory<V, C, ? extends TemplateResolver<V>>> getResolverFactoryByName();
+
+    StrSubstitutor getSubstitutor();
+
+    JsonWriter getJsonWriter();
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolverFactory.java
new file mode 100644
index 0000000..3e3c8ef
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolverFactory.java
@@ -0,0 +1,25 @@
+/*
+ * 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.json.template.resolver;
+
+interface TemplateResolverFactory<V, C extends TemplateResolverContext<V, C>, R extends TemplateResolver<V>> {
+
+    String getName();
+
+    R create(C context, TemplateResolverConfig config);
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolvers.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolvers.java
new file mode 100644
index 0000000..a4b1165
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolvers.java
@@ -0,0 +1,414 @@
+/*
+ * 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.json.template.resolver;
+
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.layout.json.template.JsonTemplateLayout.EventTemplateAdditionalField;
+import org.apache.logging.log4j.layout.json.template.util.JsonReader;
+import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public enum TemplateResolvers {;
+
+    private static final String RESOLVER_FIELD_NAME = "$resolver";
+
+    private static abstract class UnresolvableTemplateResolver
+            implements TemplateResolver<Object> {
+
+        @Override
+        public final boolean isResolvable() {
+            return false;
+        }
+
+        @Override
+        public final boolean isResolvable(Object value) {
+            return false;
+        }
+
+    }
+
+    private static final TemplateResolver<?> EMPTY_ARRAY_RESOLVER =
+            new UnresolvableTemplateResolver() {
+                @Override
+                public void resolve(final Object value, final JsonWriter jsonWriter) {
+                    jsonWriter.writeArrayStart();
+                    jsonWriter.writeArrayEnd();
+                }
+            };
+
+    private static final TemplateResolver<?> EMPTY_OBJECT_RESOLVER =
+            new UnresolvableTemplateResolver() {
+                @Override
+                public void resolve(final Object value, final JsonWriter jsonWriter) {
+                    jsonWriter.writeObjectStart();
+                    jsonWriter.writeObjectEnd();
+                }
+            };
+
+    private static final TemplateResolver<?> NULL_RESOLVER =
+            new UnresolvableTemplateResolver() {
+                @Override
+                public void resolve(final Object value, final JsonWriter jsonWriter) {
+                    jsonWriter.writeNull();
+                }
+            };
+
+    public static <V, C extends TemplateResolverContext<V, C>> TemplateResolver<V> ofTemplate(
+            final C context,
+            final String template) {
+
+        // Read the template.
+        final Object node;
+        try {
+            node = JsonReader.read(template);
+        } catch (final Exception error) {
+            final String message = String.format("failed parsing template (template=%s)", template);
+            throw new RuntimeException(message, error);
+        }
+
+        // Append the additional fields.
+        if (context instanceof EventResolverContext) {
+            final EventResolverContext eventResolverContext = (EventResolverContext) context;
+            final EventTemplateAdditionalField[] additionalFields = eventResolverContext.getAdditionalFields();
+            appendAdditionalFields(node, additionalFields);
+        }
+
+        // Resolve the template.
+        return ofObject(context, node);
+
+    }
+
+    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.getType()) {
+                    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 type %s for additional field: %s",
+                                additionalFieldKey, additionalField.getType());
+                        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) {
+        if (object == null) {
+            @SuppressWarnings("unchecked")
+            final TemplateResolver<V> nullResolver = (TemplateResolver<V>) NULL_RESOLVER;
+            return nullResolver;
+        } else if (object instanceof List) {
+            @SuppressWarnings("unchecked")
+            final List<Object> list = (List<Object>) object;
+            return ofList(context, list);
+        } else if (object instanceof Map) {
+            @SuppressWarnings("unchecked")
+            final Map<String, Object> map = (Map<String, Object>) object;
+            return ofMap(context, map);
+        } else if (object instanceof String) {
+            final String string = (String) object;
+            return ofString(context, string);
+        } else if (object instanceof Number) {
+            final Number number = (Number) object;
+            return ofNumber(number);
+        } else if (object instanceof Boolean) {
+            final boolean value = (boolean) object;
+            return ofBoolean(value);
+        } else {
+            final String message = String.format(
+                    "invalid JSON node type (class=%s)",
+                    object.getClass().getName());
+            throw new IllegalArgumentException(message);
+        }
+    }
+
+    private static <V, C extends TemplateResolverContext<V, C>> TemplateResolver<V> ofList(
+            final C context,
+            final List<Object> list) {
+
+        // Create resolver for each children.
+        final List<TemplateResolver<V>> itemResolvers = list
+                .stream()
+                .map(item -> {
+                    final TemplateResolver<V> itemResolver = ofObject(context, item);
+                    if (itemResolver.isFlattening()) {
+                        throw new IllegalArgumentException(
+                                "flattening resolvers are not allowed in lists");
+                    }
+                    return itemResolver;
+                })
+                .collect(Collectors.toList());
+
+        // Short-circuit if the array is empty.
+        if (itemResolvers.isEmpty()) {
+            @SuppressWarnings("unchecked")
+            final TemplateResolver<V> emptyArrayResolver =
+                    (TemplateResolver<V>) EMPTY_ARRAY_RESOLVER;
+            return emptyArrayResolver;
+        }
+
+        // Create a parent resolver collecting each child resolver execution.
+        return (final V value, final JsonWriter jsonWriter) -> {
+            jsonWriter.writeArrayStart();
+            for (int itemResolverIndex = 0;
+                 itemResolverIndex < itemResolvers.size();
+                 itemResolverIndex++) {
+                if (itemResolverIndex > 0) {
+                    jsonWriter.writeSeparator();
+                }
+                final TemplateResolver<V> itemResolver = itemResolvers.get(itemResolverIndex);
+                itemResolver.resolve(value, jsonWriter);
+            }
+            jsonWriter.writeArrayEnd();
+        };
+
+    }
+
+    private static <V, C extends TemplateResolverContext<V, C>> TemplateResolver<V> ofMap(
+            final C context,
+            final Map<String, Object> map) {
+
+        // Check if this is a resolver request.
+        if (map.containsKey(RESOLVER_FIELD_NAME)) {
+            return ofResolver(context, map);
+        }
+
+        // Create resolver for each object field.
+        final List<String> fieldNames = new ArrayList<>();
+        final List<TemplateResolver<V>> fieldResolvers = new ArrayList<>();
+        map.forEach((fieldName, fieldValue) -> {
+            final TemplateResolver<V> fieldResolver = ofObject(context, fieldValue);
+            final boolean resolvable = fieldResolver.isResolvable();
+            if (resolvable) {
+                fieldNames.add(fieldName);
+                fieldResolvers.add(fieldResolver);
+            }
+        });
+
+        // Short-circuit if the object is empty.
+        final int fieldCount = fieldNames.size();
+        if (fieldCount == 0) {
+            @SuppressWarnings("unchecked")
+            final TemplateResolver<V> emptyObjectResolver =
+                    (TemplateResolver<V>) EMPTY_OBJECT_RESOLVER;
+            return emptyObjectResolver;
+        }
+
+        // Prepare field names to avoid escape and truncation costs at runtime.
+        final List<String> fieldPrefixes = fieldNames
+                .stream()
+                .map(fieldName -> {
+                    try (JsonWriter jsonWriter = context.getJsonWriter()) {
+                        jsonWriter.writeString(fieldName);
+                        jsonWriter.getStringBuilder().append(':');
+                        return jsonWriter.getStringBuilder().toString();
+                    }
+                })
+                .collect(Collectors.toList());
+
+        return new TemplateResolver<V>() {
+
+            @Override
+            public boolean isResolvable() {
+                // We have already excluded unresolvable ones while collecting
+                // the resolvers. Hence it is safe to return true here.
+                return true;
+            }
+
+            /**
+             * The parent resolver checking if each child is resolvable given
+             * the passed {@code value}.
+             *
+             * This is an optimization to skip the rendering of a parent if all
+             * its children are not resolvable given the passed {@code value}.
+             */
+            @Override
+            public boolean isResolvable(final V value) {
+                for (int fieldIndex = 0; fieldIndex < fieldCount; fieldIndex++) {
+                    final TemplateResolver<V> fieldResolver = fieldResolvers.get(fieldIndex);
+                    final boolean resolvable = fieldResolver.isResolvable(value);
+                    if (resolvable) {
+                        return true;
+                    }
+                }
+                return false;
+            }
+
+            /**
+             * The parent resolver combining all child resolver executions.
+              */
+            @Override
+            public void resolve(final V value, final JsonWriter jsonWriter) {
+                final StringBuilder jsonWriterStringBuilder = jsonWriter.getStringBuilder();
+                jsonWriter.writeObjectStart();
+                for (int resolvedFieldCount = 0, fieldIndex = 0; fieldIndex < fieldCount; fieldIndex++) {
+                    final TemplateResolver<V> fieldResolver = fieldResolvers.get(fieldIndex);
+                    final boolean resolvable = fieldResolver.isResolvable(value);
+                    if (!resolvable) {
+                        continue;
+                    }
+                    final boolean succeedingEntry = resolvedFieldCount > 0;
+                    final boolean flattening = fieldResolver.isFlattening();
+                    if (flattening) {
+                        final int initLength = jsonWriterStringBuilder.length();
+                        fieldResolver.resolve(value, jsonWriter, succeedingEntry);
+                        final boolean resolved = jsonWriterStringBuilder.length() > initLength;
+                        if (resolved) {
+                            resolvedFieldCount++;
+                        }
+                    } else {
+                        if (succeedingEntry) {
+                            jsonWriter.writeSeparator();
+                        }
+                        final String fieldPrefix = fieldPrefixes.get(fieldIndex);
+                        jsonWriter.writeRawString(fieldPrefix);
+                        fieldResolver.resolve(value, jsonWriter, succeedingEntry);
+                        resolvedFieldCount++;
+                    }
+                }
+                jsonWriter.writeObjectEnd();
+            }
+
+        };
+
+    }
+
+    private static <V, C extends TemplateResolverContext<V, C>> TemplateResolver<V> ofResolver(
+            final C context,
+            final Map<String, Object> map) {
+
+        // Extract the resolver name.
+        final Object resolverNameObject = map.get(RESOLVER_FIELD_NAME);
+        if (!(resolverNameObject instanceof String)) {
+            throw new IllegalArgumentException(
+                    "invalid resolver name: " + resolverNameObject);
+        }
+        final String resolverName = (String) resolverNameObject;
+
+        // Retrieve the resolver.
+        final TemplateResolverFactory<V, C, ? extends TemplateResolver<V>> resolverFactory =
+                context.getResolverFactoryByName().get(resolverName);
+        if (resolverFactory == null) {
+            throw new IllegalArgumentException("unknown resolver: " + resolverName);
+        }
+        final TemplateResolverConfig resolverConfig = new TemplateResolverConfig(map);
+        return resolverFactory.create(context, resolverConfig);
+
+    }
+
+    private static <V, C extends TemplateResolverContext<V, C>> TemplateResolver<V> ofString(
+            final C context,
+            final String fieldValue) {
+
+        // Check if substitution needed at all. (Copied logic from
+        // AbstractJacksonLayout.valueNeedsLookup() method.)
+        final boolean substitutionNeeded = fieldValue.contains("${");
+        final JsonWriter contextJsonWriter = context.getJsonWriter();
+        if (substitutionNeeded) {
+
+            // 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 (replacedText == null) {
+                    // noinspection unchecked
+                    return (TemplateResolver<V>) NULL_RESOLVER;
+                } else {
+                    // Prepare the escaped replacement first.
+                    final String escapedReplacedText =
+                            contextJsonWriter.use(() ->
+                                    contextJsonWriter.writeString(replacedText));
+                    // Create a resolver dedicated to the escaped replacement.
+                    return (final V value, final JsonWriter jsonWriter) ->
+                            jsonWriter.writeRawString(escapedReplacedText);
+                }
+            }
+
+        }
+
+        // Write the field value as is.
+        else {
+            final String escapedFieldValue =
+                    contextJsonWriter.use(() ->
+                            contextJsonWriter.writeString(fieldValue));
+            return (final V value, final JsonWriter jsonWriter) ->
+                    jsonWriter.writeRawString(escapedFieldValue);
+        }
+
+    }
+
+    private static <V> TemplateResolver<V> ofNumber(final Number number) {
+        final String numberString = String.valueOf(number);
+        return (final V ignored, final JsonWriter jsonWriter) ->
+                jsonWriter.writeRawString(numberString);
+    }
+
+    private static <V> TemplateResolver<V> ofBoolean(final boolean value) {
+        return (final V ignored, final JsonWriter jsonWriter) ->
+                jsonWriter.writeBoolean(value);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadContextDataResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadContextDataResolver.java
new file mode 100644
index 0000000..66efe17
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadContextDataResolver.java
@@ -0,0 +1,357 @@
+/*
+ * 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.json.template.resolver;
+
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+import org.apache.logging.log4j.layout.json.template.util.Recycler;
+import org.apache.logging.log4j.layout.json.template.util.RecyclerFactory;
+import org.apache.logging.log4j.util.ReadOnlyStringMap;
+import org.apache.logging.log4j.util.TriConsumer;
+
+import java.util.Map;
+import java.util.regex.Pattern;
+
+/**
+ * Mapped Diagnostic Context (MDC), aka. Thread Context Data, resolver.
+ *
+ * <h3>Configuration</h3>
+ *
+ * <pre>
+ * config        = singleAccess | multiAccess
+ *
+ * singleAccess  = key , [ stringified ]
+ * key           = "key" -> string
+ * stringified   = "stringified" -> boolean
+ *
+ * multiAccess   = [ pattern ] , [ flatten ] , [ stringified ]
+ * pattern       = "pattern" -> string
+ * flatten       = "flatten" -> ( boolean | flattenConfig )
+ * flattenConfig = [ flattenPrefix ]
+ * flattenPrefix = "prefix" -> string
+ * </pre>
+ *
+ * Note that <tt>singleAccess</tt> resolves the MDC value as is, whilst
+ * <tt>multiAccess</tt> resolves a multitude of MDC values. If <tt>flatten</tt>
+ * is provided, <tt>multiAccess</tt> merges the values with the parent,
+ * otherwise creates a new JSON object containing the values.
+ *
+ * <h3>Examples</h3>
+ *
+ * Resolve the <tt>userRole</tt> MDC value:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "mdc",
+ *   "key": "userRole"
+ * }
+ * </pre>
+ *
+ * Resolve the string representation of the <tt>userRank</tt> MDC value:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "mdc",
+ *   "key": "userRank",
+ *   "stringified": true
+ * }
+ * </pre>
+ *
+ * Resolve all MDC entries into an object:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "mdc"
+ * }
+ * </pre>
+ *
+ * Resolve all MDC entries into an object such that values are converted to
+ * string:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "mdc",
+ *   "stringified": true
+ * }
+ * </pre>
+ *
+ * Merge all MDC entries whose keys are matching with the
+ * <tt>user(Role|Rank)</tt> regex into the parent:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "mdc",
+ *   "flatten": true,
+ *   "pattern": "user(Role|Rank)"
+ * }
+ * </pre>
+ *
+ * After converting the corresponding entries to string, merge all MDC entries
+ * to parent such that keys are prefixed with <tt>_</tt>:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "mdc",
+ *   "stringified": true,
+ *   "flatten": {
+ *     "prefix": "_"
+ *   }
+ * }
+ * </pre>
+ */
+final class ThreadContextDataResolver implements EventResolver {
+
+    private final EventResolver internalResolver;
+
+    ThreadContextDataResolver(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
+        this.internalResolver = createResolver(context, config);
+    }
+
+    private static EventResolver createResolver(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
+        final Object flattenObject = config.getObject("flatten");
+        final boolean flatten;
+        if (flattenObject == null) {
+            flatten = false;
+        } else if (flattenObject instanceof Boolean) {
+            flatten = (boolean) flattenObject;
+        } else if (flattenObject instanceof Map) {
+            flatten = true;
+        } else {
+            throw new IllegalArgumentException("invalid flatten option: " + config);
+        }
+        final String key = config.getString("key");
+        final String prefix = config.getString(new String[] {"flatten", "prefix"});
+        final String pattern = config.getString("pattern");
+        final boolean stringified = config.getBoolean("stringified", false);
+        if (key != null) {
+            if (flatten) {
+                throw new IllegalArgumentException(
+                        "both key and flatten options cannot be supplied: " + config);
+            }
+            return createKeyResolver(key, stringified);
+        } else {
+            final RecyclerFactory recyclerFactory = context.getRecyclerFactory();
+            return createResolver(recyclerFactory, flatten, prefix, pattern, stringified);
+        }
+    }
+
+    private static EventResolver createKeyResolver(
+            final String key,
+            final boolean stringified) {
+        return new EventResolver() {
+
+            @Override
+            public boolean isResolvable(final LogEvent logEvent) {
+                final ReadOnlyStringMap contextData = logEvent.getContextData();
+                return contextData != null && contextData.containsKey(key);
+            }
+
+            @Override
+            public void resolve(final LogEvent logEvent, final JsonWriter jsonWriter) {
+                final ReadOnlyStringMap contextData = logEvent.getContextData();
+                final Object value = contextData == null ? null : contextData.getValue(key);
+                if (stringified) {
+                    final String valueString = String.valueOf(value);
+                    jsonWriter.writeString(valueString);
+                } else {
+                    jsonWriter.writeValue(value);
+                }
+            }
+
+        };
+    }
+
+    private static EventResolver createResolver(
+            final RecyclerFactory recyclerFactory,
+            final boolean flatten,
+            final String prefix,
+            final String pattern,
+            final boolean stringified) {
+
+        // Compile the pattern.
+        final Pattern compiledPattern =
+                pattern == null
+                        ? null
+                        : Pattern.compile(pattern);
+
+        // Create the recycler for the loop context.
+        final Recycler<LoopContext> loopContextRecycler =
+                recyclerFactory.create(() -> {
+                    final LoopContext loopContext = new LoopContext();
+                    if (prefix != null) {
+                        loopContext.prefix = prefix;
+                        loopContext.prefixedKey = new StringBuilder(prefix);
+                    }
+                    loopContext.pattern = compiledPattern;
+                    loopContext.stringified = stringified;
+                    return loopContext;
+                });
+
+        // Create the resolver.
+        return createResolver(flatten, loopContextRecycler);
+
+    }
+
+    private static EventResolver createResolver(
+            final boolean flatten,
+            final Recycler<LoopContext> loopContextRecycler) {
+        return new EventResolver() {
+
+            @Override
+            public boolean isFlattening() {
+                return flatten;
+            }
+
+            @Override
+            public boolean isResolvable(final LogEvent logEvent) {
+                final ReadOnlyStringMap contextData = logEvent.getContextData();
+                return contextData != null && !contextData.isEmpty();
+            }
+
+            @Override
+            public void resolve(final LogEvent value, final JsonWriter jsonWriter) {
+                throw new UnsupportedOperationException();
+            }
+
+            @Override
+            public void resolve(
+                    final LogEvent logEvent,
+                    final JsonWriter jsonWriter,
+                    final boolean succeedingEntry) {
+
+                // Retrieve the context data.
+                final ReadOnlyStringMap contextData = logEvent.getContextData();
+                if (contextData == null || contextData.isEmpty()) {
+                    if (!flatten) {
+                        jsonWriter.writeNull();
+                    }
+                    return;
+                }
+
+                // Resolve the context data.
+                if (!flatten) {
+                    jsonWriter.writeObjectStart();
+                }
+                final LoopContext loopContext = loopContextRecycler.acquire();
+                loopContext.jsonWriter = jsonWriter;
+                loopContext.initJsonWriterStringBuilderLength = jsonWriter.getStringBuilder().length();
+                loopContext.succeedingEntry = flatten && succeedingEntry;
+                try {
+                    contextData.forEach(LoopMethod.INSTANCE, loopContext);
+                } finally {
+                    loopContextRecycler.release(loopContext);
+                }
+                if (!flatten) {
+                    jsonWriter.writeObjectEnd();
+                }
+
+            }
+
+        };
+    }
+
+    private static final class LoopContext {
+
+        private String prefix;
+
+        private StringBuilder prefixedKey;
+
+        private Pattern pattern;
+
+        private boolean stringified;
+
+        private JsonWriter jsonWriter;
+
+        private int initJsonWriterStringBuilderLength;
+
+        private boolean succeedingEntry;
+
+    }
+
+    private static final class LoopMethod implements TriConsumer<String, Object, LoopContext> {
+
+        private static final LoopMethod INSTANCE = new LoopMethod();
+
+        @Override
+        public void accept(
+                final String key,
+                final Object value,
+                final LoopContext loopContext) {
+            final boolean keyMatched =
+                    loopContext.pattern == null ||
+                            loopContext.pattern.matcher(key).matches();
+            if (keyMatched) {
+                final boolean succeedingEntry =
+                        loopContext.succeedingEntry ||
+                                loopContext.initJsonWriterStringBuilderLength <
+                                        loopContext.jsonWriter.getStringBuilder().length();
+                if (succeedingEntry) {
+                    loopContext.jsonWriter.writeSeparator();
+                }
+                if (loopContext.prefix == null) {
+                    loopContext.jsonWriter.writeObjectKey(key);
+                } else {
+                    loopContext.prefixedKey.setLength(loopContext.prefix.length());
+                    loopContext.prefixedKey.append(key);
+                    loopContext.jsonWriter.writeObjectKey(loopContext.prefixedKey);
+                }
+                if (loopContext.stringified && !(value instanceof String)) {
+                    final String valueString = String.valueOf(value);
+                    loopContext.jsonWriter.writeString(valueString);
+                } else {
+                    loopContext.jsonWriter.writeValue(value);
+                }
+            }
+        }
+
+    }
+
+    static String getName() {
+        return "mdc";
+    }
+
+    @Override
+    public boolean isFlattening() {
+        return internalResolver.isFlattening();
+    }
+
+    @Override
+    public boolean isResolvable(final LogEvent logEvent) {
+        final ReadOnlyStringMap contextData = logEvent.getContextData();
+        return contextData != null && !contextData.isEmpty();
+    }
+
+    @Override
+    public void resolve(
+            final LogEvent logEvent,
+            final JsonWriter jsonWriter) {
+        internalResolver.resolve(logEvent, jsonWriter);
+    }
+
+    @Override
+    public void resolve(
+            final LogEvent logEvent,
+            final JsonWriter jsonWriter,
+            final boolean succeedingEntry) {
+        internalResolver.resolve(logEvent, jsonWriter, succeedingEntry);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadContextDataResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadContextDataResolverFactory.java
new file mode 100644
index 0000000..3ef164d
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadContextDataResolverFactory.java
@@ -0,0 +1,43 @@
+/*
+ * 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.json.template.resolver;
+
+final class ThreadContextDataResolverFactory
+        implements EventResolverFactory<ThreadContextDataResolver> {
+
+    private static final ThreadContextDataResolverFactory INSTANCE =
+            new ThreadContextDataResolverFactory();
+
+    private ThreadContextDataResolverFactory() {}
+
+    static ThreadContextDataResolverFactory getInstance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public String getName() {
+        return ThreadContextDataResolver.getName();
+    }
+
+    @Override
+    public ThreadContextDataResolver create(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
+        return new ThreadContextDataResolver(context, config);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadContextStackResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadContextStackResolver.java
new file mode 100644
index 0000000..6a9af12
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadContextStackResolver.java
@@ -0,0 +1,107 @@
+/*
+ * 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.json.template.resolver;
+
+import org.apache.logging.log4j.ThreadContext;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+
+import java.util.Optional;
+import java.util.regex.Pattern;
+
+/**
+ * Nested Diagnostic Context (NDC), aka. Thread Context Stack, resolver.
+ *
+ * <h3>Configuration</h3>
+ *
+ * <pre>
+ * config  = [ pattern ]
+ * pattern = "pattern" -> string
+ * </pre>
+ *
+ * <h3>Examples</h3>
+ *
+ * Resolve all NDC values into a list:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "ndc"
+ * }
+ * </pre>
+ *
+ * Resolve all NDC values matching with the <tt>pattern</tt> regex:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "ndc",
+ *   "pattern": "user(Role|Rank):\\w+"
+ * }
+ * </pre>
+ */
+final class ThreadContextStackResolver implements EventResolver {
+
+    private final Pattern itemPattern;
+
+    ThreadContextStackResolver(final TemplateResolverConfig config) {
+        this.itemPattern = Optional
+                .ofNullable(config.getString("pattern"))
+                .map(Pattern::compile)
+                .orElse(null);
+    }
+
+    static String getName() {
+        return "ndc";
+    }
+
+    @Override
+    public boolean isResolvable(final LogEvent logEvent) {
+        final ThreadContext.ContextStack contextStack = logEvent.getContextStack();
+        return contextStack.getDepth() > 0;
+    }
+
+    @Override
+    public void resolve(
+            final LogEvent logEvent,
+            final JsonWriter jsonWriter) {
+        final ThreadContext.ContextStack contextStack = logEvent.getContextStack();
+        if (contextStack.getDepth() == 0) {
+            jsonWriter.writeNull();
+            return;
+        }
+        boolean arrayStarted = false;
+        for (final String contextStackItem : contextStack.asList()) {
+            final boolean matched =
+                    itemPattern == null ||
+                            itemPattern.matcher(contextStackItem).matches();
+            if (matched) {
+                if (arrayStarted) {
+                    jsonWriter.writeSeparator();
+                } else {
+                    jsonWriter.writeArrayStart();
+                    arrayStarted = true;
+                }
+                jsonWriter.writeString(contextStackItem);
+            }
+        }
+        if (arrayStarted) {
+            jsonWriter.writeArrayEnd();
+        } else {
+            jsonWriter.writeNull();
+        }
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadContextStackResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadContextStackResolverFactory.java
new file mode 100644
index 0000000..82a5c23
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadContextStackResolverFactory.java
@@ -0,0 +1,43 @@
+/*
+ * 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.json.template.resolver;
+
+final class ThreadContextStackResolverFactory
+        implements EventResolverFactory<ThreadContextStackResolver> {
+
+    private static final ThreadContextStackResolverFactory INSTANCE
+            = new ThreadContextStackResolverFactory();
+
+    private ThreadContextStackResolverFactory() {}
+
+    static ThreadContextStackResolverFactory getInstance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public String getName() {
+        return ThreadContextStackResolver.getName();
+    }
+
+    @Override
+    public ThreadContextStackResolver create(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
+        return new ThreadContextStackResolver(config);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadResolver.java
new file mode 100644
index 0000000..a316afe
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadResolver.java
@@ -0,0 +1,90 @@
+/*
+ * 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.json.template.resolver;
+
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+
+/**
+ * Thread resolver.
+ *
+ * <h3>Configuration</h3>
+ *
+ * <pre>
+ * config = "field" -> ( "name" | "id" | "priority" )
+ * </pre>
+ *
+ * <h3>Examples</h3>
+ *
+ * Resolve the thread name:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "thread",
+ *   "field": "name"
+ * }
+ * </pre>
+ */
+final class ThreadResolver implements EventResolver {
+
+    private static final EventResolver NAME_RESOLVER =
+            (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
+                final String threadName = logEvent.getThreadName();
+                jsonWriter.writeString(threadName);
+            };
+
+    private static final EventResolver ID_RESOLVER =
+            (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
+                final long threadId = logEvent.getThreadId();
+                jsonWriter.writeNumber(threadId);
+            };
+
+    private static final EventResolver PRIORITY_RESOLVER =
+            (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
+                final int threadPriority = logEvent.getThreadPriority();
+                jsonWriter.writeNumber(threadPriority);
+            };
+
+    private final EventResolver internalResolver;
+
+    ThreadResolver(final TemplateResolverConfig config) {
+        this.internalResolver = createInternalResolver(config);
+    }
+
+    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;
+        }
+        throw new IllegalArgumentException("unknown field: " + config);
+    }
+
+    static String getName() {
+        return "thread";
+    }
+
+    @Override
+    public void resolve(
+            final LogEvent logEvent,
+            final JsonWriter jsonWriter) {
+        internalResolver.resolve(logEvent, jsonWriter);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadResolverFactory.java
new file mode 100644
index 0000000..75df1e3
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadResolverFactory.java
@@ -0,0 +1,41 @@
+/*
+ * 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.json.template.resolver;
+
+final class ThreadResolverFactory implements EventResolverFactory<ThreadResolver> {
+
+    private static final ThreadResolverFactory INSTANCE = new ThreadResolverFactory();
+
+    private ThreadResolverFactory() {}
+
+    static ThreadResolverFactory getInstance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public String getName() {
+        return ThreadResolver.getName();
+    }
+
+    @Override
+    public ThreadResolver create(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
+        return new ThreadResolver(config);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TimestampResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TimestampResolver.java
new file mode 100644
index 0000000..1ea6e56
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TimestampResolver.java
@@ -0,0 +1,505 @@
+/*
+ * 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.json.template.resolver;
+
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.time.Instant;
+import org.apache.logging.log4j.core.util.datetime.FastDateFormat;
+import org.apache.logging.log4j.layout.json.template.JsonTemplateLayoutDefaults;
+import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+
+import java.util.Calendar;
+import java.util.Locale;
+import java.util.TimeZone;
+
+/**
+ * Timestamp resolver.
+ *
+ * <h3>Configuration</h3>
+ *
+ * <pre>
+ * config        = [ patternConfig | epochConfig ]
+ *
+ * patternConfig = "pattern" -> ( [ format ] , [ timeZone ] , [ locale ] )
+ * format        = "format" -> string
+ * timeZone      = "timeZone" -> string
+ * locale        = "locale" -> (
+ *                     language                                   |
+ *                   ( language , "_" , country )                 |
+ *                   ( language , "_" , country , "_" , variant )
+ *                 )
+ *
+ * epochConfig   = "epoch" -> ( unit , [ rounded ] )
+ * unit          = "unit" -> (
+ *                     "nanos"         |
+ *                     "millis"        |
+ *                     "secs"          |
+ *                     "millis.nanos"  |
+ *                     "secs.nanos"    |
+ *                  )
+ * rounded       = "rounded" -> boolean
+ * </pre>
+ *
+ * If no configuration options are provided, <tt>pattern-config</tt> is
+ * employed. There {@link
+ * JsonTemplateLayoutDefaults#getTimestampFormatPattern()}, {@link
+ * JsonTemplateLayoutDefaults#getTimeZone()}, {@link
+ * JsonTemplateLayoutDefaults#getLocale()} are used as defaults for
+ * <tt>pattern</tt>, <tt>timeZone</tt>, and <tt>locale</tt>, respectively.
+ *
+ * In <tt>epoch-config</tt>, <tt>millis.nanos</tt>, <tt>secs.nanos</tt> stand
+ * for the fractional component in nanoseconds.
+ *
+ * <h3>Examples</h3>
+ *
+ * <table>
+ * <tr>
+ *     <td>Configuration</td>
+ *     <td>Output</td>
+ * </tr>
+ * <tr>
+ *     <td><pre>
+ * {
+ *   "$resolver": "timestamp"
+ * }
+ *     </pre></td>
+ *     <td><pre>
+ * 2020-02-07T13:38:47.098+02:00
+ *     </pre></td>
+ * </tr>
+ * <tr>
+ *     <td><pre>
+ * {
+ *   "$resolver": "timestamp",
+ *   "pattern": {
+ *     "format": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
+ *     "timeZone": "UTC",
+ *     "locale": "en_US"
+ *   }
+ * }
+ *     </pre></td>
+ *     <td><pre>
+ * 2020-02-07T13:38:47.098Z
+ *     </pre></td>
+ * </tr>
+ * <tr>
+ *     <td><pre>
+ * {
+ *   "$resolver": "timestamp",
+ *   "epoch": {
+ *     "unit": "secs"
+ *   }
+ * }
+ *     </pre></td>
+ *     <td><pre>
+ * 1581082727.982123456
+ *     </pre></td>
+ * </tr>
+ * <tr>
+ *     <td><pre>
+ * {
+ *   "$resolver": "timestamp",
+ *   "epoch": {
+ *     "unit": "secs",
+ *     "rounded": true
+ *   }
+ * }
+ *     </pre></td>
+ *     <td><pre>
+ * 1581082727
+ *     </pre></td>
+ * </tr>
+ * <tr>
+ *     <td><pre>
+ * {
+ *   "$resolver": "timestamp",
+ *   "epoch": {
+ *     "unit": "secs.nanos"
+ *   }
+ * }
+ *     </pre></td>
+ *     <td><pre>
+ *            982123456
+ *     </pre></td>
+ * </tr>
+ * <tr>
+ *     <td><pre>
+ * {
+ *   "$resolver": "timestamp",
+ *   "epoch": {
+ *     "unit": "millis"
+ *   }
+ * }
+ *     </pre></td>
+ *     <td><pre>
+ * 1581082727982.123456
+ *     </pre></td>
+ * </tr>
+ * <tr>
+ *     <td><pre>
+ * {
+ *   "$resolver": "timestamp",
+ *   "epoch": {
+ *     "unit": "millis",
+ *     "rounded": true
+ *   }
+ * }
+ *     </pre></td>
+ *     <td><pre>
+ * 1581082727982
+ *     </pre></td>
+ * </tr>
+ * <tr>
+ *     <td><pre>
+ * {
+ *   "$resolver": "timestamp",
+ *   "epoch": {
+ *     "unit": "millis.nanos"
+ *   }
+ * }
+ *     </pre></td>
+ *     <td><pre>
+ *              123456
+ *     </pre></td>
+ * </tr>
+ * <tr>
+ *     <td><pre>
+ * {
+ *   "$resolver": "timestamp",
+ *   "epoch": {
+ *     "unit": "nanos"
+ *   }
+ * }
+ *     </pre></td>
+ *     <td><pre>
+ * 1581082727982123456
+ *     </pre></td>
+ * </tr>
+ * </table>
+ */
+final class TimestampResolver implements EventResolver {
+
+    private final EventResolver internalResolver;
+
+    TimestampResolver(final TemplateResolverConfig config) {
+        this.internalResolver = createResolver(config);
+    }
+
+    private static EventResolver createResolver(
+            final TemplateResolverConfig config) {
+        final boolean patternProvided = config.exists("pattern");
+        final boolean epochProvided = config.exists("epoch");
+        if (patternProvided && epochProvided) {
+            throw new IllegalArgumentException(
+                    "conflicting configuration options are provided: " + config);
+        }
+        return epochProvided
+                ? createEpochResolver(config)
+                : createFormatResolver(config);
+    }
+
+    /**
+     * Context for GC-free formatted timestamp resolver.
+     */
+    private static final class FormatResolverContext {
+
+        private final FastDateFormat timestampFormat;
+
+        private final Calendar calendar;
+
+        private final StringBuilder formattedTimestampBuilder;
+
+        private FormatResolverContext(
+                final TimeZone timeZone,
+                final Locale locale,
+                final FastDateFormat timestampFormat) {
+            this.timestampFormat = timestampFormat;
+            this.formattedTimestampBuilder = new StringBuilder();
+            this.calendar = Calendar.getInstance(timeZone, locale);
+            timestampFormat.format(calendar, formattedTimestampBuilder);
+        }
+
+        private static FormatResolverContext fromConfig(
+                final TemplateResolverConfig config) {
+            final String format = readFormat(config);
+            final TimeZone timeZone = readTimeZone(config);
+            final Locale locale = readLocale(config);
+            final FastDateFormat fastDateFormat =
+                    FastDateFormat.getInstance(format, timeZone, locale);
+            return new FormatResolverContext(timeZone, locale, fastDateFormat);
+        }
+
+        private static String readFormat(final TemplateResolverConfig config) {
+            final String format = config.getString(new String[]{"pattern", "format"});
+            if (format == null) {
+                return JsonTemplateLayoutDefaults.getTimestampFormatPattern();
+            }
+            try {
+                FastDateFormat.getInstance(format);
+            } catch (final IllegalArgumentException error) {
+                throw new IllegalArgumentException(
+                        "invalid timestamp format: " + config,
+                        error);
+            }
+            return format;
+        }
+
+        private static TimeZone readTimeZone(final TemplateResolverConfig config) {
+            final String timeZoneId = config.getString(new String[]{"pattern", "timeZone"});
+            if (timeZoneId == null) {
+                return JsonTemplateLayoutDefaults.getTimeZone();
+            }
+            boolean found = false;
+            for (final String availableTimeZone : TimeZone.getAvailableIDs()) {
+                if (availableTimeZone.equalsIgnoreCase(timeZoneId)) {
+                    found = true;
+                    break;
+                }
+            }
+            if (!found) {
+                throw new IllegalArgumentException(
+                        "invalid timestamp time zone: " + config);
+            }
+            return TimeZone.getTimeZone(timeZoneId);
+        }
+
+        private static Locale readLocale(final TemplateResolverConfig config) {
+            final String locale = config.getString(new String[]{"pattern", "locale"});
+            if (locale == null) {
+                return JsonTemplateLayoutDefaults.getLocale();
+            }
+            final String[] localeFields = locale.split("_", 3);
+            switch (localeFields.length) {
+                case 1: return new Locale(localeFields[0]);
+                case 2: return new Locale(localeFields[0], localeFields[1]);
+                case 3: return new Locale(localeFields[0], localeFields[1], localeFields[2]);
+            }
+            throw new IllegalArgumentException("invalid timestamp locale: " + config);
+        }
+
+    }
+
+    /**
+     * GC-free formatted timestamp resolver.
+     */
+    private static final class FormatResolver implements EventResolver {
+
+        private final FormatResolverContext formatResolverContext;
+
+        private FormatResolver(final FormatResolverContext formatResolverContext) {
+            this.formatResolverContext = formatResolverContext;
+        }
+
+        @Override
+        public synchronized void resolve(
+                final LogEvent logEvent,
+                final JsonWriter jsonWriter) {
+
+            // Format timestamp if it doesn't match the last cached one.
+            final long timestampMillis = logEvent.getTimeMillis();
+            if (formatResolverContext.calendar.getTimeInMillis() != timestampMillis) {
+
+                // Format the timestamp.
+                formatResolverContext.formattedTimestampBuilder.setLength(0);
+                formatResolverContext.calendar.setTimeInMillis(timestampMillis);
+                formatResolverContext.timestampFormat.format(
+                        formatResolverContext.calendar,
+                        formatResolverContext.formattedTimestampBuilder);
+
+                // Write the formatted timestamp.
+                final StringBuilder jsonWriterStringBuilder = jsonWriter.getStringBuilder();
+                final int startIndex = jsonWriterStringBuilder.length();
+                jsonWriter.writeString(formatResolverContext.formattedTimestampBuilder);
+
+                // Cache the written value.
+                formatResolverContext.formattedTimestampBuilder.setLength(0);
+                formatResolverContext.formattedTimestampBuilder.append(
+                        jsonWriterStringBuilder,
+                        startIndex,
+                        jsonWriterStringBuilder.length());
+
+            }
+
+            // Write the cached formatted timestamp.
+            else {
+                jsonWriter.writeRawString(
+                        formatResolverContext.formattedTimestampBuilder);
+            }
+
+        }
+
+    }
+
+    private static EventResolver createFormatResolver(
+            final TemplateResolverConfig config) {
+        final FormatResolverContext formatResolverContext =
+                FormatResolverContext.fromConfig(config);
+        return new FormatResolver(formatResolverContext);
+    }
+
+    private static EventResolver createEpochResolver(
+            final TemplateResolverConfig config) {
+        final String unit = config.getString(new String[]{"epoch", "unit"});
+        final Boolean rounded = config.getBoolean(new String[]{"epoch", "rounded"});
+        if ("nanos".equals(unit) && !Boolean.FALSE.equals(rounded)) {
+            return EPOCH_NANOS_RESOLVER;
+        } else if ("millis".equals(unit)) {
+            return !Boolean.TRUE.equals(rounded)
+                    ? EPOCH_MILLIS_RESOLVER
+                    : EPOCH_MILLIS_ROUNDED_RESOLVER;
+        } else if ("millis.nanos".equals(unit) && rounded == null) {
+                return EPOCH_MILLIS_NANOS_RESOLVER;
+        } else if ("secs".equals(unit)) {
+            return !Boolean.TRUE.equals(rounded)
+                    ? EPOCH_SECS_RESOLVER
+                    : EPOCH_SECS_ROUNDED_RESOLVER;
+        } else if ("secs.nanos".equals(unit) && rounded == null) {
+            return EPOCH_SECS_NANOS_RESOLVER;
+        }
+        throw new IllegalArgumentException(
+                "invalid epoch configuration: " + config);
+    }
+
+    private static final class EpochResolutionRecord {
+
+        private static final int MAX_LONG_LENGTH =
+                String.valueOf(Long.MAX_VALUE).length();
+
+        private Instant instant;
+
+        private char[] resolution = new char[/* integral: */MAX_LONG_LENGTH + /* dot: */1 + /* fractional: */MAX_LONG_LENGTH ];
+
+        private int resolutionLength;
+
+        private EpochResolutionRecord() {}
+
+    }
+
+    private static abstract class EpochResolver implements EventResolver {
+
+        private final EpochResolutionRecord resolutionRecord =
+                new EpochResolutionRecord();
+
+        @Override
+        public synchronized void resolve(
+                final LogEvent logEvent,
+                final JsonWriter jsonWriter) {
+            final Instant logEventInstant = logEvent.getInstant();
+            if (logEventInstant.equals(resolutionRecord.instant)) {
+                jsonWriter.writeRawString(
+                        resolutionRecord.resolution,
+                        0,
+                        resolutionRecord.resolutionLength);
+            } else {
+                resolutionRecord.instant = logEventInstant;
+                final StringBuilder stringBuilder = jsonWriter.getStringBuilder();
+                final int startIndex = stringBuilder.length();
+                resolve(logEventInstant, jsonWriter);
+                resolutionRecord.resolutionLength = stringBuilder.length() - startIndex;
+                stringBuilder.getChars(
+                        startIndex,
+                        stringBuilder.length(),
+                        resolutionRecord.resolution,
+                        0);
+            }
+        }
+
+        abstract void resolve(Instant logEventInstant, JsonWriter jsonWriter);
+
+    }
+
+    private static final EventResolver EPOCH_NANOS_RESOLVER =
+            new EpochResolver() {
+                @Override
+                void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
+                    final long nanos = epochNanos(logEventInstant);
+                    jsonWriter.writeNumber(nanos);
+                }
+            };
+
+    private static final EventResolver EPOCH_MILLIS_RESOLVER =
+            new EpochResolver() {
+                @Override
+                void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
+                    final StringBuilder jsonWriterStringBuilder = jsonWriter.getStringBuilder();
+                    final long nanos = epochNanos(logEventInstant);
+                    jsonWriterStringBuilder.append(nanos);
+                    jsonWriterStringBuilder.insert(jsonWriterStringBuilder.length() - 6, '.');
+                }
+            };
+
+    private static final EventResolver EPOCH_MILLIS_ROUNDED_RESOLVER =
+            new EpochResolver() {
+                @Override
+                void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
+                    jsonWriter.writeNumber(logEventInstant.getEpochMillisecond());
+                }
+            };
+
+    private static final EventResolver EPOCH_MILLIS_NANOS_RESOLVER =
+            new EpochResolver() {
+                @Override
+                void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
+                    final long nanos = epochNanos(logEventInstant);
+                    final long fraction = nanos % 1_000_000L;
+                    jsonWriter.writeNumber(fraction);
+                }
+            };
+
+    private static final EventResolver EPOCH_SECS_RESOLVER =
+            new EpochResolver() {
+                @Override
+                void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
+                    final StringBuilder jsonWriterStringBuilder = jsonWriter.getStringBuilder();
+                    final long nanos = epochNanos(logEventInstant);
+                    jsonWriterStringBuilder.append(nanos);
+                    jsonWriterStringBuilder.insert(jsonWriterStringBuilder.length() - 9, '.');
+                }
+            };
+
+    private static final EventResolver EPOCH_SECS_ROUNDED_RESOLVER =
+            new EpochResolver() {
+                @Override
+                void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
+                    jsonWriter.writeNumber(logEventInstant.getEpochSecond());
+                }
+            };
+
+    private static final EventResolver EPOCH_SECS_NANOS_RESOLVER =
+            new EpochResolver() {
+                @Override
+                void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
+                    jsonWriter.writeNumber(logEventInstant.getNanoOfSecond());
+                }
+            };
+
+    private static long epochNanos(Instant instant) {
+        return 1_000_000_000L * instant.getEpochSecond() + instant.getNanoOfSecond();
+    }
+
+    static String getName() {
+        return "timestamp";
+    }
+
+    @Override
+    public void resolve(
+            final LogEvent logEvent,
+            final JsonWriter jsonWriter) {
+        internalResolver.resolve(logEvent, jsonWriter);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TimestampResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TimestampResolverFactory.java
new file mode 100644
index 0000000..f1547f2
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TimestampResolverFactory.java
@@ -0,0 +1,41 @@
+/*
+ * 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.json.template.resolver;
+
+final class TimestampResolverFactory implements EventResolverFactory<TimestampResolver> {
+
+    private static final TimestampResolverFactory INSTANCE = new TimestampResolverFactory();
+
+    private TimestampResolverFactory() {}
+
+    static TimestampResolverFactory getInstance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public String getName() {
+        return TimestampResolver.getName();
+    }
+
+    @Override
+    public TimestampResolver create(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
+        return new TimestampResolver(config);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/DummyRecycler.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/DummyRecycler.java
new file mode 100644
index 0000000..2aae11f
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/DummyRecycler.java
@@ -0,0 +1,37 @@
+/*
+ * 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.json.template.util;
+
+import java.util.function.Supplier;
+
+public class DummyRecycler<V> implements Recycler<V> {
+
+    private final Supplier<V> supplier;
+
+    public DummyRecycler(final Supplier<V> supplier) {
+        this.supplier = supplier;
+    }
+
+    @Override
+    public V acquire() {
+        return supplier.get();
+    }
+
+    @Override
+    public void release(final V value) {}
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/DummyRecyclerFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/DummyRecyclerFactory.java
new file mode 100644
index 0000000..dc3a8a1
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/DummyRecyclerFactory.java
@@ -0,0 +1,39 @@
+/*
+ * 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.json.template.util;
+
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+public class DummyRecyclerFactory implements RecyclerFactory {
+
+    private static final DummyRecyclerFactory INSTANCE = new DummyRecyclerFactory();
+
+    private DummyRecyclerFactory() {}
+
+    public static DummyRecyclerFactory getInstance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public <V> Recycler<V> create(
+            final Supplier<V> supplier,
+            final Consumer<V> cleaner) {
+        return new DummyRecycler<V>(supplier);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/JsonReader.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/JsonReader.java
new file mode 100644
index 0000000..1a9f43e
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/JsonReader.java
@@ -0,0 +1,447 @@
+/*
+ * 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.json.template.util;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.text.CharacterIterator;
+import java.text.StringCharacterIterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A simple JSON parser mapping tokens to basic Java types.
+ * <p>
+ * The type mapping is as follows:
+ * <p>
+ * <ul>
+ * <li><tt>object</tt>s are mapped to {@link LinkedHashMap LinkedHashMap&lt;String,Object&gt;}
+ * <li><tt>array</tt>s are mapped to {@link LinkedList}
+ * <li><tt>string</tt>s are mapped to {@link String} with proper Unicode and
+ * escape character conversion
+ * <li><tt>true</tt>, <tt>false</tt>, and <tt>null</tt> are mapped to their Java
+ * counterparts
+ * <li>floating point <tt>number</tt>s are mapped to {@link BigDecimal}
+ * <li>integral <tt>number</tt>s are mapped to either primitive types
+ * (<tt>int</tt>, <tt>long</tt>) or {@link BigInteger}
+ * </ul>
+ * <p>
+ * This code is heavily influenced by the reader of
+ * <a href="https://github.com/bolerio/mjson/blob/e7a4da2daa6e17a63ec057948bc30818e8f44686/src/java/mjson/Json.java#L2684">mjson</a>.
+ */
+public final class JsonReader {
+
+    private enum Delimiter {
+
+        OBJECT_START("{"),
+
+        OBJECT_END("}"),
+
+        ARRAY_START("["),
+
+        ARRAY_END("]"),
+
+        COLON(":"),
+
+        COMMA(",");
+
+        private final String string;
+
+        Delimiter(final String string) {
+            this.string = string;
+        }
+
+        private static boolean exists(final Object token) {
+            for (Delimiter delimiter : values()) {
+                if (delimiter.string.equals(token)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+    }
+
+    private CharacterIterator it;
+
+    private int readCharIndex = -1;
+
+    private char readChar;
+
+    private int readTokenStartIndex = -1;
+
+    private Object readToken;
+
+    private StringBuilder buffer = new StringBuilder();
+
+    private JsonReader() {}
+
+    public static Object read(final String string) {
+        final JsonReader reader = new JsonReader();
+        return reader.read(new StringCharacterIterator(string));
+    }
+
+    private Object read(final CharacterIterator ci) {
+        it = ci;
+        readCharIndex = 0;
+        readChar = it.first();
+        final Object token = readToken();
+        if (token instanceof Delimiter) {
+            final String message = String.format(
+                    "was not expecting %s at index %d",
+                    readToken, readTokenStartIndex);
+            throw new IllegalArgumentException(message);
+        }
+        skipWhiteSpace();
+        if (it.getIndex() != it.getEndIndex()) {
+            final String message = String.format(
+                    "was not expecting input at index %d: %c",
+                    readCharIndex, readChar);
+            throw new IllegalArgumentException(message);
+        }
+        return token;
+    }
+
+    private Object readToken() {
+        skipWhiteSpace();
+        readTokenStartIndex = readCharIndex;
+        final char prevChar = readChar;
+        readChar();
+        switch (prevChar) {
+
+            case '"':
+                readToken = readString();
+                break;
+
+            case '[':
+                readToken = readArray();
+                break;
+
+            case ']':
+                readToken = Delimiter.ARRAY_END;
+                break;
+
+            case ',':
+                readToken = Delimiter.COMMA;
+                break;
+
+            case '{':
+                readToken = readObject();
+                break;
+
+            case '}':
+                readToken = Delimiter.OBJECT_END;
+                break;
+
+            case ':':
+                readToken = Delimiter.COLON;
+                break;
+
+            case 't':
+                readToken = readTrue();
+                break;
+
+            case 'f':
+                readToken = readFalse();
+                break;
+
+            case 'n':
+                readToken = readNull();
+                break;
+
+            default:
+                unreadChar();
+                if (Character.isDigit(readChar) || readChar == '-') {
+                    readToken = readNumber();
+                } else {
+                    String message = String.format(
+                            "invalid character at index %d: %c",
+                            readCharIndex, readChar);
+                    throw new IllegalArgumentException(message);
+                }
+
+        }
+        return readToken;
+    }
+
+    private void skipWhiteSpace() {
+        do {
+            if (!Character.isWhitespace(readChar)) {
+                break;
+            }
+        } while (readChar() != CharacterIterator.DONE);
+    }
+
+    private char readChar() {
+        if (it.getIndex() == it.getEndIndex()) {
+            throw new IllegalArgumentException("premature end of input");
+        }
+        readChar = it.next();
+        readCharIndex = it.getIndex();
+        return readChar;
+    }
+
+    private void unreadChar() {
+        readChar = it.previous();
+        readCharIndex = it.getIndex();
+    }
+
+    private String readString() {
+        buffer.setLength(0);
+        while (readChar != '"') {
+            if (readChar == '\\') {
+                readChar();
+                if (readChar == 'u') {
+                    final char unicodeChar = readUnicodeChar();
+                    bufferChar(unicodeChar);
+                } else {
+                    switch (readChar) {
+                        case '"':
+                        case '\\':
+                            bufferReadChar();
+                            break;
+                        case 'b':
+                            bufferChar('\b');
+                            break;
+                        case 'f':
+                            bufferChar('\f');
+                            break;
+                        case 'n':
+                            bufferChar('\n');
+                            break;
+                        case 'r':
+                            bufferChar('\r');
+                            break;
+                        case 't':
+                            bufferChar('\t');
+                            break;
+                        default: {
+                            final String message = String.format(
+                                    "was expecting an escape character at index %d: %c",
+                                    readCharIndex, readChar);
+                            throw new IllegalArgumentException(message);
+                        }
+                    }
+                }
+            } else {
+                bufferReadChar();
+            }
+        }
+        readChar();
+        return buffer.toString();
+    }
+
+    private void bufferReadChar() {
+        bufferChar(readChar);
+    }
+
+    private void bufferChar(final char c) {
+        buffer.append(c);
+        readChar();
+    }
+
+    private char readUnicodeChar() {
+        int value = 0;
+        for (int i = 0; i < 4; i++) {
+            readChar();
+            if (readChar >= '0' && readChar <= '9') {
+                value = (value << 4) + readChar - '0';
+            } else if (readChar >= 'a' && readChar <= 'f') {
+                value = (value << 4) + (readChar - 'a') + 10;
+            } else if (readChar >= 'A' && readChar <= 'F') {
+                value = (value << 4) + (readChar - 'A') + 10;
+            } else {
+                final String message = String.format(
+                        "was expecting a unicode character at index %d: %c",
+                        readCharIndex, readChar);
+                throw new IllegalArgumentException(message);
+            }
+        }
+        return (char) value;
+    }
+
+    private Map<String, Object> readObject() {
+        final Map<String, Object> object = new LinkedHashMap<>();
+        String key = readObjectKey();
+        while (readToken != Delimiter.OBJECT_END) {
+            expectDelimiter(Delimiter.COLON, readToken());
+            if (readToken != Delimiter.OBJECT_END) {
+                Object value = readToken();
+                object.put(key, value);
+                if (readToken() == Delimiter.COMMA) {
+                    key = readObjectKey();
+                    if (key == null || Delimiter.exists(key)) {
+                        String message = String.format(
+                                "was expecting an object key at index %d: %s",
+                                readTokenStartIndex, readToken);
+                        throw new IllegalArgumentException(message);
+                    }
+                } else {
+                    expectDelimiter(Delimiter.OBJECT_END, readToken);
+                }
+            }
+        }
+        return object;
+    }
+
+    private List<Object> readArray() {
+        @SuppressWarnings("JdkObsolete")
+        final List<Object> array = new LinkedList<>();
+        readToken();
+        while (readToken != Delimiter.ARRAY_END) {
+            if (readToken instanceof Delimiter) {
+                final String message = String.format(
+                        "was expecting an array element at index %d: %s",
+                        readTokenStartIndex, readToken);
+                throw new IllegalArgumentException(message);
+            }
+            array.add(readToken);
+            if (readToken() == Delimiter.COMMA) {
+                if (readToken() == Delimiter.ARRAY_END) {
+                    final String message = String.format(
+                            "was expecting an array element at index %d: %s",
+                            readTokenStartIndex, readToken);
+                    throw new IllegalArgumentException(message);
+                }
+            } else {
+                expectDelimiter(Delimiter.ARRAY_END, readToken);
+            }
+        }
+        return array;
+    }
+
+    private String readObjectKey() {
+        readToken();
+        if (readToken == Delimiter.OBJECT_END) {
+            return null;
+        } else if (readToken instanceof String) {
+            return (String) readToken;
+        } else {
+            final String message = String.format(
+                    "was expecting an object key at index %d: %s",
+                    readTokenStartIndex, readToken);
+            throw new IllegalArgumentException(message);
+        }
+    }
+
+    private void expectDelimiter(
+            final Delimiter expectedDelimiter,
+            final Object actualToken) {
+        if (!expectedDelimiter.equals(actualToken)) {
+            String message = String.format(
+                    "was expecting %s at index %d: %s",
+                    expectedDelimiter, readTokenStartIndex, actualToken);
+            throw new IllegalArgumentException(message);
+        }
+    }
+
+    private boolean readTrue() {
+        if (readChar != 'r' || readChar() != 'u' || readChar() != 'e') {
+            String message = String.format(
+                    "was expecting keyword 'true' at index %d: %s",
+                    readCharIndex, readChar);
+            throw new IllegalArgumentException(message);
+        }
+        readChar();
+        return true;
+    }
+
+    private boolean readFalse() {
+        if (readChar != 'a' || readChar() != 'l' || readChar() != 's' || readChar() != 'e') {
+            String message = String.format(
+                    "was expecting keyword 'false' at index %d: %s",
+                    readCharIndex, readChar);
+            throw new IllegalArgumentException(message);
+        }
+        readChar();
+        return false;
+    }
+
+    private Object readNull() {
+        if (readChar != 'u' || readChar() != 'l' || readChar() != 'l') {
+            String message = String.format(
+                    "was expecting keyword 'null' at index %d: %s",
+                    readCharIndex, readChar);
+            throw new IllegalArgumentException(message);
+        }
+        readChar();
+        return null;
+    }
+
+    private Number readNumber() {
+
+        // Read sign.
+        buffer.setLength(0);
+        if (readChar == '-') {
+            bufferReadChar();
+        }
+
+        // Read fraction.
+        boolean floatingPoint = false;
+        bufferDigits();
+        if (readChar == '.') {
+            bufferReadChar();
+            bufferDigits();
+            floatingPoint = true;
+        }
+
+        // Read exponent.
+        if (readChar == 'e' || readChar == 'E') {
+            floatingPoint = true;
+            bufferReadChar();
+            if (readChar == '+' || readChar == '-') {
+                bufferReadChar();
+            }
+            bufferDigits();
+        }
+
+        // Convert the read number.
+        final String string = buffer.toString();
+        if (floatingPoint) {
+            return new BigDecimal(string);
+        } else {
+            final BigInteger bigInteger = new BigInteger(string);
+            try {
+                return bigInteger.intValueExact();
+            } catch (ArithmeticException ignoredIntOverflow) {
+                try {
+                    return bigInteger.longValueExact();
+                } catch (ArithmeticException ignoredLongOverflow) {
+                    return bigInteger;
+                }
+            }
+        }
+
+    }
+
+    private void bufferDigits() {
+        boolean found = false;
+        while (Character.isDigit(readChar)) {
+            found = true;
+            bufferReadChar();
+        }
+        if (!found) {
+            final String message = String.format(
+                    "was expecting a digit at index %d: %c",
+                    readCharIndex, readChar);
+            throw new IllegalArgumentException(message);
+        }
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/JsonWriter.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/JsonWriter.java
new file mode 100644
index 0000000..a0dad93
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/JsonWriter.java
@@ -0,0 +1,889 @@
+/*
+ * 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.json.template.util;
+
+import org.apache.logging.log4j.util.BiConsumer;
+import org.apache.logging.log4j.util.IndexedReadOnlyStringMap;
+import org.apache.logging.log4j.util.StringBuilderFormattable;
+import org.apache.logging.log4j.util.StringMap;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * A simple JSON writer with support for common Java data types.
+ * <p>
+ * The following types have specific handlers:
+ * <p>
+ * <ul>
+ *     <li> <tt>null</tt> input
+ *     <li>{@link Map}, {@link IndexedReadOnlyStringMap}, {@link StringMap}
+ *     <li>{@link Collection} and {@link List}
+ *     <li>{@link Number} ({@link BigDecimal}, {@link BigInteger}, {@link Float},
+ *     {@link Double}, {@link Byte}, {@link Short}, {@link Integer}, and
+ *     {@link Long})
+ *     <li>{@link Boolean}
+ *     <li>{@link StringBuilderFormattable}
+ *     <li>arrays of primitve types
+ *     <tt>char/boolean/byte/short/int/long/float/double</tt> and {@link Object}
+ *     <li>{@link CharSequence} and <tt>char[]</tt> with necessary escaping
+ * </ul>
+ * <p>
+ * JSON standard quoting routines are borrowed from
+ * <a href="https://github.com/FasterXML/jackson-core">Jackson</a>.
+ */
+public final class JsonWriter implements AutoCloseable, Cloneable {
+
+    private final static char[] HEX_CHARS = "0123456789ABCDEF".toCharArray();
+
+    /**
+     * Lookup table used for determining which output characters in 7-bit ASCII
+     * range (i.e., first 128 Unicode code points, single-byte UTF-8 characters)
+     * need to be quoted.
+     *<p>
+     * Value of 0 means "no escaping"; other positive values, that value is
+     * character to use after backslash; and negative values, that generic
+     * (backslash - u) escaping is to be used.
+     */
+    private final static int[] ESC_CODES;
+    static {
+        int[] table = new int[128];
+        // Control chars need generic escape sequence
+        for (int i = 0; i < 32; ++i) {
+            // 04-Mar-2011, tatu: Used to use "-(i + 1)", replaced with constant
+            table[i] = -1;
+        }
+        // Others (and some within that range too) have explicit shorter sequences
+        table['"'] = '"';
+        table['\\'] = '\\';
+        // Escaping of slash is optional, so let's not add it
+        table[0x08] = 'b';
+        table[0x09] = 't';
+        table[0x0C] = 'f';
+        table[0x0A] = 'n';
+        table[0x0D] = 'r';
+        ESC_CODES = table;
+    }
+
+    private final char[] quoteBuffer;
+
+    private final StringBuilder stringBuilder;
+
+    private final StringBuilder formattableBuffer;
+
+    private final int maxStringLength;
+
+    private final String truncatedStringSuffix;
+
+    private final String quotedTruncatedStringSuffix;
+
+    private JsonWriter(final Builder builder) {
+        this.quoteBuffer = new char[]{'\\', '-', '0', '0', '-', '-'};
+        this.stringBuilder = new StringBuilder();
+        this.formattableBuffer = new StringBuilder();
+        this.maxStringLength = builder.maxStringLength;
+        this.truncatedStringSuffix = builder.truncatedStringSuffix;
+        this.quotedTruncatedStringSuffix = quoteString(builder.truncatedStringSuffix);
+    }
+
+    private String quoteString(final String string) {
+        final int startIndex = stringBuilder.length();
+        quoteString(string, 0, string.length());
+        final StringBuilder quotedStringBuilder = new StringBuilder();
+        quotedStringBuilder.append(stringBuilder, startIndex, stringBuilder.length());
+        final String quotedString = quotedStringBuilder.toString();
+        stringBuilder.setLength(startIndex);
+        return quotedString;
+    }
+
+    public String use(Runnable runnable) {
+        final int startIndex = stringBuilder.length();
+        runnable.run();
+        final StringBuilder sliceStringBuilder = new StringBuilder();
+        sliceStringBuilder.append(stringBuilder, startIndex, stringBuilder.length());
+        stringBuilder.setLength(startIndex);
+        return sliceStringBuilder.toString();
+    }
+
+    public StringBuilder getStringBuilder() {
+        return stringBuilder;
+    }
+
+    public int getMaxStringLength() {
+        return maxStringLength;
+    }
+
+    public String getTruncatedStringSuffix() {
+        return truncatedStringSuffix;
+    }
+
+    public void writeValue(final Object value) {
+
+        // null
+        if (value == null) {
+            writeNull();
+        }
+
+        // map
+        else if (value instanceof IndexedReadOnlyStringMap) {
+            final IndexedReadOnlyStringMap map = (IndexedReadOnlyStringMap) value;
+            writeObject(map);
+        } else if (value instanceof StringMap) {
+            final StringMap map = (StringMap) value;
+            writeObject(map);
+        } else if (value instanceof Map) {
+            @SuppressWarnings("unchecked")
+            final Map<String, Object> map = (Map<String, Object>) value;
+            writeObject(map);
+        }
+
+        // list & collection
+        else if (value instanceof List) {
+            @SuppressWarnings("unchecked")
+            final List<Object> list = (List<Object>) value;
+            writeArray(list);
+        } else if (value instanceof Collection) {
+            @SuppressWarnings("unchecked")
+            final Collection<Object> collection = (Collection<Object>) value;
+            writeArray(collection);
+        }
+
+        // number & boolean
+        else if (value instanceof Number) {
+            final Number number = (Number) value;
+            writeNumber(number);
+        } else if (value instanceof Boolean) {
+            final boolean booleanValue = (boolean) value;
+            writeBoolean(booleanValue);
+        }
+
+        // formattable
+        else if (value instanceof StringBuilderFormattable) {
+            final StringBuilderFormattable formattable = (StringBuilderFormattable) value;
+            writeString(formattable);
+        }
+
+        // arrays
+        else if (value instanceof char[]) {
+            final char[] charValues = (char[]) value;
+            writeArray(charValues);
+        } else if (value instanceof boolean[]) {
+            final boolean[] booleanValues = (boolean[]) value;
+            writeArray(booleanValues);
+        } else if (value instanceof byte[]) {
+            final byte[] byteValues = (byte[]) value;
+            writeArray(byteValues);
+        } else if (value instanceof short[]) {
+            final short[] shortValues = (short[]) value;
+            writeArray(shortValues);
+        } else if (value instanceof int[]) {
+            final int[] intValues = (int[]) value;
+            writeArray(intValues);
+        } else if (value instanceof long[]) {
+            final long[] longValues = (long[]) value;
+            writeArray(longValues);
+        } else if (value instanceof float[]) {
+            final float[] floatValues = (float[]) value;
+            writeArray(floatValues);
+        } else if (value instanceof double[]) {
+            final double[] doubleValues = (double[]) value;
+            writeArray(doubleValues);
+        } else if (value instanceof Object[]) {
+            final Object[] values = (Object[]) value;
+            writeArray(values);
+        }
+
+        // string
+        else {
+            final String stringValue = value instanceof String
+                    ? (String) value
+                    : String.valueOf(value);
+            writeString(stringValue);
+        }
+
+    }
+
+    public void writeObject(final StringMap map) {
+        if (map == null) {
+            writeNull();
+        } else {
+            writeObjectStart();
+            final boolean[] firstEntry = {true};
+            map.forEach((final String key, final Object value) -> {
+                if (key == null) {
+                    throw new IllegalArgumentException("null keys are not allowed");
+                }
+                if (firstEntry[0]) {
+                    firstEntry[0] = false;
+                } else {
+                    writeSeparator();
+                }
+                writeObjectKey(key);
+                writeValue(value);
+            });
+            writeObjectEnd();
+        }
+    }
+
+    public void writeObject(final IndexedReadOnlyStringMap map) {
+        if (map == null) {
+            writeNull();
+        } else {
+            writeObjectStart();
+            for (int entryIndex = 0; entryIndex < map.size(); entryIndex++) {
+                final String key = map.getKeyAt(entryIndex);
+                final Object value = map.getValueAt(entryIndex);
+                if (entryIndex > 0) {
+                    writeSeparator();
+                }
+                writeObjectKey(key);
+                writeValue(value);
+            }
+            writeObjectEnd();
+        }
+    }
+
+    public void writeObject(final Map<String, Object> map) {
+        if (map == null) {
+            writeNull();
+        } else {
+            writeObjectStart();
+            final boolean[] firstEntry = {true};
+            map.forEach((final String key, final Object value) -> {
+                if (key == null) {
+                    throw new IllegalArgumentException("null keys are not allowed");
+                }
+                if (firstEntry[0]) {
+                    firstEntry[0] = false;
+                } else {
+                    writeSeparator();
+                }
+                writeObjectKey(key);
+                writeValue(value);
+            });
+            writeObjectEnd();
+        }
+    }
+
+    public void writeObjectStart() {
+        stringBuilder.append('{');
+    }
+
+    public void writeObjectEnd() {
+        stringBuilder.append('}');
+    }
+
+    public void writeObjectKey(final CharSequence key) {
+        writeString(key);
+        stringBuilder.append(':');
+    }
+
+    public void writeArray(final List<Object> items) {
+        if (items == null) {
+            writeNull();
+        } else {
+            writeArrayStart();
+            for (int itemIndex = 0; itemIndex < items.size(); itemIndex++) {
+                if (itemIndex > 0) {
+                    writeSeparator();
+                }
+                final Object item = items.get(itemIndex);
+                writeValue(item);
+            }
+            writeArrayEnd();
+        }
+    }
+
+    public void writeArray(final Collection<Object> items) {
+        if (items == null) {
+            writeNull();
+        } else {
+            writeArrayStart();
+            final boolean[] firstItem = {true};
+            items.forEach((final Object item) -> {
+                if (firstItem[0]) {
+                    firstItem[0] = false;
+                } else {
+                    writeSeparator();
+                }
+                writeValue(item);
+            });
+            writeArrayEnd();
+        }
+    }
+
+    public void writeArray(final char[] items) {
+        if (items == null) {
+            writeNull();
+        } else {
+            writeArrayStart();
+            for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {
+                if (itemIndex > 0) {
+                    writeSeparator();
+                }
+                stringBuilder.append('"');
+                quoteString(items, itemIndex, 1);
+                stringBuilder.append('"');
+            }
+            writeArrayEnd();
+        }
+    }
+
+    public void writeArray(final boolean[] items) {
+        if (items == null) {
+            writeNull();
+        } else {
+            writeArrayStart();
+            for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {
+                if (itemIndex > 0) {
+                    writeSeparator();
+                }
+                final boolean item = items[itemIndex];
+                writeBoolean(item);
+            }
+            writeArrayEnd();
+        }
+    }
+
+    public void writeArray(final byte[] items) {
+        if (items == null) {
+            writeNull();
+        } else {
+            writeArrayStart();
+            for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {
+                if (itemIndex > 0) {
+                    writeSeparator();
+                }
+                final byte item = items[itemIndex];
+                writeNumber(item);
+            }
+            writeArrayEnd();
+        }
+    }
+
+    public void writeArray(final short[] items) {
+        if (items == null) {
+            writeNull();
+        } else {
+            writeArrayStart();
+            for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {
+                if (itemIndex > 0) {
+                    writeSeparator();
+                }
+                final short item = items[itemIndex];
+                writeNumber(item);
+            }
+            writeArrayEnd();
+        }
+    }
+
+    public void writeArray(final int[] items) {
+        if (items == null) {
+            writeNull();
+        } else {
+            writeArrayStart();
+            for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {
+                if (itemIndex > 0) {
+                    writeSeparator();
+                }
+                final int item = items[itemIndex];
+                writeNumber(item);
+            }
+            writeArrayEnd();
+        }
+    }
+
+    public void writeArray(final long[] items) {
+        if (items == null) {
+            writeNull();
+        } else {
+            writeArrayStart();
+            for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {
+                if (itemIndex > 0) {
+                    writeSeparator();
+                }
+                final long item = items[itemIndex];
+                writeNumber(item);
+            }
+            writeArrayEnd();
+        }
+    }
+
+    public void writeArray(final float[] items) {
+        if (items == null) {
+            writeNull();
+        } else {
+            writeArrayStart();
+            for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {
+                if (itemIndex > 0) {
+                    writeSeparator();
+                }
+                final float item = items[itemIndex];
+                writeNumber(item);
+            }
+            writeArrayEnd();
+        }
+    }
+
+    public void writeArray(final double[] items) {
+        if (items == null) {
+            writeNull();
+        } else {
+            writeArrayStart();
+            for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {
+                if (itemIndex > 0) {
+                    writeSeparator();
+                }
+                final double item = items[itemIndex];
+                writeNumber(item);
+            }
+            writeArrayEnd();
+        }
+    }
+
+    public void writeArray(final Object[] items) {
+        if (items == null) {
+            writeNull();
+        } else {
+            writeArrayStart();
+            for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {
+                if (itemIndex > 0) {
+                    writeSeparator();
+                }
+                final Object item = items[itemIndex];
+                writeValue(item);
+            }
+            writeArrayEnd();
+        }
+    }
+
+    public void writeArrayStart() {
+        stringBuilder.append('[');
+    }
+
+    public void writeArrayEnd() {
+        stringBuilder.append(']');
+    }
+
+    public void writeSeparator() {
+        stringBuilder.append(',');
+    }
+
+    public <S> void writeString(
+            final BiConsumer<StringBuilder, S> emitter,
+            final S state) {
+        Objects.requireNonNull(emitter, "emitter");
+        stringBuilder.append('"');
+        formattableBuffer.setLength(0);
+        emitter.accept(formattableBuffer, state);
+        final int length = formattableBuffer.length();
+        // Handle max. string length complying input.
+        if (length <= maxStringLength) {
+            quoteString(formattableBuffer, 0, length);
+        }
+        // Handle max. string length violating input.
+        else {
+            quoteString(formattableBuffer, 0, maxStringLength);
+            stringBuilder.append(quotedTruncatedStringSuffix);
+        }
+        stringBuilder.append('"');
+    }
+
+    public void writeString(final StringBuilderFormattable formattable) {
+        if (formattable == null) {
+            writeNull();
+        } else {
+            stringBuilder.append('"');
+            formattableBuffer.setLength(0);
+            formattable.formatTo(formattableBuffer);
+            final int length = formattableBuffer.length();
+            // Handle max. string length complying input.
+            if (length <= maxStringLength) {
+                quoteString(formattableBuffer, 0, length);
+            }
+            // Handle max. string length violating input.
+            else {
+                quoteString(formattableBuffer, 0, maxStringLength);
+                stringBuilder.append(quotedTruncatedStringSuffix);
+            }
+            stringBuilder.append('"');
+        }
+    }
+
+    public void writeString(final CharSequence seq) {
+        if (seq == null) {
+            writeNull();
+        } else {
+            writeString(seq, 0, seq.length());
+        }
+    }
+
+    public void writeString(
+            final CharSequence seq,
+            final int offset,
+            final int length) {
+
+        // Handle null input.
+        if (seq == null) {
+            writeNull();
+            return;
+        }
+
+        // Check arguments.
+        if (offset < 0) {
+            throw new IllegalArgumentException(
+                    "was expecting a positive offset: " + offset);
+        }
+        if (length < 0) {
+            throw new IllegalArgumentException(
+                    "was expecting a positive length: " + length);
+        }
+
+        stringBuilder.append('"');
+        // Handle max. string length complying input.
+        if (length <= maxStringLength) {
+            quoteString(seq, offset, length);
+        }
+        // Handle max. string length violating input.
+        else {
+            quoteString(seq, offset, maxStringLength);
+            stringBuilder.append(quotedTruncatedStringSuffix);
+        }
+        stringBuilder.append('"');
+
+    }
+
+    /**
+     * Quote text contents using JSON standard quoting.
+     */
+    private void quoteString(
+            final CharSequence seq,
+            final int offset,
+            final int length) {
+        final int limit = offset + length;
+        int i = offset;
+        outer:
+        while (i < limit) {
+            while (true) {
+                final char c = seq.charAt(i);
+                if (c < ESC_CODES.length && ESC_CODES[c] != 0) {
+                    break;
+                }
+                stringBuilder.append(c);
+                if (++i >= limit) {
+                    break outer;
+                }
+            }
+            final char d = seq.charAt(i++);
+            final int escCode = ESC_CODES[d];
+            final int quoteBufferLength = escCode < 0
+                    ? quoteNumeric(d)
+                    : quoteNamed(escCode);
+            stringBuilder.append(quoteBuffer, 0, quoteBufferLength);
+        }
+    }
+
+    public void writeString(final char[] buffer) {
+        if (buffer == null) {
+            writeNull();
+        } else {
+            writeString(buffer, 0, buffer.length);
+        }
+    }
+
+    public void writeString(
+            final char[] buffer,
+            final int offset,
+            final int length) {
+
+        // Handle null input.
+        if (buffer == null) {
+            writeNull();
+            return;
+        }
+
+        // Check arguments.
+        if (offset < 0) {
+            throw new IllegalArgumentException(
+                    "was expecting a positive offset: " + offset);
+        }
+        if (length < 0) {
+            throw new IllegalArgumentException(
+                    "was expecting a positive length: " + length);
+        }
+
+        stringBuilder.append('"');
+        // Handle max. string length complying input.
+        if (length <= maxStringLength) {
+            quoteString(buffer, offset, length);
+        }
+        // Handle max. string length violating input.
+        else {
+            quoteString(buffer, offset, maxStringLength);
+            stringBuilder.append(quotedTruncatedStringSuffix);
+        }
+        stringBuilder.append('"');
+
+    }
+
+    /**
+     * Quote text contents using JSON standard quoting.
+     */
+    private void quoteString(
+            final char[] buffer,
+            final int offset,
+            final int length) {
+        final int limit = offset + length;
+        int i = offset;
+        outer:
+        while (i < limit) {
+            while (true) {
+                final char c = buffer[i];
+                if (c < ESC_CODES.length && ESC_CODES[c] != 0) {
+                    break;
+                }
+                stringBuilder.append(c);
+                if (++i >= limit) {
+                    break outer;
+                }
+            }
+            final char d = buffer[i++];
+            final int escCode = ESC_CODES[d];
+            final int quoteBufferLength = escCode < 0
+                    ? quoteNumeric(d)
+                    : quoteNamed(escCode);
+            stringBuilder.append(quoteBuffer, 0, quoteBufferLength);
+        }
+    }
+
+    private int quoteNumeric(final int value) {
+        quoteBuffer[1] = 'u';
+        // We know it's a control char, so only the last 2 chars are non-0
+        quoteBuffer[4] = HEX_CHARS[value >> 4];
+        quoteBuffer[5] = HEX_CHARS[value & 0xF];
+        return 6;
+    }
+
+    private int quoteNamed(final int esc) {
+        quoteBuffer[1] = (char) esc;
+        return 2;
+    }
+
+    private void writeNumber(final Number number) {
+        if (number instanceof BigDecimal) {
+            final BigDecimal decimalNumber = (BigDecimal) number;
+            writeNumber(decimalNumber);
+        } else if (number instanceof BigInteger) {
+            final BigInteger integerNumber = (BigInteger) number;
+            writeNumber(integerNumber);
+        } else if (number instanceof Double) {
+            final double doubleNumber = (Double) number;
+            writeNumber(doubleNumber);
+        } else if (number instanceof Float) {
+            final float floatNumber = (float) number;
+            writeNumber(floatNumber);
+        } else if (number instanceof Byte ||
+                number instanceof Short ||
+                number instanceof Integer ||
+                number instanceof Long) {
+            final long longNumber = number.longValue();
+            writeNumber(longNumber);
+        } else {
+            final long longNumber = number.longValue();
+            final double doubleValue = number.doubleValue();
+            if (Double.compare(longNumber, doubleValue) == 0) {
+                writeNumber(longNumber);
+            } else {
+                writeNumber(doubleValue);
+            }
+        }
+    }
+
+    public void writeNumber(final BigDecimal number) {
+        if (number == null) {
+            writeNull();
+        } else {
+            stringBuilder.append(number);
+        }
+    }
+
+    public void writeNumber(final BigInteger number) {
+        if (number == null) {
+            writeNull();
+        } else {
+            stringBuilder.append(number);
+        }
+    }
+
+    public void writeNumber(final float number) {
+        stringBuilder.append(number);
+    }
+
+    public void writeNumber(final double number) {
+        stringBuilder.append(number);
+    }
+
+    public void writeNumber(final short number) {
+        stringBuilder.append(number);
+    }
+
+    public void writeNumber(final int number) {
+        stringBuilder.append(number);
+    }
+
+    public void writeNumber(final long number) {
+        stringBuilder.append(number);
+    }
+
+    public void writeNumber(final long integralPart, final long fractionalPart) {
+        if (fractionalPart < 0) {
+            throw new IllegalArgumentException(
+                    "was expecting a positive fraction: " + fractionalPart);
+        }
+        stringBuilder.append(integralPart);
+        if (fractionalPart != 0) {
+            stringBuilder.append('.');
+            stringBuilder.append(fractionalPart);
+        }
+    }
+
+    public void writeBoolean(final boolean value) {
+        writeRawString(value ? "true" : "false");
+    }
+
+    public void writeNull() {
+        writeRawString("null");
+    }
+
+    public void writeRawString(final CharSequence seq) {
+        Objects.requireNonNull(seq, "seq");
+        writeRawString(seq, 0, seq.length());
+    }
+
+    public void writeRawString(
+            final CharSequence seq,
+            final int offset,
+            final int length) {
+
+        // Check arguments.
+        Objects.requireNonNull(seq, "seq");
+        if (offset < 0) {
+            throw new IllegalArgumentException(
+                    "was expecting a positive offset: " + offset);
+        }
+        if (length < 0) {
+            throw new IllegalArgumentException(
+                    "was expecting a positive length: " + length);
+        }
+
+        // Write characters.
+        final int limit = offset + length;
+        stringBuilder.append(seq, offset, limit);
+
+    }
+
+    public void writeRawString(final char[] buffer) {
+        Objects.requireNonNull(buffer, "buffer");
+        writeRawString(buffer, 0, buffer.length);
+    }
+
+    public void writeRawString(
+            final char[] buffer,
+            final int offset,
+            final int length) {
+
+        // Check arguments.
+        Objects.requireNonNull(buffer, "buffer");
+        if (offset < 0) {
+            throw new IllegalArgumentException(
+                    "was expecting a positive offset: " + offset);
+        }
+        if (length < 0) {
+            throw new IllegalArgumentException(
+                    "was expecting a positive length: " + length);
+        }
+
+        // Write characters.
+        stringBuilder.append(buffer, offset, length);
+
+    }
+
+    @Override
+    public void close() {
+        stringBuilder.setLength(0);
+    }
+
+    @Override
+    @SuppressWarnings("MethodDoesntCallSuperMethod")
+    public JsonWriter clone() {
+        final JsonWriter jsonWriter = newBuilder()
+                .setMaxStringLength(maxStringLength)
+                .setTruncatedStringSuffix(truncatedStringSuffix)
+                .build();
+        jsonWriter.stringBuilder.append(stringBuilder);
+        return jsonWriter;
+    }
+
+    public static Builder newBuilder() {
+        return new Builder();
+    }
+
+    public static final class Builder {
+
+        private int maxStringLength;
+
+        private String truncatedStringSuffix;
+
+        public int getMaxStringLength() {
+            return maxStringLength;
+        }
+
+        public Builder setMaxStringLength(final int maxStringLength) {
+            this.maxStringLength = maxStringLength;
+            return this;
+        }
+
+        public String getTruncatedStringSuffix() {
+            return truncatedStringSuffix;
+        }
+
+        public Builder setTruncatedStringSuffix(final String truncatedStringSuffix) {
+            this.truncatedStringSuffix = truncatedStringSuffix;
+            return this;
+        }
+
+        public JsonWriter build() {
+            validate();
+            return new JsonWriter(this);
+        }
+
+        private void validate() {
+            if (maxStringLength <= 0) {
+                throw new IllegalArgumentException(
+                        "was expecting maxStringLength > 0: " +
+                                maxStringLength);
+            }
+            Objects.requireNonNull(truncatedStringSuffix, "truncatedStringSuffix");
+        }
+
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/MapAccessor.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/MapAccessor.java
new file mode 100644
index 0000000..a4c140f
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/MapAccessor.java
@@ -0,0 +1,139 @@
+/*
+ * 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.json.template.util;
+
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Objects;
+
+public class MapAccessor {
+
+    private final Map<String, Object> map;
+
+    public MapAccessor(final Map<String, Object> map) {
+        this.map = Objects.requireNonNull(map, "map");
+    }
+
+    public String getString(final String key) {
+        final String[] path = {key};
+        return getObject(path, String.class);
+    }
+
+    public String getString(final String[] path) {
+        return getObject(path, String.class);
+    }
+
+    public boolean getBoolean(final String key, final boolean defaultValue) {
+        final String[] path = {key};
+        return getBoolean(path, defaultValue);
+    }
+
+    public boolean getBoolean(final String[] path, final boolean defaultValue) {
+        final Boolean value = getObject(path, Boolean.class);
+        return value == null ? defaultValue : value;
+    }
+
+    public Boolean getBoolean(final String key) {
+        final String[] path = {key};
+        return getObject(path, Boolean.class);
+    }
+
+    public Boolean getBoolean(final String[] path) {
+        return getObject(path, Boolean.class);
+    }
+
+    public Integer getInteger(final String key) {
+        final String[] path = {key};
+        return getInteger(path);
+    }
+
+    public Integer getInteger(final String[] path) {
+        return getObject(path, Integer.class);
+    }
+
+    public boolean exists(final String key) {
+        final String[] path = {key};
+        return exists(path);
+    }
+
+    public boolean exists(final String[] path) {
+        final Object value = getObject(path, Object.class);
+        return value != null;
+    }
+
+    public Object getObject(final String key) {
+        final String[] path = {key};
+        return getObject(path, Object.class);
+    }
+
+    public <T> T getObject(final String key, final Class<T> clazz) {
+        final String[] path = {key};
+        return getObject(path, clazz);
+    }
+
+    public Object getObject(final String[] path) {
+        return getObject(path, Object.class);
+    }
+
+    public <T> T getObject(final String[] path, final Class<T> clazz) {
+        Objects.requireNonNull(path, "path");
+        Objects.requireNonNull(clazz, "clazz");
+        if (path.length == 0) {
+            throw new IllegalArgumentException("empty path");
+        }
+        Object parent = map;
+        for (final String key : path) {
+            if (!(parent instanceof Map)) {
+                return null;
+            }
+            @SuppressWarnings("unchecked")
+            final Map<String, Object> parentMap = (Map<String, Object>) parent;
+            parent = parentMap.get(key);
+        }
+        if (parent != null && !clazz.isInstance(parent)) {
+            final String message = String.format(
+                    "was expecting %s at path %s: %s (of type %s)",
+                    clazz.getSimpleName(),
+                    Arrays.asList(path),
+                    parent,
+                    parent.getClass().getCanonicalName());
+            throw new IllegalArgumentException(message);
+        }
+        @SuppressWarnings("unchecked")
+        final T typedValue = (T) parent;
+        return typedValue;
+    }
+
+    @Override
+    public boolean equals(final Object instance) {
+        if (this == instance) return true;
+        if (instance == null || getClass() != instance.getClass()) return false;
+        final MapAccessor that = (MapAccessor) instance;
+        return map.equals(that.map);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(map);
+    }
+
+    @Override
+    public String toString() {
+        return map.toString();
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/QueueingRecycler.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/QueueingRecycler.java
new file mode 100644
index 0000000..5f091bd
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/QueueingRecycler.java
@@ -0,0 +1,61 @@
+/*
+ * 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.json.template.util;
+
+import java.util.Queue;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+public class QueueingRecycler<V> implements Recycler<V> {
+
+    private final Supplier<V> supplier;
+
+    private final Consumer<V> cleaner;
+
+    private final Queue<V> queue;
+
+    public QueueingRecycler(
+            final Supplier<V> supplier,
+            final Consumer<V> cleaner,
+            final Queue<V> queue) {
+        this.supplier = supplier;
+        this.cleaner = cleaner;
+        this.queue = queue;
+    }
+
+    // Visible for tests.
+    Queue<V> getQueue() {
+        return queue;
+    }
+
+    @Override
+    public V acquire() {
+        final V value = queue.poll();
+        if (value == null) {
+            return supplier.get();
+        } else {
+            cleaner.accept(value);
+            return value;
+        }
+    }
+
+    @Override
+    public void release(final V value) {
+        queue.offer(value);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/QueueingRecyclerFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/QueueingRecyclerFactory.java
new file mode 100644
index 0000000..c549522
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/QueueingRecyclerFactory.java
@@ -0,0 +1,40 @@
+/*
+ * 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.json.template.util;
+
+import java.util.Queue;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+public class QueueingRecyclerFactory implements RecyclerFactory {
+
+    private final Supplier<Queue<Object>> queueSupplier;
+
+    public QueueingRecyclerFactory(final Supplier<Queue<Object>> queueSupplier) {
+        this.queueSupplier = queueSupplier;
+    }
+
+    @Override
+    public <V> Recycler<V> create(
+            final Supplier<V> supplier,
+            final Consumer<V> cleaner) {
+        @SuppressWarnings("unchecked")
+        final Queue<V> queue = (Queue<V>) queueSupplier.get();
+        return new QueueingRecycler<V>(supplier, cleaner, queue);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/Recycler.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/Recycler.java
new file mode 100644
index 0000000..b6a0c89
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/Recycler.java
@@ -0,0 +1,25 @@
+/*
+ * 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.json.template.util;
+
+public interface Recycler<V> {
+
+    V acquire();
+
+    void release(V value);
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/RecyclerFactories.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/RecyclerFactories.java
new file mode 100644
index 0000000..7564e8d
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/RecyclerFactories.java
@@ -0,0 +1,205 @@
+/*
+ * 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.json.template.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;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Queue;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.function.Supplier;
+
+public enum RecyclerFactories {;
+
+    private static final String JCTOOLS_QUEUE_CLASS_SUPPLIER_PATH =
+            "org.jctools.queues.MpmcArrayQueue.new";
+
+    private static final boolean JCTOOLS_QUEUE_CLASS_AVAILABLE =
+            isJctoolsQueueClassAvailable();
+
+    private static boolean isJctoolsQueueClassAvailable() {
+        try {
+            final String className = JCTOOLS_QUEUE_CLASS_SUPPLIER_PATH
+                    .replaceAll("\\.new$", "");
+            LoaderUtil.loadClass(className);
+            return true;
+        } catch (final ClassNotFoundException ignored) {
+            return false;
+        }
+    }
+
+    @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.
+        int defaultCapacity = Math.max(
+                2 * Runtime.getRuntime().availableProcessors() + 1,
+                8);
+
+        // TLA-, MPMC-, or ABQ-based queueing factory -- if nothing is specified.
+        if (recyclerFactorySpec == null) {
+            if (Constants.ENABLE_THREADLOCALS) {
+                return ThreadLocalRecyclerFactory.getInstance();
+            } else {
+                final Supplier<Queue<Object>> queueSupplier =
+                        JCTOOLS_QUEUE_CLASS_AVAILABLE
+                                ? () -> new MpmcArrayQueue<>(defaultCapacity)
+                                : () -> new ArrayBlockingQueue<>(defaultCapacity);
+                return new QueueingRecyclerFactory(queueSupplier);
+            }
+        }
+
+        // Is a dummy factory requested?
+        else if (recyclerFactorySpec.equals("dummy")) {
+            return DummyRecyclerFactory.getInstance();
+        }
+
+        // Is a TLA factory requested?
+        else if (recyclerFactorySpec.equals("threadLocal")) {
+            return ThreadLocalRecyclerFactory.getInstance();
+        }
+
+        // Is a queueing factory requested?
+        else if (recyclerFactorySpec.startsWith("queue")) {
+            return readQueueingRecyclerFactory(recyclerFactorySpec, defaultCapacity);
+        }
+
+        // Bogus input, bail out.
+        else {
+            throw new IllegalArgumentException(
+                    "invalid recycler factory: " + recyclerFactorySpec);
+        }
+
+    }
+
+    private static RecyclerFactory readQueueingRecyclerFactory(
+            final String recyclerFactorySpec,
+            final int defaultCapacity) {
+
+        // Parse the spec.
+        final String queueFactorySpec = recyclerFactorySpec.substring(
+                "queue".length() +
+                        (recyclerFactorySpec.startsWith("queue:")
+                                ? 1
+                                : 0));
+        final Map<String, StringParameterParser.Value> parsedValues =
+                StringParameterParser.parse(
+                        queueFactorySpec,
+                        new LinkedHashSet<>(Arrays.asList("supplier", "capacity")));
+
+        // Read the supplier path.
+        final StringParameterParser.Value supplierValue = parsedValues.get("supplier");
+        final String supplierPath;
+        if (supplierValue == null || supplierValue instanceof StringParameterParser.NullValue) {
+            supplierPath = JCTOOLS_QUEUE_CLASS_AVAILABLE
+                    ? JCTOOLS_QUEUE_CLASS_SUPPLIER_PATH
+                    : "java.util.concurrent.ArrayBlockingQueue.new";
+        } else {
+            supplierPath = supplierValue.toString();
+        }
+
+        // Read the capacity.
+        final StringParameterParser.Value capacityValue = parsedValues.get("capacity");
+        final int capacity;
+        if (capacityValue == null || capacityValue instanceof StringParameterParser.NullValue) {
+            capacity = defaultCapacity;
+        } else {
+            try {
+                capacity = Integer.parseInt(capacityValue.toString());
+            } catch (final NumberFormatException error) {
+                throw new IllegalArgumentException(
+                        "failed reading capacity in queueing recycler " +
+                                "factory: " + queueFactorySpec, error);
+            }
+        }
+
+        // Execute the read spec.
+        return createRecyclerFactory(queueFactorySpec, supplierPath, capacity);
+
+    }
+
+    private static RecyclerFactory createRecyclerFactory(
+            final String queueFactorySpec,
+            final String supplierPath,
+            final int capacity) {
+        final int supplierPathSplitterIndex = supplierPath.lastIndexOf('.');
+        if (supplierPathSplitterIndex < 0) {
+            throw new IllegalArgumentException(
+                    "invalid supplier in queueing recycler factory: " +
+                            queueFactorySpec);
+        }
+        final String supplierClassName = supplierPath.substring(0, supplierPathSplitterIndex);
+        final String supplierMethodName = supplierPath.substring(supplierPathSplitterIndex + 1);
+        try {
+            final Class<?> supplierClass = LoaderUtil.loadClass(supplierClassName);
+            final Supplier<Queue<Object>> queueSupplier;
+            if ("new".equals(supplierMethodName)) {
+                final Constructor<?> supplierCtor =
+                        supplierClass.getDeclaredConstructor(int.class);
+                queueSupplier = () -> {
+                    try {
+                        @SuppressWarnings("unchecked")
+                        final Queue<Object> typedQueue =
+                                (Queue<Object>) supplierCtor.newInstance(capacity);
+                        return typedQueue;
+                    } catch (final Exception error) {
+                        throw new RuntimeException(
+                                "recycler queue construction failed for factory: " +
+                                        queueFactorySpec, error);
+                    }
+                };
+            } else {
+                final Method supplierMethod =
+                        supplierClass.getMethod(supplierMethodName, int.class);
+                queueSupplier = () -> {
+                    try {
+                        @SuppressWarnings("unchecked")
+                        final Queue<Object> typedQueue =
+                                (Queue<Object>) supplierMethod.invoke(null, capacity);
+                        return typedQueue;
+                    } catch (final Exception error) {
+                        throw new RuntimeException(
+                                "recycler queue construction failed for factory: " +
+                                        queueFactorySpec, error);
+                    }
+                };
+            }
+            return new QueueingRecyclerFactory(queueSupplier);
+        } catch (final Exception error) {
+            throw new RuntimeException(
+                    "failed executing queueing recycler factory: " +
+                            queueFactorySpec, error);
+        }
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/RecyclerFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/RecyclerFactory.java
new file mode 100644
index 0000000..3b7737c
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/RecyclerFactory.java
@@ -0,0 +1,31 @@
+/*
+ * 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.json.template.util;
+
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+@FunctionalInterface
+public interface RecyclerFactory {
+
+    default <V> Recycler<V> create(Supplier<V> supplier) {
+        return create(supplier, ignored -> {});
+    }
+
+    <V> Recycler<V> create(Supplier<V> supplier, Consumer<V> cleaner);
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/StringParameterParser.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/StringParameterParser.java
new file mode 100644
index 0000000..018f6b7
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/StringParameterParser.java
@@ -0,0 +1,292 @@
+package org.apache.logging.log4j.layout.json.template.util;
+
+import org.apache.logging.log4j.util.Strings;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.Callable;
+
+public enum StringParameterParser {;
+
+    public enum Values {;
+
+        static NullValue nullValue() {
+            return NullValue.INSTANCE;
+        }
+
+        static StringValue stringValue(final String string) {
+            return new StringValue(string);
+        }
+
+        static DoubleQuotedStringValue doubleQuotedStringValue(
+                final String doubleQuotedString) {
+            return new DoubleQuotedStringValue(doubleQuotedString);
+        }
+
+    }
+
+    public interface Value {}
+
+    public static final class NullValue implements Value {
+
+        private static final NullValue INSTANCE = new NullValue();
+
+        private NullValue() {}
+
+        @Override
+        public String toString() {
+            return null;
+        }
+
+    }
+
+    public static final class StringValue implements Value {
+
+        private final String string;
+
+        private StringValue(String string) {
+            this.string = string;
+        }
+
+        public String getString() {
+            return string;
+        }
+
+        @Override
+        public boolean equals(Object object) {
+            if (this == object) return true;
+            if (object == null || getClass() != object.getClass()) return false;
+            StringValue that = (StringValue) object;
+            return string.equals(that.string);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(string);
+        }
+
+        @Override
+        public String toString() {
+            return string;
+        }
+
+    }
+
+    public static final class DoubleQuotedStringValue implements Value {
+
+        private final String doubleQuotedString;
+
+        private DoubleQuotedStringValue(String doubleQuotedString) {
+            this.doubleQuotedString = doubleQuotedString;
+        }
+
+        public String getDoubleQuotedString() {
+            return doubleQuotedString;
+        }
+
+        @Override
+        public boolean equals(Object object) {
+            if (this == object) return true;
+            if (object == null || getClass() != object.getClass()) return false;
+            DoubleQuotedStringValue that = (DoubleQuotedStringValue) object;
+            return doubleQuotedString.equals(that.doubleQuotedString);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(doubleQuotedString);
+        }
+
+        @Override
+        public String toString() {
+            return doubleQuotedString.replaceAll("\\\\\"", "\"");
+        }
+
+    }
+
+    private enum State { READING_KEY, READING_VALUE }
+
+    private static final class Parser implements Callable<Map<String, Value>> {
+
+        private final String input;
+
+        private final Map<String, Value> map;
+
+        private State state;
+
+        private int i;
+
+        private String key;
+
+        private Parser(final String input) {
+            this.input = Objects.requireNonNull(input, "input");
+            this.map = new LinkedHashMap<>();
+            this.state = State.READING_KEY;
+            this.i = 0;
+            this.key = null;
+        }
+
+        @Override
+        public Map<String, Value> call() {
+            while (true) {
+                skipWhitespace();
+                if (i >= input.length()) {
+                    break;
+                }
+                switch (state) {
+                    case READING_KEY:
+                        readKey();
+                        break;
+                    case READING_VALUE:
+                        readValue();
+                        break;
+                    default:
+                        throw new IllegalStateException("unknown state: " + state);
+                }
+            }
+            if (state == State.READING_VALUE) {
+                map.put(key, Values.nullValue());
+            }
+            return map;
+        }
+
+        private void readKey() {
+            final int eq = input.indexOf('=', i);
+            final int co = input.indexOf(',', i);
+            final int j;
+            final int nextI;
+            if (eq < 0 && co < 0) {
+                // Neither '=', nor ',' was found.
+                j = nextI = input.length();
+            } else if (eq < 0) {
+                // Found ','.
+                j = nextI = co;
+            } else if (co < 0) {
+                // Found '='.
+                j = eq;
+                nextI = eq + 1;
+            } else if (eq < co) {
+                // Found '=...,'.
+                j = eq;
+                nextI = eq + 1;
+            } else {
+                // Found ',...='.
+                j = co;
+                nextI = co;
+            }
+            key = input.substring(i, j).trim();
+            if (Strings.isEmpty(key)) {
+                final String message = String.format(
+                        "failed to locate key at index %d: %s",
+                        i, input);
+                throw new IllegalArgumentException(message);
+            }
+            if (map.containsKey(key)) {
+                final String message = String.format(
+                        "conflicting key at index %d: %s",
+                        i, input);
+                throw new IllegalArgumentException(message);
+            }
+            state = State.READING_VALUE;
+            i = nextI;
+        }
+
+        private void readValue() {
+            final boolean doubleQuoted = input.charAt(i) == '"';
+            if (doubleQuoted) {
+                readDoubleQuotedStringValue();
+            } else {
+                readStringValue();
+            }
+            key = null;
+            state = State.READING_KEY;
+        }
+
+        private void readDoubleQuotedStringValue() {
+            int j = i + 1;
+            while (j < input.length()) {
+                if (input.charAt(j) == '"' && input.charAt(j - 1) != '\\') {
+                    break;
+                } else {
+                    j++;
+                }
+            }
+            if (j >= input.length()) {
+                final String message = String.format(
+                        "failed to locate the end of double-quoted content starting at index %d: %s",
+                        i, input);
+                throw new IllegalArgumentException(message);
+            }
+            final String content = input
+                    .substring(i + 1, j)
+                    .replaceAll("\\\\\"", "\"");
+            final Value value = Values.doubleQuotedStringValue(content);
+            map.put(key, value);
+            i = j + 1;
+            skipWhitespace();
+            if (i < input.length()) {
+                if (input.charAt(i) != ',') {
+                    final String message = String.format(
+                            "was expecting comma at index %d: %s",
+                            i, input);
+                    throw new IllegalArgumentException(message);
+                }
+                i++;
+            }
+        }
+
+        private void skipWhitespace() {
+            while (i < input.length()) {
+                final char c = input.charAt(i);
+                if (!Character.isWhitespace(c)) {
+                    break;
+                } else {
+                    i++;
+                }
+            }
+        }
+
+        private void readStringValue() {
+            int j = input.indexOf(',', i/* + 1*/);
+            if (j < 0) {
+                j = input.length();
+            }
+            final String content = input.substring(i, j);
+            final String trimmedContent = content.trim();
+            final Value value = trimmedContent.isEmpty()
+                    ? Values.nullValue()
+                    : Values.stringValue(trimmedContent);
+            map.put(key, value);
+            i += content.length() + 1;
+        }
+
+    }
+
+    public static Map<String, Value> parse(final String input) {
+        return parse(input, null);
+    }
+
+    public static Map<String, Value> parse(
+            final String input,
+            final Set<String> allowedKeys) {
+        if (Strings.isBlank(input)) {
+            return Collections.emptyMap();
+        }
+        final Map<String, Value> map = new Parser(input).call();
+        final Set<String> actualKeys = map.keySet();
+        for (final String actualKey : actualKeys) {
+            final boolean allowed = allowedKeys == null || allowedKeys.contains(actualKey);
+            if (!allowed) {
+                final String message = String.format(
+                        "unknown key \"%s\" is found in input: %s",
+                        actualKey, input);
+                throw new IllegalArgumentException(message);
+            }
+        }
+        return map;
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/ThreadLocalRecycler.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/ThreadLocalRecycler.java
new file mode 100644
index 0000000..1c58d4f
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/ThreadLocalRecycler.java
@@ -0,0 +1,45 @@
+/*
+ * 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.json.template.util;
+
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+public class ThreadLocalRecycler<V> implements Recycler<V> {
+
+    private final Consumer<V> cleaner;
+
+    private final ThreadLocal<V> holder;
+
+    public ThreadLocalRecycler(
+            final Supplier<V> supplier,
+            final Consumer<V> cleaner) {
+        this.cleaner = cleaner;
+        this.holder = ThreadLocal.withInitial(supplier);
+    }
+
+    @Override
+    public V acquire() {
+        final V value = holder.get();
+        cleaner.accept(value);
+        return value;
+    }
+
+    @Override
+    public void release(final V value) {}
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/ThreadLocalRecyclerFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/ThreadLocalRecyclerFactory.java
new file mode 100644
index 0000000..8ea6c61
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/ThreadLocalRecyclerFactory.java
@@ -0,0 +1,40 @@
+/*
+ * 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.json.template.util;
+
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+public class ThreadLocalRecyclerFactory implements RecyclerFactory {
+
+    private static final ThreadLocalRecyclerFactory INSTANCE =
+            new ThreadLocalRecyclerFactory();
+
+    private ThreadLocalRecyclerFactory() {}
+
+    public static ThreadLocalRecyclerFactory getInstance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public <V> Recycler<V> create(
+            final Supplier<V> supplier,
+            final Consumer<V> cleaner) {
+        return new ThreadLocalRecycler<>(supplier, cleaner);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/TruncatingBufferedPrintWriter.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/TruncatingBufferedPrintWriter.java
new file mode 100644
index 0000000..37338e6
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/TruncatingBufferedPrintWriter.java
@@ -0,0 +1,60 @@
+/*
+ * 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.json.template.util;
+
+import java.io.PrintWriter;
+
+public final class TruncatingBufferedPrintWriter extends PrintWriter {
+
+    private final TruncatingBufferedWriter writer;
+
+    private TruncatingBufferedPrintWriter(final TruncatingBufferedWriter writer) {
+        super(writer, false);
+        this.writer = writer;
+    }
+
+    public static TruncatingBufferedPrintWriter ofCapacity(final int capacity) {
+        if (capacity < 0) {
+            throw new IllegalArgumentException(
+                    "was expecting a non-negative capacity: " + capacity);
+        }
+        final TruncatingBufferedWriter writer = new TruncatingBufferedWriter(capacity);
+        return new TruncatingBufferedPrintWriter(writer);
+    }
+
+    public char[] getBuffer() {
+        return writer.getBuffer();
+    }
+
+    public int getPosition() {
+        return writer.getPosition();
+    }
+
+    public int getCapacity() {
+        return writer.getCapacity();
+    }
+
+    public boolean isTruncated() {
+        return writer.isTruncated();
+    }
+
+    @Override
+    public void close() {
+        writer.close();
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/TruncatingBufferedWriter.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/TruncatingBufferedWriter.java
new file mode 100644
index 0000000..e31f21c
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/TruncatingBufferedWriter.java
@@ -0,0 +1,208 @@
+/*
+ * 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.json.template.util;
+
+import java.io.Writer;
+import java.util.Objects;
+
+public final class TruncatingBufferedWriter extends Writer {
+
+    private final char[] buffer;
+
+    private int position;
+
+    private boolean truncated;
+
+    TruncatingBufferedWriter(final int capacity) {
+        this.buffer = new char[capacity];
+        this.position = 0;
+        this.truncated = false;
+    }
+
+    char[] getBuffer() {
+        return buffer;
+    }
+
+    int getPosition() {
+        return position;
+    }
+
+    int getCapacity() {
+        return buffer.length;
+    }
+
+    boolean isTruncated() {
+        return truncated;
+    }
+
+    @Override
+    public void write(final int c) {
+        if (position < buffer.length) {
+            buffer[position++] = (char) c;
+        } else {
+            truncated = true;
+        }
+    }
+
+    @Override
+    public void write(final char[] source) {
+        Objects.requireNonNull(source, "source");
+        write(source, 0, source.length);
+    }
+
+    @Override
+    public void write(final char[] source, final int offset, final int length) {
+
+        // Check arguments.
+        Objects.requireNonNull(source, "source");
+        if (offset < 0 || offset >= source.length) {
+            throw new IndexOutOfBoundsException("invalid offset: " + offset);
+        }
+        if (length < 0 || Math.addExact(offset, length) > source.length) {
+            throw new IndexOutOfBoundsException("invalid length: " + length);
+        }
+
+        // If input fits as is.
+        final int maxLength = buffer.length - position;
+        if (length < maxLength) {
+            System.arraycopy(source, offset, buffer, position, length);
+            position += length;
+        }
+
+        // If truncation is possible.
+        else if (maxLength > 0) {
+            System.arraycopy(source, offset, buffer, position, maxLength);
+            position += maxLength;
+            truncated = true;
+        }
+
+    }
+
+    @Override
+    public void write(final String string) {
+
+        // Check arguments.
+        Objects.requireNonNull(string, "string");
+        final int length = string.length();
+        final int maxLength = buffer.length - position;
+
+        // If input fits as is.
+        if (length < maxLength) {
+            string.getChars(0, length, buffer, position);
+            position += length;
+        }
+
+        // If truncation is possible.
+        else if (maxLength > 0) {
+            string.getChars(0, maxLength, buffer, position);
+            position += maxLength;
+            truncated = true;
+        }
+
+    }
+
+    @Override
+    public void write(final String string, final int offset, final int length) {
+
+        // Check arguments.
+        Objects.requireNonNull(string, "string");
+        if (offset < 0 || offset >= string.length()) {
+            throw new IndexOutOfBoundsException("invalid offset: " + offset);
+        }
+        if (length < 0 || Math.addExact(offset, length) > string.length()) {
+            throw new IndexOutOfBoundsException("invalid length: " + length);
+        }
+
+        // If input fits as is.
+        final int maxLength = buffer.length - position;
+        if (length < maxLength) {
+            string.getChars(offset, offset + length, buffer, position);
+            position += length;
+        }
+
+        // If truncation is possible.
+        else if (maxLength > 0) {
+            string.getChars(offset, offset + maxLength, buffer, position);
+            position += maxLength;
+            truncated = true;
+        }
+
+    }
+
+    @Override
+    public Writer append(final char c) {
+        write(c);
+        return this;
+    }
+
+    @Override
+    public Writer append(final CharSequence seq) {
+        return seq == null
+                ? append("null", 0, 4)
+                : append(seq, 0, seq.length());
+    }
+
+    @Override
+    public Writer append(final CharSequence seq, final int start, final int end) {
+
+        // Short-circuit on null sequence.
+        if (seq == null) {
+            write("null");
+            return this;
+        }
+
+        // Check arguments.
+        if (start < 0 || start >= seq.length()) {
+            throw new IndexOutOfBoundsException("invalid start: " + start);
+        }
+        if (end < start || end > seq.length()) {
+            throw new IndexOutOfBoundsException("invalid end: " + end);
+        }
+
+        // If input fits as is.
+        final int length = end - start;
+        final int maxLength = buffer.length - position;
+        if (length < maxLength) {
+            for (int i = start; i < end; i++) {
+                final char c = seq.charAt(i);
+                buffer[position++] = c;
+            }
+        }
+
+        // If truncation is possible.
+        else if (maxLength > 0) {
+            final int truncatedEnd = start + maxLength;
+            for (int i = start; i < truncatedEnd; i++) {
+                final char c = seq.charAt(i);
+                buffer[position++] = c;
+            }
+            truncated = true;
+        }
+        return this;
+
+    }
+
+    @Override
+    public void flush() {}
+
+    @Override
+    public void close() {
+        position = 0;
+        truncated = false;
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/Uris.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/Uris.java
new file mode 100644
index 0000000..65cd863
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/Uris.java
@@ -0,0 +1,138 @@
+/*
+ * 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.json.template.util;
+
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.status.StatusLogger;
+import org.apache.logging.log4j.util.LoaderUtil;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URI;
+import java.net.URL;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+public enum Uris {;
+
+    private static final Logger LOGGER = StatusLogger.getLogger();
+
+    /**
+     * Reads {@link URI} specs of scheme <tt>classpath</tt> and <tt>file</tt>.
+     *
+     * @param spec the {@link URI} spec, e.g., <tt>file:/holy/cow.txt</tt> or
+     *             <tt>classpath:/holy/cat.txt</tt>
+     * @param charset used {@link Charset} for decoding the file
+     */
+    public static String readUri(final String spec, final Charset charset) {
+        Objects.requireNonNull(spec, "spec");
+        Objects.requireNonNull(charset, "charset");
+        try {
+            final URI uri = new URI(spec);
+            return unsafeReadUri(uri, charset);
+        } catch (final Exception error) {
+            throw new RuntimeException("failed reading URI: " + spec, error);
+        }
+    }
+
+    /**
+     * Reads {@link URI}s of scheme <tt>classpath</tt> and <tt>file</tt>.
+     *
+     * @param uri the {@link URI}, e.g., <tt>file:/holy/cow.txt</tt> or
+     *             <tt>classpath:/holy/cat.txt</tt>
+     * @param charset used {@link Charset} for decoding the file
+     */
+    public static String readUri(final URI uri, final Charset charset) {
+        Objects.requireNonNull(uri, "uri");
+        Objects.requireNonNull(charset, "charset");
+        try {
+            return unsafeReadUri(uri, charset);
+        } catch (final Exception error) {
+            throw new RuntimeException("failed reading URI: " + uri, error);
+        }
+    }
+
+    private static String unsafeReadUri(
+            final URI uri,
+            final Charset charset)
+            throws Exception {
+        final String uriScheme = uri.getScheme().toLowerCase();
+        switch (uriScheme) {
+            case "classpath":
+                return readClassPathUri(uri, charset);
+            case "file":
+                return readFileUri(uri, charset);
+            default: {
+                throw new IllegalArgumentException("unknown scheme in URI: " + uri);
+            }
+        }
+    }
+
+    private static String readFileUri(
+            final URI uri,
+            final Charset charset)
+            throws IOException {
+        final Path path = Paths.get(uri);
+        try (final BufferedReader fileReader = Files.newBufferedReader(path, charset)) {
+            return consumeReader(fileReader);
+        }
+    }
+
+    private static String readClassPathUri(
+            final URI uri,
+            final Charset charset)
+            throws IOException {
+        final String spec = uri.toString();
+        final String path = spec.substring("classpath:".length());
+        final List<URL> resources = new ArrayList<>(LoaderUtil.findResources(path));
+        if (resources.isEmpty()) {
+            final String message = String.format(
+                    "could not locate classpath resource (path=%s)", path);
+            throw new RuntimeException(message);
+        }
+        final URL resource = resources.get(0);
+        if (resources.size() > 1) {
+            final String message = String.format(
+                    "for URI %s found %d resources, using the first one: %s",
+                    uri, resources.size(), resource);
+            LOGGER.warn(message);
+        }
+        try (final InputStream inputStream = resource.openStream()) {
+            try (final InputStreamReader reader = new InputStreamReader(inputStream, charset);
+                 final BufferedReader bufferedReader = new BufferedReader(reader)) {
+                return consumeReader(bufferedReader);
+            }
+        }
+    }
+
+    private static String consumeReader(final BufferedReader reader) throws IOException {
+        final StringBuilder builder = new StringBuilder();
+        String line;
+        while ((line = reader.readLine()) != null) {
+            builder.append(line);
+        }
+        return builder.toString();
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/resources/EcsLayout.json b/log4j-layout-json-template/src/main/resources/EcsLayout.json
new file mode 100644
index 0000000..dee7a84
--- /dev/null
+++ b/log4j-layout-json-template/src/main/resources/EcsLayout.json
@@ -0,0 +1,46 @@
+{
+  "@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",
+    "stringified": true
+  }
+}
diff --git a/log4j-layout-json-template/src/main/resources/GelfLayout.json b/log4j-layout-json-template/src/main/resources/GelfLayout.json
new file mode 100644
index 0000000..dd43cc8
--- /dev/null
+++ b/log4j-layout-json-template/src/main/resources/GelfLayout.json
@@ -0,0 +1,41 @@
+{
+  "version": "1.1",
+  "host": "${hostName}",
+  "short_message": {
+    "$resolver": "message",
+    "stringified": true
+  },
+  "full_message": {
+    "$resolver": "exception",
+    "field": "stackTrace",
+    "stringified": true
+  },
+  "timestamp": {
+    "$resolver": "timestamp",
+    "epoch": {
+      "unit": "secs"
+    }
+  },
+  "level": {
+    "$resolver": "level",
+    "field": "severity",
+    "severity": {
+      "field": "code"
+    }
+  },
+  "_logger": {
+    "$resolver": "logger",
+    "field": "name"
+  },
+  "_thread": {
+    "$resolver": "thread",
+    "field": "name"
+  },
+  "_mdc": {
+    "$resolver": "mdc",
+    "flatten": {
+      "prefix": "_"
+    },
+    "stringified": true
+  }
+}
diff --git a/log4j-layout-json-template/src/main/resources/JsonLayout.json b/log4j-layout-json-template/src/main/resources/JsonLayout.json
new file mode 100644
index 0000000..503e2cd
--- /dev/null
+++ b/log4j-layout-json-template/src/main/resources/JsonLayout.json
@@ -0,0 +1,83 @@
+{
+  "instant": {
+    "epochSecond": {
+      "$resolver": "timestamp",
+      "epoch": {
+        "unit": "secs",
+        "rounded": true
+      }
+    },
+    "nanoOfSecond": {
+      "$resolver": "timestamp",
+      "epoch": {
+        "unit": "secs.nanos"
+      }
+    }
+  },
+  "thread": {
+    "$resolver": "thread",
+    "field": "name"
+  },
+  "level": {
+    "$resolver": "level",
+    "field": "name"
+  },
+  "loggerName": {
+    "$resolver": "logger",
+    "field": "name"
+  },
+  "message": {
+    "$resolver": "message",
+    "stringified": true
+  },
+  "thrown": {
+    "message": {
+      "$resolver": "exception",
+      "field": "message"
+    },
+    "name": {
+      "$resolver": "exception",
+      "field": "className"
+    },
+    "extendedStackTrace": {
+      "$resolver": "exception",
+      "field": "stackTrace"
+    }
+  },
+  "contextStack": {
+    "$resolver": "ndc"
+  },
+  "endOfBatch": {
+    "$resolver": "endOfBatch"
+  },
+  "loggerFqcn": {
+    "$resolver": "logger",
+    "field": "fqcn"
+  },
+  "threadId": {
+    "$resolver": "thread",
+    "field": "id"
+  },
+  "threadPriority": {
+    "$resolver": "thread",
+    "field": "priority"
+  },
+  "source": {
+    "class": {
+      "$resolver": "source",
+      "field": "className"
+    },
+    "method": {
+      "$resolver": "source",
+      "field": "methodName"
+    },
+    "file": {
+      "$resolver": "source",
+      "field": "fileName"
+    },
+    "line": {
+      "$resolver": "source",
+      "field": "lineNumber"
+    }
+  }
+}
diff --git a/log4j-layout-json-template/src/main/resources/LogstashJsonEventLayoutV1.json b/log4j-layout-json-template/src/main/resources/LogstashJsonEventLayoutV1.json
new file mode 100644
index 0000000..3225930
--- /dev/null
+++ b/log4j-layout-json-template/src/main/resources/LogstashJsonEventLayoutV1.json
@@ -0,0 +1,58 @@
+{
+  "mdc": {
+    "$resolver": "mdc"
+  },
+  "exception": {
+    "exception_class": {
+      "$resolver": "exception",
+      "field": "className"
+    },
+    "exception_message": {
+      "$resolver": "exception",
+      "field": "message",
+      "stringified": true
+    },
+    "stacktrace": {
+      "$resolver": "exception",
+      "field": "stackTrace",
+      "stringified": true
+    }
+  },
+  "line_number": {
+    "$resolver": "source",
+    "field": "lineNumber"
+  },
+  "class": {
+    "$resolver": "source",
+    "field": "className"
+  },
+  "@version": 1,
+  "source_host": "${hostName}",
+  "message": {
+    "$resolver": "message",
+    "stringified": true
+  },
+  "thread_name": {
+    "$resolver": "thread",
+    "field": "name"
+  },
+  "@timestamp": {
+    "$resolver": "timestamp"
+  },
+  "level": {
+    "$resolver": "level",
+    "field": "name"
+  },
+  "file": {
+    "$resolver": "source",
+    "field": "fileName"
+  },
+  "method": {
+    "$resolver": "source",
+    "field": "methodName"
+  },
+  "logger_name": {
+    "$resolver": "logger",
+    "field": "name"
+  }
+}
diff --git a/log4j-layout-json-template/src/main/resources/StackTraceElementLayout.json b/log4j-layout-json-template/src/main/resources/StackTraceElementLayout.json
new file mode 100644
index 0000000..218a01a
--- /dev/null
+++ b/log4j-layout-json-template/src/main/resources/StackTraceElementLayout.json
@@ -0,0 +1,18 @@
+{
+  "class": {
+    "$resolver": "stackTraceElement",
+    "field": "className"
+  },
+  "method": {
+    "$resolver": "stackTraceElement",
+    "field": "methodName"
+  },
+  "file": {
+    "$resolver": "stackTraceElement",
+    "field": "fileName"
+  },
+  "line": {
+    "$resolver": "stackTraceElement",
+    "field": "lineNumber"
+  }
+}
diff --git a/log4j-layout-json-template/src/site/manual/index.md b/log4j-layout-json-template/src/site/manual/index.md
new file mode 100644
index 0000000..b8cb6e3
--- /dev/null
+++ b/log4j-layout-json-template/src/site/manual/index.md
@@ -0,0 +1,32 @@
+<!-- vim: set syn=markdown : -->
+<!--
+    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.
+-->
+
+# Apache Log4j JSON Template Layout module
+
+This module provides a customizable and efficient JSON layout.
+
+## Requirements
+
+This module was introduced in Log4j 3.0.0 and requires Jackson.
+
+Some features may require optional [dependencies](../runtime-dependencies.html).
+These dependencies are specified in the documentation for those features.
+
+Some Log4j features require external dependencies. See the
+[Dependency Tree](dependencies.html#Dependency_Tree) for the exact list of JAR
+files needed for these features.
diff --git a/log4j-layout-json-template/src/site/site.xml b/log4j-layout-json-template/src/site/site.xml
new file mode 100644
index 0000000..962392e
--- /dev/null
+++ b/log4j-layout-json-template/src/site/site.xml
@@ -0,0 +1,55 @@
+<!--
+ 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.
+-->
+<project name="Log4j Core"
+         xmlns="http://maven.apache.org/DECORATION/1.4.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/DECORATION/1.4.0 http://maven.apache.org/xsd/decoration-1.4.0.xsd">
+
+  <body>
+
+    <links>
+      <item name="Apache" href="http://www.apache.org/" />
+      <item name="Logging Services" href="http://logging.apache.org/"/>
+      <item name="Log4j" href="../index.html"/>
+    </links>
+
+    <!-- Component-specific reports -->
+    <menu ref="reports"/>
+
+	<!-- Overall Project Info -->
+    <menu name="Log4j Project Information" img="icon-info-sign">
+      <item name="Dependencies" href="../dependencies.html" />
+      <item name="Dependency Convergence" href="../dependency-convergence.html" />
+      <item name="Dependency Management" href="../dependency-management.html" />
+      <item name="Project Team" href="../team-list.html" />
+      <item name="Mailing Lists" href="../mail-lists.html" />
+      <item name="Issue Tracking" href="../issue-tracking.html" />
+      <item name="Project License" href="../license.html" />
+      <item name="Source Repository" href="../source-repository.html" />
+      <item name="Project Summary" href="../project-summary.html" />
+    </menu>
+
+    <menu name="Log4j Project Reports" img="icon-cog">
+      <item name="Changes Report" href="../changes-report.html" />
+      <item name="JIRA Report" href="../jira-report.html" />
+      <item name="Surefire Report" href="../surefire-report.html" />
+      <item name="RAT Report" href="../rat-report.html" />
+    </menu>
+
+  </body>
+
+</project>
diff --git a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/BlackHoleByteBufferDestination.java b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/BlackHoleByteBufferDestination.java
new file mode 100644
index 0000000..01b3d4a
--- /dev/null
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/BlackHoleByteBufferDestination.java
@@ -0,0 +1,50 @@
+/*
+ * 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.json.template;
+
+import org.apache.logging.log4j.core.layout.ByteBufferDestination;
+
+import java.nio.ByteBuffer;
+
+class BlackHoleByteBufferDestination implements ByteBufferDestination {
+
+    private final ByteBuffer byteBuffer;
+
+    BlackHoleByteBufferDestination(final int maxByteCount) {
+        this.byteBuffer = ByteBuffer.allocate(maxByteCount);
+    }
+
+    @Override
+    public ByteBuffer getByteBuffer() {
+        return byteBuffer;
+    }
+
+    @Override
+    public ByteBuffer drain(final ByteBuffer byteBuffer) {
+        byteBuffer.clear();
+        return byteBuffer;
+    }
+
+    @Override
+    public void writeBytes(final ByteBuffer byteBuffer) {
+        byteBuffer.clear();
+    }
+
+    @Override
+    public void writeBytes(final byte[] buffer, final int offset, final int length) {}
+
+}
diff --git a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/EcsLayoutTest.java b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/EcsLayoutTest.java
new file mode 100644
index 0000000..8f5593d
--- /dev/null
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/EcsLayoutTest.java
@@ -0,0 +1,90 @@
+package org.apache.logging.log4j.layout.json.template;
+
+import co.elastic.logging.log4j2.EcsLayout;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.config.Configuration;
+import org.apache.logging.log4j.core.config.DefaultConfiguration;
+import org.apache.logging.log4j.layout.json.template.JsonTemplateLayout.EventTemplateAdditionalField;
+import org.assertj.core.api.Assertions;
+import org.junit.Test;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import static org.apache.logging.log4j.layout.json.template.LayoutComparisonHelpers.renderUsing;
+
+public class EcsLayoutTest {
+
+    private static final Configuration CONFIGURATION = new DefaultConfiguration();
+
+    private static final String SERVICE_NAME = "test";
+
+    private static final String EVENT_DATASET = SERVICE_NAME + ".log";
+
+    private static final JsonTemplateLayout JSON_TEMPLATE_LAYOUT = JsonTemplateLayout
+            .newBuilder()
+            .setConfiguration(CONFIGURATION)
+            .setEventTemplateUri("classpath:EcsLayout.json")
+            .setEventTemplateAdditionalFields(
+                    JsonTemplateLayout
+                            .EventTemplateAdditionalFields
+                            .newBuilder()
+                            .setAdditionalFields(
+                                    new EventTemplateAdditionalField[]{
+                                            EventTemplateAdditionalField
+                                                    .newBuilder()
+                                                    .setKey("service.name")
+                                                    .setValue(SERVICE_NAME)
+                                                    .build(),
+                                            EventTemplateAdditionalField
+                                                    .newBuilder()
+                                                    .setKey("event.dataset")
+                                                    .setValue(EVENT_DATASET)
+                                                    .build()
+                                    })
+                            .build())
+            .build();
+
+    private static final EcsLayout ECS_LAYOUT = EcsLayout
+            .newBuilder()
+            .setConfiguration(CONFIGURATION)
+            .setServiceName(SERVICE_NAME)
+            .setEventDataset(EVENT_DATASET)
+            .build();
+
+    @Test
+    public void test_lite_log_events() {
+        final List<LogEvent> logEvents = LogEventFixture.createLiteLogEvents(1_000);
+        test(logEvents);
+    }
+
+    @Test
+    public void test_full_log_events() {
+        final List<LogEvent> logEvents = LogEventFixture.createFullLogEvents(1_000);
+        test(logEvents);
+    }
+
+    private static void test(final Collection<LogEvent> logEvents) {
+        for (final LogEvent logEvent : logEvents) {
+            test(logEvent);
+        }
+    }
+
+    private static void test(final LogEvent logEvent) {
+        final Map<String, Object> jsonTemplateLayoutMap = renderUsingJsonTemplateLayout(logEvent);
+        final Map<String, Object> ecsLayoutMap = renderUsingEcsLayout(logEvent);
+        Assertions.assertThat(jsonTemplateLayoutMap).isEqualTo(ecsLayoutMap);
+    }
+
+    private static Map<String, Object> renderUsingJsonTemplateLayout(
+            final LogEvent logEvent) {
+        return renderUsing(logEvent, JSON_TEMPLATE_LAYOUT);
+    }
+
+    private static Map<String, Object> renderUsingEcsLayout(
+            final LogEvent logEvent) {
+        return renderUsing(logEvent, ECS_LAYOUT);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/GelfLayoutTest.java b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/GelfLayoutTest.java
new file mode 100644
index 0000000..795546b
--- /dev/null
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/GelfLayoutTest.java
@@ -0,0 +1,109 @@
+package org.apache.logging.log4j.layout.json.template;
+
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.config.Configuration;
+import org.apache.logging.log4j.core.config.DefaultConfiguration;
+import org.apache.logging.log4j.core.layout.GelfLayout;
+import org.apache.logging.log4j.core.time.Instant;
+import org.apache.logging.log4j.layout.json.template.JsonTemplateLayout.EventTemplateAdditionalField;
+import org.assertj.core.api.Assertions;
+import org.junit.Test;
+
+import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import static org.apache.logging.log4j.layout.json.template.LayoutComparisonHelpers.renderUsing;
+
+public class GelfLayoutTest {
+
+    private static final Configuration CONFIGURATION = new DefaultConfiguration();
+
+    private static final String HOST_NAME = "localhost";
+
+    private static final JsonTemplateLayout JSON_TEMPLATE_LAYOUT = JsonTemplateLayout
+            .newBuilder()
+            .setConfiguration(CONFIGURATION)
+            .setEventTemplateUri("classpath:GelfLayout.json")
+            .setEventTemplateAdditionalFields(
+                    JsonTemplateLayout
+                            .EventTemplateAdditionalFields
+                            .newBuilder()
+                            .setAdditionalFields(
+                                    new EventTemplateAdditionalField[]{
+                                            EventTemplateAdditionalField
+                                                    .newBuilder()
+                                                    .setKey("host")
+                                                    .setValue(HOST_NAME)
+                                                    .build()
+                                    })
+                            .build())
+            .build();
+
+    private static final GelfLayout GELF_LAYOUT = GelfLayout
+            .newBuilder()
+            .setConfiguration(CONFIGURATION)
+            .setHost(HOST_NAME)
+            .setCompressionType(GelfLayout.CompressionType.OFF)
+            .build();
+
+    @Test
+    public void test_lite_log_events() {
+        final List<LogEvent> logEvents = LogEventFixture.createLiteLogEvents(1_000);
+        test(logEvents);
+    }
+
+    @Test
+    public void test_full_log_events() {
+        final List<LogEvent> logEvents = LogEventFixture.createFullLogEvents(1_000);
+        test(logEvents);
+    }
+
+    private static void test(final Collection<LogEvent> logEvents) {
+        for (final LogEvent logEvent : logEvents) {
+            test(logEvent);
+        }
+    }
+
+    private static void test(final LogEvent logEvent) {
+        final Map<String, Object> jsonTemplateLayoutMap = renderUsingJsonTemplateLayout(logEvent);
+        final Map<String, Object> gelfLayoutMap = renderUsingGelfLayout(logEvent);
+        verifyTimestamp(logEvent.getInstant(), jsonTemplateLayoutMap, gelfLayoutMap);
+        Assertions.assertThat(jsonTemplateLayoutMap).isEqualTo(gelfLayoutMap);
+    }
+
+    private static Map<String, Object> renderUsingJsonTemplateLayout(
+            final LogEvent logEvent) {
+        return renderUsing(logEvent, JSON_TEMPLATE_LAYOUT);
+    }
+
+    private static Map<String, Object> renderUsingGelfLayout(
+            final LogEvent logEvent) {
+        return renderUsing(logEvent, GELF_LAYOUT);
+    }
+
+    /**
+     * Handle timestamps individually to avoid floating-point comparison hiccups.
+     */
+    private static void verifyTimestamp(
+            final Instant logEventInstant,
+            final Map<String, Object> jsonTemplateLayoutMap,
+            final Map<String, Object> gelfLayoutMap) {
+        final BigDecimal jsonTemplateLayoutTimestamp =
+                (BigDecimal) jsonTemplateLayoutMap.remove("timestamp");
+        final BigDecimal gelfLayoutTimestamp =
+                (BigDecimal) gelfLayoutMap.remove("timestamp");
+        final String description = String.format(
+                "instantEpochSecs=%d.%d, jsonTemplateLayoutTimestamp=%s, gelfLayoutTimestamp=%s",
+                logEventInstant.getEpochSecond(),
+                logEventInstant.getNanoOfSecond(),
+                jsonTemplateLayoutTimestamp,
+                gelfLayoutTimestamp);
+        Assertions
+                .assertThat(jsonTemplateLayoutTimestamp.compareTo(gelfLayoutTimestamp))
+                .as(description)
+                .isEqualTo(0);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JacksonFixture.java b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JacksonFixture.java
new file mode 100644
index 0000000..a2ebe6f
--- /dev/null
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JacksonFixture.java
@@ -0,0 +1,29 @@
+/*
+ * 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.json.template;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+public enum JacksonFixture {;
+
+    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+    public static ObjectMapper getObjectMapper() {
+        return OBJECT_MAPPER;
+    }
+
+}
diff --git a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonLayoutTest.java b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonLayoutTest.java
new file mode 100644
index 0000000..6c2c64d
--- /dev/null
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonLayoutTest.java
@@ -0,0 +1,71 @@
+package org.apache.logging.log4j.layout.json.template;
+
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.config.Configuration;
+import org.apache.logging.log4j.core.config.DefaultConfiguration;
+import org.apache.logging.log4j.core.layout.JsonLayout;
+import org.assertj.core.api.Assertions;
+import org.junit.Test;
+
... 9623 lines suppressed ...