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:12 UTC

[logging-log4j2] branch release-2.x updated (736bcb5 -> 4513817)

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

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


    from 736bcb5  Add new articles
     new c11ed6f  #335 Initial import of JsonTemplateLayout from LogstashLayout.
     new 4513817  Add GitHub Actions CI support to release-2.x branch.

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../workflows}/maven-toolchains.xml                |    0
 .github/workflows/maven.yml                        |   87 +
 .../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 ++
 117 files changed, 18326 insertions(+), 253 deletions(-)
 copy {workflows => .github/workflows}/maven-toolchains.xml (100%)
 create mode 100644 .github/workflows/maven.yml
 create mode 100644 log4j-layout-json-template/pom.xml
 create mode 100644 log4j-layout-json-template/revapi.json
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayout.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutDefaults.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EndOfBatchResolver.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EndOfBatchResolverFactory.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EventResolver.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EventResolverContext.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EventResolverFactories.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EventResolverFactory.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionInternalResolverFactory.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionResolver.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionResolverFactory.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionRootCauseResolver.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionRootCauseResolverFactory.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LevelResolver.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LevelResolverFactory.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LoggerResolver.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LoggerResolverFactory.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MainMapResolver.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MainMapResolverFactory.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MapResolver.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MapResolverFactory.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MarkerResolver.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MarkerResolverFactory.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MessageResolver.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MessageResolverFactory.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/PatternResolver.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/PatternResolverFactory.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/SourceResolver.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/SourceResolverFactory.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceElementObjectResolver.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceElementObjectResolverContext.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceElementObjectResolverFactories.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceElementObjectResolverFactory.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceObjectResolver.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceResolver.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceStringResolver.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolver.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolverConfig.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolverContext.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolverFactory.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolvers.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadContextDataResolver.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadContextDataResolverFactory.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadContextStackResolver.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadContextStackResolverFactory.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadResolver.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadResolverFactory.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TimestampResolver.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TimestampResolverFactory.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/DummyRecycler.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/DummyRecyclerFactory.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/JsonReader.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/JsonWriter.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/MapAccessor.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/QueueingRecycler.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/QueueingRecyclerFactory.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/Recycler.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/RecyclerFactories.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/RecyclerFactory.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/StringParameterParser.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/ThreadLocalRecycler.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/ThreadLocalRecyclerFactory.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/TruncatingBufferedPrintWriter.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/TruncatingBufferedWriter.java
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/Uris.java
 create mode 100644 log4j-layout-json-template/src/main/resources/EcsLayout.json
 create mode 100644 log4j-layout-json-template/src/main/resources/GelfLayout.json
 create mode 100644 log4j-layout-json-template/src/main/resources/JsonLayout.json
 create mode 100644 log4j-layout-json-template/src/main/resources/LogstashJsonEventLayoutV1.json
 create mode 100644 log4j-layout-json-template/src/main/resources/StackTraceElementLayout.json
 create mode 100644 log4j-layout-json-template/src/site/manual/index.md
 create mode 100644 log4j-layout-json-template/src/site/site.xml
 create mode 100644 log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/BlackHoleByteBufferDestination.java
 create mode 100644 log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/EcsLayoutTest.java
 create mode 100644 log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/GelfLayoutTest.java
 create mode 100644 log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JacksonFixture.java
 create mode 100644 log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonLayoutTest.java
 create mode 100644 log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutConcurrentEncodeTest.java
 create mode 100644 log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutGcFreeTest.java
 create mode 100644 log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutNullEventDelimiterTest.java
 create mode 100644 log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutTest.java
 create mode 100644 log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/LayoutComparisonHelpers.java
 create mode 100644 log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/LogEventFixture.java
 create mode 100644 log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/LogstashIT.java
 create mode 100644 log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/util/JsonReaderTest.java
 create mode 100644 log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/util/JsonWriterTest.java
 create mode 100644 log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/util/RecyclerFactoriesTest.java
 create mode 100644 log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/util/StringParameterParserTest.java
 create mode 100644 log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/util/TruncatingBufferedWriterTest.java
 create mode 100644 log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/util/UrisTest.java
 create mode 100644 log4j-layout-json-template/src/test/resources/gcFreeJsonTemplateLayoutLogging.xml
 create mode 100644 log4j-layout-json-template/src/test/resources/nullEventDelimitedJsonTemplateLayoutLogging.xml
 create mode 100644 log4j-layout-json-template/src/test/resources/testJsonTemplateLayout.json
 create mode 100644 log4j-perf/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutBenchmark.java
 create mode 100644 log4j-perf/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutBenchmarkReport.java
 create mode 100644 log4j-perf/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutBenchmarkState.java
 create mode 100644 src/site/asciidoc/manual/json-template-layout.adoc
 create mode 100644 src/site/xdoc/manual/json-template-layout.xml.vm


[logging-log4j2] 02/02: Add GitHub Actions CI support to release-2.x branch.

Posted by vy...@apache.org.
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 45138174ea99f246a6223c0c9cdd2666714821f8
Author: Volkan Yazıcı <vo...@gmail.com>
AuthorDate: Thu Aug 20 15:58:49 2020 +0200

    Add GitHub Actions CI support to release-2.x branch.
---
 .github/workflows/maven-toolchains.xml | 37 +++++++++++++++
 .github/workflows/maven.yml            | 87 ++++++++++++++++++++++++++++++++++
 2 files changed, 124 insertions(+)

diff --git a/.github/workflows/maven-toolchains.xml b/.github/workflows/maven-toolchains.xml
new file mode 100644
index 0000000..066a50f
--- /dev/null
+++ b/.github/workflows/maven-toolchains.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF8"?>
+<!--
+  ~ 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.
+  -->
+<toolchains>
+  <toolchain>
+    <type>jdk</type>
+    <provides>
+      <version>1.8</version>
+    </provides>
+    <configuration>
+      <jdkHome>${env.JAVA_HOME_8_X64}</jdkHome>
+    </configuration>
+  </toolchain>
+  <toolchain>
+    <type>jdk</type>
+    <provides>
+      <version>11</version>
+    </provides>
+    <configuration>
+      <jdkHome>${env.JAVA_HOME_11_X64}</jdkHome>
+    </configuration>
+  </toolchain>
+</toolchains>
diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
new file mode 100644
index 0000000..b99a32e
--- /dev/null
+++ b/.github/workflows/maven.yml
@@ -0,0 +1,87 @@
+name: Maven
+
+on: [push]
+
+jobs:
+  build:
+
+    runs-on: ${{ matrix.os }}
+
+    strategy:
+      matrix:
+        os: [ubuntu-latest, windows-latest, macos-latest]
+
+    steps:
+
+      - name: Checkout repository
+        uses: actions/checkout@v2
+
+      - name: Setup Maven caching
+        uses: actions/cache@v2
+        with:
+          path: ~/.m2/repository
+          key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
+          restore-keys: |
+            ${{ runner.os }}-maven-
+
+      - name: Setup JDK 11
+        uses: actions/setup-java@v1
+        with:
+          java-version: 11
+          java-package: jdk
+          architecture: x64
+
+      - name: Setup JDK 8
+        uses: actions/setup-java@v1
+        with:
+          java-version: 8
+          java-package: jdk
+          architecture: x64
+
+      - name: Inspect environment (Linux)
+        if: runner.os == 'Linux'
+        run: env | grep '^JAVA'
+
+      - name: Build with Maven (Linux)
+        if: runner.os == 'Linux'
+        run: ./mvnw -V -B -e -DtrimStackTrace=false -Dmaven.test.failure.ignore=true -Dsurefire.rerunFailingTestsCount=1 --global-toolchains .github/workflows/maven-toolchains.xml verify
+
+      - name: Publish Test Results (Linux)
+        if: runner.os == 'Linux'
+        uses: scacap/action-surefire-report@v1
+        with:
+          github_token: ${{ secrets.GITHUB_TOKEN }}
+          check_name: 'Test Report (Linux)'
+          report_paths: '**/*-reports/TEST-*.xml'
+
+      - name: Inspect environment (Windows)
+        if: runner.os == 'Windows'
+        run: set java
+
+      - name: Build with Maven (Windows)
+        if: runner.os == 'Windows'
+        run: ./mvnw -V -B -e -DtrimStackTrace=false "-Dmaven.test.failure.ignore=true" "-Dsurefire.rerunFailingTestsCount=1" --global-toolchains ".github\workflows\maven-toolchains.xml" verify
+
+      - name: Publish Test Results (Windows)
+        if: runner.os == 'Windows'
+        uses: scacap/action-surefire-report@v1
+        with:
+          github_token: ${{ secrets.GITHUB_TOKEN }}
+          check_name: 'Test Report (Windows)'
+          report_paths: '**/*-reports/TEST-*.xml'
+
+      - name: Inspect environment (MacOS)
+        if: runner.os == 'macOS'
+        run: env | grep '^JAVA'
+
+      - name: Build with Maven (MacOS)
+        if: runner.os == 'macOS'
+        run: ./mvnw -V -B -e -DtrimStackTrace=false -Dmaven.test.failure.ignore=true -Dsurefire.rerunFailingTestsCount=1 --global-toolchains .github/workflows/maven-toolchains.xml verify
+
+      - name: Publish Test Results (MacOS)
+        if: runner.os == 'macOS'
+        uses: scacap/action-surefire-report@v1
+        with:
+          github_token: ${{ secrets.GITHUB_TOKEN }}
+          check_name: 'Test Report (MacOS)'
+          report_paths: '**/*-reports/TEST-*.xml'


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

Posted by vy...@apache.org.
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 ...